diff --git a/AGENTS.md b/AGENTS.md index 2528807..83590ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,8 +15,15 @@ does not manage their location or lifecycle. ## Conventions -- Deno 2, TypeScript, strict mode. `@std` packages are the only allowed - dependencies (runtime included); nothing outside `@std`. +- Deno 2, TypeScript, strict mode. `@std` packages are the default dependency; + the sole exception is the runtime edge, `src/runtime.ts`, which uses `node:` + builtins so the engine also runs on Node through JSR's npm-compatibility layer + (see ADR-0007). It is the only module allowed to import `node:*`, and the only + one permitted to reference `npm:@types/node`. `src/` must not import `@std/fs` + either: that package is Deno-only (`exists` and `walk` call the `Deno` global, + so it works under `deno test` and throws under Node), which is why existence + checks and tree walks go through the runtime edge's `pathExists` and + `walkDirectoryTree`. - TypeScript source in `src/`, tests in `tests/`. `tests/integration_test.ts` exercises real local git repositories under a temp dir. - Machine-readable git: use `--porcelain` output formats and `GitRunner` (an diff --git a/README.md b/README.md index be103a6..0f30e8b 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,8 @@ Use the `/cli` subpath export — the runnable CLI entry — not the bare `jsr:@wazoo/workspace` package (which exposes the library and produces a no-op `wspace` when installed globally). `-A` grants the read/write/run permissions `wspace` needs; omit it if you prefer to grant specific flags (e.g. -`--allow-read --allow-write --allow-run=git`) or use Prompts. +`--allow-read --allow-write --allow-run=git,gh,robocopy,rmdir,rm`) or use +Prompts. Or build a local binary: @@ -217,6 +218,19 @@ deno task build Requires a Deno runtime (v2+). The `wspace` binary has no runtime dependencies. +### Running without a Deno install + +JSR also serves the package in Node-compatible form, which needs no Deno +installation: + +```sh +npx jsr add @wazoo/workspace +node node_modules/@wazoo/workspace/src/cli.js check --json +``` + +JSR forwards `exports` but not `bin`, so this runs the CLI rather than putting a +`wspace` command on `PATH`; add a one-line wrapper if you want one. + ## Development ```sh diff --git a/deno.json b/deno.json index 05c39a8..f6c9bac 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@wazoo/workspace", - "version": "0.7.0", + "version": "0.8.0", "license": "MIT", "exports": { ".": "./src/mod.ts", @@ -15,10 +15,11 @@ "@std/cli": "jsr:@std/cli@^1.0.0", "@std/fs": "jsr:@std/fs@^1.0.0", "@std/path": "jsr:@std/path@^1.0.0", - "@std/yaml": "jsr:@std/yaml@^1.2.0" + "@std/yaml": "jsr:@std/yaml@^1.2.0", + "@types/node": "npm:@types/node@^22" }, "tasks": { - "build": "deno compile --allow-read --allow-write --allow-run=git --output wspace src/cli.ts", + "build": "deno compile --allow-read --allow-write --allow-run=git,gh,robocopy,rmdir,rm --output wspace src/cli.ts", "ci": "deno fmt --check && deno lint && deno task lint:imports && deno check src/ tests/ && deno task test", "lint:imports": "deno run --allow-read scripts/lint_imports.ts", "test": "deno test --allow-read --allow-write --allow-run --allow-env" diff --git a/deno.lock b/deno.lock index a420d43..2958dc0 100644 --- a/deno.lock +++ b/deno.lock @@ -9,7 +9,8 @@ "jsr:@std/internal@^1.0.14": "1.0.14", "jsr:@std/path@1": "1.1.6", "jsr:@std/path@^1.1.5": "1.1.6", - "jsr:@std/yaml@^1.2.0": "1.2.0" + "jsr:@std/yaml@^1.2.0": "1.2.0", + "npm:@types/node@22": "22.20.5" }, "jsr": { "@std/assert@1.0.19": { @@ -48,13 +49,25 @@ "integrity": "20beb41e4983ba3437dbefac62b14061ab058e8a187596f19d28ff9035f6e6cf" } }, + "npm": { + "@types/node@22.20.5": { + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dependencies": [ + "undici-types" + ] + }, + "undici-types@6.21.0": { + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + } + }, "workspace": { "dependencies": [ "jsr:@std/assert@1", "jsr:@std/cli@1", "jsr:@std/fs@1", "jsr:@std/path@1", - "jsr:@std/yaml@^1.2.0" + "jsr:@std/yaml@^1.2.0", + "npm:@types/node@22" ] } } diff --git a/docs/adr/0007-runtime-neutral-engine.md b/docs/adr/0007-runtime-neutral-engine.md new file mode 100644 index 0000000..d54984f --- /dev/null +++ b/docs/adr/0007-runtime-neutral-engine.md @@ -0,0 +1,139 @@ +# ADR-0007: The engine runs on Deno and Node, through one runtime edge + +- Status: accepted +- Date: 2026-10 +- Amends: ADR-0006 (its purge strategies named `Deno.remove` directly) +- Related: issues [#160](https://github.com/wazootech/workspace-cli/issues/160), + [#159](https://github.com/wazootech/workspace-cli/issues/159), + [#135](https://github.com/wazootech/workspace-cli/issues/135), + [#29](https://github.com/wazootech/workspace-cli/issues/29) + +## Context + +`wspace` had exactly one distribution path that did not require a Deno +installation: a compiled binary built by `deno task build`. Everything else — +`deno run jsr:@wazoo/workspace/cli`, `deno install` — presumes Deno on the +machine. + +JSR also serves an **npm-compatibility** form of every package, which +`npx jsr +add @wazoo/workspace` installs without Deno present at all. Measured +against the published `0.7.0`, that form is _mechanically_ ready: + +- it ships transpiled `.js` beside the `.ts`, +- it rewrites the `@/x.ts` alias to the correct relative path per directory, and + `jsr:@std/fs` to `@jsr/std__fs`, +- Node 24 loaded the whole module graph, resolved `@jsr/std__cli/parse-args`, + and honoured `import.meta.main`. + +It then died at `ReferenceError: Deno is not defined`. JSR transpiles; it does +not shim Deno globals. That single fact — not packaging, not the alias, not the +`@std` specs — was the whole barrier between "JSR serves a Node package" and +"JSR serves a Node package that runs". + +That error had a second source the first pass missed. The CLI's own `Deno.*` +calls were the visible ones; `@std/fs` — a dependency, not our code — reaches +the `Deno` global too, in `exists` and `walk`. + +Two constraints frame the decision. JSR's npm-compat layer forwards only +`exports`, never `bin` +([#135](https://github.com/wazootech/workspace-cli/issues/135)), so a `wspace` +**command** on `PATH` still needs a wrapper; the port buys a runnable **library +and CLI entry**, not a shim. And the alternative route — compiled binaries per +platform ([#29](https://github.com/wazootech/workspace-cli/issues/29)) — ships +no cross-platform runtime code but does ship a large artifact matrix. + +## Decision + +1. **Remove every `Deno.*` global from `src/`.** `Deno.args` and `Deno.exit` in + `src/cli.ts` were the last two. + +2. **Confine all host access to one module, `src/runtime.ts`.** It imports + `node:child_process`, `node:fs/promises`, `node:os` and `node:process`, which + Deno implements natively, so a single implementation serves both runtimes. + Nothing else in `src/` may import `node:*` **or `@std/fs`** (item 8). This is + the one documented exception to the "@std only" rule; `src/runtime.ts` is + also the only module permitted to reference `npm:@types/node`, which is now + the project's sole non-`@std` dependency (type-only, declared in + `deno.json`). + +3. **`runProcess` preserves the two subprocess behaviours callers depend on.** + It resolves for _every_ exit code — including non-zero — so callers keep + classifying failure from `code`, and it rejects only when the binary cannot + be spawned at all. That rejection is load-bearing: it is how + `createGitHubRepoWithFallback` recognises "gh CLI is not available". Stdin is + detached so a subprocess cannot block on a credential prompt. + +4. **Host differences are normalised in exactly one place each, and never by + hand at a call site.** `Deno.build.os` says `"windows"` where + `process.platform` says `"win32"`, so Windows checks go through + `isWindows()`. `Deno.args` has no Node equivalent, so `cliArgs` derives + arguments from `process.argv.slice(2)`, which is correct in all three + invocation shapes — `deno run` (`[deno, script, ...]`), a compiled binary + (`[exe, exe, ...]`, Deno duplicates the path to mimic Node), and + `node