@rness/cli 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,8 @@ Not an agent — no LLM loop.
6
6
 
7
7
  ## Install
8
8
 
9
- npm create rness <org> # start a workspace for github.com/<org>, or join its .rness
9
+ npm create rness # prompts for the organization, then the repositories for your workspace
10
+ npm create rness acme # the organization github.com/acme, no first prompt
10
11
  npm i -g @rness/cli # the command is `rness`
11
12
  npx @rness/cli --help
12
13
 
@@ -15,9 +16,12 @@ Inside a workspace, every `rness` delegates to the copy pinned in
15
16
 
16
17
  ## Commands
17
18
 
18
- rness create <org> [--dir <path>] [--repos a,b] [--pm npm|pnpm|yarn|bun] [--ssh] [--skip-install] -y
19
- rness add <repo> [--scopes apps/web,packages/ui] [--ssh] -y
20
- rness sync [--scope <name>] [--check] [--pull] -y
19
+ rness create [<org>] [--repos a,b] [--pm npm|pnpm|yarn|bun] [--ssh|--https] [--skip-install] -y
20
+ rness add <repo> [--scopes apps/web,packages/ui] [--ssh|--https] -y
21
+ rness sync [--all] [--scope <name>] [--check] [--pull] -y
22
+ rness upgrade [<version>] -y
23
+ rness login [--setup-git|--no-setup-git]
24
+ rness logout
21
25
  rness context [--scope <name>] [--json]
22
26
  rness validate
23
27
 
@@ -25,9 +29,185 @@ Inside a workspace, every `rness` delegates to the copy pinned in
25
29
  confirmation in a terminal; pass `-y`/`--yes` in scripts. `sync --check`
26
30
  writes nothing and exits 1 when a block is out of date — use it in CI.
27
31
 
32
+ ### Log in
33
+
34
+ rness login # shows a code and https://github.com/login/device
35
+ rness logout
36
+
37
+ `rness login` connects rness to your GitHub account (OAuth device flow: you
38
+ approve a code in a browser, on any machine). Logged in, `create` lists the
39
+ private repositories you can access, offers your organizations to choose
40
+ from, and says as whom it looks at one (`member acme (as you)`). The
41
+ `create` wizard offers the login itself when it would otherwise list
42
+ anonymously.
43
+
44
+ - The login is one file, `~/.config/rness/auth.json` (`$XDG_CONFIG_HOME`,
45
+ `%APPDATA%` on Windows), readable by you only. The access token lives 8
46
+ hours and is renewed on its own. `GITHUB_TOKEN`, then `GH_TOKEN`, win over
47
+ it — CI needs no login.
48
+ - rness asks for `repo` and `read:org`: GitHub has no read-only scope for
49
+ private repositories. It only lists and clones.
50
+ - A new workspace has to reach GitHub before teammates can join it. Logged
51
+ in, the `create` wizard offers to do it: it creates the private repository
52
+ `<org>/.rness` and pushes the context with `git`. Declined, refused by
53
+ GitHub, anonymous or with `--yes`, it prints the two manual steps instead
54
+ — create the empty repository on github.com, then `git remote add origin …
55
+ && git push`. No other tool is ever needed.
56
+ - An organization that restricts OAuth apps hides its private repositories
57
+ until an owner approves "Rness"; `create` says so, with the link.
58
+ - Over HTTPS, rness's own clones and pulls carry the login — through the
59
+ environment of that one git command, never in a URL or `.git/config`. For
60
+ your own `git pull` and `git push`, `login` offers to make rness git's
61
+ credential helper for github.com (`--setup-git`, `--no-setup-git`);
62
+ `logout` undoes it. It needs a global install (`npm i -g @rness/cli`): a
63
+ copy run through `npx` lives in a cache. Over SSH none of this is needed.
64
+ The helper names `node` by its absolute path: after a Node upgrade through
65
+ a version manager (mise, nvm), run `rness login --setup-git` again.
66
+ - `rness logout` forgets the login; revoke the authorization itself in
67
+ GitHub's settings (the command prints the link).
68
+
69
+ ### SSH or HTTPS
70
+
71
+ `create` and `add` test your SSH access to github.com once
72
+ (`ssh -T git@github.com`) and write `git@github.com:` URLs when GitHub accepts
73
+ your key, `https://github.com/` ones otherwise; `--ssh` and `--https` decide
74
+ without the test. In a terminal `ssh` may ask for your key's passphrase;
75
+ without one (`--yes`, CI) it never prompts, so a key that needs a passphrase
76
+ counts as no access unless an agent holds it.
77
+
78
+ `rness.json` is authoritative: a URL written there is cloned as written, never
79
+ converted. In a workspace whose catalogue clones over SSH, `create` (joining)
80
+ and `sync` stop before the first clone when the SSH test fails and say how to
81
+ set up a key; `add` offers "Clone <name> over HTTPS instead?" (or `--https`),
82
+ which writes an HTTPS URL for that repository only.
83
+
84
+ Without an agent, a protected key is asked for again by every `git clone`.
85
+ Load it once per session: `ssh-add --apple-use-keychain ~/.ssh/id_ed25519` on
86
+ macOS, `ssh-add ~/.ssh/id_ed25519` elsewhere.
87
+
88
+ ### Colour
89
+
90
+ In a terminal `create`, `login`, `add`, `sync` and `upgrade` run as one
91
+ session: an intro, a line per finished step, spinners, a "Next" box, an
92
+ outro — in the same gutter as the questions. A spinner never animates while
93
+ git might ask for a passphrase: rness first tests SSH unattended, and clones
94
+ animate only when that passed. The help and the banner are coloured. Through a pipe, in CI, or with `NO_COLOR=1`, the output is plain text —
95
+ the same bytes as before 0.5.0; `FORCE_COLOR=1` paints it anyway. No
96
+ dependency does this: `util.styleText`, and a banner kept as a constant.
97
+
98
+ ### Upgrade
99
+
100
+ rness upgrade # the latest release; `rness upgrade 0.5.0` for another
101
+ npx @rness/cli@latest upgrade # from a workspace pinned below 0.5.0
102
+
103
+ The version is written in one place, `.rness/package.json`. `upgrade` pins it
104
+ there (exact, the rest of the file untouched), installs with the workspace's
105
+ package manager, then syncs through the new copy; a failed install restores
106
+ the file. Commit `.rness`. Teammates pull, and the next `rness` command in that
107
+ workspace installs the new pin itself — frozen, so the working tree stays
108
+ clean — and carries on: nothing to remember, nothing to run.
109
+ `RNESS_NO_INSTALL=1` turns that off and restores the warning, for CI, a
110
+ container image, or a machine that cannot install. A workspace still on 0.5.0
111
+ has no such catch-up: moving it to 0.5.1 is the last manual install.
112
+
113
+ Usually nobody runs `upgrade` at all. A workspace ships
114
+ `.github/dependabot.yml`, so each release opens a pull request on the
115
+ organization's `.rness`: the pin and the lockfile in the diff, `validate`
116
+ running on the new version, review, merge. `upgrade` writes that file into
117
+ workspaces created before 0.5.1.
118
+
119
+ `upgrade` is never delegated to the pinned copy, which is what it replaces
120
+ — so a workspace pinned below 0.5.0, whose copy has no such command, starts
121
+ with the `npx` form.
122
+
123
+ An upgrade touches `.rness` only: a generated block is current when its hash
124
+ is, whatever wrote it, so no `AGENTS.md` of the organization changes unless
125
+ its content does.
126
+
28
127
  Exit codes: 0 success, 1 failure, 2 usage — or a refusal without a TTY.
29
128
  `RNESS_DEBUG=1` adds stack traces; `RNESS_NO_DELEGATE=1` skips the delegation.
30
129
 
130
+ ## 0.5.1 — the version of a workspace moves by pull request
131
+
132
+ - Joining an organization pinned to an older `@rness/cli` no longer mentions
133
+ versions, and no longer invites a newcomer to move the whole team: the
134
+ workspace is served by the version the organization agreed on.
135
+ - A workspace whose pin moved is reinstalled on the next command, with the
136
+ package manager's frozen install so the working tree stays clean. Set
137
+ `RNESS_NO_INSTALL=1` to keep the old warning instead — for CI, a container
138
+ image, or a machine that cannot install.
139
+ - New workspaces ship `.github/dependabot.yml`, and `rness upgrade` writes it
140
+ into workspaces created earlier: a pull request per release, reviewed, with
141
+ `validate` running on the new version before the merge.
142
+ - A delegated child can no longer wait on an invisible SSH passphrase prompt:
143
+ it fails with a readable line instead of hanging.
144
+ - A join that wrote its blocks succeeds even when a repository nobody picked
145
+ failed to clone; those failures are hints.
146
+
147
+ ## 0.5.0 — the organization is the workspace; rness.json is its catalogue
148
+
149
+ - `create <org>` names the organization, not a directory any more (`--org
150
+ <org>` and the prompt "What is your GitHub organization named?" do the
151
+ same). The workspace directory is `./<org>`, with the organization's exact
152
+ name; `--dir` is removed. With `npm create`, flags go after `--`:
153
+ `npm create rness acme -- --yes`.
154
+ - SSH first: `create` and `add` write `git@github.com:` URLs when
155
+ `ssh -T git@github.com` accepts your key, HTTPS ones otherwise (0.4.0
156
+ wrote HTTPS unless `--ssh`). `--https` is new. A private `.rness` you reach
157
+ over SSH is now found when joining. See "SSH or HTTPS" above.
158
+ - `create` does not offer repositories whose name starts with a dot
159
+ (`.github`, …).
160
+ - Colour, a banner and progress lines in a terminal (see "Colour" above);
161
+ nothing changes for scripts.
162
+ - `rness login` / `rness logout` are new: private repositories are listed,
163
+ and cloned over HTTPS with the login (see "Log in" above). Without a login
164
+ the note reads `rness login lists the private ones you can access`.
165
+ - `rness upgrade` is new (see "Upgrade" above). From 0.4.0:
166
+ `npx @rness/cli@latest upgrade`.
167
+ - The header of a generated block no longer names the CLI version
168
+ (`<!-- rness · scope: … -->`), and `sync` decides by the hash like
169
+ `validate`: upgrading rewrites no `AGENTS.md`. Blocks written by 0.2–0.4
170
+ stay as they are until their content changes. A 0.4.0 CLI calls the new
171
+ header stale — upgrade every copy together.
172
+ - The scaffold's CI workflow reads the version from `package.json` instead of
173
+ naming it; `upgrade` migrates the old line.
174
+ - `.rness/rness.json` is the organization's catalogue — every repository
175
+ rness knows about, shared by the team. Your workspace is the part of it
176
+ you cloned into `org/`; two teammates can clone different repositories,
177
+ and nothing but `org/` records the choice. A choice never removes a
178
+ repository from the catalogue.
179
+ - `create` lists the organization's repositories to search and pick from,
180
+ with every catalogue repository pre-selected when joining (a catalogue
181
+ repository the listing does not return is still offered, marked
182
+ `in .rness`). A picked catalogue repository is cloned; a picked new one is
183
+ added to the catalogue, as `rness add` does. Without a picker, `--repos`
184
+ is the selection; a join without `--repos` clones the whole catalogue. A
185
+ cancelled or declined prompt writes nothing and exits 0.
186
+ - The listing needs no credentials. Without a `GITHUB_TOKEN` (or `GH_TOKEN`)
187
+ only public repositories are listed — and a personal account always lists
188
+ only its public ones; add the others later with `rness add <repo>`.
189
+ - `rness sync` works on your clones: it writes the blocks of the repositories
190
+ in `org/`, never of `rness.json` as a whole. In a terminal it asks "Do you
191
+ want to sync as well?" with the catalogue repositories you have not cloned
192
+ ("Select all", or pick some); what you pick is cloned first. With `--yes`,
193
+ or `--check`, nothing is cloned and one `not cloned:` line names them
194
+ (`--check` does not fail on it). `rness sync --all` clones them all without
195
+ asking. A clone `rness.json` does not know gets no block and a
196
+ `not in rness.json:` line pointing at `rness add`.
197
+ - A workspace pinned to 0.4.x clones every catalogue repository on sync —
198
+ `create` runs the pinned copy's sync after joining.
199
+ - Joining a workspace whose `.rness` pins an older `@rness/cli` than the one
200
+ you run says so, and that `rness upgrade` moves it.
201
+ - Repository and scope names take what GitHub allows, in lowercase: letters,
202
+ digits, `.`, `_` and `-` (`my_lib`, `angular.js`). A name that differs only
203
+ by case is declared in lowercase. A 0.4.0 CLI rejects a `rness.json` that
204
+ holds a `.` or a `_` in a name — upgrade every copy together.
205
+ - Organization names follow GitHub's rule — letters, digits and single
206
+ hyphens, not at either end, at most 39 characters — in `--org` and in
207
+ `rness.json`'s `"org"`; the case is kept as typed.
208
+ - `"org"` in `rness.json` now accepts uppercase letters: a 0.4.0 CLI rejects
209
+ a workspace whose `"org"` has any, so upgrade every copy together.
210
+
31
211
  ## 0.4.0 — create, add, the shims
32
212
 
33
213
  - `npm create rness <org>` starts a new workspace for `github.com/<org>` or
@@ -57,9 +237,26 @@ Exit codes: 0 success, 1 failure, 2 usage — or a refusal without a TTY.
57
237
  pnpm --filter @rness/cli test # node:test on the TypeScript sources
58
238
  pnpm --filter @rness/cli typecheck
59
239
  pnpm --filter @rness/cli build # tsup → dist/
60
- RNESS_NO_DELEGATE=1 node packages/cli/src/bin/rness.ts --help
240
+ pnpm dev --help # the CLI, straight from these sources
241
+
242
+ `pnpm dev` runs `packages/cli/src/bin/rness.ts`: Node ≥ 24 strips the types,
243
+ so nothing is built between an edit and the next run. It turns delegation off,
244
+ because inside a workspace the launcher would otherwise hand the command to the
245
+ published `@rness/cli` pinned in `.rness/` rather than to the sources under
246
+ test; `RNESS_NO_DELEGATE=0 pnpm dev …` puts it back, to exercise delegation
247
+ itself.
248
+
249
+ To reach it from a workspace elsewhere on the machine, link it once:
250
+
251
+ ln -s "$PWD/scripts/dev.mjs" ~/.local/bin/rness-dev # from the repository root
252
+ cd ~/somewhere/acme && rness-dev validate
253
+
254
+ `create` is never delegated, so it always runs these sources — and it refuses
255
+ to run inside a workspace, so call it from an empty directory.
61
256
 
62
- Inside a workspace whose `.rness/` pins a published `@rness/cli`, set
63
- `RNESS_NO_DELEGATE=1` to run this source tree instead of delegating.
257
+ `pnpm verdaccio` is the other loop: a local registry that serves this build as
258
+ a real package, for what only shows up through an install (`npm create rness`,
259
+ the pinned copy, a version bump). It is slower, and its caches have to be
260
+ forgotten on every redeploy, which `deploy` now does.
64
261
 
65
262
  Node ≥ 24. MIT.
package/dist/bin/rness.js CHANGED
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  VERSION,
4
+ driftWarning,
5
+ ensureInstalled,
4
6
  findDelegate,
7
+ findWorkspace,
8
+ pinDrift,
9
+ pinnedBinPath,
5
10
  reportError,
6
11
  run
7
- } from "../chunk-IW5IHSPC.js";
12
+ } from "../chunk-C6HBGV5V.js";
8
13
 
9
14
  // src/bin/rness.ts
15
+ import { spawnSync } from "child_process";
10
16
  import { pathToFileURL } from "url";
11
17
 
12
18
  // src/core/node-version.ts
@@ -33,7 +39,39 @@ async function main() {
33
39
  return 1;
34
40
  }
35
41
  const argv = process.argv.slice(2);
36
- const skipDelegation = argv[0] === "create" || process.env["RNESS_NO_DELEGATE"] === "1";
42
+ const own = [
43
+ "create",
44
+ "upgrade",
45
+ "update",
46
+ "login",
47
+ "logout",
48
+ "git-credential"
49
+ ].includes(argv[0] ?? "");
50
+ const noDelegate = process.env["RNESS_NO_DELEGATE"] === "1";
51
+ const skipDelegation = own || noDelegate;
52
+ const command = argv[0];
53
+ if (command !== void 0 && !command.startsWith("-") && !skipDelegation) {
54
+ const ws = await findWorkspace(process.cwd()).catch(() => null);
55
+ const outcome = ws === null ? null : await ensureInstalled(ws.rnessDir, {
56
+ announce: (pin) => process.stderr.write(`installing @rness/cli ${pin} in .rness\u2026
57
+ `)
58
+ });
59
+ if (outcome !== null && !outcome.installed) {
60
+ const warning = await driftWarning(process.cwd());
61
+ if (warning !== null) process.stderr.write(`${warning}
62
+ `);
63
+ }
64
+ if (ws !== null && outcome?.installed === true && await pinDrift(ws.rnessDir) === null) {
65
+ const pinnedBin = await pinnedBinPath(ws.rnessDir);
66
+ if (pinnedBin !== null) {
67
+ const { status } = spawnSync(process.execPath, [pinnedBin, ...argv], {
68
+ cwd: process.cwd(),
69
+ stdio: "inherit"
70
+ });
71
+ return status ?? 1;
72
+ }
73
+ }
74
+ }
37
75
  const delegate = skipDelegation ? null : await findDelegate(process.cwd(), VERSION);
38
76
  if (delegate !== null) {
39
77
  const broken = (reason) => new Error(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bin/rness.ts","../../src/core/node-version.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { pathToFileURL } from 'node:url'\n\nimport { run } from '../cli.ts'\nimport { findDelegate } from '../core/delegate.ts'\nimport { MIN_NODE_MAJOR, isSupportedNode } from '../core/node-version.ts'\nimport { reportError } from '../report.ts'\nimport { VERSION } from '../version.ts'\n\n// `rness context | head`: once the reader has its lines it closes the pipe and\n// every further write emits EPIPE on stdout. Nothing is left to deliver, so end\n// quietly with success. This is the one process.exit() in the code base — it\n// runs only when there is no output left to drain (see main() below).\nprocess.stdout.on('error', (e: NodeJS.ErrnoException) => {\n if (e.code === 'EPIPE') process.exit(0)\n throw e\n})\n\n// Set process.exitCode rather than calling process.exit(): process.exit()\n// terminates before Node drains an async stdout pipe, truncating piped output\n// at the ~64 KiB pipe buffer (breaks `rness context --json | jq`, `$(...)`).\nasync function main(): Promise<number> {\n if (!isSupportedNode(process.versions.node)) {\n process.stderr.write(\n `rness needs Node ${MIN_NODE_MAJOR} or newer (running ${process.version})\\n`\n )\n return 1\n }\n const argv = process.argv.slice(2)\n const skipDelegation =\n argv[0] === 'create' || process.env['RNESS_NO_DELEGATE'] === '1'\n const delegate = skipDelegation\n ? null\n : await findDelegate(process.cwd(), VERSION)\n if (delegate !== null) {\n const broken = (reason: string): Error =>\n new Error(\n `@rness/cli ${delegate.version} pinned in ${delegate.root}/.rness is not installed correctly (${reason}); reinstall in .rness/`\n )\n if (process.env['RNESS_DEBUG'] === '1') {\n process.stderr.write(\n `rness: delegating to @rness/cli ${delegate.version} (${delegate.entry})\\n`\n )\n }\n let mod: Record<string, unknown>\n try {\n mod = (await import(pathToFileURL(delegate.entry).href)) as Record<\n string,\n unknown\n >\n } catch (e) {\n throw broken(e instanceof Error ? e.message : String(e))\n }\n if (typeof mod['run'] !== 'function')\n throw broken('dist/index.js does not export run')\n const delegateRun = mod['run'] as (argv: string[]) => Promise<unknown>\n const code = await delegateRun(argv)\n return typeof code === 'number' ? code : 1\n }\n return run(argv)\n}\n\nmain()\n .then((code) => {\n process.exitCode = code\n })\n .catch((e: unknown) => {\n process.exitCode = reportError(e)\n })\n","export const MIN_NODE_MAJOR = 24\n\nexport function nodeMajor(version: string): number {\n const major = Number.parseInt(version.split('.')[0] ?? '', 10)\n return Number.isNaN(major) ? 0 : major\n}\n\nexport function isSupportedNode(version: string): boolean {\n return nodeMajor(version) >= MIN_NODE_MAJOR\n}\n"],"mappings":";;;;;;;;;AACA,SAAS,qBAAqB;;;ACDvB,IAAM,iBAAiB;AAEvB,SAAS,UAAU,SAAyB;AACjD,QAAM,QAAQ,OAAO,SAAS,QAAQ,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE;AAC7D,SAAO,OAAO,MAAM,KAAK,IAAI,IAAI;AACnC;AAEO,SAAS,gBAAgB,SAA0B;AACxD,SAAO,UAAU,OAAO,KAAK;AAC/B;;;ADIA,QAAQ,OAAO,GAAG,SAAS,CAAC,MAA6B;AACvD,MAAI,EAAE,SAAS,QAAS,SAAQ,KAAK,CAAC;AACtC,QAAM;AACR,CAAC;AAKD,eAAe,OAAwB;AACrC,MAAI,CAAC,gBAAgB,QAAQ,SAAS,IAAI,GAAG;AAC3C,YAAQ,OAAO;AAAA,MACb,oBAAoB,cAAc,sBAAsB,QAAQ,OAAO;AAAA;AAAA,IACzE;AACA,WAAO;AAAA,EACT;AACA,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,QAAM,iBACJ,KAAK,CAAC,MAAM,YAAY,QAAQ,IAAI,mBAAmB,MAAM;AAC/D,QAAM,WAAW,iBACb,OACA,MAAM,aAAa,QAAQ,IAAI,GAAG,OAAO;AAC7C,MAAI,aAAa,MAAM;AACrB,UAAM,SAAS,CAAC,WACd,IAAI;AAAA,MACF,cAAc,SAAS,OAAO,cAAc,SAAS,IAAI,uCAAuC,MAAM;AAAA,IACxG;AACF,QAAI,QAAQ,IAAI,aAAa,MAAM,KAAK;AACtC,cAAQ,OAAO;AAAA,QACb,mCAAmC,SAAS,OAAO,KAAK,SAAS,KAAK;AAAA;AAAA,MACxE;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,YAAO,MAAM,OAAO,cAAc,SAAS,KAAK,EAAE;AAAA,IAIpD,SAAS,GAAG;AACV,YAAM,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,IACzD;AACA,QAAI,OAAO,IAAI,KAAK,MAAM;AACxB,YAAM,OAAO,mCAAmC;AAClD,UAAM,cAAc,IAAI,KAAK;AAC7B,UAAM,OAAO,MAAM,YAAY,IAAI;AACnC,WAAO,OAAO,SAAS,WAAW,OAAO;AAAA,EAC3C;AACA,SAAO,IAAI,IAAI;AACjB;AAEA,KAAK,EACF,KAAK,CAAC,SAAS;AACd,UAAQ,WAAW;AACrB,CAAC,EACA,MAAM,CAAC,MAAe;AACrB,UAAQ,WAAW,YAAY,CAAC;AAClC,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/bin/rness.ts","../../src/core/node-version.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { spawnSync } from 'node:child_process'\nimport { pathToFileURL } from 'node:url'\n\nimport { run } from '../cli.ts'\nimport { findDelegate, pinnedBinPath } from '../core/delegate.ts'\nimport { MIN_NODE_MAJOR, isSupportedNode } from '../core/node-version.ts'\nimport { driftWarning, ensureInstalled, pinDrift } from '../core/pinned.ts'\nimport { findWorkspace } from '../core/workspace.ts'\nimport { reportError } from '../report.ts'\nimport { VERSION } from '../version.ts'\n\n// `rness context | head`: once the reader has its lines it closes the pipe and\n// every further write emits EPIPE on stdout. Nothing is left to deliver, so end\n// quietly with success. This is the one process.exit() in the code base — it\n// runs only when there is no output left to drain (see main() below).\nprocess.stdout.on('error', (e: NodeJS.ErrnoException) => {\n if (e.code === 'EPIPE') process.exit(0)\n throw e\n})\n\n// Set process.exitCode rather than calling process.exit(): process.exit()\n// terminates before Node drains an async stdout pipe, truncating piped output\n// at the ~64 KiB pipe buffer (breaks `rness context --json | jq`, `$(...)`).\nasync function main(): Promise<number> {\n if (!isSupportedNode(process.versions.node)) {\n process.stderr.write(\n `rness needs Node ${MIN_NODE_MAJOR} or newer (running ${process.version})\\n`\n )\n return 1\n }\n const argv = process.argv.slice(2)\n // `create` makes a workspace and `upgrade` replaces the pinned copy: both\n // are served by the copy the user invoked (spec 0006 §3). The login concerns\n // the machine, not a workspace — and git runs `git-credential` from inside\n // a clone, on every fetch: no delegation, and no warning there either.\n const own = [\n 'create',\n 'upgrade',\n 'update',\n 'login',\n 'logout',\n 'git-credential',\n ].includes(argv[0] ?? '')\n const noDelegate = process.env['RNESS_NO_DELEGATE'] === '1'\n const skipDelegation = own || noDelegate\n const command = argv[0]\n if (command !== undefined && !command.startsWith('-') && !skipDelegation) {\n const ws = await findWorkspace(process.cwd()).catch(() => null)\n // The version was decided by the pull request that moved the pin, so\n // catching up asks nothing (spec 0008 §4).\n const outcome =\n ws === null\n ? null\n : await ensureInstalled(ws.rnessDir, {\n announce: (pin) =>\n process.stderr.write(`installing @rness/cli ${pin} in .rness…\\n`),\n })\n if (outcome !== null && !outcome.installed) {\n const warning = await driftWarning(process.cwd())\n if (warning !== null) process.stderr.write(`${warning}\\n`)\n }\n // The pinned tree was just rebuilt: importing out of it in this process is\n // fragile, so the freshly installed copy runs as its own process.\n // Only hand off when the install actually closed the drift: a child that\n // would install again on every run must never be spawned.\n if (\n ws !== null &&\n outcome?.installed === true &&\n (await pinDrift(ws.rnessDir)) === null\n ) {\n const pinnedBin = await pinnedBinPath(ws.rnessDir)\n if (pinnedBin !== null) {\n const { status } = spawnSync(process.execPath, [pinnedBin, ...argv], {\n cwd: process.cwd(),\n stdio: 'inherit',\n })\n return status ?? 1\n }\n }\n }\n const delegate = skipDelegation\n ? null\n : await findDelegate(process.cwd(), VERSION)\n if (delegate !== null) {\n const broken = (reason: string): Error =>\n new Error(\n `@rness/cli ${delegate.version} pinned in ${delegate.root}/.rness is not installed correctly (${reason}); reinstall in .rness/`\n )\n if (process.env['RNESS_DEBUG'] === '1') {\n process.stderr.write(\n `rness: delegating to @rness/cli ${delegate.version} (${delegate.entry})\\n`\n )\n }\n let mod: Record<string, unknown>\n try {\n mod = (await import(pathToFileURL(delegate.entry).href)) as Record<\n string,\n unknown\n >\n } catch (e) {\n throw broken(e instanceof Error ? e.message : String(e))\n }\n if (typeof mod['run'] !== 'function')\n throw broken('dist/index.js does not export run')\n const delegateRun = mod['run'] as (argv: string[]) => Promise<unknown>\n const code = await delegateRun(argv)\n return typeof code === 'number' ? code : 1\n }\n return run(argv)\n}\n\nmain()\n .then((code) => {\n process.exitCode = code\n })\n .catch((e: unknown) => {\n process.exitCode = reportError(e)\n })\n","export const MIN_NODE_MAJOR = 24\n\nexport function nodeMajor(version: string): number {\n const major = Number.parseInt(version.split('.')[0] ?? '', 10)\n return Number.isNaN(major) ? 0 : major\n}\n\nexport function isSupportedNode(version: string): boolean {\n return nodeMajor(version) >= MIN_NODE_MAJOR\n}\n"],"mappings":"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","names":[]}