borgmcp 2.0.11 → 2.1.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 +11 -3
- package/dist/assimilate-cmd.d.ts +22 -4
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +109 -110
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts +7 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +69 -25
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts +3 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +26 -12
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +25 -7
- package/dist/cli-help.js.map +1 -1
- package/dist/config.d.ts +7 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -4
- package/dist/config.js.map +1 -1
- package/dist/remote-client.js +1 -1
- package/dist/remote-client.js.map +1 -1
- package/dist/repository-cube-init.d.ts +72 -0
- package/dist/repository-cube-init.d.ts.map +1 -0
- package/dist/repository-cube-init.js +341 -0
- package/dist/repository-cube-init.js.map +1 -0
- package/dist/repository-identity.d.ts +29 -0
- package/dist/repository-identity.d.ts.map +1 -0
- package/dist/repository-identity.js +143 -0
- package/dist/repository-identity.js.map +1 -0
- package/dist/server-errors.d.ts +17 -0
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +32 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/server-facade.d.ts +11 -1
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +33 -3
- package/dist/server-facade.js.map +1 -1
- package/dist/server-handshake.d.ts +38 -4
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +175 -13
- package/dist/server-handshake.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +9 -7
- package/docs/LOCAL_SERVER.md +44 -9
- package/docs/RELEASING.md +21 -4
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +179 -137
- package/src/assimilate-deps.ts +96 -24
- package/src/claude.ts +34 -14
- package/src/cli-help.ts +28 -7
- package/src/config.ts +30 -8
- package/src/remote-client.ts +1 -1
- package/src/repository-cube-init.ts +452 -0
- package/src/repository-identity.ts +199 -0
- package/src/server-errors.ts +41 -0
- package/src/server-facade.ts +47 -2
- package/src/server-handshake.ts +258 -14
|
@@ -8,7 +8,7 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
|
|
|
8
8
|
|
|
9
9
|
## Deliberate Transformations
|
|
10
10
|
|
|
11
|
-
- Replaced the monorepo dependency on `borgmcp-shared` with the exact audited registry release `borgmcp-shared@0.6.
|
|
11
|
+
- Replaced the monorepo dependency on `borgmcp-shared` with the exact audited registry release `borgmcp-shared@0.6.4` and a fresh standalone lockfile.
|
|
12
12
|
- Replaced local template, role-section, drone-address, and log high-water-mark implementations with `borgmcp-shared` exports.
|
|
13
13
|
- Removed monorepo-only website anti-drift tests and re-anchored remaining filesystem tests to this repository.
|
|
14
14
|
- Removed consumer lifecycle hooks, parent-directory deployment scripts, minification, and private integration-environment configuration.
|
|
@@ -21,8 +21,8 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
|
|
|
21
21
|
lightweight release tag failed before packaging. The reviewed `2.0.1`
|
|
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
|
-
package creation or npm publication. The `2.0.8`, `2.0.9`,
|
|
25
|
-
published, so the next candidate identity is `2.
|
|
24
|
+
package creation or npm publication. The `2.0.8`, `2.0.9`, `2.0.10`, `2.0.11`, and
|
|
25
|
+
`2.1.0` successors were published, so the next candidate identity is `2.1.1`. Extraction and
|
|
26
26
|
versioning do not authorize publication.
|
|
27
27
|
|
|
28
28
|
## Review Holds
|
|
@@ -38,9 +38,11 @@ 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.6.
|
|
42
|
-
`
|
|
43
|
-
`borgmcp@
|
|
41
|
+
resolves to the audited registry `borgmcp-shared@0.6.4` with integrity
|
|
42
|
+
`sha512-Wm4b0uoOAw9JCz5OTHD0Q2uXKkeWYdkVksdeZvRG8l62XGMY+G8GkNEsZT9L533LbVbQ29GhgF0htjDenQThDg==`.
|
|
43
|
+
The matching `borgmcp-server@0.1.21` release is published with integrity
|
|
44
|
+
`sha512-g0QH4cKcsKuDyZs1MvBmvCoCLaZ4wVI04yHCGSvcAeWghWxSBH0IIF8Q5OFi1pLB7TZXOVP6D3QAabUQ2eJTFg==`
|
|
45
|
+
and pins the same `borgmcp-shared@0.6.4` contract. Client `borgmcp@2.1.0` is published. The immutable `v2.0.7`
|
|
44
46
|
attempt failed before publication and remains preserved. Publication of the next
|
|
45
|
-
candidate remains gated by reviewed `v2.
|
|
47
|
+
candidate remains gated by reviewed `v2.1.1` source, a fresh annotated tag, and
|
|
46
48
|
the exact-artifact and protected-publication gates.
|
package/docs/LOCAL_SERVER.md
CHANGED
|
@@ -32,10 +32,28 @@ Commands:
|
|
|
32
32
|
status Report verified runtime evidence.
|
|
33
33
|
update Verify and activate a local server artifact.
|
|
34
34
|
invite Create a single-use invitation in an interactive terminal.
|
|
35
|
+
cube init Initialize this Git repository's cube; does not create a drone.
|
|
35
36
|
|
|
36
37
|
Run borg server <command> --help for server command options.
|
|
37
38
|
```
|
|
38
39
|
|
|
40
|
+
`borg server cube init --help` prints:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
borg server cube init — initialize this Git repository's cube without creating a drone
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
borg server cube init [options]
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--host <host> Borg server host or URL (bare hosts default to HTTPS)
|
|
50
|
+
--enroll Prompt for a hidden enrollment invitation
|
|
51
|
+
--cube-name <name> Repository cube name (otherwise edit the proposed name)
|
|
52
|
+
--template software-dev|starter New-cube template (default: software-dev)
|
|
53
|
+
--yes, -y Accept new-cube defaults; never adopt by name
|
|
54
|
+
--help, -h Show this help
|
|
55
|
+
```
|
|
56
|
+
|
|
39
57
|
Local server client credentials are stored in the owner-controlled
|
|
40
58
|
`~/.borg/credentials` file with mode 0600. Fresh same-machine setup provisions
|
|
41
59
|
the first owner record there, so bare `borg assimilate` can use it without an
|
|
@@ -73,10 +91,25 @@ exactly; the credential becomes active only after the versioned response is
|
|
|
73
91
|
decoded and the authenticated protocol handshake succeeds. A new process
|
|
74
92
|
resumes that pending enrollment before displaying another invitation prompt.
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
94
|
+
`borg assimilate` and `borg server cube init` share one repository-cube flow.
|
|
95
|
+
An existing local or server repository association is resolved without prompts.
|
|
96
|
+
Otherwise Borg checks accessible cubes against the exact proposed name. One
|
|
97
|
+
match displays the cube, repository, and server and requires explicit interactive
|
|
98
|
+
confirmation before the server atomically associates it; multiple matches fail
|
|
99
|
+
closed. `--yes` never adopts a cube by name. With no match, an authorized owner
|
|
100
|
+
continues to the creation guide, which shows the repository and server, proposes
|
|
101
|
+
an editable name, offers the Software Development and Starter templates, and
|
|
102
|
+
asks for one confirmation. `--cube-name <name>` and `--template
|
|
103
|
+
software-dev|starter` supply those creation values directly; `--yes` accepts the
|
|
104
|
+
repository default name and Software Development template. `borg server cube
|
|
105
|
+
init` stops after authoritative cube readback and never creates a drone. Bare
|
|
106
|
+
repositories fail closed.
|
|
107
|
+
|
|
108
|
+
For repositories with a canonical public `origin`, that origin is the stable
|
|
109
|
+
repository identity. Repositories without one receive an invisible UUID stored
|
|
110
|
+
in an owner-only local file. Its lookup key is an HMAC of the canonical Git
|
|
111
|
+
common directory, so linked worktrees share one identity and local paths are not
|
|
112
|
+
stored in cleartext. A malformed identity secret or state file fails closed.
|
|
80
113
|
|
|
81
114
|
The connection is HTTPS-only. Borg validates the server trust material, stores
|
|
82
115
|
parent enrollment credentials in `~/.borg/credentials` and session credentials
|
|
@@ -89,7 +122,8 @@ The lifecycle facade invokes the separately installed `borgmcp-server`; it does
|
|
|
89
122
|
not bundle the server into the client. The server must be running and trusted
|
|
90
123
|
before assimilation. An owner enrollment carrying the persisted `create_cube`
|
|
91
124
|
capability creates one idempotent cube per repository during normal
|
|
92
|
-
assimilation
|
|
125
|
+
assimilation or `borg server cube init`, using Software Development or Starter;
|
|
126
|
+
repeating an
|
|
93
127
|
ambiguous request does not duplicate the cube, and distinct repositories can
|
|
94
128
|
create distinct bounded cubes. An ordinary enrolled client is denied before a
|
|
95
129
|
create request is sent. Cloud-only capabilities fail explicitly rather than
|
|
@@ -139,8 +173,9 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
|
|
|
139
173
|
|
|
140
174
|
## Release status
|
|
141
175
|
|
|
142
|
-
This self-hosted path
|
|
143
|
-
|
|
144
|
-
and
|
|
145
|
-
|
|
176
|
+
This self-hosted path pins published `borgmcp-shared@0.6.4` and requires the
|
|
177
|
+
matching published `borgmcp-server@0.1.21` for protocol v5 repository resolve
|
|
178
|
+
and association. The server owner-enrollment, repository resolve/association,
|
|
179
|
+
cube-create, attach, restart, log, and SSE implementation must also pass the full
|
|
180
|
+
process-level local dogfood gate. Until that gate opens the self-hosted path remains preview-only, and the client
|
|
146
181
|
publish is deferred accordingly.
|
package/docs/RELEASING.md
CHANGED
|
@@ -97,8 +97,22 @@ The annotated `v2.0.10` tag object
|
|
|
97
97
|
`3f7003b65eadc74cd949857e831e1181a1aad2ff`. Workflow run `30063538271`, attempt 1,
|
|
98
98
|
successfully published that exact source as `borgmcp@2.0.10`; the registry records integrity
|
|
99
99
|
`sha512-BkFrq75mF7ih0/7Z7RsXsJYo43qNs4ng7tSnvcFCl3k6r+ex1FvTkah0+KQNinDSrv6a+HLWahUe8875so8zew==`.
|
|
100
|
+
Never move, replace, reuse, or rerun that tag or workflow.
|
|
101
|
+
|
|
102
|
+
The annotated `v2.0.11` tag object
|
|
103
|
+
`fb95f4278137d1d7871d945c467eb33984abe1c4` peels to protected-main commit
|
|
104
|
+
`0676d93d3e8a64ba583e95f164d122b39687b53a`. Workflow run `30073844074`, attempt 1,
|
|
105
|
+
successfully published that exact source as `borgmcp@2.0.11`; the registry records integrity
|
|
106
|
+
`sha512-429njJ7HVmWhpm8E71RoLzG57Qehy3RSxEzs6fQofVoOIiZXnK4QQDwbQe9MPfhKifbvVN5lGcDKtKAjuC76ww==`.
|
|
107
|
+
Never move, replace, reuse, or rerun that tag or workflow.
|
|
108
|
+
|
|
109
|
+
The annotated `v2.1.0` tag object
|
|
110
|
+
`e0b189bb0dd1f1b12369ab81870113925e1afc41` peels to protected-main commit
|
|
111
|
+
`10f18e4010c9c518c0ea2184d42ed5388642a53f`. Workflow run `30166498938`, attempt 1,
|
|
112
|
+
successfully published that exact source as `borgmcp@2.1.0`; the same-run artifact report records integrity
|
|
113
|
+
`sha512-Gm9n69y3C0feNDYRPEOnShLCscQJ/BoylcRMH+sDDQJmdcbjY8+U6dDTcFv3w+uH5P/IVwJydB8TGRHPXL0FQA==`.
|
|
100
114
|
Never move, replace, reuse, or rerun that tag or workflow. The next candidate
|
|
101
|
-
uses the unused `v2.
|
|
115
|
+
uses the unused `v2.1.1` identity from a fresh reviewed protected-main commit
|
|
102
116
|
and requires the complete release gate again.
|
|
103
117
|
|
|
104
118
|
## Release Prerequisites
|
|
@@ -110,9 +124,12 @@ Before creating the release tag, independently verify all of these conditions:
|
|
|
110
124
|
- the extraction review confirms no private backend secrets, deployment
|
|
111
125
|
configuration, customer data, local state, or duplicated shared contracts
|
|
112
126
|
entered the public package;
|
|
113
|
-
- the exact audited registry dependency `borgmcp-shared@0.6.
|
|
114
|
-
its canonical tarball and integrity
|
|
115
|
-
-
|
|
127
|
+
- the exact audited registry dependency `borgmcp-shared@0.6.4` remains locked to
|
|
128
|
+
its canonical tarball and integrity
|
|
129
|
+
`sha512-Wm4b0uoOAw9JCz5OTHD0Q2uXKkeWYdkVksdeZvRG8l62XGMY+G8GkNEsZT9L533LbVbQ29GhgF0htjDenQThDg==`;
|
|
130
|
+
- the client and matching published `borgmcp-server@0.1.21`, integrity
|
|
131
|
+
`sha512-g0QH4cKcsKuDyZs1MvBmvCoCLaZ4wVI04yHCGSvcAeWghWxSBH0IIF8Q5OFi1pLB7TZXOVP6D3QAabUQ2eJTFg==`, which pins
|
|
132
|
+
`borgmcp-shared@0.6.4`, pass the complete local dogfood gate;
|
|
116
133
|
- the selected stable client version is unused and the exact release commit is
|
|
117
134
|
on protected `main`;
|
|
118
135
|
- the repository and protected npm environment settings pass an operator audit;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "borgmcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
71
|
-
"borgmcp-shared": "0.6.
|
|
71
|
+
"borgmcp-shared": "0.6.4",
|
|
72
72
|
"chalk": "^5.3.0",
|
|
73
73
|
"prompts": "^2.4.2",
|
|
74
74
|
"which": "^4.0.0"
|
package/src/assimilate-cmd.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
occupiedRoleIdsForAutoRole,
|
|
8
8
|
pickDefaultRole,
|
|
9
9
|
} from './role-resolver.js';
|
|
10
|
-
import { deriveCubeName, parseGitRemote, sanitizeRemoteUrl } from './cube-name.js';
|
|
11
10
|
import { validateName } from './name-validator.js';
|
|
12
11
|
import { renderAssimilationWelcome } from './assimilate-welcome.js';
|
|
13
12
|
import { shellEscape } from './shell-escape.js';
|
|
@@ -42,7 +41,15 @@ import { installBorgPlugin } from './opencode-plugin.js';
|
|
|
42
41
|
import { computeOpenCodePort, connectOpenCodeDrone, createOpenCodeLaunchKickoff, injectInitialKickoff } from './opencode-drone.js';
|
|
43
42
|
import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
|
|
44
43
|
import { normalizeServerEndpoint } from './server-endpoint.js';
|
|
45
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
BorgServerError,
|
|
46
|
+
CubeCreationConfirmationError,
|
|
47
|
+
CubeCreationOutcomeUnknownError,
|
|
48
|
+
LegacySessionCredentialCollisionError,
|
|
49
|
+
RepositoryAssociationOperationError,
|
|
50
|
+
RepositoryAssociationOutcomeUnknownError,
|
|
51
|
+
RepositoryAssociationResolutionError,
|
|
52
|
+
} from './server-errors.js';
|
|
46
53
|
import type { SeatStatus } from './seat-probe.js';
|
|
47
54
|
import type { ServerSessionOperation } from './config.js';
|
|
48
55
|
import type { ExpectedBinding, FinalizeServerSeatOutcome, PersistedLocalSeat } from './cubes.js';
|
|
@@ -50,6 +57,20 @@ import type { SeatBinding, BindPendingSeatOutcome } from './seats.js';
|
|
|
50
57
|
import { createHash } from 'node:crypto';
|
|
51
58
|
import { buildOpenCodeLaunchArgs, type LaunchApprovalDecision } from './cli-tool-approval.js';
|
|
52
59
|
import { resolveWorkingRepo, type WorkingRepo } from './working-repo.js';
|
|
60
|
+
import type {
|
|
61
|
+
AssociateRepositoryCubeResponse,
|
|
62
|
+
CreateCubeRepository,
|
|
63
|
+
CubeTemplate,
|
|
64
|
+
} from 'borgmcp-shared/protocol';
|
|
65
|
+
import {
|
|
66
|
+
initializeRepositoryCube,
|
|
67
|
+
RepositoryAssociationConfirmationError,
|
|
68
|
+
RepositoryAssociationSaveError,
|
|
69
|
+
validRepositoryCubeName,
|
|
70
|
+
type RepositoryCubeCreation,
|
|
71
|
+
type RepositoryCubeResolution,
|
|
72
|
+
} from './repository-cube-init.js';
|
|
73
|
+
import type { GitRepositoryContext, RepositoryAssociation } from './repository-identity.js';
|
|
53
74
|
|
|
54
75
|
const PRIVATE_STATE_UNAVAILABLE_COPY = [
|
|
55
76
|
'Borg could not safely prepare its private local state.',
|
|
@@ -73,6 +94,7 @@ export interface AssimilateFlags {
|
|
|
73
94
|
export interface AssimilateArgs {
|
|
74
95
|
role: string | undefined;
|
|
75
96
|
flags: AssimilateFlags;
|
|
97
|
+
mode?: 'assimilate' | 'cube-init';
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
export interface CubeSummary {
|
|
@@ -222,6 +244,10 @@ export interface AssimilateDeps {
|
|
|
222
244
|
scrubPending: () => Promise<unknown>;
|
|
223
245
|
}) => Promise<FinalizeServerSeatOutcome>;
|
|
224
246
|
findProjectRoot: (cwd: string) => string;
|
|
247
|
+
resolveRepositoryContext: (cwd: string) => Promise<GitRepositoryContext | null>;
|
|
248
|
+
getRepositoryIdentity: (context: GitRepositoryContext) => Promise<CreateCubeRepository>;
|
|
249
|
+
getRepositoryAssociation: (trustIdentity: string, repository: CreateCubeRepository) => Promise<RepositoryAssociation | null>;
|
|
250
|
+
saveRepositoryAssociation: (trustIdentity: string, repository: CreateCubeRepository, association: RepositoryAssociation) => Promise<void>;
|
|
225
251
|
|
|
226
252
|
// gh#673 P2 (WI-1): write the borg-regen SessionStart hook into the
|
|
227
253
|
// launch root's .claude/settings.local.json (project-local; idempotent).
|
|
@@ -249,14 +275,31 @@ export interface AssimilateDeps {
|
|
|
249
275
|
serverCapabilities?: readonly string[];
|
|
250
276
|
} | null>;
|
|
251
277
|
|
|
278
|
+
resolveRepositoryCube: (
|
|
279
|
+
apiUrl: string,
|
|
280
|
+
token: string,
|
|
281
|
+
input: { workingRepoName: string; repository: CreateCubeRepository },
|
|
282
|
+
serverTrustIdentity?: string,
|
|
283
|
+
) => Promise<RepositoryCubeResolution>;
|
|
284
|
+
associateRepositoryCube: (
|
|
285
|
+
apiUrl: string,
|
|
286
|
+
token: string,
|
|
287
|
+
input: { cubeId: string; workingRepoName: string; repository: CreateCubeRepository },
|
|
288
|
+
serverTrustIdentity?: string,
|
|
289
|
+
) => Promise<AssociateRepositoryCubeResponse>;
|
|
252
290
|
listCubes: (apiUrl: string, token: string, serverTrustIdentity?: string) => Promise<CubeSummary[]>;
|
|
253
291
|
getCube: (apiUrl: string, token: string, cubeId: string, serverTrustIdentity?: string) => Promise<CubeDetail>;
|
|
254
292
|
createCube: (
|
|
255
293
|
apiUrl: string,
|
|
256
294
|
token: string,
|
|
257
|
-
params: {
|
|
295
|
+
params: {
|
|
296
|
+
name: string;
|
|
297
|
+
workingRepoName: string;
|
|
298
|
+
repository: CreateCubeRepository;
|
|
299
|
+
template: Exclude<CubeTemplate, 'default'>;
|
|
300
|
+
},
|
|
258
301
|
serverTrustIdentity?: string,
|
|
259
|
-
) => Promise<
|
|
302
|
+
) => Promise<RepositoryCubeCreation>;
|
|
260
303
|
assimilate: (
|
|
261
304
|
apiUrl: string,
|
|
262
305
|
token: string,
|
|
@@ -301,11 +344,6 @@ function affirmative(answer: string): boolean {
|
|
|
301
344
|
return normalized === '' || normalized === 'y' || normalized === 'yes';
|
|
302
345
|
}
|
|
303
346
|
|
|
304
|
-
function isLocalCubePresentationName(name: string): boolean {
|
|
305
|
-
return name.length >= 1 && name.length <= 120 &&
|
|
306
|
-
/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(name);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
347
|
async function selectAssimilationAuthority(
|
|
310
348
|
flags: AssimilateFlags,
|
|
311
349
|
deps: AssimilateDeps,
|
|
@@ -509,6 +547,7 @@ export async function runAssimilate(
|
|
|
509
547
|
args: AssimilateArgs,
|
|
510
548
|
deps: AssimilateDeps
|
|
511
549
|
): Promise<number> {
|
|
550
|
+
const mode = args.mode ?? 'assimilate';
|
|
512
551
|
// ----- Input validation (before any subprocess work) -----
|
|
513
552
|
if (args.role !== undefined) {
|
|
514
553
|
const v = validateName(args.role);
|
|
@@ -524,6 +563,29 @@ export async function runAssimilate(
|
|
|
524
563
|
return 1;
|
|
525
564
|
}
|
|
526
565
|
}
|
|
566
|
+
if (args.flags.cubeName !== undefined && !validRepositoryCubeName(args.flags.cubeName.trim())) {
|
|
567
|
+
deps.stderr('Invalid cube name. Use 1-120 letters, digits, spaces, dots, underscores, or hyphens, starting with a letter or digit.\n');
|
|
568
|
+
return 1;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
let repositoryContext: GitRepositoryContext | null;
|
|
572
|
+
try {
|
|
573
|
+
repositoryContext = await deps.resolveRepositoryContext(deps.cwd());
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error instanceof Error && error.message === 'BARE_REPOSITORY') {
|
|
576
|
+
deps.stderr('borg assimilate requires a non-bare repository worktree. Clone or check out the repository, then retry.\n');
|
|
577
|
+
return 1;
|
|
578
|
+
}
|
|
579
|
+
repositoryContext = null;
|
|
580
|
+
}
|
|
581
|
+
if (!repositoryContext) {
|
|
582
|
+
deps.stderr(
|
|
583
|
+
'No Git repository was found for this directory.\n' +
|
|
584
|
+
'Nothing was changed.\n' +
|
|
585
|
+
'Run this command inside a Git repository.\n',
|
|
586
|
+
);
|
|
587
|
+
return 1;
|
|
588
|
+
}
|
|
527
589
|
|
|
528
590
|
try {
|
|
529
591
|
await deps.preparePrivateRoot();
|
|
@@ -555,86 +617,9 @@ export async function runAssimilate(
|
|
|
555
617
|
return reportServerFailure(deps, authority.apiUrl, localSeatReadError);
|
|
556
618
|
}
|
|
557
619
|
|
|
558
|
-
|
|
559
|
-
// Resolve and, where necessary, confirm local presentation data before an
|
|
560
|
-
// owner invitation can be consumed. A declined/underivable basename must
|
|
561
|
-
// not leave a successfully enrolled client behind.
|
|
562
|
-
const projectRoot = deps.findProjectRoot(deps.cwd());
|
|
563
|
-
let cubeName: string | null;
|
|
564
|
-
if (args.flags.cubeName) {
|
|
565
|
-
cubeName = args.flags.cubeName;
|
|
566
|
-
} else {
|
|
567
|
-
const remoteResult = deps.runSync('git', ['remote', 'get-url', 'origin'], projectRoot);
|
|
568
|
-
const remoteUrl = remoteResult.status === 0 ? remoteResult.stdout : null;
|
|
569
|
-
const sanitizedRemote = remoteUrl ? sanitizeRemoteUrl(remoteUrl) : null;
|
|
570
|
-
const parsedRepo = sanitizedRemote ? parseGitRemote(sanitizedRemote) : null;
|
|
571
|
-
|
|
572
|
-
if (!parsedRepo) {
|
|
573
|
-
const bareResult = deps.runSync('git', ['rev-parse', '--is-bare-repository'], projectRoot);
|
|
574
|
-
if (bareResult.status === 0 && bareResult.stdout.trim() === 'true') {
|
|
575
|
-
deps.stderr(
|
|
576
|
-
'borg assimilate requires a non-bare repository worktree. ' +
|
|
577
|
-
(authority.kind === 'server'
|
|
578
|
-
? `Clone or check out the repository, then rerun ${localAssimilateCommand(authority.apiUrl)}.\n`
|
|
579
|
-
: 'Clone or check out the repository, then retry.\n'),
|
|
580
|
-
);
|
|
581
|
-
return 1;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
cubeName = deriveCubeName(projectRoot, remoteUrl);
|
|
586
|
-
if (!cubeName) {
|
|
587
|
-
deps.stderr(
|
|
588
|
-
'Could not derive a cube name from this repository. ' +
|
|
589
|
-
(authority.kind === 'server'
|
|
590
|
-
? `Rerun ${localAssimilateCommand(authority.apiUrl)} with \`--cube-name <name>\`.\n`
|
|
591
|
-
: 'Pass --cube-name <name> and retry.\n'),
|
|
592
|
-
);
|
|
593
|
-
return 1;
|
|
594
|
-
}
|
|
620
|
+
const projectRoot = repositoryContext.root;
|
|
595
621
|
|
|
596
|
-
|
|
597
|
-
if (sanitizedRemote) {
|
|
598
|
-
deps.stderr(
|
|
599
|
-
`Could not parse the origin remote; using directory name '${cubeName}' as the cube name.\n`,
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
if (!args.flags.yes) {
|
|
603
|
-
if (!deps.isTTY()) {
|
|
604
|
-
deps.stderr(
|
|
605
|
-
`Using directory name '${cubeName}' as the cube name requires confirmation. ` +
|
|
606
|
-
(authority.kind === 'server'
|
|
607
|
-
? `Rerun ${localAssimilateCommand(authority.apiUrl)} with \`--cube-name <name>\` or \`--yes\`.\n`
|
|
608
|
-
: 'Re-run with --cube-name <name> or --yes.\n'),
|
|
609
|
-
);
|
|
610
|
-
return 1;
|
|
611
|
-
}
|
|
612
|
-
const confirmed = await deps.prompt(
|
|
613
|
-
`No usable origin remote was found. Use directory name '${cubeName}' as the cube name? [Y/n]: `,
|
|
614
|
-
);
|
|
615
|
-
if (!affirmative(confirmed)) {
|
|
616
|
-
deps.stderr(
|
|
617
|
-
authority.kind === 'server'
|
|
618
|
-
? `Cube creation for ${authority.apiUrl} was cancelled. Rerun ` +
|
|
619
|
-
`${localAssimilateCommand(authority.apiUrl)} with \`--cube-name <name>\`.\n`
|
|
620
|
-
: 'Cube creation cancelled. Re-run with --cube-name <name> to choose a name.\n',
|
|
621
|
-
);
|
|
622
|
-
return 1;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (authority.kind === 'server' && !isLocalCubePresentationName(cubeName)) {
|
|
629
|
-
deps.stderr(
|
|
630
|
-
`Invalid cube name for ${authority.apiUrl}. Use 1–120 letters, digits, spaces, dots, ` +
|
|
631
|
-
'underscores, or hyphens, starting with a letter or digit. Rerun ' +
|
|
632
|
-
`${localAssimilateCommand(authority.apiUrl)} with \`--cube-name <name>\`.\n`,
|
|
633
|
-
);
|
|
634
|
-
return 1;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
let auth: { token: string; apiUrl: string; serverTrustIdentity: string };
|
|
622
|
+
let auth: { token: string; apiUrl: string; serverTrustIdentity: string; serverCapabilities: readonly string[] };
|
|
638
623
|
{
|
|
639
624
|
try {
|
|
640
625
|
let serverAuth: {
|
|
@@ -696,6 +681,7 @@ export async function runAssimilate(
|
|
|
696
681
|
token: serverAuth.token,
|
|
697
682
|
apiUrl: authority.apiUrl,
|
|
698
683
|
serverTrustIdentity: serverAuth.trustIdentity,
|
|
684
|
+
serverCapabilities: serverAuth.serverCapabilities ?? [],
|
|
699
685
|
};
|
|
700
686
|
} catch (error) {
|
|
701
687
|
return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true);
|
|
@@ -717,67 +703,123 @@ export async function runAssimilate(
|
|
|
717
703
|
// (no chdir has happened yet; this is a stable starting point).
|
|
718
704
|
const originalCwd = deps.cwd();
|
|
719
705
|
|
|
720
|
-
|
|
721
|
-
// gh#653 B4: announce each network step. These calls take 2–5s and were
|
|
722
|
-
// previously silent, so a user read the wait as a hang and Ctrl-C'd mid-run.
|
|
723
|
-
deps.stderr('Checking your cubes…\n');
|
|
724
|
-
let allCubes: CubeSummary[];
|
|
706
|
+
let initialized;
|
|
725
707
|
try {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
708
|
+
initialized = await initializeRepositoryCube({
|
|
709
|
+
mode,
|
|
710
|
+
context: repositoryContext,
|
|
711
|
+
serverOrigin: auth.apiUrl,
|
|
712
|
+
flags: args.flags,
|
|
713
|
+
canCreate: auth.serverCapabilities.includes('create_cube'),
|
|
714
|
+
}, {
|
|
715
|
+
isTTY: deps.isTTY,
|
|
716
|
+
prompt: deps.prompt,
|
|
717
|
+
write: deps.stderr,
|
|
718
|
+
getIdentity: deps.getRepositoryIdentity,
|
|
719
|
+
getAssociation: (repository) => deps.getRepositoryAssociation(auth.serverTrustIdentity, repository),
|
|
720
|
+
saveAssociation: (repository, association) =>
|
|
721
|
+
deps.saveRepositoryAssociation(auth.serverTrustIdentity, repository, association),
|
|
722
|
+
resolveAssociation: (repository, workingRepoName) => deps.resolveRepositoryCube(
|
|
723
|
+
auth.apiUrl,
|
|
724
|
+
auth.token,
|
|
725
|
+
{ repository, workingRepoName },
|
|
726
|
+
auth.serverTrustIdentity,
|
|
727
|
+
),
|
|
728
|
+
listCubes: () => deps.listCubes(auth.apiUrl, auth.token, auth.serverTrustIdentity),
|
|
729
|
+
associateCube: (params) => deps.associateRepositoryCube(
|
|
738
730
|
auth.apiUrl,
|
|
739
731
|
auth.token,
|
|
740
|
-
|
|
732
|
+
params,
|
|
741
733
|
auth.serverTrustIdentity,
|
|
734
|
+
),
|
|
735
|
+
getCube: (cubeId) => deps.getCube(auth.apiUrl, auth.token, cubeId, auth.serverTrustIdentity),
|
|
736
|
+
createCube: (params) => deps.createCube(auth.apiUrl, auth.token, params, auth.serverTrustIdentity),
|
|
737
|
+
});
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (error instanceof RepositoryAssociationOutcomeUnknownError) {
|
|
740
|
+
deps.stderr(
|
|
741
|
+
'Repository cube association outcome is unknown.\n' +
|
|
742
|
+
'The server may have created the repository binding; no local repository association was saved and no drone was created.\n' +
|
|
743
|
+
'Run the same command again; Borg will first resolve the authoritative server association without creating a cube.\n',
|
|
742
744
|
);
|
|
743
|
-
|
|
744
|
-
return reportServerFailure(deps, authority.apiUrl, error);
|
|
745
|
+
return 1;
|
|
745
746
|
}
|
|
746
|
-
|
|
747
|
-
} else {
|
|
748
|
-
// ----- Step 4a: First-drone bootstrap (template selection) -----
|
|
749
|
-
let chosenTemplate: string | undefined;
|
|
750
|
-
if (args.flags.noTemplate ||
|
|
751
|
-
(args.flags.template !== undefined && args.flags.template !== 'default')) {
|
|
747
|
+
if (error instanceof RepositoryAssociationResolutionError) {
|
|
752
748
|
deps.stderr(
|
|
753
|
-
|
|
754
|
-
|
|
749
|
+
'Repository cube association could not be resolved.\n' +
|
|
750
|
+
'Verify that the server is reachable and the client and server versions match, then run the same command again.\n' +
|
|
751
|
+
'No cube, repository binding, or drone was created.\n',
|
|
755
752
|
);
|
|
756
753
|
return 1;
|
|
757
754
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
cubeDetail = await deps.createCube(
|
|
771
|
-
auth.apiUrl,
|
|
772
|
-
auth.token,
|
|
773
|
-
createParams,
|
|
774
|
-
auth.serverTrustIdentity,
|
|
755
|
+
if (error instanceof RepositoryAssociationOperationError) {
|
|
756
|
+
const recovery = error.failure === 'repository-already-associated'
|
|
757
|
+
? 'This repository is already associated with another cube. Run the same command again to use the existing managed association, or ask the server operator to correct the repository binding.'
|
|
758
|
+
: error.failure === 'cube-already-associated'
|
|
759
|
+
? 'The selected cube is already associated with another repository. Choose a different cube, or run the command from the repository already linked to that cube.'
|
|
760
|
+
: error.failure === 'access-denied'
|
|
761
|
+
? 'This enrolled client does not have permission to manage the selected cube. Ask the server operator to grant this client management access, then run the same command again.'
|
|
762
|
+
: 'The selected cube does not have valid authoritative roles. Ask the server operator to repair its role configuration, or choose another cube.';
|
|
763
|
+
deps.stderr(
|
|
764
|
+
'Repository cube association could not be completed.\n' +
|
|
765
|
+
`${recovery}\n` +
|
|
766
|
+
'No cube, repository binding, or drone was created.\n',
|
|
775
767
|
);
|
|
776
|
-
|
|
777
|
-
|
|
768
|
+
return 1;
|
|
769
|
+
}
|
|
770
|
+
if (error instanceof RepositoryAssociationConfirmationError) {
|
|
771
|
+
deps.stderr(
|
|
772
|
+
'Repository cube association could not be confirmed.\n' +
|
|
773
|
+
'The server may have created the repository binding; no local repository association was saved and no drone was created.\n' +
|
|
774
|
+
'Run the same command again; Borg will resolve authoritative server state before creating or associating a cube.\n',
|
|
775
|
+
);
|
|
776
|
+
return 1;
|
|
777
|
+
}
|
|
778
|
+
if (error instanceof RepositoryAssociationSaveError) {
|
|
779
|
+
deps.stderr(
|
|
780
|
+
'The repository cube was confirmed, but Borg could not save its local repository association.\n' +
|
|
781
|
+
'No drone was created.\n' +
|
|
782
|
+
'Run the same command again; the server will resolve the existing cube and restore the local association.\n',
|
|
783
|
+
);
|
|
784
|
+
return 1;
|
|
785
|
+
}
|
|
786
|
+
if (error instanceof CubeCreationOutcomeUnknownError) {
|
|
787
|
+
deps.stderr(
|
|
788
|
+
'Cube creation outcome is unconfirmed.\n' +
|
|
789
|
+
'The server may have created the cube and repository binding; no local repository association was saved and no drone was created.\n' +
|
|
790
|
+
'Run the same command again; Borg will resolve authoritative server state before creating a cube.\n',
|
|
791
|
+
);
|
|
792
|
+
return 1;
|
|
778
793
|
}
|
|
779
|
-
|
|
794
|
+
if (error instanceof CubeCreationConfirmationError) {
|
|
795
|
+
deps.stderr(
|
|
796
|
+
'Cube creation could not be confirmed.\n' +
|
|
797
|
+
'The server may have created the cube and repository binding; no local repository association was saved and no drone was created.\n' +
|
|
798
|
+
'Run the same command again; Borg will resolve authoritative server state before creating a cube.\n',
|
|
799
|
+
);
|
|
800
|
+
return 1;
|
|
801
|
+
}
|
|
802
|
+
if (error instanceof BorgServerError && error.code === 'CREATE_CUBE_DENIED') {
|
|
803
|
+
deps.stderr(
|
|
804
|
+
`This enrolled client cannot create a cube on ${auth.apiUrl}. ` +
|
|
805
|
+
`Ask the server operator to grant access to a cube, then rerun ${localAssimilateCommand(auth.apiUrl)}.\n`,
|
|
806
|
+
);
|
|
807
|
+
return 1;
|
|
808
|
+
}
|
|
809
|
+
if (error instanceof BorgServerError) {
|
|
810
|
+
return reportServerFailure(deps, auth.apiUrl, error);
|
|
811
|
+
}
|
|
812
|
+
deps.stderr(
|
|
813
|
+
'Repository cube initialization failed.\n' +
|
|
814
|
+
'The server may have created or associated a cube; local repository state may be incomplete and no drone was created.\n' +
|
|
815
|
+
'Run the same command again; Borg will resolve authoritative server state before creating or associating a cube.\n',
|
|
816
|
+
);
|
|
817
|
+
return 1;
|
|
780
818
|
}
|
|
819
|
+
if (initialized.kind === 'stop') return initialized.code;
|
|
820
|
+
const cubeDetail = initialized.creation.cube as CubeDetail;
|
|
821
|
+
const isFirstDrone = (cubeDetail.drones?.length ?? 0) === 0;
|
|
822
|
+
if (mode === 'cube-init') return 0;
|
|
781
823
|
|
|
782
824
|
// Read the worktree identity before role selection. A live local seat must
|
|
783
825
|
// retain its original role so the attach request reuses the exact durable
|
|
@@ -1754,7 +1796,7 @@ export async function runAssimilate(
|
|
|
1754
1796
|
serverUrl,
|
|
1755
1797
|
directory: agentCwd,
|
|
1756
1798
|
droneLabel: result.drone_label,
|
|
1757
|
-
cubeName:
|
|
1799
|
+
cubeName: cubeDetail.name,
|
|
1758
1800
|
})
|
|
1759
1801
|
.then(() => injectInitialKickoff(launchKickoff))
|
|
1760
1802
|
.catch(() => {});
|