From dceabc38c5784599e777e4cc1262a779b6fff3b1 Mon Sep 17 00:00:00 2001 From: hamdymohamedak Date: Sun, 26 Apr 2026 11:54:43 +0300 Subject: [PATCH 1/4] feat: postinstall scaffolds config and npm run runly Add scripts/postinstall.mjs to create runly.config.js with defaults, inject scripts.runly when missing, and ship the script in the published package. Made-with: Cursor --- dist/resolve-node.d.ts.map | 2 +- dist/resolve-node.js | 2 + dist/resolve-node.js.map | 2 +- package.json | 6 +- scripts/postinstall.mjs | 122 +++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 scripts/postinstall.mjs diff --git a/dist/resolve-node.d.ts.map b/dist/resolve-node.d.ts.map index a992b03..4f324a6 100644 --- a/dist/resolve-node.d.ts.map +++ b/dist/resolve-node.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"resolve-node.d.ts","sourceRoot":"","sources":["../src/resolve-node.ts"],"names":[],"mappings":"AAUA,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqCrF;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAIjF;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD"} \ No newline at end of file +{"version":3,"file":"resolve-node.d.ts","sourceRoot":"","sources":["../src/resolve-node.ts"],"names":[],"mappings":"AAUA,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwCrF;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAIjF;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD"} \ No newline at end of file diff --git a/dist/resolve-node.js b/dist/resolve-node.js index 20fe411..495f972 100644 --- a/dist/resolve-node.js +++ b/dist/resolve-node.js @@ -8,6 +8,8 @@ function npxCmd() { } export function resolveNodeExecPath(versionSpec, cwd) { return new Promise((resolve, reject) => { + // Windows: spawning `.cmd` without a shell often yields `spawn EINVAL` (Node + libuv). + // Use `shell: true` so `npx.cmd` runs under the default shell (cmd.exe). const win32 = process.platform === "win32"; const child = spawn(npxCmd(), [ "--yes", diff --git a/dist/resolve-node.js.map b/dist/resolve-node.js.map index d199a3a..7a1fe0c 100644 --- a/dist/resolve-node.js.map +++ b/dist/resolve-node.js.map @@ -1 +1 @@ -{"version":3,"file":"resolve-node.js","sourceRoot":"","sources":["../src/resolve-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC;;GAEG;AACH,SAAS,MAAM;IACb,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,WAAmB,EAAE,GAAW;IAClE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,KAAK,CACjB,MAAM,EAAE,EACR;YACE,OAAO;YACP,WAAW;YACX,QAAQ,WAAW,EAAE;YACrB,IAAI;YACJ,MAAM;YACN,IAAI;YACJ,wCAAwC;SACzC,EACD,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAC3C,CAAC;QACF,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YAC7B,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YAC7B,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACzB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,WAAW,KAAK,GAAG,IAAI,QAAQ,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;gBACrF,OAAO;YACT,CAAC;YACD,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;YACrB,IAAI,CAAC,CAAC,EAAE,CAAC;gBACP,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,WAAW,EAAE,CAAC,CAAC,CAAC;gBACzD,OAAO;YACT,CAAC;YACD,OAAO,CAAC,CAAC,CAAC,CAAC;QACb,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,OAA2B;IACvE,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IACrD,MAAM,IAAI,GAAG,OAAO,IAAI,EAAE,CAAC;IAC3B,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC3B,CAAC"} \ No newline at end of file +{"version":3,"file":"resolve-node.js","sourceRoot":"","sources":["../src/resolve-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC;;GAEG;AACH,SAAS,MAAM;IACb,OAAO,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,WAAmB,EAAE,GAAW;IAClE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,uFAAuF;QACvF,yEAAyE;QACzE,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;QAC3C,MAAM,KAAK,GAAG,KAAK,CACjB,MAAM,EAAE,EACR;YACE,OAAO;YACP,WAAW;YACX,QAAQ,WAAW,EAAE;YACrB,IAAI;YACJ,MAAM;YACN,IAAI;YACJ,wCAAwC;SACzC,EACD,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CACzD,CAAC;QACF,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YAC7B,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;YAC7B,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACzB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,WAAW,KAAK,GAAG,IAAI,QAAQ,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;gBACrF,OAAO;YACT,CAAC;YACD,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;YACrB,IAAI,CAAC,CAAC,EAAE,CAAC;gBACP,MAAM,CAAC,IAAI,KAAK,CAAC,wBAAwB,WAAW,EAAE,CAAC,CAAC,CAAC;gBACzD,OAAO;YACT,CAAC;YACD,OAAO,CAAC,CAAC,CAAC,CAAC;QACb,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,OAA2B;IACvE,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IACrD,MAAM,IAAI,GAAG,OAAO,IAAI,EAAE,CAAC;IAC3B,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC;AAC3B,CAAC"} \ No newline at end of file diff --git a/package.json b/package.json index e1acd18..c1bb47d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hamdymohamedak/runly", - "version": "0.1.0", + "version": "0.1.1", "publishConfig": { "access": "public" }, @@ -19,9 +19,11 @@ } }, "files": [ - "dist" + "dist", + "scripts/postinstall.mjs" ], "scripts": { + "postinstall": "node ./scripts/postinstall.mjs", "test": "node --test test/smoke.test.js", "ci:runly-smoke": "node dist/cli.js -c examples/all-versions-pass/runly.config.mjs", "build": "tsc", diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs new file mode 100644 index 0000000..fc7d273 --- /dev/null +++ b/scripts/postinstall.mjs @@ -0,0 +1,122 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +const PKG = "@hamdymohamedak/runly"; +const CONFIG_CANDIDATES = ["runly.config.mjs", "runly.config.js", "runly.config.cjs"]; + +function isInsideNodeModules(dir) { + return dir.split(sep).includes("node_modules"); +} + +function installContextRoot() { + const init = process.env.INIT_CWD; + if (init) return init; + const cwd = process.cwd(); + if (isInsideNodeModules(cwd)) return null; + return cwd; +} + +function readPkg(dir) { + const p = join(dir, "package.json"); + if (!existsSync(p)) return null; + try { + return JSON.parse(readFileSync(p, "utf8")); + } catch { + return null; + } +} + +function declaresRunly(pkg) { + if (!pkg || typeof pkg !== "object") return false; + const blocks = [ + pkg.dependencies, + pkg.devDependencies, + pkg.optionalDependencies, + pkg.peerDependencies, + ]; + for (const b of blocks) { + if (b && typeof b === "object" && PKG in b) return true; + } + return false; +} + +/** First directory from `start` upward whose package.json lists this package. */ +function findConsumerRoot(start) { + let dir = start; + for (;;) { + const pkg = readPkg(dir); + if (pkg && declaresRunly(pkg)) return { dir, pkg }; + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} + +function hasRunlyConfig(projectRoot) { + return CONFIG_CANDIDATES.some((name) => existsSync(join(projectRoot, name))); +} + +function defaultConfigBody() { + // One-liner so first `npm run runly` succeeds without a test/ tree; users can switch to `node --test` etc. + return `{ + versions: ["18", "20", "22"], + bail: false, + run: { + argv: ["node", "-e", "console.log('runly ok', process.version)"], + shell: false, + }, +}`; +} + +function writePackageJson(projectRoot, pkg) { + const pkgPath = join(projectRoot, "package.json"); + writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8"); +} + +/** Ensures `scripts.runly` so `npm run runly` works. Returns true if package.json was written. */ +function ensureRunlyNpmScript(pkg) { + const prev = pkg.scripts; + const scripts = + prev != null && typeof prev === "object" && !Array.isArray(prev) + ? { ...prev } + : {}; + if (Object.hasOwn(scripts, "runly")) return false; + scripts.runly = "runly"; + pkg.scripts = scripts; + return true; +} + +function main() { + if (process.env.RUNLY_SKIP_POSTINSTALL === "1" || process.env.RUNLY_SKIP_POSTINSTALL === "true") { + return; + } + if (process.env.npm_config_global === "true") { + return; + } + + const start = installContextRoot(); + if (!start) return; + + const found = findConsumerRoot(start); + if (!found) return; + + const { dir: projectRoot, pkg } = found; + + if (!hasRunlyConfig(projectRoot)) { + const isEsm = pkg.type === "module"; + const body = defaultConfigBody(); + const content = isEsm + ? `/** @type {import("${PKG}").RunlyConfig} */\nexport default ${body};\n` + : `/** @type {import("${PKG}").RunlyConfig} */\nmodule.exports = ${body};\n`; + + writeFileSync(join(projectRoot, "runly.config.js"), content, "utf8"); + } + + if (ensureRunlyNpmScript(pkg)) { + writePackageJson(projectRoot, pkg); + } +} + +main(); From 425e71036acdde7edf50a2d96099ed53dc604b7b Mon Sep 17 00:00:00 2001 From: hamdymohamedak Date: Sun, 26 Apr 2026 12:19:11 +0300 Subject: [PATCH 2/4] feat: replace postinstall with runly init CLI (v0.2.0) Made-with: Cursor --- .cursor/skills/runly/SKILL.md | 40 +++++-- .cursor/skills/runly/skill/SKILL.md | 42 +++++--- .github/workflows/ci.yml | 8 +- README.md | 30 +++++- Skill/SKILL.md | 155 ++++++++++++++++++++++++++++ dist/cli.js | 43 +++++--- dist/cli.js.map | 2 +- dist/config-paths.d.ts | 6 ++ dist/config-paths.d.ts.map | 1 + dist/config-paths.js | 28 +++++ dist/config-paths.js.map | 1 + dist/index.d.ts | 8 ++ dist/index.d.ts.map | 2 +- dist/index.js | 23 +++++ dist/index.js.map | 2 +- dist/init.d.ts | 6 ++ dist/init.d.ts.map | 1 + dist/init.js | 65 ++++++++++++ dist/init.js.map | 1 + package.json | 7 +- scripts/ci-verify-init.mjs | 94 +++++++++++++++++ scripts/postinstall.mjs | 122 ---------------------- src/cli.ts | 51 ++++++--- src/config-paths.ts | 29 ++++++ src/index.ts | 25 +++++ src/init.ts | 66 ++++++++++++ 26 files changed, 672 insertions(+), 186 deletions(-) create mode 100644 Skill/SKILL.md create mode 100644 dist/config-paths.d.ts create mode 100644 dist/config-paths.d.ts.map create mode 100644 dist/config-paths.js create mode 100644 dist/config-paths.js.map create mode 100644 dist/init.d.ts create mode 100644 dist/init.d.ts.map create mode 100644 dist/init.js create mode 100644 dist/init.js.map create mode 100644 scripts/ci-verify-init.mjs delete mode 100644 scripts/postinstall.mjs create mode 100644 src/config-paths.ts create mode 100644 src/init.ts diff --git a/.cursor/skills/runly/SKILL.md b/.cursor/skills/runly/SKILL.md index e4c575a..3a272fa 100644 --- a/.cursor/skills/runly/SKILL.md +++ b/.cursor/skills/runly/SKILL.md @@ -3,8 +3,9 @@ name: runly description: >- Runs the same Node.js command under multiple Node versions from one config file (runly.config.mjs/js/cjs), resolving each runtime via npx and the npm `node` - package. Use when the user mentions Runly, @hamdymohamedak/runly, multi-version - Node testing, Node matrix in CI, runly.config, or running tests across Node 18/20/22. + package. Use `runly init` to scaffold config and npm script; use `loadConfig()` in code. + Use when the user mentions Runly, @hamdymohamedak/runly, multi-version Node testing, + Node matrix in CI, runly.config, or running tests across Node 18/20/22. --- # Runly @@ -12,7 +13,7 @@ description: >- ## What it is - **npm package**: `@hamdymohamedak/runly` (scoped; unscoped name `runly` is blocked on npm as too similar to `runjs`). -- **CLI binary name**: `runly` (after `npm install`, use `npx runly` from the project root). +- **CLI binary name**: `runly` — use **`npx runly init`** once to create config + **`npm run runly`**, or **`npx runly`** for one-off runs. - **Purpose**: For each entry in `versions`, resolve a real `node` binary for that spec, prepend its directory to `PATH`, then spawn the configured command so that command’s default `node` is that matrix version—without requiring nvm/fnm/asdf on the machine. ## Requirements @@ -30,8 +31,24 @@ One-off without saving to `package.json`: ```bash npx @hamdymohamedak/runly +npx @hamdymohamedak/runly init ``` +## `runly init` (scaffold) + +From the **project root** (after **`npm install -D @hamdymohamedak/runly`**): + +```bash +npx runly init +``` + +- Creates **`runly.config.js`** only if none of **`runly.config.mjs`**, **`runly.config.js`**, or **`runly.config.cjs`** exists (otherwise prints a message and exits **0**). +- Uses **`export default`** when **`package.json`** has **`"type": "module"`**, else **`module.exports`**. +- Adds **`"runly": "runly"`** under **`scripts`** in **`package.json`** when the file exists and **`scripts.runly`** is not already set. +- Default **`run`** in the scaffold is a small **`node -e`** smoke command; edit to **`node --test`**, **`npm test`**, etc. + +Programmatic: **`initRunlyProject(cwd)`** from **`@hamdymohamedak/runly`**. + ## Config file discovery From **current working directory**, first file that exists: @@ -40,7 +57,7 @@ From **current working directory**, first file that exists: 2. `runly.config.js` 3. `runly.config.cjs` -Override: `runly -c /path/to/config.mjs` or `runly --config /path/to/config.mjs`. +Override: `runly -c /path/to/config.mjs` or `runly --config /path/to/runly.config.mjs`. Config must be **JavaScript** (ESM or CJS). Runly does **not** load `.ts` configs unless the user wires a loader themselves. Export **`default`** as the config object (or the module’s default export after dynamic `import()`). @@ -95,15 +112,16 @@ export default defineConfig({ }); ``` -Exported types: **`RunlyConfig`**, **`RunlyRun`**. The matrix runner **`runMatrix`** lives in source but is **not** part of the published `exports`—treat **`defineConfig` + types** as the library surface for dependents. +Exported types: **`RunlyConfig`**, **`RunlyRun`**. **`loadConfig(cwd?)`** loads the default config or throws with a hint to run **`npx runly init`**. The matrix runner **`runMatrix`** is not exported—use the CLI or compose from source. ## CLI -| Flag | Meaning | -|------|---------| +| Command / flag | Meaning | +|----------------|---------| +| `runly init` | Scaffold **`runly.config.js`** and **`scripts.runly`** (see above). | +| `runly` | Run matrix using config in cwd. | | `-c`, `--config` | Path to config file. | - -No other flags in the current CLI. +| `runly help` | Usage. | ## Exit codes @@ -118,7 +136,7 @@ No other flags in the current CLI. ## CI -Install deps, `cd` to repo root (or set `cwd` in config), run `npx runly`. No global version manager required if `npx` can fetch the `node` package. +Install deps, `cd` to repo root, run **`npx runly init`** once if the repo has no config, then **`npm run runly`** or **`npx runly`**. No global version manager required if `npx` can fetch the `node` package. ## Limitations @@ -129,7 +147,7 @@ Install deps, `cd` to repo root (or set `cwd` in config), run `npx runly`. No gl ## Reference in this repo - User-facing docs: [README.md](../../../README.md) -- Working demo: [matrix-demo/](../../../matrix-demo/) (`npm install` + `npm run matrix` from that folder; uses `file:..` to depend on the parent package). +- Example config: [examples/all-versions-pass/runly.config.mjs](../../../examples/all-versions-pass/runly.config.mjs) ## Links diff --git a/.cursor/skills/runly/skill/SKILL.md b/.cursor/skills/runly/skill/SKILL.md index e4c575a..4e3b9d9 100644 --- a/.cursor/skills/runly/skill/SKILL.md +++ b/.cursor/skills/runly/skill/SKILL.md @@ -3,8 +3,9 @@ name: runly description: >- Runs the same Node.js command under multiple Node versions from one config file (runly.config.mjs/js/cjs), resolving each runtime via npx and the npm `node` - package. Use when the user mentions Runly, @hamdymohamedak/runly, multi-version - Node testing, Node matrix in CI, runly.config, or running tests across Node 18/20/22. + package. Use `runly init` to scaffold config and npm script; use `loadConfig()` in code. + Use when the user mentions Runly, @hamdymohamedak/runly, multi-version Node testing, + Node matrix in CI, runly.config, or running tests across Node 18/20/22. --- # Runly @@ -12,7 +13,7 @@ description: >- ## What it is - **npm package**: `@hamdymohamedak/runly` (scoped; unscoped name `runly` is blocked on npm as too similar to `runjs`). -- **CLI binary name**: `runly` (after `npm install`, use `npx runly` from the project root). +- **CLI binary name**: `runly` — use **`npx runly init`** once to create config + **`npm run runly`**, or **`npx runly`** for one-off runs. - **Purpose**: For each entry in `versions`, resolve a real `node` binary for that spec, prepend its directory to `PATH`, then spawn the configured command so that command’s default `node` is that matrix version—without requiring nvm/fnm/asdf on the machine. ## Requirements @@ -30,8 +31,24 @@ One-off without saving to `package.json`: ```bash npx @hamdymohamedak/runly +npx @hamdymohamedak/runly init ``` +## `runly init` (scaffold) + +From the **project root** (after **`npm install -D @hamdymohamedak/runly`**): + +```bash +npx runly init +``` + +- Creates **`runly.config.js`** only if none of **`runly.config.mjs`**, **`runly.config.js`**, or **`runly.config.cjs`** exists (otherwise prints a message and exits **0**). +- Uses **`export default`** when **`package.json`** has **`"type": "module"`**, else **`module.exports`**. +- Adds **`"runly": "runly"`** under **`scripts`** in **`package.json`** when the file exists and **`scripts.runly`** is not already set. +- Default **`run`** in the scaffold is a small **`node -e`** smoke command; edit to **`node --test`**, **`npm test`**, etc. + +Programmatic: **`initRunlyProject(cwd)`** from **`@hamdymohamedak/runly`**. + ## Config file discovery From **current working directory**, first file that exists: @@ -40,7 +57,7 @@ From **current working directory**, first file that exists: 2. `runly.config.js` 3. `runly.config.cjs` -Override: `runly -c /path/to/config.mjs` or `runly --config /path/to/config.mjs`. +Override: `runly -c /path/to/config.mjs` or `runly --config /path/to/runly.config.mjs`. Config must be **JavaScript** (ESM or CJS). Runly does **not** load `.ts` configs unless the user wires a loader themselves. Export **`default`** as the config object (or the module’s default export after dynamic `import()`). @@ -95,15 +112,16 @@ export default defineConfig({ }); ``` -Exported types: **`RunlyConfig`**, **`RunlyRun`**. The matrix runner **`runMatrix`** lives in source but is **not** part of the published `exports`—treat **`defineConfig` + types** as the library surface for dependents. +Exported types: **`RunlyConfig`**, **`RunlyRun`**. **`loadConfig(cwd?)`** loads the default config or throws with a hint to run **`npx runly init`**. The matrix runner **`runMatrix`** is not exported—use the CLI or compose from source. ## CLI -| Flag | Meaning | -|------|---------| +| Command / flag | Meaning | +|----------------|---------| +| `runly init` | Scaffold **`runly.config.js`** and **`scripts.runly`** (see above). | +| `runly` | Run matrix using config in cwd. | | `-c`, `--config` | Path to config file. | - -No other flags in the current CLI. +| `runly help` | Usage. | ## Exit codes @@ -118,7 +136,7 @@ No other flags in the current CLI. ## CI -Install deps, `cd` to repo root (or set `cwd` in config), run `npx runly`. No global version manager required if `npx` can fetch the `node` package. +Install deps, `cd` to repo root, run **`npx runly init`** once if the repo has no config, then **`npm run runly`** or **`npx runly`**. No global version manager required if `npx` can fetch the `node` package. ## Limitations @@ -128,8 +146,8 @@ Install deps, `cd` to repo root (or set `cwd` in config), run `npx runly`. No gl ## Reference in this repo -- User-facing docs: [README.md](../../../README.md) -- Working demo: [matrix-demo/](../../../matrix-demo/) (`npm install` + `npm run matrix` from that folder; uses `file:..` to depend on the parent package). +- User-facing docs: [README.md](../../../../README.md) +- Example config: [examples/all-versions-pass/runly.config.mjs](../../../../examples/all-versions-pass/runly.config.mjs) ## Links diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82c0626..25ce341 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,6 @@ -# Cross-OS × host Node matrix: build Runly, run smoke tests, execute Runly against -# multiple Node runtimes via npx (no nvm on the runner). +# Cross-OS × host Node matrix: build Runly, verify `runly init` scaffolds a consumer +# (runly.config.js + scripts.runly + npm run runly) from the packed tarball, smoke tests, +# then Runly e2e against multiple Node runtimes via npx (no nvm on the runner). name: CI on: @@ -39,6 +40,9 @@ jobs: - name: Typecheck / build run: npm run build + - name: runly init scaffold (pack tarball → CJS & ESM consumers → npm run runly) + run: npm run ci:verify-init + - name: Unit smoke (repo test/) run: node --test test/smoke.test.js diff --git a/README.md b/README.md index 85135b8..52544f9 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,35 @@ npm install -D @hamdymohamedak/runly The executable name is **`runly`** (see `bin` in `package.json`). +### First-time setup (`init`) + +From your project root, create a default `runly.config.js` and add an npm script **`runly`** when `package.json` exists: + +```bash +npx runly init +``` + +If any of `runly.config.mjs`, `runly.config.js`, or `runly.config.cjs` already exists, `init` does nothing (idempotent). + +### Run the matrix + From the directory that contains your config: ```bash npx runly ``` +Or, after `init`: + +```bash +npm run runly +``` + Without adding a dev dependency: ```bash npx @hamdymohamedak/runly +npx @hamdymohamedak/runly init # scaffold config in cwd ``` --- @@ -114,15 +133,18 @@ export default defineConfig({ }); ``` -Exported types include `RunlyConfig` and `RunlyRun` for use in your own tooling. +Exported types include `RunlyConfig` and `RunlyRun`. **`loadConfig(cwd?)`** loads the first config file in `cwd` (same discovery as the CLI) or throws with a hint to run **`npx runly init`**. **`initRunlyProject(cwd?)`** is the programmatic equivalent of **`runly init`**. --- ## CLI -| Flag | Description | -|------|-------------| +| Command / flag | Description | +|----------------|-------------| +| `runly init` | Create `runly.config.js` with defaults and add `"runly": "runly"` to `package.json` when present. No-op if a config file already exists. | +| `runly`, `runly -c ` | Run the matrix (search for config in cwd, or use `-c` / `--config`). | | `-c`, `--config` | Path to a config file. If omitted, Runly searches for `runly.config.mjs`, then `.js`, then `.cjs` in the current working directory. | +| `runly help` | Print usage. | --- @@ -147,7 +169,7 @@ On Windows, `npx.cmd` is used for the resolution step. ## Continuous integration -Install dependencies as usual, ensure Node and npm are available, then invoke `npx runly` (or `npx @hamdymohamedak/runly`) from the repository root where the config lives. No extra global Node switcher is required on the runner image as long as `npx` can fetch the `node` package. +Install dependencies as usual, ensure Node and npm are available. If the repo has no Runly config yet, run **`npx runly init`** once at the root (or commit a config file). Then invoke **`npx runly`** (or **`npm run runly`**) from the repository root. No extra global Node switcher is required on the runner image as long as `npx` can fetch the `node` package. --- diff --git a/Skill/SKILL.md b/Skill/SKILL.md new file mode 100644 index 0000000..9d04a7e --- /dev/null +++ b/Skill/SKILL.md @@ -0,0 +1,155 @@ +--- +name: runly +description: >- + Runs the same Node.js command under multiple Node versions from one config file + (runly.config.mjs/js/cjs), resolving each runtime via npx and the npm `node` + package. Use `runly init` to scaffold config and npm script; use `loadConfig()` in code. + Use when the user mentions Runly, @hamdymohamedak/runly, multi-version Node testing, + Node matrix in CI, runly.config, or running tests across Node 18/20/22. +--- + +# Runly + +## What it is + +- **npm package**: `@hamdymohamedak/runly` (scoped; unscoped name `runly` is blocked on npm as too similar to `runjs`). +- **CLI binary name**: `runly` — use **`npx runly init`** once to create config + **`npm run runly`**, or **`npx runly`** for one-off runs. +- **Purpose**: For each entry in `versions`, resolve a real `node` binary for that spec, prepend its directory to `PATH`, then spawn the configured command so that command’s default `node` is that matrix version—without requiring nvm/fnm/asdf on the machine. + +## Requirements + +- Node **≥ 18** for the Runly CLI process. +- **npm** with **npx** (first-time version resolution may hit the network). + +## Install + +```bash +npm install -D @hamdymohamedak/runly +``` + +One-off without saving to `package.json`: + +```bash +npx @hamdymohamedak/runly +npx @hamdymohamedak/runly init +``` + +## `runly init` (scaffold) + +From the **project root** (after **`npm install -D @hamdymohamedak/runly`**): + +```bash +npx runly init +``` + +- Creates **`runly.config.js`** only if none of **`runly.config.mjs`**, **`runly.config.js`**, or **`runly.config.cjs`** exists (otherwise prints a message and exits **0**). +- Uses **`export default`** when **`package.json`** has **`"type": "module"`**, else **`module.exports`**. +- Adds **`"runly": "runly"`** under **`scripts`** in **`package.json`** when the file exists and **`scripts.runly`** is not already set. +- Default **`run`** in the scaffold is a small **`node -e`** smoke command; edit to **`node --test`**, **`npm test`**, etc. + +Programmatic: **`initRunlyProject(cwd)`** from **`@hamdymohamedak/runly`**. + +## Config file discovery + +From **current working directory**, first file that exists: + +1. `runly.config.mjs` +2. `runly.config.js` +3. `runly.config.cjs` + +Override: `runly -c /path/to/config.mjs` or `runly --config /path/to/runly.config.mjs`. + +Config must be **JavaScript** (ESM or CJS). Runly does **not** load `.ts` configs unless the user wires a loader themselves. Export **`default`** as the config object (or the module’s default export after dynamic `import()`). + +## Config shape (`RunlyConfig`) + +| Field | Type | Required | Notes | +|-------|------|----------|--------| +| `versions` | `string[]` | Yes | Non-empty. Each string is an npm **`node`** package spec (e.g. `"20"`, `"20.10.0"`, `"lts/*"`). Passed as `node@` when resolving. | +| `run` | `RunlyRun` | Yes | See below. | +| `cwd` | `string` | No | Child `cwd`; default Runly’s `process.cwd()`. | +| `bail` | `boolean` | No | If `true`, stop after first failing version. Default `false`. | +| `env` | `NodeJS.ProcessEnv` | No | Merged into child env; `PATH` is overridden per row so matrix `node` is first. | + +### `RunlyRun` + +- **String**: treated as one shell command → `spawn` with **`shell: true`** (single string in argv). +- **`{ argv: string[]; shell?: boolean }`**: `spawn(argv[0], argv.slice(1), { shell, stdio: 'inherit' })`. Default **`shell: false`** for array form. + +**Guidance**: For predictable use of the matrix Node, prefer **`argv`** whose first token is **`node`** (e.g. `["node", "--test", "test/"]`). String form like `"npm test"` depends on how npm/scripts resolve `node`. + +## Minimal examples + +```javascript +// runly.config.mjs +export default { + versions: ["18", "20", "22"], + bail: false, + run: { + argv: ["node", "--test", "test/"], + shell: false, + }, +}; +``` + +```javascript +export default { + versions: ["20", "22"], + run: "npm test", +}; +``` + +## TypeScript / IDE helper + +From the same package (public export `"."` only): + +```javascript +import { defineConfig } from "@hamdymohamedak/runly"; + +export default defineConfig({ + versions: ["18", "20", "22"], + run: { argv: ["node", "--test", "test/"], shell: false }, +}); +``` + +Exported types: **`RunlyConfig`**, **`RunlyRun`**. **`loadConfig(cwd?)`** loads the default config or throws with a hint to run **`npx runly init`**. The matrix runner **`runMatrix`** is not exported—use the CLI or compose from source. + +## CLI + +| Command / flag | Meaning | +|----------------|---------| +| `runly init` | Scaffold **`runly.config.js`** and **`scripts.runly`** (see above). | +| `runly` | Run matrix using config in cwd. | +| `-c`, `--config` | Path to config file. | +| `runly help` | Usage. | + +## Exit codes + +- **0**: every version’s child exited with code `0`. +- **1**: any child non-zero, resolution failure, or config error. stderr includes `Failed: ` when some matrix rows failed. + +## Internals (for debugging) + +1. Resolve binary: `npx` with `--yes --package node@ -- node -e "process.stdout.write(process.execPath)"` (on Windows: **`npx.cmd`**). +2. `PATH`: prepend `dirname(process.execPath)` for that Node for the child only. +3. Output: banner `━━━ Node ━━━` per row; child **stdio inherited**. + +## CI + +Install deps, `cd` to repo root, run **`npx runly init`** once if the repo has no config, then **`npm run runly`** or **`npx runly`**. No global version manager required if `npx` can fetch the `node` package. + +## Limitations + +- Network may be needed for `npx` / registry on cold caches. +- Windows: `npx.cmd` for resolution; usual Windows spawn/shell rules apply. +- Package **`bin`** in `package.json` must use paths like `dist/cli.js` (no `./` prefix)—npm may strip invalid `bin` entries on publish. + +## Reference in this repo + +- User-facing docs: [README.md](../README.md) +- Example config: [examples/all-versions-pass/runly.config.mjs](../examples/all-versions-pass/runly.config.mjs) + +## Links + +- npm: `https://www.npmjs.com/package/@hamdymohamedak/runly` +- Source: `https://github.com/hamdymohamedak/Runly` diff --git a/dist/cli.js b/dist/cli.js index 74a9771..d760fa1 100755 --- a/dist/cli.js +++ b/dist/cli.js @@ -1,19 +1,9 @@ #!/usr/bin/env node -import { existsSync } from "node:fs"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; +import { resolveRunlyConfigPathOrThrow } from "./config-paths.js"; +import { initRunlyProject } from "./init.js"; import { runMatrix } from "./run-matrix.js"; -const CANDIDATES = ["runly.config.mjs", "runly.config.js", "runly.config.cjs"]; -function resolveConfigPath(explicit) { - if (explicit) - return resolve(explicit); - for (const name of CANDIDATES) { - const p = resolve(process.cwd(), name); - if (existsSync(p)) - return p; - } - throw new Error(`No config found. Create ${CANDIDATES.join(" or ")} or pass -c /path/to/runly.config.mjs`); -} async function loadConfig(configPath) { const abs = resolve(configPath); const mod = (await import(pathToFileURL(abs).href)); @@ -26,14 +16,41 @@ async function loadConfig(configPath) { } return c; } +function printHelp() { + console.error(`Runly — run one command per Node version (matrix) + +Usage: + runly Load config from cwd and run the matrix + runly init Create runly.config.js and add npm script "runly" + runly -c Use a specific config file + +Examples: + npx runly init + npm run runly + +Docs: https://github.com/hamdymohamedak/Runly`); +} async function main() { const args = process.argv.slice(2); + if (args[0] === "help" || args[0] === "--help" || args[0] === "-h") { + printHelp(); + process.exit(0); + } + if (args[0] === "init") { + const r = initRunlyProject(process.cwd()); + if (r === "exists") { + console.error("A Runly config already exists (runly.config.mjs, .js, or .cjs). Nothing to do."); + process.exit(0); + } + console.error("Created runly.config.js with default matrix. Added npm script \"runly\" if package.json is present."); + process.exit(0); + } const configFlag = args.findIndex((a) => a === "-c" || a === "--config"); let explicit; if (configFlag >= 0 && args[configFlag + 1]) { explicit = args[configFlag + 1]; } - const configPath = resolveConfigPath(explicit); + const configPath = resolveRunlyConfigPathOrThrow(process.cwd(), explicit); const config = await loadConfig(configPath); const results = await runMatrix(config); const failed = results.filter((r) => !r.ok); diff --git a/dist/cli.js.map b/dist/cli.js.map index a149cfd..5e0e839 100644 --- a/dist/cli.js.map +++ b/dist/cli.js.map @@ -1 +1 @@ -{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/config-paths.d.ts b/dist/config-paths.d.ts new file mode 100644 index 0000000..c3e5fa2 --- /dev/null +++ b/dist/config-paths.d.ts @@ -0,0 +1,6 @@ +export declare const RUNLY_CONFIG_CANDIDATES: readonly ["runly.config.mjs", "runly.config.js", "runly.config.cjs"]; +export declare function hasAnyRunlyConfig(cwd: string): boolean; +/** Resolved path to an existing config, or `null`. */ +export declare function findRunlyConfigPath(cwd: string): string | null; +export declare function resolveRunlyConfigPathOrThrow(cwd: string, explicit?: string): string; +//# sourceMappingURL=config-paths.d.ts.map \ No newline at end of file diff --git a/dist/config-paths.d.ts.map b/dist/config-paths.d.ts.map new file mode 100644 index 0000000..1d0a558 --- /dev/null +++ b/dist/config-paths.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"config-paths.d.ts","sourceRoot":"","sources":["../src/config-paths.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,uBAAuB,sEAAuE,CAAC;AAE5G,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAKtD;AAED,sDAAsD;AACtD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM9D;AAED,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAOpF"} \ No newline at end of file diff --git a/dist/config-paths.js b/dist/config-paths.js new file mode 100644 index 0000000..7433725 --- /dev/null +++ b/dist/config-paths.js @@ -0,0 +1,28 @@ +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +export const RUNLY_CONFIG_CANDIDATES = ["runly.config.mjs", "runly.config.js", "runly.config.cjs"]; +export function hasAnyRunlyConfig(cwd) { + for (const name of RUNLY_CONFIG_CANDIDATES) { + if (existsSync(resolve(cwd, name))) + return true; + } + return false; +} +/** Resolved path to an existing config, or `null`. */ +export function findRunlyConfigPath(cwd) { + for (const name of RUNLY_CONFIG_CANDIDATES) { + const p = resolve(cwd, name); + if (existsSync(p)) + return p; + } + return null; +} +export function resolveRunlyConfigPathOrThrow(cwd, explicit) { + if (explicit) + return resolve(explicit); + const found = findRunlyConfigPath(cwd); + if (found) + return found; + throw new Error(`No Runly config found. Run: npx runly init (or create ${RUNLY_CONFIG_CANDIDATES.join(", ")})`); +} +//# sourceMappingURL=config-paths.js.map \ No newline at end of file diff --git a/dist/config-paths.js.map b/dist/config-paths.js.map new file mode 100644 index 0000000..91becae --- /dev/null +++ b/dist/config-paths.js.map @@ -0,0 +1 @@ +{"version":3,"file":"config-paths.js","sourceRoot":"","sources":["../src/config-paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,kBAAkB,EAAE,iBAAiB,EAAE,kBAAkB,CAAU,CAAC;AAE5G,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,KAAK,MAAM,IAAI,IAAI,uBAAuB,EAAE,CAAC;QAC3C,IAAI,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;IAClD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,KAAK,MAAM,IAAI,IAAI,uBAAuB,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC7B,IAAI,UAAU,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,GAAW,EAAE,QAAiB;IAC1E,IAAI,QAAQ;QAAE,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IACxB,MAAM,IAAI,KAAK,CACb,yDAAyD,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC/F,CAAC;AACJ,CAAC"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index 146c32b..3b092eb 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,4 +1,12 @@ +import { findRunlyConfigPath, hasAnyRunlyConfig, RUNLY_CONFIG_CANDIDATES } from "./config-paths.js"; +import { initRunlyProject } from "./init.js"; import type { RunlyConfig } from "./types.js"; export type { RunlyConfig, RunlyRun } from "./types.js"; +export { RUNLY_CONFIG_CANDIDATES, findRunlyConfigPath, hasAnyRunlyConfig, initRunlyProject }; export declare function defineConfig(config: RunlyConfig): RunlyConfig; +/** + * Load the first existing Runly config in `cwd` (same discovery order as the CLI). + * @throws If no config file exists — run `npx runly init` or create a config manually. + */ +export declare function loadConfig(cwd?: string): Promise; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map index 387ea2e..2d961d8 100644 --- a/dist/index.d.ts.map +++ b/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAExD,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAE7D"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACpG,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;AAE7F,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAE7D;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CAclF"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index e48e8c0..6407b3b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,4 +1,27 @@ +import { pathToFileURL } from "node:url"; +import { findRunlyConfigPath, hasAnyRunlyConfig, RUNLY_CONFIG_CANDIDATES } from "./config-paths.js"; +import { initRunlyProject } from "./init.js"; +export { RUNLY_CONFIG_CANDIDATES, findRunlyConfigPath, hasAnyRunlyConfig, initRunlyProject }; export function defineConfig(config) { return config; } +/** + * Load the first existing Runly config in `cwd` (same discovery order as the CLI). + * @throws If no config file exists — run `npx runly init` or create a config manually. + */ +export async function loadConfig(cwd = process.cwd()) { + const path = findRunlyConfigPath(cwd); + if (!path) { + throw new Error(`No Runly config found in ${cwd}. Run: npx runly init`); + } + const mod = (await import(pathToFileURL(path).href)); + const c = mod.default ?? mod; + if (!c?.versions?.length) { + throw new Error("Config must export `versions` (non-empty array)"); + } + if (!c.run) { + throw new Error("Config must export `run` (command to execute per version)"); + } + return c; +} //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map index f76654f..c9f2aa9 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC9C,OAAO,MAAM,CAAC;AAChB,CAAC"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACpG,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAK7C,OAAO,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;AAE7F,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC9C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAC1D,MAAM,IAAI,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,4BAA4B,GAAG,uBAAuB,CAAC,CAAC;IAC1E,CAAC;IACD,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAA8B,CAAC;IAClF,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,IAAK,GAA8B,CAAC;IACzD,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"} \ No newline at end of file diff --git a/dist/init.d.ts b/dist/init.d.ts new file mode 100644 index 0000000..c9ab9ca --- /dev/null +++ b/dist/init.d.ts @@ -0,0 +1,6 @@ +/** + * Create `runly.config.js` with defaults and add `"runly": "runly"` to package.json when possible. + * @returns `"exists"` if any runly config file is already present, otherwise `"created"`. + */ +export declare function initRunlyProject(cwd: string): "exists" | "created"; +//# sourceMappingURL=init.d.ts.map \ No newline at end of file diff --git a/dist/init.d.ts.map b/dist/init.d.ts.map new file mode 100644 index 0000000..262f427 --- /dev/null +++ b/dist/init.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAgDA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAalE"} \ No newline at end of file diff --git a/dist/init.js b/dist/init.js new file mode 100644 index 0000000..d58d3cb --- /dev/null +++ b/dist/init.js @@ -0,0 +1,65 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { hasAnyRunlyConfig } from "./config-paths.js"; +const PKG = "@hamdymohamedak/runly"; +const DEFAULT_FILE = "runly.config.js"; +function readPkgJson(cwd) { + const p = join(cwd, "package.json"); + if (!existsSync(p)) + return null; + try { + return JSON.parse(readFileSync(p, "utf8")); + } + catch { + return null; + } +} +function defaultConfigBody() { + return `{ + versions: ["18", "20", "22"], + bail: false, + run: { + argv: ["node", "-e", "console.log('runly ok', process.version)"], + shell: false, + }, +}`; +} +function tryAddRunlyScriptToDisk(cwd) { + const pkgPath = join(cwd, "package.json"); + if (!existsSync(pkgPath)) + return; + let pkg; + try { + pkg = JSON.parse(readFileSync(pkgPath, "utf8")); + } + catch { + return; + } + const prev = pkg.scripts; + const scripts = prev != null && typeof prev === "object" && !Array.isArray(prev) + ? { ...prev } + : {}; + if (Object.hasOwn(scripts, "runly")) + return; + scripts.runly = "runly"; + pkg.scripts = scripts; + writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); +} +/** + * Create `runly.config.js` with defaults and add `"runly": "runly"` to package.json when possible. + * @returns `"exists"` if any runly config file is already present, otherwise `"created"`. + */ +export function initRunlyProject(cwd) { + if (hasAnyRunlyConfig(cwd)) + return "exists"; + const pkg = readPkgJson(cwd); + const isEsm = pkg?.type === "module"; + const body = defaultConfigBody(); + const content = isEsm + ? `/** @type {import("${PKG}").RunlyConfig} */\nexport default ${body};\n` + : `/** @type {import("${PKG}").RunlyConfig} */\nmodule.exports = ${body};\n`; + writeFileSync(join(cwd, DEFAULT_FILE), content, "utf8"); + tryAddRunlyScriptToDisk(cwd); + return "created"; +} +//# sourceMappingURL=init.js.map \ No newline at end of file diff --git a/dist/init.js.map b/dist/init.js.map new file mode 100644 index 0000000..22e4e4c --- /dev/null +++ b/dist/init.js.map @@ -0,0 +1 @@ +{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,GAAG,GAAG,uBAAuB,CAAC;AACpC,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAEvC,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IACpC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAA4B,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB;IACxB,OAAO;;;;;;;EAOP,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB,CAAC,GAAW;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO;IACjC,IAAI,GAA4B,CAAC;IACjC,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC;IACzB,MAAM,OAAO,GACX,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAC9D,CAAC,CAAC,EAAE,GAAI,IAA+B,EAAE;QACzC,CAAC,CAAC,EAAE,CAAC;IACT,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;QAAE,OAAO;IAC5C,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC;IACxB,GAAG,CAAC,OAAO,GAAG,OAAO,CAAC;IACtB,aAAa,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,iBAAiB,CAAC,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IAE5C,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,GAAG,EAAE,IAAI,KAAK,QAAQ,CAAC;IACrC,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,KAAK;QACnB,CAAC,CAAC,sBAAsB,GAAG,sCAAsC,IAAI,KAAK;QAC1E,CAAC,CAAC,sBAAsB,GAAG,wCAAwC,IAAI,KAAK,CAAC;IAE/E,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IACxD,uBAAuB,CAAC,GAAG,CAAC,CAAC;IAC7B,OAAO,SAAS,CAAC;AACnB,CAAC"} \ No newline at end of file diff --git a/package.json b/package.json index c1bb47d..9866f03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hamdymohamedak/runly", - "version": "0.1.1", + "version": "0.2.0", "publishConfig": { "access": "public" }, @@ -19,13 +19,12 @@ } }, "files": [ - "dist", - "scripts/postinstall.mjs" + "dist" ], "scripts": { - "postinstall": "node ./scripts/postinstall.mjs", "test": "node --test test/smoke.test.js", "ci:runly-smoke": "node dist/cli.js -c examples/all-versions-pass/runly.config.mjs", + "ci:verify-init": "node scripts/ci-verify-init.mjs", "build": "tsc", "dev": "tsc --watch", "prepublishOnly": "npm run build", diff --git a/scripts/ci-verify-init.mjs b/scripts/ci-verify-init.mjs new file mode 100644 index 0000000..5dced1e --- /dev/null +++ b/scripts/ci-verify-init.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node +/** + * CI: pack this repo, install tarball into fresh consumers, run `runly init`, assert + * runly.config.js + scripts.runly, then `npm run runly`. + */ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const PKG = "@hamdymohamedak/runly"; +const repoRoot = fileURLToPath(new URL("..", import.meta.url)); +process.chdir(repoRoot); + +function run(cmd, args, opts = {}) { + const r = spawnSync(cmd, args, { stdio: "inherit", shell: process.platform === "win32", ...opts }); + if (r.error) throw r.error; + if (r.status !== 0) process.exit(r.status ?? 1); +} + +const version = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version; +const packDir = mkdtempSync(join(tmpdir(), "runly-pack-")); +run("npm", ["pack", "--pack-destination", packDir, "--ignore-scripts"]); +const tgz = join(packDir, `hamdymohamedak-runly-${version}.tgz`); +if (!existsSync(tgz)) { + console.error("ci-verify-init: tarball missing:", tgz); + process.exit(1); +} + +function verifyConsumer(label, pkgJson, assertConfig) { + const consumer = mkdtempSync(join(tmpdir(), `runly-consumer-${label}-`)); + const body = { + ...pkgJson, + dependencies: { + ...(pkgJson.dependencies && typeof pkgJson.dependencies === "object" ? pkgJson.dependencies : {}), + [PKG]: pathToFileURL(tgz).href, + }, + }; + writeFileSync(join(consumer, "package.json"), `${JSON.stringify(body, null, 2)}\n`); + run("npm", ["install", "--no-fund", "--no-audit"], { cwd: consumer }); + + run("npx", ["runly", "init"], { cwd: consumer }); + + const cfg = join(consumer, "runly.config.js"); + if (!existsSync(cfg)) { + console.error(`[${label}] missing runly.config.js after runly init`); + process.exit(1); + } + assertConfig(readFileSync(cfg, "utf8")); + + const installed = JSON.parse(readFileSync(join(consumer, "package.json"), "utf8")); + if (installed.scripts?.runly !== "runly") { + console.error(`[${label}] expected scripts.runly === "runly", got:`, installed.scripts); + process.exit(1); + } + + run("npm", ["run", "runly"], { cwd: consumer }); + console.error(`[${label}] runly init + matrix: OK`); +} + +verifyConsumer( + "cjs", + { + name: "init-ci-cjs", + version: "1.0.0", + private: true, + type: "commonjs", + }, + (text) => { + if (!text.includes("module.exports")) { + console.error("[cjs] expected module.exports in runly.config.js"); + process.exit(1); + } + }, +); + +verifyConsumer( + "esm", + { + name: "init-ci-esm", + version: "1.0.0", + private: true, + type: "module", + }, + (text) => { + if (!text.includes("export default")) { + console.error("[esm] expected export default in runly.config.js"); + process.exit(1); + } + }, +); + +console.error("ci-verify-init: all checks passed"); diff --git a/scripts/postinstall.mjs b/scripts/postinstall.mjs deleted file mode 100644 index fc7d273..0000000 --- a/scripts/postinstall.mjs +++ /dev/null @@ -1,122 +0,0 @@ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { dirname, join, sep } from "node:path"; -import { fileURLToPath } from "node:url"; - -const PKG = "@hamdymohamedak/runly"; -const CONFIG_CANDIDATES = ["runly.config.mjs", "runly.config.js", "runly.config.cjs"]; - -function isInsideNodeModules(dir) { - return dir.split(sep).includes("node_modules"); -} - -function installContextRoot() { - const init = process.env.INIT_CWD; - if (init) return init; - const cwd = process.cwd(); - if (isInsideNodeModules(cwd)) return null; - return cwd; -} - -function readPkg(dir) { - const p = join(dir, "package.json"); - if (!existsSync(p)) return null; - try { - return JSON.parse(readFileSync(p, "utf8")); - } catch { - return null; - } -} - -function declaresRunly(pkg) { - if (!pkg || typeof pkg !== "object") return false; - const blocks = [ - pkg.dependencies, - pkg.devDependencies, - pkg.optionalDependencies, - pkg.peerDependencies, - ]; - for (const b of blocks) { - if (b && typeof b === "object" && PKG in b) return true; - } - return false; -} - -/** First directory from `start` upward whose package.json lists this package. */ -function findConsumerRoot(start) { - let dir = start; - for (;;) { - const pkg = readPkg(dir); - if (pkg && declaresRunly(pkg)) return { dir, pkg }; - const parent = dirname(dir); - if (parent === dir) break; - dir = parent; - } - return null; -} - -function hasRunlyConfig(projectRoot) { - return CONFIG_CANDIDATES.some((name) => existsSync(join(projectRoot, name))); -} - -function defaultConfigBody() { - // One-liner so first `npm run runly` succeeds without a test/ tree; users can switch to `node --test` etc. - return `{ - versions: ["18", "20", "22"], - bail: false, - run: { - argv: ["node", "-e", "console.log('runly ok', process.version)"], - shell: false, - }, -}`; -} - -function writePackageJson(projectRoot, pkg) { - const pkgPath = join(projectRoot, "package.json"); - writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8"); -} - -/** Ensures `scripts.runly` so `npm run runly` works. Returns true if package.json was written. */ -function ensureRunlyNpmScript(pkg) { - const prev = pkg.scripts; - const scripts = - prev != null && typeof prev === "object" && !Array.isArray(prev) - ? { ...prev } - : {}; - if (Object.hasOwn(scripts, "runly")) return false; - scripts.runly = "runly"; - pkg.scripts = scripts; - return true; -} - -function main() { - if (process.env.RUNLY_SKIP_POSTINSTALL === "1" || process.env.RUNLY_SKIP_POSTINSTALL === "true") { - return; - } - if (process.env.npm_config_global === "true") { - return; - } - - const start = installContextRoot(); - if (!start) return; - - const found = findConsumerRoot(start); - if (!found) return; - - const { dir: projectRoot, pkg } = found; - - if (!hasRunlyConfig(projectRoot)) { - const isEsm = pkg.type === "module"; - const body = defaultConfigBody(); - const content = isEsm - ? `/** @type {import("${PKG}").RunlyConfig} */\nexport default ${body};\n` - : `/** @type {import("${PKG}").RunlyConfig} */\nmodule.exports = ${body};\n`; - - writeFileSync(join(projectRoot, "runly.config.js"), content, "utf8"); - } - - if (ensureRunlyNpmScript(pkg)) { - writePackageJson(projectRoot, pkg); - } -} - -main(); diff --git a/src/cli.ts b/src/cli.ts index 3dbd1ff..52ed743 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,23 +1,11 @@ #!/usr/bin/env node -import { existsSync } from "node:fs"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; +import { resolveRunlyConfigPathOrThrow } from "./config-paths.js"; +import { initRunlyProject } from "./init.js"; import { runMatrix } from "./run-matrix.js"; import type { RunlyConfig } from "./types.js"; -const CANDIDATES = ["runly.config.mjs", "runly.config.js", "runly.config.cjs"]; - -function resolveConfigPath(explicit?: string): string { - if (explicit) return resolve(explicit); - for (const name of CANDIDATES) { - const p = resolve(process.cwd(), name); - if (existsSync(p)) return p; - } - throw new Error( - `No config found. Create ${CANDIDATES.join(" or ")} or pass -c /path/to/runly.config.mjs`, - ); -} - async function loadConfig(configPath: string): Promise { const abs = resolve(configPath); const mod = (await import(pathToFileURL(abs).href)) as { default?: RunlyConfig }; @@ -31,15 +19,48 @@ async function loadConfig(configPath: string): Promise { return c; } +function printHelp(): void { + console.error(`Runly — run one command per Node version (matrix) + +Usage: + runly Load config from cwd and run the matrix + runly init Create runly.config.js and add npm script "runly" + runly -c Use a specific config file + +Examples: + npx runly init + npm run runly + +Docs: https://github.com/hamdymohamedak/Runly`); +} + async function main() { const args = process.argv.slice(2); + + if (args[0] === "help" || args[0] === "--help" || args[0] === "-h") { + printHelp(); + process.exit(0); + } + + if (args[0] === "init") { + const r = initRunlyProject(process.cwd()); + if (r === "exists") { + console.error("A Runly config already exists (runly.config.mjs, .js, or .cjs). Nothing to do."); + process.exit(0); + } + console.error( + "Created runly.config.js with default matrix. Added npm script \"runly\" if package.json is present.", + ); + process.exit(0); + } + const configFlag = args.findIndex((a) => a === "-c" || a === "--config"); let explicit: string | undefined; if (configFlag >= 0 && args[configFlag + 1]) { explicit = args[configFlag + 1]; } - const configPath = resolveConfigPath(explicit); + const configPath = resolveRunlyConfigPathOrThrow(process.cwd(), explicit); const config = await loadConfig(configPath); const results = await runMatrix(config); diff --git a/src/config-paths.ts b/src/config-paths.ts new file mode 100644 index 0000000..8d9debd --- /dev/null +++ b/src/config-paths.ts @@ -0,0 +1,29 @@ +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; + +export const RUNLY_CONFIG_CANDIDATES = ["runly.config.mjs", "runly.config.js", "runly.config.cjs"] as const; + +export function hasAnyRunlyConfig(cwd: string): boolean { + for (const name of RUNLY_CONFIG_CANDIDATES) { + if (existsSync(resolve(cwd, name))) return true; + } + return false; +} + +/** Resolved path to an existing config, or `null`. */ +export function findRunlyConfigPath(cwd: string): string | null { + for (const name of RUNLY_CONFIG_CANDIDATES) { + const p = resolve(cwd, name); + if (existsSync(p)) return p; + } + return null; +} + +export function resolveRunlyConfigPathOrThrow(cwd: string, explicit?: string): string { + if (explicit) return resolve(explicit); + const found = findRunlyConfigPath(cwd); + if (found) return found; + throw new Error( + `No Runly config found. Run: npx runly init (or create ${RUNLY_CONFIG_CANDIDATES.join(", ")})`, + ); +} diff --git a/src/index.ts b/src/index.ts index cf064f8..4a22888 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,32 @@ +import { pathToFileURL } from "node:url"; +import { findRunlyConfigPath, hasAnyRunlyConfig, RUNLY_CONFIG_CANDIDATES } from "./config-paths.js"; +import { initRunlyProject } from "./init.js"; import type { RunlyConfig } from "./types.js"; export type { RunlyConfig, RunlyRun } from "./types.js"; +export { RUNLY_CONFIG_CANDIDATES, findRunlyConfigPath, hasAnyRunlyConfig, initRunlyProject }; + export function defineConfig(config: RunlyConfig): RunlyConfig { return config; } + +/** + * Load the first existing Runly config in `cwd` (same discovery order as the CLI). + * @throws If no config file exists — run `npx runly init` or create a config manually. + */ +export async function loadConfig(cwd: string = process.cwd()): Promise { + const path = findRunlyConfigPath(cwd); + if (!path) { + throw new Error(`No Runly config found in ${cwd}. Run: npx runly init`); + } + const mod = (await import(pathToFileURL(path).href)) as { default?: RunlyConfig }; + const c = mod.default ?? (mod as unknown as RunlyConfig); + if (!c?.versions?.length) { + throw new Error("Config must export `versions` (non-empty array)"); + } + if (!c.run) { + throw new Error("Config must export `run` (command to execute per version)"); + } + return c; +} diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 0000000..9401a9b --- /dev/null +++ b/src/init.ts @@ -0,0 +1,66 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { hasAnyRunlyConfig } from "./config-paths.js"; + +const PKG = "@hamdymohamedak/runly"; +const DEFAULT_FILE = "runly.config.js"; + +function readPkgJson(cwd: string) { + const p = join(cwd, "package.json"); + if (!existsSync(p)) return null; + try { + return JSON.parse(readFileSync(p, "utf8")) as Record; + } catch { + return null; + } +} + +function defaultConfigBody(): string { + return `{ + versions: ["18", "20", "22"], + bail: false, + run: { + argv: ["node", "-e", "console.log('runly ok', process.version)"], + shell: false, + }, +}`; +} + +function tryAddRunlyScriptToDisk(cwd: string): void { + const pkgPath = join(cwd, "package.json"); + if (!existsSync(pkgPath)) return; + let pkg: Record; + try { + pkg = JSON.parse(readFileSync(pkgPath, "utf8")); + } catch { + return; + } + const prev = pkg.scripts; + const scripts = + prev != null && typeof prev === "object" && !Array.isArray(prev) + ? { ...(prev as Record) } + : {}; + if (Object.hasOwn(scripts, "runly")) return; + scripts.runly = "runly"; + pkg.scripts = scripts; + writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); +} + +/** + * Create `runly.config.js` with defaults and add `"runly": "runly"` to package.json when possible. + * @returns `"exists"` if any runly config file is already present, otherwise `"created"`. + */ +export function initRunlyProject(cwd: string): "exists" | "created" { + if (hasAnyRunlyConfig(cwd)) return "exists"; + + const pkg = readPkgJson(cwd); + const isEsm = pkg?.type === "module"; + const body = defaultConfigBody(); + const content = isEsm + ? `/** @type {import("${PKG}").RunlyConfig} */\nexport default ${body};\n` + : `/** @type {import("${PKG}").RunlyConfig} */\nmodule.exports = ${body};\n`; + + writeFileSync(join(cwd, DEFAULT_FILE), content, "utf8"); + tryAddRunlyScriptToDisk(cwd); + return "created"; +} From fc829e9577d7b3eaa82d34a6097cc32f83073b11 Mon Sep 17 00:00:00 2001 From: hamdymohamedak Date: Sun, 26 Apr 2026 12:33:43 +0300 Subject: [PATCH 3/4] add the Skill.md file at the init command --- .cursor/skills/runly/SKILL.md | 3 ++- .cursor/skills/runly/skill/SKILL.md | 3 ++- README.md | 6 +++--- dist/cli.js | 2 +- dist/init.d.ts | 3 +-- dist/init.js | 31 ++++++++++++++++++++++------- package.json | 5 +++-- scripts/ci-verify-init.mjs | 12 ++++++++++- src/cli.ts | 2 +- src/init.ts | 28 ++++++++++++++++++++++++-- {Skill => templates}/SKILL.md | 9 +++++---- 11 files changed, 79 insertions(+), 25 deletions(-) rename {Skill => templates}/SKILL.md (91%) diff --git a/.cursor/skills/runly/SKILL.md b/.cursor/skills/runly/SKILL.md index 3a272fa..ca7ed67 100644 --- a/.cursor/skills/runly/SKILL.md +++ b/.cursor/skills/runly/SKILL.md @@ -43,6 +43,7 @@ npx runly init ``` - Creates **`runly.config.js`** only if none of **`runly.config.mjs`**, **`runly.config.js`**, or **`runly.config.cjs`** exists (otherwise prints a message and exits **0**). +- Writes **`SKILL.md`** in the project root when missing (Cursor agent skill template from the package); you may move it to **`.cursor/skills/runly/SKILL.md`**. - Uses **`export default`** when **`package.json`** has **`"type": "module"`**, else **`module.exports`**. - Adds **`"runly": "runly"`** under **`scripts`** in **`package.json`** when the file exists and **`scripts.runly`** is not already set. - Default **`run`** in the scaffold is a small **`node -e`** smoke command; edit to **`node --test`**, **`npm test`**, etc. @@ -118,7 +119,7 @@ Exported types: **`RunlyConfig`**, **`RunlyRun`**. **`loadConfig(cwd?)`** loads | Command / flag | Meaning | |----------------|---------| -| `runly init` | Scaffold **`runly.config.js`** and **`scripts.runly`** (see above). | +| `runly init` | Scaffold **`runly.config.js`**, **`SKILL.md`** (if missing), and **`scripts.runly`** (see above). | | `runly` | Run matrix using config in cwd. | | `-c`, `--config` | Path to config file. | | `runly help` | Usage. | diff --git a/.cursor/skills/runly/skill/SKILL.md b/.cursor/skills/runly/skill/SKILL.md index 4e3b9d9..573f132 100644 --- a/.cursor/skills/runly/skill/SKILL.md +++ b/.cursor/skills/runly/skill/SKILL.md @@ -43,6 +43,7 @@ npx runly init ``` - Creates **`runly.config.js`** only if none of **`runly.config.mjs`**, **`runly.config.js`**, or **`runly.config.cjs`** exists (otherwise prints a message and exits **0**). +- Writes **`SKILL.md`** in the project root when missing (Cursor agent skill template from the package); you may move it to **`.cursor/skills/runly/SKILL.md`**. - Uses **`export default`** when **`package.json`** has **`"type": "module"`**, else **`module.exports`**. - Adds **`"runly": "runly"`** under **`scripts`** in **`package.json`** when the file exists and **`scripts.runly`** is not already set. - Default **`run`** in the scaffold is a small **`node -e`** smoke command; edit to **`node --test`**, **`npm test`**, etc. @@ -118,7 +119,7 @@ Exported types: **`RunlyConfig`**, **`RunlyRun`**. **`loadConfig(cwd?)`** loads | Command / flag | Meaning | |----------------|---------| -| `runly init` | Scaffold **`runly.config.js`** and **`scripts.runly`** (see above). | +| `runly init` | Scaffold **`runly.config.js`**, **`SKILL.md`** (if missing), and **`scripts.runly`** (see above). | | `runly` | Run matrix using config in cwd. | | `-c`, `--config` | Path to config file. | | `runly help` | Usage. | diff --git a/README.md b/README.md index 52544f9..df3e968 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ The executable name is **`runly`** (see `bin` in `package.json`). ### First-time setup (`init`) -From your project root, create a default `runly.config.js` and add an npm script **`runly`** when `package.json` exists: +From your project root, create a default `runly.config.js`, a **`SKILL.md`** agent-skill template (when `SKILL.md` is not already present), and add an npm script **`runly`** when `package.json` exists: ```bash npx runly init ``` -If any of `runly.config.mjs`, `runly.config.js`, or `runly.config.cjs` already exists, `init` does nothing (idempotent). +If any of `runly.config.mjs`, `runly.config.js`, or `runly.config.cjs` already exists, `init` does nothing for the config (idempotent). Existing **`SKILL.md`** is never overwritten. ### Run the matrix @@ -141,7 +141,7 @@ Exported types include `RunlyConfig` and `RunlyRun`. **`loadConfig(cwd?)`** load | Command / flag | Description | |----------------|-------------| -| `runly init` | Create `runly.config.js` with defaults and add `"runly": "runly"` to `package.json` when present. No-op if a config file already exists. | +| `runly init` | Create `runly.config.js` with defaults, copy **`SKILL.md`** when missing, and add `"runly": "runly"` to `package.json` when present. No-op for config if a config file already exists. | | `runly`, `runly -c ` | Run the matrix (search for config in cwd, or use `-c` / `--config`). | | `-c`, `--config` | Path to a config file. If omitted, Runly searches for `runly.config.mjs`, then `.js`, then `.cjs` in the current working directory. | | `runly help` | Print usage. | diff --git a/dist/cli.js b/dist/cli.js index d760fa1..bfb9f40 100755 --- a/dist/cli.js +++ b/dist/cli.js @@ -42,7 +42,7 @@ async function main() { console.error("A Runly config already exists (runly.config.mjs, .js, or .cjs). Nothing to do."); process.exit(0); } - console.error("Created runly.config.js with default matrix. Added npm script \"runly\" if package.json is present."); + console.error("Created runly.config.js with default matrix, SKILL.md (if missing), and npm script \"runly\" if package.json is present."); process.exit(0); } const configFlag = args.findIndex((a) => a === "-c" || a === "--config"); diff --git a/dist/init.d.ts b/dist/init.d.ts index c9ab9ca..7a93bb4 100644 --- a/dist/init.d.ts +++ b/dist/init.d.ts @@ -1,6 +1,5 @@ /** - * Create `runly.config.js` with defaults and add `"runly": "runly"` to package.json when possible. + * Create `runly.config.js` with defaults, copy `SKILL.md` when absent, and add `"runly": "runly"` to package.json when possible. * @returns `"exists"` if any runly config file is already present, otherwise `"created"`. */ export declare function initRunlyProject(cwd: string): "exists" | "created"; -//# sourceMappingURL=init.d.ts.map \ No newline at end of file diff --git a/dist/init.js b/dist/init.js index d58d3cb..593cdf9 100644 --- a/dist/init.js +++ b/dist/init.js @@ -1,8 +1,10 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import { hasAnyRunlyConfig } from "./config-paths.js"; const PKG = "@hamdymohamedak/runly"; const DEFAULT_FILE = "runly.config.js"; +const SKILL_FILE = "SKILL.md"; function readPkgJson(cwd) { const p = join(cwd, "package.json"); if (!existsSync(p)) @@ -23,7 +25,6 @@ function defaultConfigBody() { shell: false, }, }`; -} function tryAddRunlyScriptToDisk(cwd) { const pkgPath = join(cwd, "package.json"); if (!existsSync(pkgPath)) @@ -45,10 +46,26 @@ function tryAddRunlyScriptToDisk(cwd) { pkg.scripts = scripts; writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); } -/** - * Create `runly.config.js` with defaults and add `"runly": "runly"` to package.json when possible. - * @returns `"exists"` if any runly config file is already present, otherwise `"created"`. - */ +function bundledSkillTemplatePath() { + const here = dirname(fileURLToPath(import.meta.url)); + const packageRoot = join(here, ".."); + const p = join(packageRoot, "templates", SKILL_FILE); + return existsSync(p) ? p : null; +} +function tryWriteSkillTemplate(cwd) { + const target = join(cwd, SKILL_FILE); + if (existsSync(target)) + return; + const src = bundledSkillTemplatePath(); + if (!src) + return; + try { + writeFileSync(target, readFileSync(src, "utf8")); + } + catch { + /* ignore */ + } +} export function initRunlyProject(cwd) { if (hasAnyRunlyConfig(cwd)) return "exists"; @@ -59,7 +76,7 @@ export function initRunlyProject(cwd) { ? `/** @type {import("${PKG}").RunlyConfig} */\nexport default ${body};\n` : `/** @type {import("${PKG}").RunlyConfig} */\nmodule.exports = ${body};\n`; writeFileSync(join(cwd, DEFAULT_FILE), content, "utf8"); + tryWriteSkillTemplate(cwd); tryAddRunlyScriptToDisk(cwd); return "created"; } -//# sourceMappingURL=init.js.map \ No newline at end of file diff --git a/package.json b/package.json index 9866f03..0551959 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hamdymohamedak/runly", - "version": "0.2.0", + "version": "0.2.1", "publishConfig": { "access": "public" }, @@ -19,7 +19,8 @@ } }, "files": [ - "dist" + "dist", + "templates/SKILL.md" ], "scripts": { "test": "node --test test/smoke.test.js", diff --git a/scripts/ci-verify-init.mjs b/scripts/ci-verify-init.mjs index 5dced1e..3c661b2 100644 --- a/scripts/ci-verify-init.mjs +++ b/scripts/ci-verify-init.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /** * CI: pack this repo, install tarball into fresh consumers, run `runly init`, assert - * runly.config.js + scripts.runly, then `npm run runly`. + * runly.config.js + SKILL.md + scripts.runly, then `npm run runly`. */ import { spawnSync } from "node:child_process"; import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; @@ -49,6 +49,16 @@ function verifyConsumer(label, pkgJson, assertConfig) { } assertConfig(readFileSync(cfg, "utf8")); + const skill = join(consumer, "SKILL.md"); + if (!existsSync(skill)) { + console.error(`[${label}] missing SKILL.md after runly init`); + process.exit(1); + } + if (!readFileSync(skill, "utf8").includes("name: runly")) { + console.error(`[${label}] SKILL.md looks invalid`); + process.exit(1); + } + const installed = JSON.parse(readFileSync(join(consumer, "package.json"), "utf8")); if (installed.scripts?.runly !== "runly") { console.error(`[${label}] expected scripts.runly === "runly", got:`, installed.scripts); diff --git a/src/cli.ts b/src/cli.ts index 52ed743..5c097ac 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -49,7 +49,7 @@ async function main() { process.exit(0); } console.error( - "Created runly.config.js with default matrix. Added npm script \"runly\" if package.json is present.", + "Created runly.config.js with default matrix, SKILL.md (if missing), and npm script \"runly\" if package.json is present.", ); process.exit(0); } diff --git a/src/init.ts b/src/init.ts index 9401a9b..c056e75 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,9 +1,11 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import { hasAnyRunlyConfig } from "./config-paths.js"; const PKG = "@hamdymohamedak/runly"; const DEFAULT_FILE = "runly.config.js"; +const SKILL_FILE = "SKILL.md"; function readPkgJson(cwd: string) { const p = join(cwd, "package.json"); @@ -46,8 +48,29 @@ function tryAddRunlyScriptToDisk(cwd: string): void { writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); } +/** Path to bundled `templates/SKILL.md` inside the installed package (next to `dist/`). */ +function bundledSkillTemplatePath(): string | null { + const here = dirname(fileURLToPath(import.meta.url)); + const packageRoot = join(here, ".."); + const p = join(packageRoot, "templates", SKILL_FILE); + return existsSync(p) ? p : null; +} + +/** Copy agent skill template next to config; skip if `SKILL.md` already exists. */ +function tryWriteSkillTemplate(cwd: string): void { + const target = join(cwd, SKILL_FILE); + if (existsSync(target)) return; + const src = bundledSkillTemplatePath(); + if (!src) return; + try { + writeFileSync(target, readFileSync(src, "utf8")); + } catch { + /* ignore missing or unreadable template */ + } +} + /** - * Create `runly.config.js` with defaults and add `"runly": "runly"` to package.json when possible. + * Create `runly.config.js` with defaults, copy `SKILL.md` when absent, and add `"runly": "runly"` to package.json when possible. * @returns `"exists"` if any runly config file is already present, otherwise `"created"`. */ export function initRunlyProject(cwd: string): "exists" | "created" { @@ -61,6 +84,7 @@ export function initRunlyProject(cwd: string): "exists" | "created" { : `/** @type {import("${PKG}").RunlyConfig} */\nmodule.exports = ${body};\n`; writeFileSync(join(cwd, DEFAULT_FILE), content, "utf8"); + tryWriteSkillTemplate(cwd); tryAddRunlyScriptToDisk(cwd); return "created"; } diff --git a/Skill/SKILL.md b/templates/SKILL.md similarity index 91% rename from Skill/SKILL.md rename to templates/SKILL.md index 9d04a7e..f83dfc5 100644 --- a/Skill/SKILL.md +++ b/templates/SKILL.md @@ -43,6 +43,7 @@ npx runly init ``` - Creates **`runly.config.js`** only if none of **`runly.config.mjs`**, **`runly.config.js`**, or **`runly.config.cjs`** exists (otherwise prints a message and exits **0**). +- Writes **`SKILL.md`** in the project root (from the package template) when **`SKILL.md`** does not already exist—useful for Cursor agent skills (you can move it to **`.cursor/skills/runly/SKILL.md`** if you prefer). - Uses **`export default`** when **`package.json`** has **`"type": "module"`**, else **`module.exports`**. - Adds **`"runly": "runly"`** under **`scripts`** in **`package.json`** when the file exists and **`scripts.runly`** is not already set. - Default **`run`** in the scaffold is a small **`node -e`** smoke command; edit to **`node --test`**, **`npm test`**, etc. @@ -118,7 +119,7 @@ Exported types: **`RunlyConfig`**, **`RunlyRun`**. **`loadConfig(cwd?)`** loads | Command / flag | Meaning | |----------------|---------| -| `runly init` | Scaffold **`runly.config.js`** and **`scripts.runly`** (see above). | +| `runly init` | Scaffold **`runly.config.js`**, **`SKILL.md`**, and **`scripts.runly`** (see above). | | `runly` | Run matrix using config in cwd. | | `-c`, `--config` | Path to config file. | | `runly help` | Usage. | @@ -144,10 +145,10 @@ Install deps, `cd` to repo root, run **`npx runly init`** once if the repo has n - Windows: `npx.cmd` for resolution; usual Windows spawn/shell rules apply. - Package **`bin`** in `package.json` must use paths like `dist/cli.js` (no `./` prefix)—npm may strip invalid `bin` entries on publish. -## Reference in this repo +## Reference -- User-facing docs: [README.md](../README.md) -- Example config: [examples/all-versions-pass/runly.config.mjs](../examples/all-versions-pass/runly.config.mjs) +- README: [https://github.com/hamdymohamedak/Runly#readme](https://github.com/hamdymohamedak/Runly#readme) +- npm: [https://www.npmjs.com/package/@hamdymohamedak/runly](https://www.npmjs.com/package/@hamdymohamedak/runly) ## Links From 71788b7a333020c2ec230981d5c9def3076ae9ef Mon Sep 17 00:00:00 2001 From: hamdymohamedak Date: Sun, 26 Apr 2026 12:36:17 +0300 Subject: [PATCH 4/4] fix(ci): use file:./ tarball in verify-init for Windows npm npm install from absolute file:// URLs under RUNNER~1 temp paths fails on windows-latest (corrupted tarball / ENOENT). Copy the pack into each consumer and depend with file:./name.tgz. Include rebuilt dist output aligned with init/SKILL scaffolding. Made-with: Cursor --- dist/cli.js.map | 2 +- dist/init.d.ts | 1 + dist/init.d.ts.map | 2 +- dist/init.js | 10 +++++++++- dist/init.js.map | 2 +- scripts/ci-verify-init.mjs | 12 ++++++++---- 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/dist/cli.js.map b/dist/cli.js.map index 5e0e839..78544d7 100644 --- a/dist/cli.js.map +++ b/dist/cli.js.map @@ -1 +1 @@ -{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,KAAK,UAAU,UAAU,CAAC,UAAkB;IAC1C,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAChC,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAA8B,CAAC;IACjF,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,IAAK,GAA8B,CAAC;IACzD,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,CAAC,KAAK,CAAC;;;;;;;;;;;8CAW8B,CAAC,CAAC;AAChD,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEnC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACnE,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;QACvB,MAAM,CAAC,GAAG,gBAAgB,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK,QAAQ,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,gFAAgF,CAAC,CAAC;YAChG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,CAAC,KAAK,CACX,qGAAqG,CACtG,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,UAAU,CAAC,CAAC;IACzE,IAAI,QAA4B,CAAC;IACjC,IAAI,UAAU,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,EAAE,CAAC;QAC5C,QAAQ,GAAG,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,UAAU,GAAG,6BAA6B,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC,CAAC;IAExC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5C,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,aAAa,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;AAC/C,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/init.d.ts b/dist/init.d.ts index 7a93bb4..6f1cb82 100644 --- a/dist/init.d.ts +++ b/dist/init.d.ts @@ -3,3 +3,4 @@ * @returns `"exists"` if any runly config file is already present, otherwise `"created"`. */ export declare function initRunlyProject(cwd: string): "exists" | "created"; +//# sourceMappingURL=init.d.ts.map \ No newline at end of file diff --git a/dist/init.d.ts.map b/dist/init.d.ts.map index 262f427..e37a919 100644 --- a/dist/init.d.ts.map +++ b/dist/init.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAgDA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAalE"} \ No newline at end of file +{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAuEA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAclE"} \ No newline at end of file diff --git a/dist/init.js b/dist/init.js index 593cdf9..8d7859c 100644 --- a/dist/init.js +++ b/dist/init.js @@ -25,6 +25,7 @@ function defaultConfigBody() { shell: false, }, }`; +} function tryAddRunlyScriptToDisk(cwd) { const pkgPath = join(cwd, "package.json"); if (!existsSync(pkgPath)) @@ -46,12 +47,14 @@ function tryAddRunlyScriptToDisk(cwd) { pkg.scripts = scripts; writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); } +/** Path to bundled `templates/SKILL.md` inside the installed package (next to `dist/`). */ function bundledSkillTemplatePath() { const here = dirname(fileURLToPath(import.meta.url)); const packageRoot = join(here, ".."); const p = join(packageRoot, "templates", SKILL_FILE); return existsSync(p) ? p : null; } +/** Copy agent skill template next to config; skip if `SKILL.md` already exists. */ function tryWriteSkillTemplate(cwd) { const target = join(cwd, SKILL_FILE); if (existsSync(target)) @@ -63,9 +66,13 @@ function tryWriteSkillTemplate(cwd) { writeFileSync(target, readFileSync(src, "utf8")); } catch { - /* ignore */ + /* ignore missing or unreadable template */ } } +/** + * Create `runly.config.js` with defaults, copy `SKILL.md` when absent, and add `"runly": "runly"` to package.json when possible. + * @returns `"exists"` if any runly config file is already present, otherwise `"created"`. + */ export function initRunlyProject(cwd) { if (hasAnyRunlyConfig(cwd)) return "exists"; @@ -80,3 +87,4 @@ export function initRunlyProject(cwd) { tryAddRunlyScriptToDisk(cwd); return "created"; } +//# sourceMappingURL=init.js.map \ No newline at end of file diff --git a/dist/init.js.map b/dist/init.js.map index 22e4e4c..ab06574 100644 --- a/dist/init.js.map +++ b/dist/init.js.map @@ -1 +1 @@ -{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,GAAG,GAAG,uBAAuB,CAAC;AACpC,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAEvC,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IACpC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAA4B,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB;IACxB,OAAO;;;;;;;EAOP,CAAC;AACH,CAAC;AAED,SAAS,uBAAuB,CAAC,GAAW;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO;IACjC,IAAI,GAA4B,CAAC;IACjC,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC;IACzB,MAAM,OAAO,GACX,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAC9D,CAAC,CAAC,EAAE,GAAI,IAA+B,EAAE;QACzC,CAAC,CAAC,EAAE,CAAC;IACT,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;QAAE,OAAO;IAC5C,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC;IACxB,GAAG,CAAC,OAAO,GAAG,OAAO,CAAC;IACtB,aAAa,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,iBAAiB,CAAC,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC;IAE5C,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,MAAM,KAAK,GAAG,GAAG,EAAE,IAAI,KAAK,QAAQ,CAAC;IACrC,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,KAAK;QACnB,CAAC,CAAC,sBAAsB,GAAG,sCAAsC,IAAI,KAAK;QAC1E,CAAC,CAAC,sBAAsB,GAAG,wCAAwC,IAAI,KAAK,CAAC;IAE/E,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IACxD,uBAAuB,CAAC,GAAG,CAAC,CAAC;IAC7B,OAAO,SAAS,CAAC;AACnB,CAAC"} \ No newline at end of file +{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/scripts/ci-verify-init.mjs b/scripts/ci-verify-init.mjs index 3c661b2..5ed395a 100644 --- a/scripts/ci-verify-init.mjs +++ b/scripts/ci-verify-init.mjs @@ -4,10 +4,10 @@ * runly.config.js + SKILL.md + scripts.runly, then `npm run runly`. */ import { spawnSync } from "node:child_process"; -import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { copyFileSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { fileURLToPath } from "node:url"; const PKG = "@hamdymohamedak/runly"; const repoRoot = fileURLToPath(new URL("..", import.meta.url)); @@ -22,7 +22,8 @@ function run(cmd, args, opts = {}) { const version = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version; const packDir = mkdtempSync(join(tmpdir(), "runly-pack-")); run("npm", ["pack", "--pack-destination", packDir, "--ignore-scripts"]); -const tgz = join(packDir, `hamdymohamedak-runly-${version}.tgz`); +const tgzName = `hamdymohamedak-runly-${version}.tgz`; +const tgz = join(packDir, tgzName); if (!existsSync(tgz)) { console.error("ci-verify-init: tarball missing:", tgz); process.exit(1); @@ -30,11 +31,14 @@ if (!existsSync(tgz)) { function verifyConsumer(label, pkgJson, assertConfig) { const consumer = mkdtempSync(join(tmpdir(), `runly-consumer-${label}-`)); + // Copy tarball into the consumer tree and use file:./… so npm on Windows does not choke on + // absolute file:// URLs (8.3 paths like RUNNER~1, %7E encoding, integrity null). + copyFileSync(tgz, join(consumer, tgzName)); const body = { ...pkgJson, dependencies: { ...(pkgJson.dependencies && typeof pkgJson.dependencies === "object" ? pkgJson.dependencies : {}), - [PKG]: pathToFileURL(tgz).href, + [PKG]: `file:./${tgzName}`, }, }; writeFileSync(join(consumer, "package.json"), `${JSON.stringify(body, null, 2)}\n`);