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.
Files changed (57) hide show
  1. package/README.md +11 -3
  2. package/dist/assimilate-cmd.d.ts +13 -4
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +134 -110
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts +7 -1
  7. package/dist/assimilate-deps.d.ts.map +1 -1
  8. package/dist/assimilate-deps.js +44 -18
  9. package/dist/assimilate-deps.js.map +1 -1
  10. package/dist/claude.d.ts +3 -1
  11. package/dist/claude.d.ts.map +1 -1
  12. package/dist/claude.js +26 -12
  13. package/dist/claude.js.map +1 -1
  14. package/dist/cli-help.d.ts +2 -0
  15. package/dist/cli-help.d.ts.map +1 -1
  16. package/dist/cli-help.js +22 -5
  17. package/dist/cli-help.js.map +1 -1
  18. package/dist/config.d.ts +7 -4
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +21 -4
  21. package/dist/config.js.map +1 -1
  22. package/dist/remote-client.js +1 -1
  23. package/dist/repository-cube-init.d.ts +57 -0
  24. package/dist/repository-cube-init.d.ts.map +1 -0
  25. package/dist/repository-cube-init.js +196 -0
  26. package/dist/repository-cube-init.js.map +1 -0
  27. package/dist/repository-identity.d.ts +29 -0
  28. package/dist/repository-identity.d.ts.map +1 -0
  29. package/dist/repository-identity.js +143 -0
  30. package/dist/repository-identity.js.map +1 -0
  31. package/dist/server-errors.d.ts +6 -0
  32. package/dist/server-errors.d.ts.map +1 -1
  33. package/dist/server-errors.js +12 -0
  34. package/dist/server-errors.js.map +1 -1
  35. package/dist/server-facade.d.ts +11 -1
  36. package/dist/server-facade.d.ts.map +1 -1
  37. package/dist/server-facade.js +33 -3
  38. package/dist/server-facade.js.map +1 -1
  39. package/dist/server-handshake.d.ts +8 -4
  40. package/dist/server-handshake.d.ts.map +1 -1
  41. package/dist/server-handshake.js +15 -6
  42. package/dist/server-handshake.js.map +1 -1
  43. package/docs/EXTRACTION_PROVENANCE.md +7 -7
  44. package/docs/LOCAL_SERVER.md +35 -6
  45. package/docs/RELEASING.md +11 -3
  46. package/package.json +2 -2
  47. package/src/assimilate-cmd.ts +166 -140
  48. package/src/assimilate-deps.ts +59 -17
  49. package/src/claude.ts +34 -14
  50. package/src/cli-help.ts +25 -5
  51. package/src/config.ts +30 -8
  52. package/src/remote-client.ts +1 -1
  53. package/src/repository-cube-init.ts +273 -0
  54. package/src/repository-identity.ts +199 -0
  55. package/src/server-errors.ts +14 -0
  56. package/src/server-facade.ts +47 -2
  57. 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.2` and a fresh standalone lockfile.
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.10` successors were
25
- published, so the next candidate identity is `2.0.11`. Extraction and
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.2`. The matching
42
- `borgmcp-server@0.1.17` release is published and registry-verified. Client
43
- `borgmcp@2.0.10` is published. The immutable `v2.0.7`
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.11` source, a fresh annotated tag, and
45
+ candidate remains gated by reviewed `v2.1.0` source, a fresh annotated tag, and
46
46
  the exact-artifact and protected-publication gates.
@@ -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
- `--cube-name <name>` explicitly selects the repository cube name. Without an
77
- explicit name, Borg uses the `origin` repository name or, when `origin` is
78
- absent, proposes the sanitized repository-directory basename. Confirm
79
- interactively or pass `--yes`; bare repositories fail closed.
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, using the server-owned `default` role template; repeating an
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.2` v3 registry
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.11` identity from a fresh reviewed protected-main commit
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.2` remains locked to
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 pass the complete local dogfood gate;
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.11",
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.2",
71
+ "borgmcp-shared": "0.6.3",
72
72
  "chalk": "^5.3.0",
73
73
  "prompts": "^2.4.2",
74
74
  "which": "^4.0.0"
@@ -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 { BorgServerError, LegacySessionCredentialCollisionError } from './server-errors.js';
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: { name?: string; template?: string; projectRoot?: string },
274
+ params: {
275
+ name: string;
276
+ workingRepoName: string;
277
+ repository: CreateCubeRepository;
278
+ template: Exclude<CubeTemplate, 'default'>;
279
+ },
258
280
  serverTrustIdentity?: string,
259
- ) => Promise<CubeDetail>;
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
- // ----- Repository + cube-name preflight -----
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
- 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
- }
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
- // ----- Step 3: Cube existence check (with auto-refresh on auth failure) -----
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
- allCubes = await deps.listCubes(auth.apiUrl, auth.token, auth.serverTrustIdentity);
727
- } catch (err) {
728
- return reportServerFailure(deps, authority.apiUrl, err);
729
- }
730
- const existingCube = allCubes.find((c) => c.name === cubeName);
731
-
732
- // ----- Step 4: Fetch detail OR create cube -----
733
- let cubeDetail: CubeDetail;
734
- let isFirstDrone: boolean;
735
- if (existingCube) {
736
- try {
737
- cubeDetail = await deps.getCube(
738
- auth.apiUrl,
739
- auth.token,
740
- existingCube.id,
741
- auth.serverTrustIdentity,
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
- } catch (error) {
744
- return reportServerFailure(deps, authority.apiUrl, error);
771
+ return 1;
745
772
  }
746
- isFirstDrone = (cubeDetail.drones?.length ?? 0) === 0;
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
- `Borg server ${authority.apiUrl} supports its default cube template only. ` +
754
- `Rerun ${localAssimilateCommand(authority.apiUrl)} without \`--template\` or \`--no-template\`.\n`,
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
- chosenTemplate = 'default';
759
-
760
- // gh#653 B4: progress for the create round-trip (silent-window stall).
761
- deps.stderr(cubeName ? `Creating cube '${cubeName}'…\n` : 'Creating your cube…\n');
762
- try {
763
- const createParams = chosenTemplate
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
- } catch (error) {
777
- return reportServerFailure(deps, authority.apiUrl, error);
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
- isFirstDrone = true;
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: cubeName ?? 'borg',
1783
+ cubeName: cubeDetail.name,
1758
1784
  })
1759
1785
  .then(() => injectInitialKickoff(launchKickoff))
1760
1786
  .catch(() => {});