borgmcp 2.9.0 → 2.10.2
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/dist/assimilate-cmd.d.ts +13 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +177 -11
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +26 -1
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/assimilate-welcome.d.ts.map +1 -1
- package/dist/assimilate-welcome.js +1 -0
- package/dist/assimilate-welcome.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +27 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +13 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/config.d.ts +11 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +169 -13
- package/dist/config.js.map +1 -1
- package/dist/invitation-artifact.d.ts +33 -0
- package/dist/invitation-artifact.d.ts.map +1 -0
- package/dist/invitation-artifact.js +89 -0
- package/dist/invitation-artifact.js.map +1 -0
- package/dist/launch-gate.d.ts +5 -4
- package/dist/launch-gate.d.ts.map +1 -1
- package/dist/launch-gate.js +7 -8
- package/dist/launch-gate.js.map +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -1
- package/dist/parse-assimilate-args.js +9 -3
- package/dist/parse-assimilate-args.js.map +1 -1
- package/dist/recover-enrollment-cmd.d.ts +17 -0
- package/dist/recover-enrollment-cmd.d.ts.map +1 -0
- package/dist/recover-enrollment-cmd.js +62 -0
- package/dist/recover-enrollment-cmd.js.map +1 -0
- package/dist/regen-format.d.ts +9 -0
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +15 -0
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +27 -3
- package/dist/regen.js.map +1 -1
- package/dist/seat-store.d.ts.map +1 -1
- package/dist/seat-store.js +7 -0
- package/dist/seat-store.js.map +1 -1
- package/dist/server-facade.d.ts +1 -1
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +13 -2
- package/dist/server-facade.js.map +1 -1
- package/dist/server-handshake.d.ts +19 -3
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +86 -9
- package/dist/server-handshake.js.map +1 -1
- package/dist/server-trust.d.ts +8 -0
- package/dist/server-trust.d.ts.map +1 -1
- package/dist/server-trust.js +165 -5
- package/dist/server-trust.js.map +1 -1
- package/dist/token-store.d.ts +3 -0
- package/dist/token-store.d.ts.map +1 -1
- package/dist/token-store.js +2 -0
- package/dist/token-store.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +1 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +11 -7
- package/docs/LOCAL_SERVER.md +12 -3
- package/docs/RELEASING.md +47 -7
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +231 -26
- package/src/assimilate-deps.ts +26 -0
- package/src/assimilate-welcome.ts +1 -0
- package/src/claude.ts +22 -0
- package/src/cli-help.ts +16 -0
- package/src/config.ts +165 -10
- package/src/invitation-artifact.ts +116 -0
- package/src/launch-gate.ts +7 -8
- package/src/parse-assimilate-args.ts +11 -4
- package/src/recover-enrollment-cmd.ts +63 -0
- package/src/regen-format.ts +25 -0
- package/src/regen.ts +30 -2
- package/src/seat-store.ts +6 -0
- package/src/server-facade.ts +13 -2
- package/src/server-handshake.ts +113 -6
- package/src/server-trust.ts +180 -5
- package/src/token-store.ts +5 -0
- package/src/unknown-subcommand.ts +1 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* (`--cli`, `--remote`, agent passthrough, …) still fall through to launch.
|
|
12
12
|
*/
|
|
13
13
|
/** The subcommands the claude.ts router dispatches on (lines 107-176). */
|
|
14
|
-
export declare const KNOWN_SUBCOMMANDS: readonly ["setup", "update", "assimilate", "reset-local-seat", "spawn", "sync", "cleanup", "launch-all", "server"];
|
|
14
|
+
export declare const KNOWN_SUBCOMMANDS: readonly ["setup", "update", "assimilate", "reset-local-seat", "recover-enrollment", "spawn", "sync", "cleanup", "launch-all", "server"];
|
|
15
15
|
/**
|
|
16
16
|
* Returns the offending command string when argv[2] is an unknown non-flag
|
|
17
17
|
* positional (caller should error + exit 1), or null when it's bare `borg`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unknown-subcommand.d.ts","sourceRoot":"","sources":["../src/unknown-subcommand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,
|
|
1
|
+
{"version":3,"file":"unknown-subcommand.d.ts","sourceRoot":"","sources":["../src/unknown-subcommand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,0IAWpB,CAAC;AAEX;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAK1E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unknown-subcommand.js","sourceRoot":"","sources":["../src/unknown-subcommand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,OAAO;IACP,QAAQ;IACR,YAAY;IACZ,kBAAkB;IAClB,OAAO;IACP,MAAM;IACN,SAAS;IACT,YAAY;IACZ,QAAQ;CACA,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAyB;IACzD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,CAAC,cAAc;IACpD,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,8BAA8B;IACtE,IAAK,iBAAuC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,OAAO,KAAK,CAAC,CAAC,wCAAwC;AACxD,CAAC"}
|
|
1
|
+
{"version":3,"file":"unknown-subcommand.js","sourceRoot":"","sources":["../src/unknown-subcommand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,OAAO;IACP,QAAQ;IACR,YAAY;IACZ,kBAAkB;IAClB,oBAAoB;IACpB,OAAO;IACP,MAAM;IACN,SAAS;IACT,YAAY;IACZ,QAAQ;CACA,CAAC;AAEX;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAyB;IACzD,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,CAAC,cAAc;IACpD,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,8BAA8B;IACtE,IAAK,iBAAuC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,OAAO,KAAK,CAAC,CAAC,wCAAwC;AACxD,CAAC"}
|
|
@@ -22,7 +22,7 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
|
|
|
22
22
|
recovery, `2.0.2`, `2.0.3`, `2.0.4`, `2.0.5`, and `2.0.6` successors were
|
|
23
23
|
published and registry-verified. The immutable `2.0.7` workflow failed before
|
|
24
24
|
package creation or npm publication. The `2.0.8`, `2.0.9`, `2.0.10`, `2.0.11`,
|
|
25
|
-
`2.1.0`, and `2.1.1` successors were published, and `2.2.0` was subsequently published, and `2.3.0` was subsequently published, and `2.4.0` was subsequently published, and `2.4.1` was subsequently published, and `2.5.0` was subsequently published, and `2.6.0` was subsequently published, and `2.6.1` was subsequently published, and `2.7.0` was subsequently published, and `2.7.1` was subsequently published, and `2.7.2` was subsequently published, and `2.7.3` was subsequently published, and `2.8.0` was subsequently published, so the next candidate identity is `2.
|
|
25
|
+
`2.1.0`, and `2.1.1` successors were published, and `2.2.0` was subsequently published, and `2.3.0` was subsequently published, and `2.4.0` was subsequently published, and `2.4.1` was subsequently published, and `2.5.0` was subsequently published, and `2.6.0` was subsequently published, and `2.6.1` was subsequently published, and `2.7.0` was subsequently published, and `2.7.1` was subsequently published, and `2.7.2` was subsequently published, and `2.7.3` was subsequently published, and `2.8.0` was subsequently published, and `2.9.0` was subsequently published, and `2.10.0` was subsequently published, and the immutable `2.10.1` release attempt failed before artifact creation or publication, so the next candidate identity is `2.10.2`. Extraction and
|
|
26
26
|
versioning do not authorize publication.
|
|
27
27
|
|
|
28
28
|
## Review Holds
|
|
@@ -38,11 +38,15 @@ artifact reaches no hosted authority.
|
|
|
38
38
|
Local enrollment now uses the reviewed client-generated credential/retry
|
|
39
39
|
contract, with a pre-request `PENDING` record in the local 0600-permission seat
|
|
40
40
|
store, exact-tuple ambiguous retry, and verified activation. The contract now
|
|
41
|
-
resolves to the audited registry `borgmcp-shared@0.
|
|
42
|
-
`sha512-
|
|
43
|
-
The
|
|
44
|
-
`
|
|
45
|
-
|
|
41
|
+
resolves to the audited registry `borgmcp-shared@0.8.0` with integrity
|
|
42
|
+
`sha512-0p/ZC03Y6G4N6bfBjMD5b+WcQXcLalhU/3QqGjSWYCFKa3q2/183O53w6Tu6y8h2DkhTw5prnamsOozmVpXZew==`.
|
|
43
|
+
The coupled `borgmcp-server@0.9.0` release was published on 2026-08-02 for the
|
|
44
|
+
client `borgmcp@2.10.0` line. Its annotated tag object
|
|
45
|
+
`7021a2e1f551aa0c1716c9e3f29f7b8bc4b0ccb0` peels to protected-main commit
|
|
46
|
+
`734051375d77013f8fd5b396af12feb53d5af96d`; the same-run artifact integrity is
|
|
47
|
+
`sha512-lOxIPg3WcjSBte46iTM7SAFVN6Y0b2oizOKAJ9Q1EijBdOlmsh/cQlpUIaa8F2UJPaTDFsXy3M64gpGs6XgKzA==`
|
|
48
|
+
in the `borgmcp-server-0.9.0-release` registry decision. Both releases pin the
|
|
49
|
+
audited `borgmcp-shared@0.8.0` dependency above. Client `borgmcp@2.10.0` is published. The immutable `v2.0.7`
|
|
46
50
|
attempt failed before publication and remains preserved. Publication of the next
|
|
47
|
-
candidate remains gated by reviewed `v2.
|
|
51
|
+
candidate remains gated by reviewed `v2.10.2` source, a fresh annotated tag, and
|
|
48
52
|
the exact-artifact and protected-publication gates.
|
package/docs/LOCAL_SERVER.md
CHANGED
|
@@ -140,6 +140,14 @@ create distinct bounded cubes. An ordinary enrolled client is denied before a
|
|
|
140
140
|
create request is sent. Cloud-only capabilities fail explicitly rather than
|
|
141
141
|
being redirected.
|
|
142
142
|
|
|
143
|
+
## Agent Launch State
|
|
144
|
+
|
|
145
|
+
Borg tools are inactive unless the agent session was launched with `borg`.
|
|
146
|
+
For Claude Code and Codex, `BORG_DISABLE_LAUNCH_REMINDER` is a presence-based
|
|
147
|
+
local opt-out for launch orientation messaging; setting it does not activate
|
|
148
|
+
Borg tools or change server authorization. OpenCode has no launch-reminder
|
|
149
|
+
surface.
|
|
150
|
+
|
|
143
151
|
## Updating Borg
|
|
144
152
|
|
|
145
153
|
For an npm-global installation, use the whole-product command:
|
|
@@ -227,9 +235,10 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
|
|
|
227
235
|
|
|
228
236
|
## Release status
|
|
229
237
|
|
|
230
|
-
This
|
|
231
|
-
matching
|
|
232
|
-
association, and cube deletion. The server
|
|
238
|
+
This client candidate pins published `borgmcp-shared@0.8.0` and requires the
|
|
239
|
+
matching coupled client/server release for protocol v7 repository resolve,
|
|
240
|
+
association, and cube deletion. The already-published server 0.8.0 retains its
|
|
241
|
+
immutable older shared dependency pin and is not that coupled artifact. The server owner-enrollment, repository resolve/association,
|
|
233
242
|
cube-create, attach, restart, log, and SSE implementation must also pass the full
|
|
234
243
|
process-level local dogfood gate. Until that gate opens the self-hosted path remains preview-only, and the client
|
|
235
244
|
publish is deferred accordingly.
|
package/docs/RELEASING.md
CHANGED
|
@@ -178,8 +178,25 @@ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.8.0
|
|
|
178
178
|
`ad40e93ee9b1ff2801119c1c763ad526974ae2a6`. Workflow run `30639790368`, attempt 1,
|
|
179
179
|
successfully published that exact source as `borgmcp@2.8.0`; the same-run artifact report records integrity
|
|
180
180
|
`sha512-hKNyWwVg/8WJ/qcqaNjZVylMw2UzgFEhE7sAJmWy/BjNU4lEa6ox8N7SdbEt8hhKsySMxt+YTFbNFitRtf9u3g==`.
|
|
181
|
-
Never move, replace, reuse, or rerun that tag or workflow. The
|
|
182
|
-
|
|
181
|
+
Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.9.0` tag object
|
|
182
|
+
`26df119da33e914f857f820e68df6e887afed949` peels to protected-main commit
|
|
183
|
+
`895f12065c09ec5c0a35f770f26ec89f36f2feaa`. Workflow run `30695501563`, attempt 1,
|
|
184
|
+
successfully published that exact source as `borgmcp@2.9.0`; the same-run artifact report records integrity
|
|
185
|
+
`sha512-e10KK//zAP6xRDtUjCrLqzVyeX5zd/39JD4LksBhNVL0ZIGthu5ZLahPMrcCjGl6lZQHNTCo8Tez5c1jmThgGQ==`.
|
|
186
|
+
Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.10.0` tag object
|
|
187
|
+
`83bd13fe9943c980760db6ecdbcef0d8c413cee0` peels to protected-main commit
|
|
188
|
+
`c5d3d2cbe85644d402dfd82ce108dceca3a913a1`. Workflow run `30764262636`, attempt 1,
|
|
189
|
+
successfully published that exact source as `borgmcp@2.10.0`; the same-run artifact report records integrity
|
|
190
|
+
`sha512-YUVxWevAToQYwu/MDV2r9TrKnaZxpYiEitmLtr7OekfyE0+XZnkyHmL1gSk04JAiYquzv6ZyTRDYMnjQzS5GxA==`.
|
|
191
|
+
Never move, replace, reuse, or rerun that tag or workflow. FAILED-SUPERSEDED release record: The annotated `v2.10.1` tag object
|
|
192
|
+
`afee30802629d0f838b2446efd2241ff01964af2` peels to protected-main commit
|
|
193
|
+
`fcecd75211107994ccd0df0bf4093240f83de9b2`. Workflow run `30766475027`, attempt 1, concluded `failure`
|
|
194
|
+
during verification before artifact creation or publication. Verify job `91545993819` records the release
|
|
195
|
+
artifact build, verification, exercise, and upload steps as skipped; publish job `91546125556` was skipped.
|
|
196
|
+
Registry verification found no published `borgmcp@2.10.1` package, so no published npm artifact or SRI
|
|
197
|
+
exists for that version as of this check.
|
|
198
|
+
Never delete, move, replace, reuse, or rerun that tag, version, or workflow. The next candidate
|
|
199
|
+
uses the unused `v2.10.2` identity from a fresh reviewed protected-main commit
|
|
183
200
|
and requires the complete release gate again.
|
|
184
201
|
|
|
185
202
|
## Release Prerequisites
|
|
@@ -191,12 +208,13 @@ Before creating the release tag, independently verify all of these conditions:
|
|
|
191
208
|
- the extraction review confirms no private backend secrets, deployment
|
|
192
209
|
configuration, customer data, local state, or duplicated shared contracts
|
|
193
210
|
entered the public package;
|
|
194
|
-
- the exact audited registry dependency `borgmcp-shared@0.
|
|
211
|
+
- the exact audited registry dependency `borgmcp-shared@0.8.0` remains locked to
|
|
195
212
|
its canonical tarball and integrity
|
|
196
|
-
`sha512-
|
|
197
|
-
- the client
|
|
198
|
-
`
|
|
199
|
-
|
|
213
|
+
`sha512-0p/ZC03Y6G4N6bfBjMD5b+WcQXcLalhU/3QqGjSWYCFKa3q2/183O53w6Tu6y8h2DkhTw5prnamsOozmVpXZew==`;
|
|
214
|
+
- the coupled client/server release is published only after the server artifact
|
|
215
|
+
is rebuilt against `borgmcp-shared@0.8.0` and both pass the complete local
|
|
216
|
+
dogfood gate; the already-published server 0.8.0 remains immutable at its
|
|
217
|
+
older immutable shared dependency pin;
|
|
200
218
|
- the selected stable client version is unused and the exact release commit is
|
|
201
219
|
on protected `main`;
|
|
202
220
|
- the repository and protected npm environment settings pass an operator audit;
|
|
@@ -228,6 +246,28 @@ anything. It then deterministically updates only the manifest version, the two
|
|
|
228
246
|
root lockfile version fields, the extraction ledger, this document's immutable
|
|
229
247
|
release record, and the release-lane current-version/evidence assertions.
|
|
230
248
|
|
|
249
|
+
When the superseded tag's attempt failed before artifact creation or publication,
|
|
250
|
+
prepare the fresh recovery identity with `--workflow-conclusion failure` and omit
|
|
251
|
+
`--artifact-integrity` entirely. The preparer verifies the annotated tag and exact
|
|
252
|
+
failed workflow attempt, writes a canonical failed-superseded record stating that
|
|
253
|
+
the artifact build, verification, exercise, upload, and publish phases were all
|
|
254
|
+
skipped, and separately re-verifies the last published release as the artifact
|
|
255
|
+
provenance anchor. It also reads the authoritative registry version list before
|
|
256
|
+
writing the time-bounded fact that no published npm artifact or SRI exists for
|
|
257
|
+
the failed version. This recovery-time absence check is required before preparing
|
|
258
|
+
the replacement identity; it is not the prohibited post-publication readback of
|
|
259
|
+
the newly published release. A failed record carrying an SRI, a failed run whose
|
|
260
|
+
artifact or publish phase ran, a failed version present in the registry, or a
|
|
261
|
+
successful record without an SRI is invalid. Failed tags and workflow attempts
|
|
262
|
+
stay immutable and must never be moved, reused, or rerun.
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
npm run release:prepare -- 2.10.2 \
|
|
266
|
+
--workflow-run-id 30766475027 \
|
|
267
|
+
--workflow-run-attempt 1 \
|
|
268
|
+
--workflow-conclusion failure
|
|
269
|
+
```
|
|
270
|
+
|
|
231
271
|
Release branches use the `release/` prefix and enter protected `main` through a
|
|
232
272
|
pull request. A direct push only updates the staging branch; it does
|
|
233
273
|
not produce a release-identity verdict and grants no review skip, merge
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.2",
|
|
4
4
|
"description": "Coordinate AI coding agents in shared cubes. Works with Claude Code, Codex, and OpenCode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
74
|
-
"borgmcp-shared": "0.
|
|
74
|
+
"borgmcp-shared": "0.8.0",
|
|
75
75
|
"chalk": "^5.3.0",
|
|
76
76
|
"prompts": "^2.4.2",
|
|
77
77
|
"which": "^4.0.0"
|
package/src/assimilate-cmd.ts
CHANGED
|
@@ -56,6 +56,17 @@ import {
|
|
|
56
56
|
RepositoryAssociationOutcomeUnknownError,
|
|
57
57
|
RepositoryAssociationResolutionError,
|
|
58
58
|
} from './server-errors.js';
|
|
59
|
+
import {
|
|
60
|
+
decodeAndVerifyInvitationArtifact,
|
|
61
|
+
InvitationArtifactCompatibilityError,
|
|
62
|
+
InvitationArtifactEndpointMismatchError,
|
|
63
|
+
InvitationArtifactFormatError,
|
|
64
|
+
InvitationArtifactLegacyError,
|
|
65
|
+
InvitationArtifactRecoveryError,
|
|
66
|
+
InvitationArtifactStorageError,
|
|
67
|
+
InvitationArtifactTransportError,
|
|
68
|
+
InvitationArtifactTrustError,
|
|
69
|
+
} from './invitation-artifact.js';
|
|
59
70
|
import type { SeatStatus } from './seat-probe.js';
|
|
60
71
|
import type { ServerSessionOperation } from './config.js';
|
|
61
72
|
import type { ExpectedBinding, FinalizeServerSeatOutcome, PersistedLocalSeat } from './cubes.js';
|
|
@@ -67,6 +78,7 @@ import type {
|
|
|
67
78
|
AssociateRepositoryCubeResponse,
|
|
68
79
|
CreateCubeRepository,
|
|
69
80
|
CubeTemplate,
|
|
81
|
+
InvitationArtifact,
|
|
70
82
|
} from 'borgmcp-shared/protocol';
|
|
71
83
|
import {
|
|
72
84
|
initializeRepositoryCube,
|
|
@@ -277,7 +289,11 @@ export interface AssimilateDeps {
|
|
|
277
289
|
detectLocalServer: () => Promise<string | null>;
|
|
278
290
|
connectServer: (
|
|
279
291
|
apiUrl: string,
|
|
280
|
-
enrollment?: {
|
|
292
|
+
enrollment?: {
|
|
293
|
+
invitation: string;
|
|
294
|
+
artifact?: import('borgmcp-shared/protocol').InvitationArtifact;
|
|
295
|
+
confirmReplacement?: () => Promise<boolean>;
|
|
296
|
+
},
|
|
281
297
|
) => Promise<{
|
|
282
298
|
token: string;
|
|
283
299
|
trustIdentity: string;
|
|
@@ -291,6 +307,17 @@ export interface AssimilateDeps {
|
|
|
291
307
|
trustIdentity: string;
|
|
292
308
|
serverCapabilities?: readonly string[];
|
|
293
309
|
} | null>;
|
|
310
|
+
/** Read-only pending-artifact peek used to validate an explicit host before
|
|
311
|
+
* the normal resume path performs trust/credential work. */
|
|
312
|
+
peekPendingServerEnrollment?: () => Promise<{ origin: string; invitation: string } | null>;
|
|
313
|
+
resumePendingServerEnrollment?: (
|
|
314
|
+
onPending?: () => void,
|
|
315
|
+
) => Promise<{
|
|
316
|
+
token: string;
|
|
317
|
+
apiUrl?: string;
|
|
318
|
+
trustIdentity: string;
|
|
319
|
+
serverCapabilities?: readonly string[];
|
|
320
|
+
} | null>;
|
|
294
321
|
|
|
295
322
|
resolveRepositoryCube: (
|
|
296
323
|
apiUrl: string,
|
|
@@ -382,6 +409,11 @@ async function selectAssimilationAuthority(
|
|
|
382
409
|
|
|
383
410
|
// Only a local self-hosted server authority exists. Non-TTY and --yes must
|
|
384
411
|
// NOT infer an authority — fail closed with actionable guidance.
|
|
412
|
+
if (flags.enroll && deps.isTTY()) {
|
|
413
|
+
// The v2 enrollment artifact is itself the authority selector. Use the
|
|
414
|
+
// local origin only as a placeholder until the hidden artifact is decoded.
|
|
415
|
+
return { kind: 'server', apiUrl: DEFAULT_LOCAL_SERVER_ORIGIN };
|
|
416
|
+
}
|
|
385
417
|
if (!deps.isTTY() || flags.yes) {
|
|
386
418
|
if (deps.defaultAuthority) return deps.defaultAuthority;
|
|
387
419
|
const command = mode === 'cube-init'
|
|
@@ -429,12 +461,13 @@ async function selectAssimilationAuthority(
|
|
|
429
461
|
}
|
|
430
462
|
|
|
431
463
|
function localAssimilateCommand(
|
|
432
|
-
apiUrl: string,
|
|
464
|
+
apiUrl: string | undefined,
|
|
433
465
|
enroll = false,
|
|
434
466
|
mode: 'assimilate' | 'cube-init' = 'assimilate',
|
|
435
467
|
): string {
|
|
436
468
|
const command = mode === 'cube-init' ? 'borg server cube init' : 'borg assimilate';
|
|
437
|
-
|
|
469
|
+
const host = apiUrl === undefined ? '' : ` --host ${apiUrl}`;
|
|
470
|
+
return `\`${command}${host}${enroll ? ' --enroll' : ''}\``;
|
|
438
471
|
}
|
|
439
472
|
|
|
440
473
|
function localAssimilateRoleCommand(apiUrl: string): string {
|
|
@@ -521,6 +554,38 @@ function reportServerFailure(
|
|
|
521
554
|
);
|
|
522
555
|
return 1;
|
|
523
556
|
}
|
|
557
|
+
if (error instanceof InvitationArtifactCompatibilityError) {
|
|
558
|
+
deps.stderr(`${error.message}\n`);
|
|
559
|
+
return 1;
|
|
560
|
+
}
|
|
561
|
+
if (error instanceof InvitationArtifactLegacyError) {
|
|
562
|
+
deps.stderr(`${error.message}\n`);
|
|
563
|
+
return 1;
|
|
564
|
+
}
|
|
565
|
+
if (error instanceof InvitationArtifactFormatError) {
|
|
566
|
+
deps.stderr(`${error.message}\n`);
|
|
567
|
+
return 1;
|
|
568
|
+
}
|
|
569
|
+
if (error instanceof InvitationArtifactEndpointMismatchError) {
|
|
570
|
+
deps.stderr(`${error.message}\n`);
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
573
|
+
if (error instanceof InvitationArtifactTransportError) {
|
|
574
|
+
deps.stderr(`${error.message}\n`);
|
|
575
|
+
return 1;
|
|
576
|
+
}
|
|
577
|
+
if (error instanceof InvitationArtifactStorageError) {
|
|
578
|
+
deps.stderr(`${error.message}\n`);
|
|
579
|
+
return 1;
|
|
580
|
+
}
|
|
581
|
+
if (error instanceof InvitationArtifactRecoveryError) {
|
|
582
|
+
deps.stderr(`${error.message}\n`);
|
|
583
|
+
return 1;
|
|
584
|
+
}
|
|
585
|
+
if (error instanceof InvitationArtifactTrustError) {
|
|
586
|
+
deps.stderr(`${error.message}\n`);
|
|
587
|
+
return 1;
|
|
588
|
+
}
|
|
524
589
|
if (/HTTP 40[13]|auth(?:entication|orization)|credential.*(?:invalid|rejected)/i.test(message)) {
|
|
525
590
|
deps.stderr(
|
|
526
591
|
`The saved enrollment for ${apiUrl} was rejected. Re-run ` +
|
|
@@ -662,7 +727,126 @@ export async function runAssimilate(
|
|
|
662
727
|
return 1;
|
|
663
728
|
}
|
|
664
729
|
|
|
665
|
-
|
|
730
|
+
const hostlessEnrollment = args.flags.enroll === true &&
|
|
731
|
+
args.flags.server === undefined && deps.defaultAuthority === undefined;
|
|
732
|
+
const artifactOnlyEnrollment = hostlessEnrollment && deps.isTTY();
|
|
733
|
+
let preResumeAttempted = false;
|
|
734
|
+
let preResumedEnrollment: {
|
|
735
|
+
token: string;
|
|
736
|
+
trustIdentity: string;
|
|
737
|
+
serverCapabilities?: readonly string[];
|
|
738
|
+
} | null = null;
|
|
739
|
+
|
|
740
|
+
let prefetchedInvitation: string | undefined;
|
|
741
|
+
let prefetchedArtifact: InvitationArtifact | undefined;
|
|
742
|
+
|
|
743
|
+
if (hostlessEnrollment && !deps.isTTY()) {
|
|
744
|
+
deps.stderr(
|
|
745
|
+
'Local enrollment requires an interactive operator terminal. ' +
|
|
746
|
+
`Re-run ${localAssimilateCommand(undefined, true, mode)} from the operator’s terminal.\n`,
|
|
747
|
+
);
|
|
748
|
+
return 1;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// An explicit --host plus a new invitation must be rejected on the pure
|
|
752
|
+
// input path. Decode and compare the operator-presented artifact before any
|
|
753
|
+
// pending-enrollment lookup: the lookup enumerates the credential backend,
|
|
754
|
+
// so it must not precede this contradiction check. A matching artifact may
|
|
755
|
+
// then take the published pending-resume path until client#267 lands.
|
|
756
|
+
if (args.flags.enroll && args.flags.server !== undefined && deps.isTTY()) {
|
|
757
|
+
let preResumeOrigin: string;
|
|
758
|
+
try {
|
|
759
|
+
preResumeOrigin = normalizeServerEndpoint(args.flags.server);
|
|
760
|
+
} catch (error) {
|
|
761
|
+
deps.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
762
|
+
return 1;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
|
|
766
|
+
if (!prefetchedInvitation) {
|
|
767
|
+
deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
|
|
768
|
+
return 1;
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
|
|
772
|
+
if (prefetchedArtifact.endpoint !== preResumeOrigin) {
|
|
773
|
+
throw new InvitationArtifactEndpointMismatchError(
|
|
774
|
+
preResumeOrigin,
|
|
775
|
+
prefetchedArtifact.endpoint,
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
} catch (error) {
|
|
779
|
+
deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
|
|
780
|
+
prefetchedInvitation = undefined;
|
|
781
|
+
prefetchedArtifact = undefined;
|
|
782
|
+
return 1;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
let pendingForHost = false;
|
|
786
|
+
if (deps.peekPendingServerEnrollment !== undefined) {
|
|
787
|
+
let pending: { origin: string; invitation: string } | null = null;
|
|
788
|
+
try {
|
|
789
|
+
pending = await deps.peekPendingServerEnrollment();
|
|
790
|
+
} catch {
|
|
791
|
+
pending = null;
|
|
792
|
+
}
|
|
793
|
+
if (pending?.origin === preResumeOrigin) {
|
|
794
|
+
try {
|
|
795
|
+
const pendingArtifact = decodeAndVerifyInvitationArtifact(pending.invitation);
|
|
796
|
+
if (pendingArtifact.endpoint !== preResumeOrigin) {
|
|
797
|
+
throw new InvitationArtifactEndpointMismatchError(
|
|
798
|
+
preResumeOrigin,
|
|
799
|
+
pendingArtifact.endpoint,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
pendingForHost = true;
|
|
803
|
+
} catch (error) {
|
|
804
|
+
deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
|
|
805
|
+
return 1;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (pendingForHost) {
|
|
811
|
+
try {
|
|
812
|
+
preResumeAttempted = true;
|
|
813
|
+
preResumedEnrollment = await deps.resumeServerEnrollment(preResumeOrigin, () => {
|
|
814
|
+
deps.stderr(
|
|
815
|
+
`Resuming the pending enrollment for \`${preResumeOrigin}\`; ` +
|
|
816
|
+
'do not enter another invitation unless the server certificate was reissued; ' +
|
|
817
|
+
'if it was, request a current invitation and rerun this command.\n',
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
} catch {
|
|
821
|
+
preResumeAttempted = false;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (artifactOnlyEnrollment ||
|
|
826
|
+
preResumeAttempted && preResumedEnrollment === null) {
|
|
827
|
+
if (artifactOnlyEnrollment && deps.resumePendingServerEnrollment) {
|
|
828
|
+
preResumedEnrollment = await deps.resumePendingServerEnrollment(() => {
|
|
829
|
+
deps.stderr('Resuming the pending enrollment; no new invitation is required.\n');
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
if (artifactOnlyEnrollment && preResumedEnrollment) {
|
|
833
|
+
// The exact pending tuple was already redeemed or is being resumed.
|
|
834
|
+
} else {
|
|
835
|
+
prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
|
|
836
|
+
if (!prefetchedInvitation) {
|
|
837
|
+
deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
|
|
838
|
+
return 1;
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
|
|
842
|
+
} catch (error) {
|
|
843
|
+
deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
|
|
844
|
+
prefetchedInvitation = undefined;
|
|
845
|
+
return 1;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (!artifactOnlyEnrollment && args.flags.server === undefined && deps.defaultAuthority === undefined) {
|
|
666
850
|
const connectCommand = mode === 'cube-init'
|
|
667
851
|
? 'borg server cube init --host <host>'
|
|
668
852
|
: 'borg assimilate --host <host>';
|
|
@@ -699,8 +883,9 @@ export async function runAssimilate(
|
|
|
699
883
|
}
|
|
700
884
|
|
|
701
885
|
// ----- Step 1: Select and authenticate the local server -----
|
|
702
|
-
const
|
|
703
|
-
if (!
|
|
886
|
+
const selectedAuthority = await selectAssimilationAuthority(args.flags, deps, mode);
|
|
887
|
+
if (!selectedAuthority) return 1;
|
|
888
|
+
let authority: AssimilationAuthority = selectedAuthority;
|
|
704
889
|
if (localSeatReadError !== undefined) {
|
|
705
890
|
return reportServerFailure(deps, authority.apiUrl, localSeatReadError, false, mode);
|
|
706
891
|
}
|
|
@@ -723,33 +908,48 @@ export async function runAssimilate(
|
|
|
723
908
|
);
|
|
724
909
|
return 1;
|
|
725
910
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
911
|
+
let resumed = preResumedEnrollment;
|
|
912
|
+
if (!resumed && prefetchedArtifact === undefined && !preResumeAttempted && !artifactOnlyEnrollment) {
|
|
913
|
+
resumed = await deps.resumeServerEnrollment(authority.apiUrl, () => {
|
|
914
|
+
deps.stderr(
|
|
915
|
+
`Resuming the pending enrollment for \`${authority.apiUrl}\`; ` +
|
|
916
|
+
'do not enter another invitation unless the server certificate was reissued; ' +
|
|
917
|
+
'if it was, request a current invitation and rerun this command.\n',
|
|
918
|
+
);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
732
921
|
if (resumed) {
|
|
922
|
+
if ('apiUrl' in resumed && typeof resumed.apiUrl === 'string') {
|
|
923
|
+
authority = { kind: 'server', apiUrl: resumed.apiUrl };
|
|
924
|
+
}
|
|
733
925
|
serverAuth = resumed;
|
|
734
926
|
} else {
|
|
735
|
-
let invitation = await deps.promptSecret(
|
|
736
|
-
|
|
927
|
+
let invitation = prefetchedInvitation ?? await deps.promptSecret(
|
|
928
|
+
artifactOnlyEnrollment
|
|
929
|
+
? 'Enrollment invitation (single-use; hidden input):'
|
|
930
|
+
: `Enrollment invitation for \`${authority.apiUrl}\` (single-use; hidden input):`,
|
|
737
931
|
);
|
|
738
932
|
if (!invitation) {
|
|
739
|
-
deps.stderr(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
933
|
+
deps.stderr(artifactOnlyEnrollment
|
|
934
|
+
? 'No enrollment invitation was entered. Ask the server operator for one, then rerun `borg assimilate --enroll`.\n'
|
|
935
|
+
: `No enrollment invitation was entered for ${authority.apiUrl}. ` +
|
|
936
|
+
`Ask the server operator for one, then rerun ${localAssimilateCommand(authority.apiUrl, true, mode)}.\n`);
|
|
743
937
|
return 1;
|
|
744
938
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
939
|
+
try {
|
|
940
|
+
const artifact = prefetchedArtifact ?? decodeAndVerifyInvitationArtifact(invitation);
|
|
941
|
+
if (args.flags.server !== undefined && authority.apiUrl !== artifact.endpoint) {
|
|
942
|
+
throw new InvitationArtifactEndpointMismatchError(authority.apiUrl, artifact.endpoint);
|
|
943
|
+
}
|
|
944
|
+
authority = { kind: 'server', apiUrl: artifact.endpoint };
|
|
945
|
+
serverAuth = await deps.connectServer(authority.apiUrl, {
|
|
946
|
+
invitation,
|
|
947
|
+
artifact,
|
|
948
|
+
confirmReplacement: async () => strictAffirmative(await deps.prompt(
|
|
949
|
+
`A local enrollment for ${authority.apiUrl} already exists. Replacing it will orphan ` +
|
|
950
|
+
'the first enrolled client. Replace it? [y/N]: ',
|
|
951
|
+
)),
|
|
952
|
+
});
|
|
753
953
|
} finally {
|
|
754
954
|
// Strings cannot be zeroized in JavaScript, but drop this command's
|
|
755
955
|
// reference immediately after the exchange instead of retaining the
|
|
@@ -777,6 +977,11 @@ export async function runAssimilate(
|
|
|
777
977
|
serverTrustIdentity: serverAuth.trustIdentity,
|
|
778
978
|
serverCapabilities: serverAuth.serverCapabilities ?? [],
|
|
779
979
|
};
|
|
980
|
+
if (args.flags.enroll) {
|
|
981
|
+
deps.stderr(
|
|
982
|
+
`This machine (${deps.getHostname()}) is enrolled with Borg server \`${authority.apiUrl}\`.\n`,
|
|
983
|
+
);
|
|
984
|
+
}
|
|
780
985
|
} catch (error) {
|
|
781
986
|
return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true, mode);
|
|
782
987
|
}
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
connectLocalBorgServer,
|
|
35
35
|
createLocalBorgServerCube,
|
|
36
36
|
enrollLocalBorgServer,
|
|
37
|
+
enrollLocalBorgServerArtifact,
|
|
37
38
|
probeLocalBorgServer,
|
|
38
39
|
resolveLocalBorgServerRepositoryCube,
|
|
39
40
|
resumeLocalBorgServerEnrollment,
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
setCodexWakeTarget,
|
|
63
64
|
} from './cubes.js';
|
|
64
65
|
import { addProjectSessionStartHook } from './config-utils.js';
|
|
66
|
+
import { findPendingServerEnrollment } from './config.js';
|
|
65
67
|
import { setTerminalTitle as setTitle } from './terminal-title.js';
|
|
66
68
|
import { defaultCliChoiceDeps, resolveCliChoice } from './cli-platform.js';
|
|
67
69
|
import { prepareCodexRemoteLaunch, defaultCodexRemoteDeps } from './codex-remote.js';
|
|
@@ -240,6 +242,15 @@ export function buildDefaultAssimilateDeps(
|
|
|
240
242
|
: null,
|
|
241
243
|
connectServer: async (apiUrl, enrollment) => {
|
|
242
244
|
if (enrollment) {
|
|
245
|
+
if (enrollment.artifact) {
|
|
246
|
+
return enrollLocalBorgServerArtifact(enrollment.artifact, {
|
|
247
|
+
invitation: enrollment.invitation,
|
|
248
|
+
...(enrollment.confirmReplacement === undefined
|
|
249
|
+
? {}
|
|
250
|
+
: { confirmReplacement: enrollment.confirmReplacement }),
|
|
251
|
+
clientName: osHostname(),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
243
254
|
return enrollLocalBorgServer(apiUrl, enrollment.invitation, {
|
|
244
255
|
...(enrollment.confirmReplacement === undefined
|
|
245
256
|
? {}
|
|
@@ -253,6 +264,21 @@ export function buildDefaultAssimilateDeps(
|
|
|
253
264
|
resumeLocalBorgServerEnrollment(apiUrl, {
|
|
254
265
|
...(onPending === undefined ? {} : { onPending }),
|
|
255
266
|
}),
|
|
267
|
+
peekPendingServerEnrollment: async () => {
|
|
268
|
+
const pending = await findPendingServerEnrollment();
|
|
269
|
+
return pending === null
|
|
270
|
+
? null
|
|
271
|
+
: { origin: pending.origin, invitation: pending.invitation };
|
|
272
|
+
},
|
|
273
|
+
resumePendingServerEnrollment: async (onPending) =>
|
|
274
|
+
(async () => {
|
|
275
|
+
const pending = await findPendingServerEnrollment();
|
|
276
|
+
if (!pending) return null;
|
|
277
|
+
const result = await resumeLocalBorgServerEnrollment(pending.origin, {
|
|
278
|
+
...(onPending === undefined ? {} : { onPending }),
|
|
279
|
+
});
|
|
280
|
+
return result ? { ...result, apiUrl: pending.origin } : null;
|
|
281
|
+
})(),
|
|
256
282
|
|
|
257
283
|
resolveRepositoryCube: async (apiUrl, token, input, serverTrustIdentity) => {
|
|
258
284
|
if (serverTrustIdentity === undefined) {
|
|
@@ -33,6 +33,7 @@ export function renderAssimilationWelcome(
|
|
|
33
33
|
``,
|
|
34
34
|
`In the launched agent, run \`borg_whoami\` and \`borg_roster\` to verify the seat`,
|
|
35
35
|
`and begin coordinating.`,
|
|
36
|
+
`Launch future sessions in this repository with \`borg\`.`,
|
|
36
37
|
...teammateLines,
|
|
37
38
|
``,
|
|
38
39
|
].join('\n');
|
package/src/claude.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from './reset-local-seat-cmd.js';
|
|
44
44
|
import { parseLaunchAllArgs } from './parse-launch-all-args.js';
|
|
45
45
|
import { unknownSubcommand } from './unknown-subcommand.js';
|
|
46
|
+
import { parseRecoverEnrollmentArgs, runRecoverEnrollment } from './recover-enrollment-cmd.js';
|
|
46
47
|
import { runLaunchAll } from './launch-all-cmd.js';
|
|
47
48
|
import { buildDefaultLaunchAllDeps } from './launch-all-deps.js';
|
|
48
49
|
import { discoverDroneCandidates } from './launch-all-discovery.js';
|
|
@@ -173,6 +174,27 @@ async function main() {
|
|
|
173
174
|
const code = await runResetLocalSeat(parsed.flags, buildDefaultResetLocalSeatDeps());
|
|
174
175
|
process.exit(code);
|
|
175
176
|
}
|
|
177
|
+
if (process.argv[2] === 'recover-enrollment') {
|
|
178
|
+
const parsed = parseRecoverEnrollmentArgs(process.argv.slice(3));
|
|
179
|
+
if (!parsed.ok) {
|
|
180
|
+
if (parsed.error === 'help') {
|
|
181
|
+
process.stdout.write(clientSubcommandHelpText('recover-enrollment', [], getPackageVersion()) ?? '');
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
process.stderr.write(chalk.red(`${consolePrefix()}◼ borg recover-enrollment: ${parsed.error}\n`));
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const prompt = async (message: string): Promise<string> => {
|
|
188
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
189
|
+
try { return await rl.question(message); } finally { rl.close(); }
|
|
190
|
+
};
|
|
191
|
+
const code = await runRecoverEnrollment(parsed.flags, {
|
|
192
|
+
prompt,
|
|
193
|
+
stderr: (line) => process.stderr.write(line),
|
|
194
|
+
stdout: (line) => process.stdout.write(line),
|
|
195
|
+
});
|
|
196
|
+
process.exit(code);
|
|
197
|
+
}
|
|
176
198
|
if (process.argv[2] === 'spawn') {
|
|
177
199
|
// Deprecated; the stub prints a redirect message and exits 2.
|
|
178
200
|
const code = await runSpawn();
|