borgmcp 2.0.10 → 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/SECURITY.md +1 -1
- 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/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/local-manage-tool-result.d.ts +10 -0
- package/dist/local-manage-tool-result.d.ts.map +1 -1
- package/dist/local-manage-tool-result.js +36 -0
- package/dist/local-manage-tool-result.js.map +1 -1
- package/dist/remote-client.d.ts +8 -5
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +54 -11
- package/dist/remote-client.js.map +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/dist/tool-manifest.js +2 -2
- package/dist/tool-manifest.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +8 -8
- package/docs/LOCAL_SERVER.md +35 -6
- package/docs/RELEASING.md +25 -14
- 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/index.ts +9 -8
- package/src/local-manage-tool-result.ts +47 -0
- package/src/remote-client.ts +64 -19
- 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
- package/src/tool-manifest.ts +2 -2
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(() => {});
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -61,8 +61,47 @@ import { prepareCodexRemoteLaunch, defaultCodexRemoteDeps } from './codex-remote
|
|
|
61
61
|
import { findLoadedCodexThread } from './codex-app-server.js';
|
|
62
62
|
import { defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
|
|
63
63
|
import { ensurePrivateBorgConfigRoot } from './private-root.js';
|
|
64
|
+
import {
|
|
65
|
+
getOrCreateRepositoryIdentity,
|
|
66
|
+
getRepositoryAssociation,
|
|
67
|
+
resolveGitRepositoryContext,
|
|
68
|
+
saveRepositoryAssociation,
|
|
69
|
+
} from './repository-identity.js';
|
|
70
|
+
import { PromptInterruptedError } from './repository-cube-init.js';
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Wraps the readline question operation with the production interruption
|
|
74
|
+
* mapping. Tests inject the question operation; production uses readline.
|
|
75
|
+
*/
|
|
76
|
+
export type PromptQuestion = (message: string) => Promise<string>;
|
|
77
|
+
|
|
78
|
+
async function defaultPromptQuestion(message: string): Promise<string> {
|
|
79
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
80
|
+
try {
|
|
81
|
+
return await rl.question(message);
|
|
82
|
+
} finally {
|
|
83
|
+
rl.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createPromptAdapter(
|
|
88
|
+
question: PromptQuestion = defaultPromptQuestion,
|
|
89
|
+
): (message: string) => Promise<string> {
|
|
90
|
+
return async (message: string): Promise<string> => {
|
|
91
|
+
try {
|
|
92
|
+
return await question(message);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err instanceof Error && (err.message === 'SIGINT' || err.message === 'Interrupted by signal.')) {
|
|
95
|
+
throw new PromptInterruptedError();
|
|
96
|
+
}
|
|
97
|
+
throw err;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function buildDefaultAssimilateDeps(
|
|
103
|
+
question: PromptQuestion = defaultPromptQuestion,
|
|
104
|
+
): AssimilateDeps {
|
|
66
105
|
return {
|
|
67
106
|
runSync: (cmd, args, cwd) => {
|
|
68
107
|
const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8' });
|
|
@@ -97,14 +136,7 @@ export function buildDefaultAssimilateDeps(): AssimilateDeps {
|
|
|
97
136
|
|
|
98
137
|
stderr: (line) => process.stderr.write(line),
|
|
99
138
|
stdout: (line) => process.stdout.write(line),
|
|
100
|
-
prompt:
|
|
101
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
102
|
-
try {
|
|
103
|
-
return await rl.question(message);
|
|
104
|
-
} finally {
|
|
105
|
-
rl.close();
|
|
106
|
-
}
|
|
107
|
-
},
|
|
139
|
+
prompt: createPromptAdapter(question),
|
|
108
140
|
promptSecret: async (message: string): Promise<string> => {
|
|
109
141
|
const result = await prompts({
|
|
110
142
|
type: 'password',
|
|
@@ -178,6 +210,10 @@ export function buildDefaultAssimilateDeps(): AssimilateDeps {
|
|
|
178
210
|
: { committed: false, reason: 'activation-failed' };
|
|
179
211
|
},
|
|
180
212
|
findProjectRoot: (cwd) => cubesFindProjectRoot(cwd),
|
|
213
|
+
resolveRepositoryContext: resolveGitRepositoryContext,
|
|
214
|
+
getRepositoryIdentity: getOrCreateRepositoryIdentity,
|
|
215
|
+
getRepositoryAssociation,
|
|
216
|
+
saveRepositoryAssociation,
|
|
181
217
|
|
|
182
218
|
// gh#673 P2 (WI-1): project-local SessionStart hook for the launch root.
|
|
183
219
|
installProjectSessionHook: (projectRoot) => {
|
|
@@ -237,14 +273,16 @@ export function buildDefaultAssimilateDeps(): AssimilateDeps {
|
|
|
237
273
|
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
238
274
|
}
|
|
239
275
|
{
|
|
240
|
-
if (!params.name || !params.projectRoot) {
|
|
241
|
-
throw new Error('Local Borg server cube creation requires a repository name and root');
|
|
242
|
-
}
|
|
243
276
|
const created = await createLocalBorgServerCube(
|
|
244
277
|
apiUrl,
|
|
245
278
|
serverTrustIdentity,
|
|
246
279
|
token,
|
|
247
|
-
{
|
|
280
|
+
{
|
|
281
|
+
name: params.name,
|
|
282
|
+
workingRepoName: params.workingRepoName,
|
|
283
|
+
repository: params.repository,
|
|
284
|
+
template: params.template,
|
|
285
|
+
},
|
|
248
286
|
);
|
|
249
287
|
const cube = await remoteGetCube(created.cube_id, {
|
|
250
288
|
apiUrl,
|
|
@@ -253,16 +291,20 @@ export function buildDefaultAssimilateDeps(): AssimilateDeps {
|
|
|
253
291
|
});
|
|
254
292
|
if (
|
|
255
293
|
cube.id !== created.cube_id ||
|
|
294
|
+
cube.name !== created.name ||
|
|
256
295
|
!Array.isArray(cube.roles) ||
|
|
257
296
|
!cube.roles.some((role: { id?: string }) => role.id === created.default_worker_role_id)
|
|
258
297
|
) {
|
|
259
298
|
throw new Error('Borg server returned cube details outside the creation result');
|
|
260
299
|
}
|
|
261
300
|
return {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
301
|
+
response: created,
|
|
302
|
+
cube: {
|
|
303
|
+
id: cube.id,
|
|
304
|
+
name: cube.name,
|
|
305
|
+
roles: cube.roles,
|
|
306
|
+
drones: cube.drones ?? [],
|
|
307
|
+
},
|
|
266
308
|
};
|
|
267
309
|
}
|
|
268
310
|
},
|
package/src/claude.ts
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
|
|
20
20
|
import { spawn } from 'child_process';
|
|
21
21
|
import { randomUUID } from 'node:crypto';
|
|
22
|
+
import { realpathSync } from 'node:fs';
|
|
22
23
|
import { basename } from 'node:path';
|
|
23
24
|
import { createInterface } from 'node:readline/promises';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
24
26
|
import chalk from 'chalk';
|
|
25
27
|
import { findProjectRoot, getActiveCube, inboxPathForDrone, setCodexWakeTarget, pruneDeadCodexWakeTargets } from './cubes.js';
|
|
26
28
|
import { monitorStateRootForWorktree } from './inbox-monitor.js';
|
|
@@ -81,6 +83,23 @@ import { connectOpenCodeDrone, computeOpenCodePort, createOpenCodeLaunchKickoff,
|
|
|
81
83
|
import { buildOpenCodeLaunchArgs, defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
|
|
82
84
|
import { runEarlyServerFacade } from './server-facade.js';
|
|
83
85
|
|
|
86
|
+
export type AssimilateDepsBuilder = typeof buildDefaultAssimilateDeps;
|
|
87
|
+
|
|
88
|
+
export async function runAssimilateEntry(
|
|
89
|
+
args: readonly string[],
|
|
90
|
+
buildDeps: AssimilateDepsBuilder = buildDefaultAssimilateDeps,
|
|
91
|
+
): Promise<number> {
|
|
92
|
+
const parsed = parseAssimilateArgs([...args]);
|
|
93
|
+
if (!parsed.ok) {
|
|
94
|
+
process.stderr.write(
|
|
95
|
+
chalk.red(`${consolePrefix()}◼ borg assimilate: ${parsed.error}\n`)
|
|
96
|
+
);
|
|
97
|
+
process.stderr.write(`Run \`borg --help\` for usage.\n`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
return runAssimilate({ role: parsed.role, flags: parsed.flags }, buildDeps());
|
|
101
|
+
}
|
|
102
|
+
|
|
84
103
|
async function main() {
|
|
85
104
|
const serverExitCode = await runEarlyServerFacade(process.argv);
|
|
86
105
|
if (serverExitCode !== null) process.exit(serverExitCode);
|
|
@@ -128,16 +147,7 @@ async function main() {
|
|
|
128
147
|
process.stdout.write(assimilateHelpText(getPackageVersion()));
|
|
129
148
|
process.exit(0);
|
|
130
149
|
}
|
|
131
|
-
const
|
|
132
|
-
if (!parsed.ok) {
|
|
133
|
-
process.stderr.write(
|
|
134
|
-
chalk.red(`${consolePrefix()}◼ borg assimilate: ${parsed.error}\n`)
|
|
135
|
-
);
|
|
136
|
-
process.stderr.write(`Run \`borg --help\` for usage.\n`);
|
|
137
|
-
process.exit(1);
|
|
138
|
-
}
|
|
139
|
-
const deps = buildDefaultAssimilateDeps();
|
|
140
|
-
const code = await runAssimilate({ role: parsed.role, flags: parsed.flags }, deps);
|
|
150
|
+
const code = await runAssimilateEntry(process.argv.slice(3));
|
|
141
151
|
process.exit(code);
|
|
142
152
|
}
|
|
143
153
|
if (process.argv[2] === 'reset-local-seat') {
|
|
@@ -560,7 +570,17 @@ function ensureDetectedCliConfigured(): void {
|
|
|
560
570
|
}
|
|
561
571
|
}
|
|
562
572
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
573
|
+
function isEntryInvocation(): boolean {
|
|
574
|
+
try {
|
|
575
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
576
|
+
} catch {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (isEntryInvocation()) {
|
|
582
|
+
main().catch((error) => {
|
|
583
|
+
console.error(`${consolePrefix()}${chalk.red(`\n◼ Error: ${error.message}\n`)}`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
});
|
|
586
|
+
}
|
package/src/cli-help.ts
CHANGED
|
@@ -30,6 +30,7 @@ export function topLevelHelpText(version: string): string {
|
|
|
30
30
|
` borg assimilate [role] Join or create a cube\n` +
|
|
31
31
|
` borg assimilate --host <host> Join or create on an explicit server\n` +
|
|
32
32
|
` borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)\n` +
|
|
33
|
+
` borg server cube init Initialize this repository's cube without creating a drone\n` +
|
|
33
34
|
` borg reset-local-seat Clear ONLY this worktree's saved local seat (offline; after a rejection)\n` +
|
|
34
35
|
` borg sync [--prune] Sync this worktree's branch to origin/main\n` +
|
|
35
36
|
` borg cleanup [--prune] Report (or --prune) worktrees orphaned by evicted drones\n` +
|
|
@@ -53,11 +54,28 @@ export function serverHelpText(): string {
|
|
|
53
54
|
` stop Stop the managed local server.\n` +
|
|
54
55
|
` status Report verified runtime evidence.\n` +
|
|
55
56
|
` update Verify and activate a local server artifact.\n` +
|
|
56
|
-
` invite Create a single-use invitation in an interactive terminal.\n
|
|
57
|
+
` invite Create a single-use invitation in an interactive terminal.\n` +
|
|
58
|
+
` cube init Initialize this Git repository's cube; does not create a drone.\n\n` +
|
|
57
59
|
`Run borg server <command> --help for server command options.\n`
|
|
58
60
|
);
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/** Client-owned help for repository cube initialization without a drone. */
|
|
64
|
+
export function cubeInitHelpText(): string {
|
|
65
|
+
return (
|
|
66
|
+
`borg server cube init — initialize this Git repository's cube without creating a drone\n\n` +
|
|
67
|
+
`Usage:\n` +
|
|
68
|
+
` borg server cube init [options]\n\n` +
|
|
69
|
+
`Options:\n` +
|
|
70
|
+
` --host <host> Borg server host or URL (bare hosts default to HTTPS)\n` +
|
|
71
|
+
` --enroll Prompt for a hidden enrollment invitation\n` +
|
|
72
|
+
` --cube-name <name> Repository cube name (otherwise edit the proposed name)\n` +
|
|
73
|
+
` --template software-dev|starter New-cube template (default: software-dev)\n` +
|
|
74
|
+
` --yes, -y Skip confirmation prompts\n` +
|
|
75
|
+
` --help, -h Show this help\n`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
/**
|
|
62
80
|
* Help text for `borg assimilate --help` — the home for the full assimilate flag
|
|
63
81
|
* set. Model/provider configuration belongs to the selected agent CLI.
|
|
@@ -76,15 +94,17 @@ export function assimilateHelpText(version: string): string {
|
|
|
76
94
|
`Flags:\n` +
|
|
77
95
|
` --worktree <name> Create + launch the drone in a sibling git worktree\n` +
|
|
78
96
|
` --here Stay in the current worktree (no sibling spawn)\n` +
|
|
79
|
-
` --cube-name <name>
|
|
97
|
+
` --cube-name <name> Repository cube name (otherwise edit the proposed name)\n` +
|
|
80
98
|
` --host <host> Borg server host or URL (bare hosts default to HTTPS)\n` +
|
|
81
99
|
` --enroll Prompt for a hidden enrollment invitation in the operator terminal\n` +
|
|
82
|
-
` --template
|
|
83
|
-
` --no-template
|
|
100
|
+
` --template software-dev|starter New-cube template (default: software-dev)\n` +
|
|
101
|
+
` --no-template Unsupported for repository cube creation\n` +
|
|
84
102
|
` --cli claude|codex|opencode Agent CLI to launch (default: claude)\n` +
|
|
85
103
|
` --model claude:<model> Legacy Claude model override (configure models in the agent CLI)\n` +
|
|
86
104
|
` --yes, -y Skip confirmation prompts\n\n` +
|
|
87
|
-
`
|
|
105
|
+
`Creation shows repository context, name, template, and one confirmation. An existing\n` +
|
|
106
|
+
`repository association skips all prompts. An enrolled owner client may create an\n` +
|
|
107
|
+
`idempotent repository cube; ordinary clients\n` +
|
|
88
108
|
`require an explicit cube grant. Agent seats begin only after enrollment. Preview only.\n` +
|
|
89
109
|
`See docs/LOCAL_SERVER.md for self-hosted setup and current status.\n\n` +
|
|
90
110
|
`For local or provider-specific models, configure the selected agent CLI directly.\n` +
|