borgmcp 2.0.11 → 2.1.0
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 +13 -4
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +134 -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 +44 -18
- 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 +22 -5
- 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/repository-cube-init.d.ts +57 -0
- package/dist/repository-cube-init.d.ts.map +1 -0
- package/dist/repository-cube-init.js +196 -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 +6 -0
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +12 -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 +8 -4
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +15 -6
- package/dist/server-handshake.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +7 -7
- package/docs/LOCAL_SERVER.md +35 -6
- package/docs/RELEASING.md +11 -3
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +166 -140
- package/src/assimilate-deps.ts +59 -17
- package/src/claude.ts +34 -14
- package/src/cli-help.ts +25 -5
- package/src/config.ts +30 -8
- package/src/remote-client.ts +1 -1
- package/src/repository-cube-init.ts +273 -0
- package/src/repository-identity.ts +199 -0
- package/src/server-errors.ts +14 -0
- package/src/server-facade.ts +47 -2
- package/src/server-handshake.ts +33 -7
|
@@ -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.3` 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`, and `2.0.
|
|
25
|
-
published, so the next candidate identity is `2.0
|
|
24
|
+
package creation or npm publication. The `2.0.8`, `2.0.9`, `2.0.10`, and `2.0.11`
|
|
25
|
+
successors were published, so the next candidate identity is `2.1.0`. Extraction and
|
|
26
26
|
versioning do not authorize publication.
|
|
27
27
|
|
|
28
28
|
## Review Holds
|
|
@@ -38,9 +38,9 @@ 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
|
-
`borgmcp-server@0.1.
|
|
43
|
-
`borgmcp@2.0.
|
|
41
|
+
resolves to the audited registry `borgmcp-shared@0.6.3`. The matching
|
|
42
|
+
`borgmcp-server@0.1.20` release is published and registry-verified and pins the
|
|
43
|
+
same `borgmcp-shared@0.6.3` contract. Client `borgmcp@2.0.11` is published. The immutable `v2.0.7`
|
|
44
44
|
attempt failed before publication and remains preserved. Publication of the next
|
|
45
|
-
candidate remains gated by reviewed `v2.0
|
|
45
|
+
candidate remains gated by reviewed `v2.1.0` source, a fresh annotated tag, and
|
|
46
46
|
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 Skip confirmation prompts
|
|
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,20 @@ 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 guided repository-cube
|
|
95
|
+
flow. Borg shows the repository and server, proposes an editable name, offers
|
|
96
|
+
the Software Development and Starter templates, and asks for one confirmation.
|
|
97
|
+
`--cube-name <name>` and `--template software-dev|starter` supply those values
|
|
98
|
+
directly; `--yes` accepts the repository default name and Software Development
|
|
99
|
+
template. `borg server cube init` stops after authoritative cube readback and
|
|
100
|
+
never creates a drone. Existing repository associations use zero prompts and
|
|
101
|
+
report that creation flags were unused. Bare repositories fail closed.
|
|
102
|
+
|
|
103
|
+
For repositories with a canonical public `origin`, that origin is the stable
|
|
104
|
+
repository identity. Repositories without one receive an invisible UUID stored
|
|
105
|
+
in an owner-only local file. Its lookup key is an HMAC of the canonical Git
|
|
106
|
+
common directory, so linked worktrees share one identity and local paths are not
|
|
107
|
+
stored in cleartext. A malformed identity secret or state file fails closed.
|
|
80
108
|
|
|
81
109
|
The connection is HTTPS-only. Borg validates the server trust material, stores
|
|
82
110
|
parent enrollment credentials in `~/.borg/credentials` and session credentials
|
|
@@ -89,7 +117,8 @@ The lifecycle facade invokes the separately installed `borgmcp-server`; it does
|
|
|
89
117
|
not bundle the server into the client. The server must be running and trusted
|
|
90
118
|
before assimilation. An owner enrollment carrying the persisted `create_cube`
|
|
91
119
|
capability creates one idempotent cube per repository during normal
|
|
92
|
-
assimilation
|
|
120
|
+
assimilation or `borg server cube init`, using Software Development or Starter;
|
|
121
|
+
repeating an
|
|
93
122
|
ambiguous request does not duplicate the cube, and distinct repositories can
|
|
94
123
|
create distinct bounded cubes. An ordinary enrolled client is denied before a
|
|
95
124
|
create request is sent. Cloud-only capabilities fail explicitly rather than
|
|
@@ -139,7 +168,7 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
|
|
|
139
168
|
|
|
140
169
|
## Release status
|
|
141
170
|
|
|
142
|
-
This self-hosted path consumes the published `borgmcp-shared@0.6.
|
|
171
|
+
This self-hosted path consumes the published `borgmcp-shared@0.6.3` v4 registry
|
|
143
172
|
release. The matching server owner-enrollment, cube-create, attach, restart, log,
|
|
144
173
|
and SSE implementation must also pass the full process-level local dogfood gate.
|
|
145
174
|
Until that gate opens the self-hosted path remains preview-only, and the client
|
package/docs/RELEASING.md
CHANGED
|
@@ -97,8 +97,15 @@ 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==`.
|
|
100
107
|
Never move, replace, reuse, or rerun that tag or workflow. The next candidate
|
|
101
|
-
uses the unused `v2.0
|
|
108
|
+
uses the unused `v2.1.0` identity from a fresh reviewed protected-main commit
|
|
102
109
|
and requires the complete release gate again.
|
|
103
110
|
|
|
104
111
|
## Release Prerequisites
|
|
@@ -110,9 +117,10 @@ Before creating the release tag, independently verify all of these conditions:
|
|
|
110
117
|
- the extraction review confirms no private backend secrets, deployment
|
|
111
118
|
configuration, customer data, local state, or duplicated shared contracts
|
|
112
119
|
entered the public package;
|
|
113
|
-
- the exact audited registry dependency `borgmcp-shared@0.6.
|
|
120
|
+
- the exact audited registry dependency `borgmcp-shared@0.6.3` remains locked to
|
|
114
121
|
its canonical tarball and integrity;
|
|
115
|
-
- the client and matching server
|
|
122
|
+
- the client and matching published `borgmcp-server@0.1.20`, which pins
|
|
123
|
+
`borgmcp-shared@0.6.3`, pass the complete local dogfood gate;
|
|
116
124
|
- the selected stable client version is unused and the exact release commit is
|
|
117
125
|
on protected `main`;
|
|
118
126
|
- 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.0
|
|
3
|
+
"version": "2.1.0",
|
|
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.3",
|
|
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,12 @@ 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
|
+
} from './server-errors.js';
|
|
46
50
|
import type { SeatStatus } from './seat-probe.js';
|
|
47
51
|
import type { ServerSessionOperation } from './config.js';
|
|
48
52
|
import type { ExpectedBinding, FinalizeServerSeatOutcome, PersistedLocalSeat } from './cubes.js';
|
|
@@ -50,6 +54,14 @@ import type { SeatBinding, BindPendingSeatOutcome } from './seats.js';
|
|
|
50
54
|
import { createHash } from 'node:crypto';
|
|
51
55
|
import { buildOpenCodeLaunchArgs, type LaunchApprovalDecision } from './cli-tool-approval.js';
|
|
52
56
|
import { resolveWorkingRepo, type WorkingRepo } from './working-repo.js';
|
|
57
|
+
import type { CreateCubeRepository, CubeTemplate } from 'borgmcp-shared/protocol';
|
|
58
|
+
import {
|
|
59
|
+
initializeRepositoryCube,
|
|
60
|
+
RepositoryAssociationSaveError,
|
|
61
|
+
validRepositoryCubeName,
|
|
62
|
+
type RepositoryCubeCreation,
|
|
63
|
+
} from './repository-cube-init.js';
|
|
64
|
+
import type { GitRepositoryContext, RepositoryAssociation } from './repository-identity.js';
|
|
53
65
|
|
|
54
66
|
const PRIVATE_STATE_UNAVAILABLE_COPY = [
|
|
55
67
|
'Borg could not safely prepare its private local state.',
|
|
@@ -73,6 +85,7 @@ export interface AssimilateFlags {
|
|
|
73
85
|
export interface AssimilateArgs {
|
|
74
86
|
role: string | undefined;
|
|
75
87
|
flags: AssimilateFlags;
|
|
88
|
+
mode?: 'assimilate' | 'cube-init';
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
export interface CubeSummary {
|
|
@@ -222,6 +235,10 @@ export interface AssimilateDeps {
|
|
|
222
235
|
scrubPending: () => Promise<unknown>;
|
|
223
236
|
}) => Promise<FinalizeServerSeatOutcome>;
|
|
224
237
|
findProjectRoot: (cwd: string) => string;
|
|
238
|
+
resolveRepositoryContext: (cwd: string) => Promise<GitRepositoryContext | null>;
|
|
239
|
+
getRepositoryIdentity: (context: GitRepositoryContext) => Promise<CreateCubeRepository>;
|
|
240
|
+
getRepositoryAssociation: (trustIdentity: string, repository: CreateCubeRepository) => Promise<RepositoryAssociation | null>;
|
|
241
|
+
saveRepositoryAssociation: (trustIdentity: string, repository: CreateCubeRepository, association: RepositoryAssociation) => Promise<void>;
|
|
225
242
|
|
|
226
243
|
// gh#673 P2 (WI-1): write the borg-regen SessionStart hook into the
|
|
227
244
|
// launch root's .claude/settings.local.json (project-local; idempotent).
|
|
@@ -254,9 +271,14 @@ export interface AssimilateDeps {
|
|
|
254
271
|
createCube: (
|
|
255
272
|
apiUrl: string,
|
|
256
273
|
token: string,
|
|
257
|
-
params: {
|
|
274
|
+
params: {
|
|
275
|
+
name: string;
|
|
276
|
+
workingRepoName: string;
|
|
277
|
+
repository: CreateCubeRepository;
|
|
278
|
+
template: Exclude<CubeTemplate, 'default'>;
|
|
279
|
+
},
|
|
258
280
|
serverTrustIdentity?: string,
|
|
259
|
-
) => Promise<
|
|
281
|
+
) => Promise<RepositoryCubeCreation>;
|
|
260
282
|
assimilate: (
|
|
261
283
|
apiUrl: string,
|
|
262
284
|
token: string,
|
|
@@ -301,11 +323,6 @@ function affirmative(answer: string): boolean {
|
|
|
301
323
|
return normalized === '' || normalized === 'y' || normalized === 'yes';
|
|
302
324
|
}
|
|
303
325
|
|
|
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
326
|
async function selectAssimilationAuthority(
|
|
310
327
|
flags: AssimilateFlags,
|
|
311
328
|
deps: AssimilateDeps,
|
|
@@ -509,6 +526,7 @@ export async function runAssimilate(
|
|
|
509
526
|
args: AssimilateArgs,
|
|
510
527
|
deps: AssimilateDeps
|
|
511
528
|
): Promise<number> {
|
|
529
|
+
const mode = args.mode ?? 'assimilate';
|
|
512
530
|
// ----- Input validation (before any subprocess work) -----
|
|
513
531
|
if (args.role !== undefined) {
|
|
514
532
|
const v = validateName(args.role);
|
|
@@ -524,6 +542,29 @@ export async function runAssimilate(
|
|
|
524
542
|
return 1;
|
|
525
543
|
}
|
|
526
544
|
}
|
|
545
|
+
if (args.flags.cubeName !== undefined && !validRepositoryCubeName(args.flags.cubeName.trim())) {
|
|
546
|
+
deps.stderr('Invalid cube name. Use 1-120 letters, digits, spaces, dots, underscores, or hyphens, starting with a letter or digit.\n');
|
|
547
|
+
return 1;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let repositoryContext: GitRepositoryContext | null;
|
|
551
|
+
try {
|
|
552
|
+
repositoryContext = await deps.resolveRepositoryContext(deps.cwd());
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (error instanceof Error && error.message === 'BARE_REPOSITORY') {
|
|
555
|
+
deps.stderr('borg assimilate requires a non-bare repository worktree. Clone or check out the repository, then retry.\n');
|
|
556
|
+
return 1;
|
|
557
|
+
}
|
|
558
|
+
repositoryContext = null;
|
|
559
|
+
}
|
|
560
|
+
if (!repositoryContext) {
|
|
561
|
+
deps.stderr(
|
|
562
|
+
'No Git repository was found for this directory.\n' +
|
|
563
|
+
'Nothing was changed.\n' +
|
|
564
|
+
'Run this command inside a Git repository.\n',
|
|
565
|
+
);
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
527
568
|
|
|
528
569
|
try {
|
|
529
570
|
await deps.preparePrivateRoot();
|
|
@@ -555,86 +596,9 @@ export async function runAssimilate(
|
|
|
555
596
|
return reportServerFailure(deps, authority.apiUrl, localSeatReadError);
|
|
556
597
|
}
|
|
557
598
|
|
|
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
|
-
}
|
|
599
|
+
const projectRoot = repositoryContext.root;
|
|
584
600
|
|
|
585
|
-
|
|
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
|
-
}
|
|
595
|
-
|
|
596
|
-
if (!parsedRepo) {
|
|
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 };
|
|
601
|
+
let auth: { token: string; apiUrl: string; serverTrustIdentity: string; serverCapabilities: readonly string[] };
|
|
638
602
|
{
|
|
639
603
|
try {
|
|
640
604
|
let serverAuth: {
|
|
@@ -696,6 +660,7 @@ export async function runAssimilate(
|
|
|
696
660
|
token: serverAuth.token,
|
|
697
661
|
apiUrl: authority.apiUrl,
|
|
698
662
|
serverTrustIdentity: serverAuth.trustIdentity,
|
|
663
|
+
serverCapabilities: serverAuth.serverCapabilities ?? [],
|
|
699
664
|
};
|
|
700
665
|
} catch (error) {
|
|
701
666
|
return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true);
|
|
@@ -717,67 +682,128 @@ export async function runAssimilate(
|
|
|
717
682
|
// (no chdir has happened yet; this is a stable starting point).
|
|
718
683
|
const originalCwd = deps.cwd();
|
|
719
684
|
|
|
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[];
|
|
685
|
+
let initialized;
|
|
725
686
|
try {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
687
|
+
if (!auth.serverCapabilities.includes('create_cube') && !existing) {
|
|
688
|
+
const repository = await deps.getRepositoryIdentity(repositoryContext);
|
|
689
|
+
const association = await deps.getRepositoryAssociation(auth.serverTrustIdentity, repository);
|
|
690
|
+
if (association) {
|
|
691
|
+
initialized = await initializeRepositoryCube({
|
|
692
|
+
mode, context: repositoryContext, serverOrigin: auth.apiUrl, flags: args.flags,
|
|
693
|
+
}, {
|
|
694
|
+
isTTY: deps.isTTY,
|
|
695
|
+
prompt: deps.prompt,
|
|
696
|
+
write: deps.stderr,
|
|
697
|
+
getIdentity: async () => repository,
|
|
698
|
+
getAssociation: async () => association,
|
|
699
|
+
saveAssociation: (identity, saved) =>
|
|
700
|
+
deps.saveRepositoryAssociation(auth.serverTrustIdentity, identity, saved),
|
|
701
|
+
getCube: (cubeId) => deps.getCube(auth.apiUrl, auth.token, cubeId, auth.serverTrustIdentity),
|
|
702
|
+
createCube: (params) => deps.createCube(auth.apiUrl, auth.token, params, auth.serverTrustIdentity),
|
|
703
|
+
});
|
|
704
|
+
} else {
|
|
705
|
+
const candidateName = args.flags.cubeName ?? repositoryContext.derivedName;
|
|
706
|
+
const accessible = await deps.listCubes(auth.apiUrl, auth.token, auth.serverTrustIdentity);
|
|
707
|
+
const matches = accessible.filter((cube) => cube.name === candidateName);
|
|
708
|
+
if (matches.length !== 1) {
|
|
709
|
+
deps.stderr(matches.length === 0
|
|
710
|
+
? `This enrolled client cannot create a cube on ${auth.apiUrl}. Ask the server operator to grant access to a cube, then rerun ${localAssimilateCommand(auth.apiUrl)}.\n`
|
|
711
|
+
: `More than one accessible cube is named '${candidateName}'. Nothing was changed. Ask the server operator for an unambiguous repository cube grant.\n`);
|
|
712
|
+
return 1;
|
|
713
|
+
}
|
|
714
|
+
const cube = await deps.getCube(auth.apiUrl, auth.token, matches[0].id, auth.serverTrustIdentity);
|
|
715
|
+
const association = {
|
|
716
|
+
cubeId: cube.id,
|
|
717
|
+
name: cube.name,
|
|
718
|
+
workingRepoName: repositoryContext.derivedName,
|
|
719
|
+
template: 'default' as const,
|
|
720
|
+
};
|
|
721
|
+
try {
|
|
722
|
+
await deps.saveRepositoryAssociation(auth.serverTrustIdentity, repository, association);
|
|
723
|
+
} catch {
|
|
724
|
+
throw new RepositoryAssociationSaveError();
|
|
725
|
+
}
|
|
726
|
+
const response = {
|
|
727
|
+
result: 'resolved' as const,
|
|
728
|
+
cube_id: cube.id,
|
|
729
|
+
name: cube.name,
|
|
730
|
+
working_repo_name: repositoryContext.derivedName,
|
|
731
|
+
repository,
|
|
732
|
+
template: 'default' as const,
|
|
733
|
+
human_seat_role_id: cube.roles.find((role) => role.is_human_seat)?.id ?? '',
|
|
734
|
+
default_worker_role_id: cube.roles.find((role) => role.is_default)?.id ?? '',
|
|
735
|
+
access: 'manage' as const,
|
|
736
|
+
};
|
|
737
|
+
deps.stderr(
|
|
738
|
+
`Cube already initialized.\n Name: ${cube.name}\n Template: Default (legacy)\n` +
|
|
739
|
+
` Repository: ${repositoryContext.root}\n Server: ${auth.apiUrl}\n` +
|
|
740
|
+
(mode === 'cube-init'
|
|
741
|
+
? `No drone was created.\nNext: borg assimilate --host ${shellEscape(auth.apiUrl)}\n`
|
|
742
|
+
: 'Continuing with role and seat setup...\n'),
|
|
743
|
+
);
|
|
744
|
+
initialized = { kind: 'success' as const, creation: { response, cube }, existing: true };
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
initialized = await initializeRepositoryCube({
|
|
748
|
+
mode,
|
|
749
|
+
context: repositoryContext,
|
|
750
|
+
serverOrigin: auth.apiUrl,
|
|
751
|
+
flags: args.flags,
|
|
752
|
+
}, {
|
|
753
|
+
isTTY: deps.isTTY,
|
|
754
|
+
prompt: deps.prompt,
|
|
755
|
+
write: deps.stderr,
|
|
756
|
+
getIdentity: deps.getRepositoryIdentity,
|
|
757
|
+
getAssociation: (repository) => deps.getRepositoryAssociation(auth.serverTrustIdentity, repository),
|
|
758
|
+
saveAssociation: (repository, association) =>
|
|
759
|
+
deps.saveRepositoryAssociation(auth.serverTrustIdentity, repository, association),
|
|
760
|
+
getCube: (cubeId) => deps.getCube(auth.apiUrl, auth.token, cubeId, auth.serverTrustIdentity),
|
|
761
|
+
createCube: (params) => deps.createCube(auth.apiUrl, auth.token, params, auth.serverTrustIdentity),
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
} catch (error) {
|
|
765
|
+
if (error instanceof RepositoryAssociationSaveError) {
|
|
766
|
+
deps.stderr(
|
|
767
|
+
'The repository cube was confirmed, but Borg could not save its local repository association.\n' +
|
|
768
|
+
'No drone was created.\n' +
|
|
769
|
+
'Run the same command again; the server will resolve the existing cube and restore the local association.\n',
|
|
742
770
|
);
|
|
743
|
-
|
|
744
|
-
return reportServerFailure(deps, authority.apiUrl, error);
|
|
771
|
+
return 1;
|
|
745
772
|
}
|
|
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')) {
|
|
773
|
+
if (error instanceof CubeCreationOutcomeUnknownError) {
|
|
752
774
|
deps.stderr(
|
|
753
|
-
|
|
754
|
-
|
|
775
|
+
'Cube creation outcome is unknown.\n' +
|
|
776
|
+
'No local repository association was saved.\n' +
|
|
777
|
+
'Run the same command again; the server will resolve the original request or return a conflict.\n',
|
|
755
778
|
);
|
|
756
779
|
return 1;
|
|
757
780
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
? {
|
|
765
|
-
name: cubeName ?? undefined,
|
|
766
|
-
template: chosenTemplate,
|
|
767
|
-
projectRoot,
|
|
768
|
-
}
|
|
769
|
-
: { name: cubeName ?? undefined };
|
|
770
|
-
cubeDetail = await deps.createCube(
|
|
771
|
-
auth.apiUrl,
|
|
772
|
-
auth.token,
|
|
773
|
-
createParams,
|
|
774
|
-
auth.serverTrustIdentity,
|
|
781
|
+
if (error instanceof CubeCreationConfirmationError) {
|
|
782
|
+
deps.stderr(
|
|
783
|
+
'Cube creation could not be confirmed.\n' +
|
|
784
|
+
`${error.message}\n` +
|
|
785
|
+
'No local repository association was saved.\n' +
|
|
786
|
+
'Resolve the conflict, then run the same command again.\n',
|
|
775
787
|
);
|
|
776
|
-
|
|
777
|
-
|
|
788
|
+
return 1;
|
|
789
|
+
}
|
|
790
|
+
if (error instanceof BorgServerError && error.code === 'CREATE_CUBE_DENIED') {
|
|
791
|
+
deps.stderr(
|
|
792
|
+
`This enrolled client cannot create a cube on ${auth.apiUrl}. ` +
|
|
793
|
+
`Ask the server operator to grant access to a cube, then rerun ${localAssimilateCommand(auth.apiUrl)}.\n`,
|
|
794
|
+
);
|
|
795
|
+
return 1;
|
|
778
796
|
}
|
|
779
|
-
|
|
797
|
+
if (error instanceof BorgServerError) {
|
|
798
|
+
return reportServerFailure(deps, auth.apiUrl, error);
|
|
799
|
+
}
|
|
800
|
+
deps.stderr('Could not create cube.\nThe selected Borg server rejected the request.\nNothing was changed.\n');
|
|
801
|
+
return 1;
|
|
780
802
|
}
|
|
803
|
+
if (initialized.kind === 'stop') return initialized.code;
|
|
804
|
+
const cubeDetail = initialized.creation.cube as CubeDetail;
|
|
805
|
+
const isFirstDrone = (cubeDetail.drones?.length ?? 0) === 0;
|
|
806
|
+
if (mode === 'cube-init') return 0;
|
|
781
807
|
|
|
782
808
|
// Read the worktree identity before role selection. A live local seat must
|
|
783
809
|
// retain its original role so the attach request reuses the exact durable
|
|
@@ -1754,7 +1780,7 @@ export async function runAssimilate(
|
|
|
1754
1780
|
serverUrl,
|
|
1755
1781
|
directory: agentCwd,
|
|
1756
1782
|
droneLabel: result.drone_label,
|
|
1757
|
-
cubeName:
|
|
1783
|
+
cubeName: cubeDetail.name,
|
|
1758
1784
|
})
|
|
1759
1785
|
.then(() => injectInitialKickoff(launchKickoff))
|
|
1760
1786
|
.catch(() => {});
|