borgmcp 2.8.0 → 2.10.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/dist/assimilate-cmd.d.ts +14 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +187 -14
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +29 -1
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/assimilate-welcome.d.ts.map +1 -1
- package/dist/assimilate-welcome.js +1 -0
- package/dist/assimilate-welcome.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +27 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +1 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +13 -3
- package/dist/cli-help.js.map +1 -1
- package/dist/config.d.ts +12 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +170 -11
- package/dist/config.js.map +1 -1
- package/dist/first-run-server.d.ts +10 -2
- package/dist/first-run-server.d.ts.map +1 -1
- package/dist/first-run-server.js +54 -7
- package/dist/first-run-server.js.map +1 -1
- package/dist/invitation-artifact.d.ts +33 -0
- package/dist/invitation-artifact.d.ts.map +1 -0
- package/dist/invitation-artifact.js +89 -0
- package/dist/invitation-artifact.js.map +1 -0
- package/dist/launch-gate.d.ts +5 -4
- package/dist/launch-gate.d.ts.map +1 -1
- package/dist/launch-gate.js +7 -8
- package/dist/launch-gate.js.map +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -1
- package/dist/parse-assimilate-args.js +9 -3
- package/dist/parse-assimilate-args.js.map +1 -1
- package/dist/recover-enrollment-cmd.d.ts +17 -0
- package/dist/recover-enrollment-cmd.d.ts.map +1 -0
- package/dist/recover-enrollment-cmd.js +62 -0
- package/dist/recover-enrollment-cmd.js.map +1 -0
- package/dist/regen-format.d.ts +9 -0
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +15 -0
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +27 -3
- package/dist/regen.js.map +1 -1
- package/dist/remote-client.d.ts +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +1 -1
- package/dist/remote-client.js.map +1 -1
- package/dist/runtime-metadata.d.ts +1 -1
- package/dist/runtime-metadata.d.ts.map +1 -1
- package/dist/runtime-metadata.js +3 -3
- package/dist/runtime-metadata.js.map +1 -1
- package/dist/seat-store.d.ts.map +1 -1
- package/dist/seat-store.js +7 -0
- package/dist/seat-store.js.map +1 -1
- package/dist/server-errors.d.ts +1 -1
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js.map +1 -1
- package/dist/server-facade.d.ts +1 -1
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +2 -2
- package/dist/server-facade.js.map +1 -1
- package/dist/server-handshake.d.ts +23 -3
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +102 -7
- package/dist/server-handshake.js.map +1 -1
- package/dist/server-trust.d.ts +8 -0
- package/dist/server-trust.d.ts.map +1 -1
- package/dist/server-trust.js +165 -5
- package/dist/server-trust.js.map +1 -1
- package/dist/setup.js +5 -2
- package/dist/setup.js.map +1 -1
- package/dist/token-store.d.ts +3 -0
- package/dist/token-store.d.ts.map +1 -1
- package/dist/token-store.js +2 -0
- package/dist/token-store.js.map +1 -1
- package/dist/tool-manifest.d.ts.map +1 -1
- package/dist/tool-manifest.js +5 -3
- package/dist/tool-manifest.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +1 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/dist/update-cmd.d.ts +1 -0
- package/dist/update-cmd.d.ts.map +1 -1
- package/dist/update-cmd.js +8 -3
- package/dist/update-cmd.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +11 -7
- package/docs/LOCAL_SERVER.md +12 -3
- package/docs/RELEASING.md +17 -6
- package/package.json +2 -2
- package/src/assimilate-cmd.ts +234 -20
- package/src/assimilate-deps.ts +29 -0
- package/src/assimilate-welcome.ts +1 -0
- package/src/claude.ts +22 -0
- package/src/cli-help.ts +16 -3
- package/src/config.ts +167 -8
- package/src/first-run-server.ts +71 -8
- package/src/invitation-artifact.ts +116 -0
- package/src/launch-gate.ts +7 -8
- package/src/parse-assimilate-args.ts +11 -4
- package/src/recover-enrollment-cmd.ts +63 -0
- package/src/regen-format.ts +25 -0
- package/src/regen.ts +30 -2
- package/src/remote-client.ts +2 -2
- package/src/runtime-metadata.ts +4 -3
- package/src/seat-store.ts +6 -0
- package/src/server-errors.ts +1 -0
- package/src/server-facade.ts +2 -2
- package/src/server-handshake.ts +140 -4
- package/src/server-trust.ts +180 -5
- package/src/setup.ts +5 -2
- package/src/token-store.ts +5 -0
- package/src/tool-manifest.ts +5 -3
- package/src/unknown-subcommand.ts +1 -0
- package/src/update-cmd.ts +8 -3
package/src/assimilate-cmd.ts
CHANGED
|
@@ -56,6 +56,17 @@ import {
|
|
|
56
56
|
RepositoryAssociationOutcomeUnknownError,
|
|
57
57
|
RepositoryAssociationResolutionError,
|
|
58
58
|
} from './server-errors.js';
|
|
59
|
+
import {
|
|
60
|
+
decodeAndVerifyInvitationArtifact,
|
|
61
|
+
InvitationArtifactCompatibilityError,
|
|
62
|
+
InvitationArtifactEndpointMismatchError,
|
|
63
|
+
InvitationArtifactFormatError,
|
|
64
|
+
InvitationArtifactLegacyError,
|
|
65
|
+
InvitationArtifactRecoveryError,
|
|
66
|
+
InvitationArtifactStorageError,
|
|
67
|
+
InvitationArtifactTransportError,
|
|
68
|
+
InvitationArtifactTrustError,
|
|
69
|
+
} from './invitation-artifact.js';
|
|
59
70
|
import type { SeatStatus } from './seat-probe.js';
|
|
60
71
|
import type { ServerSessionOperation } from './config.js';
|
|
61
72
|
import type { ExpectedBinding, FinalizeServerSeatOutcome, PersistedLocalSeat } from './cubes.js';
|
|
@@ -67,6 +78,7 @@ import type {
|
|
|
67
78
|
AssociateRepositoryCubeResponse,
|
|
68
79
|
CreateCubeRepository,
|
|
69
80
|
CubeTemplate,
|
|
81
|
+
InvitationArtifact,
|
|
70
82
|
} from 'borgmcp-shared/protocol';
|
|
71
83
|
import {
|
|
72
84
|
initializeRepositoryCube,
|
|
@@ -277,7 +289,11 @@ export interface AssimilateDeps {
|
|
|
277
289
|
detectLocalServer: () => Promise<string | null>;
|
|
278
290
|
connectServer: (
|
|
279
291
|
apiUrl: string,
|
|
280
|
-
enrollment?: {
|
|
292
|
+
enrollment?: {
|
|
293
|
+
invitation: string;
|
|
294
|
+
artifact?: import('borgmcp-shared/protocol').InvitationArtifact;
|
|
295
|
+
confirmReplacement?: () => Promise<boolean>;
|
|
296
|
+
},
|
|
281
297
|
) => Promise<{
|
|
282
298
|
token: string;
|
|
283
299
|
trustIdentity: string;
|
|
@@ -291,6 +307,17 @@ export interface AssimilateDeps {
|
|
|
291
307
|
trustIdentity: string;
|
|
292
308
|
serverCapabilities?: readonly string[];
|
|
293
309
|
} | null>;
|
|
310
|
+
/** Read-only pending-artifact peek used to validate an explicit host before
|
|
311
|
+
* the normal resume path performs trust/credential work. */
|
|
312
|
+
peekPendingServerEnrollment?: () => Promise<{ origin: string; invitation: string } | null>;
|
|
313
|
+
resumePendingServerEnrollment?: (
|
|
314
|
+
onPending?: () => void,
|
|
315
|
+
) => Promise<{
|
|
316
|
+
token: string;
|
|
317
|
+
apiUrl?: string;
|
|
318
|
+
trustIdentity: string;
|
|
319
|
+
serverCapabilities?: readonly string[];
|
|
320
|
+
} | null>;
|
|
294
321
|
|
|
295
322
|
resolveRepositoryCube: (
|
|
296
323
|
apiUrl: string,
|
|
@@ -361,6 +388,11 @@ function affirmative(answer: string): boolean {
|
|
|
361
388
|
return normalized === '' || normalized === 'y' || normalized === 'yes';
|
|
362
389
|
}
|
|
363
390
|
|
|
391
|
+
function strictAffirmative(answer: string): boolean {
|
|
392
|
+
const normalized = answer.trim().toLowerCase();
|
|
393
|
+
return normalized === 'y' || normalized === 'yes';
|
|
394
|
+
}
|
|
395
|
+
|
|
364
396
|
async function selectAssimilationAuthority(
|
|
365
397
|
flags: AssimilateFlags,
|
|
366
398
|
deps: AssimilateDeps,
|
|
@@ -377,6 +409,11 @@ async function selectAssimilationAuthority(
|
|
|
377
409
|
|
|
378
410
|
// Only a local self-hosted server authority exists. Non-TTY and --yes must
|
|
379
411
|
// NOT infer an authority — fail closed with actionable guidance.
|
|
412
|
+
if (flags.enroll && deps.isTTY()) {
|
|
413
|
+
// The v2 enrollment artifact is itself the authority selector. Use the
|
|
414
|
+
// local origin only as a placeholder until the hidden artifact is decoded.
|
|
415
|
+
return { kind: 'server', apiUrl: DEFAULT_LOCAL_SERVER_ORIGIN };
|
|
416
|
+
}
|
|
380
417
|
if (!deps.isTTY() || flags.yes) {
|
|
381
418
|
if (deps.defaultAuthority) return deps.defaultAuthority;
|
|
382
419
|
const command = mode === 'cube-init'
|
|
@@ -475,6 +512,14 @@ function reportServerFailure(
|
|
|
475
512
|
);
|
|
476
513
|
return 1;
|
|
477
514
|
}
|
|
515
|
+
if (error instanceof BorgServerError && error.code === 'LOCAL_CREDENTIAL_EXISTS') {
|
|
516
|
+
deps.stderr(
|
|
517
|
+
`A local enrollment for ${apiUrl} already exists and was not replaced. ` +
|
|
518
|
+
`Re-run ${localAssimilateCommand(apiUrl, true, mode)} and explicitly confirm replacement ` +
|
|
519
|
+
'only if the first enrolled client should be abandoned.\n',
|
|
520
|
+
);
|
|
521
|
+
return 1;
|
|
522
|
+
}
|
|
478
523
|
if (error instanceof LegacySessionCredentialCollisionError) {
|
|
479
524
|
const recovery = mode === 'cube-init'
|
|
480
525
|
? localAssimilateCommand(error.origin, true, mode)
|
|
@@ -508,6 +553,38 @@ function reportServerFailure(
|
|
|
508
553
|
);
|
|
509
554
|
return 1;
|
|
510
555
|
}
|
|
556
|
+
if (error instanceof InvitationArtifactCompatibilityError) {
|
|
557
|
+
deps.stderr(`${error.message}\n`);
|
|
558
|
+
return 1;
|
|
559
|
+
}
|
|
560
|
+
if (error instanceof InvitationArtifactLegacyError) {
|
|
561
|
+
deps.stderr(`${error.message}\n`);
|
|
562
|
+
return 1;
|
|
563
|
+
}
|
|
564
|
+
if (error instanceof InvitationArtifactFormatError) {
|
|
565
|
+
deps.stderr(`${error.message}\n`);
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
568
|
+
if (error instanceof InvitationArtifactEndpointMismatchError) {
|
|
569
|
+
deps.stderr(`${error.message}\n`);
|
|
570
|
+
return 1;
|
|
571
|
+
}
|
|
572
|
+
if (error instanceof InvitationArtifactTransportError) {
|
|
573
|
+
deps.stderr(`${error.message}\n`);
|
|
574
|
+
return 1;
|
|
575
|
+
}
|
|
576
|
+
if (error instanceof InvitationArtifactStorageError) {
|
|
577
|
+
deps.stderr(`${error.message}\n`);
|
|
578
|
+
return 1;
|
|
579
|
+
}
|
|
580
|
+
if (error instanceof InvitationArtifactRecoveryError) {
|
|
581
|
+
deps.stderr(`${error.message}\n`);
|
|
582
|
+
return 1;
|
|
583
|
+
}
|
|
584
|
+
if (error instanceof InvitationArtifactTrustError) {
|
|
585
|
+
deps.stderr(`${error.message}\n`);
|
|
586
|
+
return 1;
|
|
587
|
+
}
|
|
511
588
|
if (/HTTP 40[13]|auth(?:entication|orization)|credential.*(?:invalid|rejected)/i.test(message)) {
|
|
512
589
|
deps.stderr(
|
|
513
590
|
`The saved enrollment for ${apiUrl} was rejected. Re-run ` +
|
|
@@ -649,7 +726,117 @@ export async function runAssimilate(
|
|
|
649
726
|
return 1;
|
|
650
727
|
}
|
|
651
728
|
|
|
652
|
-
|
|
729
|
+
const artifactOnlyEnrollment = args.flags.enroll === true &&
|
|
730
|
+
args.flags.server === undefined && deps.defaultAuthority === undefined && deps.isTTY();
|
|
731
|
+
let preResumeAttempted = false;
|
|
732
|
+
let preResumedEnrollment: {
|
|
733
|
+
token: string;
|
|
734
|
+
trustIdentity: string;
|
|
735
|
+
serverCapabilities?: readonly string[];
|
|
736
|
+
} | null = null;
|
|
737
|
+
|
|
738
|
+
let prefetchedInvitation: string | undefined;
|
|
739
|
+
let prefetchedArtifact: InvitationArtifact | undefined;
|
|
740
|
+
|
|
741
|
+
// An explicit --host plus a new invitation must be rejected on the pure
|
|
742
|
+
// input path. Decode and compare the operator-presented artifact before any
|
|
743
|
+
// pending-enrollment lookup: the lookup enumerates the credential backend,
|
|
744
|
+
// so it must not precede this contradiction check. A matching artifact may
|
|
745
|
+
// then take the published pending-resume path until client#267 lands.
|
|
746
|
+
if (args.flags.enroll && args.flags.server !== undefined && deps.isTTY()) {
|
|
747
|
+
let preResumeOrigin: string;
|
|
748
|
+
try {
|
|
749
|
+
preResumeOrigin = normalizeServerEndpoint(args.flags.server);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
deps.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
752
|
+
return 1;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
|
|
756
|
+
if (!prefetchedInvitation) {
|
|
757
|
+
deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
|
|
758
|
+
return 1;
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
|
|
762
|
+
if (prefetchedArtifact.endpoint !== preResumeOrigin) {
|
|
763
|
+
throw new InvitationArtifactEndpointMismatchError(
|
|
764
|
+
preResumeOrigin,
|
|
765
|
+
prefetchedArtifact.endpoint,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
} catch (error) {
|
|
769
|
+
deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
|
|
770
|
+
prefetchedInvitation = undefined;
|
|
771
|
+
prefetchedArtifact = undefined;
|
|
772
|
+
return 1;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
let pendingForHost = false;
|
|
776
|
+
if (deps.peekPendingServerEnrollment !== undefined) {
|
|
777
|
+
let pending: { origin: string; invitation: string } | null = null;
|
|
778
|
+
try {
|
|
779
|
+
pending = await deps.peekPendingServerEnrollment();
|
|
780
|
+
} catch {
|
|
781
|
+
pending = null;
|
|
782
|
+
}
|
|
783
|
+
if (pending?.origin === preResumeOrigin) {
|
|
784
|
+
try {
|
|
785
|
+
const pendingArtifact = decodeAndVerifyInvitationArtifact(pending.invitation);
|
|
786
|
+
if (pendingArtifact.endpoint !== preResumeOrigin) {
|
|
787
|
+
throw new InvitationArtifactEndpointMismatchError(
|
|
788
|
+
preResumeOrigin,
|
|
789
|
+
pendingArtifact.endpoint,
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
pendingForHost = true;
|
|
793
|
+
} catch (error) {
|
|
794
|
+
deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
|
|
795
|
+
return 1;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (pendingForHost) {
|
|
801
|
+
try {
|
|
802
|
+
preResumeAttempted = true;
|
|
803
|
+
preResumedEnrollment = await deps.resumeServerEnrollment(preResumeOrigin, () => {
|
|
804
|
+
deps.stderr(
|
|
805
|
+
`Resuming the pending enrollment for \`${preResumeOrigin}\`; ` +
|
|
806
|
+
'do not enter another invitation unless the server certificate was reissued; ' +
|
|
807
|
+
'if it was, request a current invitation and rerun this command.\n',
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
} catch {
|
|
811
|
+
preResumeAttempted = false;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (artifactOnlyEnrollment ||
|
|
816
|
+
preResumeAttempted && preResumedEnrollment === null) {
|
|
817
|
+
if (artifactOnlyEnrollment && deps.resumePendingServerEnrollment) {
|
|
818
|
+
preResumedEnrollment = await deps.resumePendingServerEnrollment(() => {
|
|
819
|
+
deps.stderr('Resuming the pending enrollment; no new invitation is required.\n');
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
if (artifactOnlyEnrollment && preResumedEnrollment) {
|
|
823
|
+
// The exact pending tuple was already redeemed or is being resumed.
|
|
824
|
+
} else {
|
|
825
|
+
prefetchedInvitation = await deps.promptSecret('Enrollment invitation (single-use; hidden input):');
|
|
826
|
+
if (!prefetchedInvitation) {
|
|
827
|
+
deps.stderr('No enrollment invitation was entered. Ask the server operator for one, then retry.\n');
|
|
828
|
+
return 1;
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
prefetchedArtifact = decodeAndVerifyInvitationArtifact(prefetchedInvitation);
|
|
832
|
+
} catch (error) {
|
|
833
|
+
deps.stderr(`${error instanceof Error ? error.message : 'The enrollment invitation is invalid.'}\n`);
|
|
834
|
+
prefetchedInvitation = undefined;
|
|
835
|
+
return 1;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!artifactOnlyEnrollment && args.flags.server === undefined && deps.defaultAuthority === undefined) {
|
|
653
840
|
const connectCommand = mode === 'cube-init'
|
|
654
841
|
? 'borg server cube init --host <host>'
|
|
655
842
|
: 'borg assimilate --host <host>';
|
|
@@ -686,8 +873,9 @@ export async function runAssimilate(
|
|
|
686
873
|
}
|
|
687
874
|
|
|
688
875
|
// ----- Step 1: Select and authenticate the local server -----
|
|
689
|
-
const
|
|
690
|
-
if (!
|
|
876
|
+
const selectedAuthority = await selectAssimilationAuthority(args.flags, deps, mode);
|
|
877
|
+
if (!selectedAuthority) return 1;
|
|
878
|
+
let authority: AssimilationAuthority = selectedAuthority;
|
|
691
879
|
if (localSeatReadError !== undefined) {
|
|
692
880
|
return reportServerFailure(deps, authority.apiUrl, localSeatReadError, false, mode);
|
|
693
881
|
}
|
|
@@ -710,27 +898,48 @@ export async function runAssimilate(
|
|
|
710
898
|
);
|
|
711
899
|
return 1;
|
|
712
900
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
901
|
+
let resumed = preResumedEnrollment;
|
|
902
|
+
if (!resumed && prefetchedArtifact === undefined && !preResumeAttempted && !artifactOnlyEnrollment) {
|
|
903
|
+
resumed = await deps.resumeServerEnrollment(authority.apiUrl, () => {
|
|
904
|
+
deps.stderr(
|
|
905
|
+
`Resuming the pending enrollment for \`${authority.apiUrl}\`; ` +
|
|
906
|
+
'do not enter another invitation unless the server certificate was reissued; ' +
|
|
907
|
+
'if it was, request a current invitation and rerun this command.\n',
|
|
908
|
+
);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
719
911
|
if (resumed) {
|
|
912
|
+
if ('apiUrl' in resumed && typeof resumed.apiUrl === 'string') {
|
|
913
|
+
authority = { kind: 'server', apiUrl: resumed.apiUrl };
|
|
914
|
+
}
|
|
720
915
|
serverAuth = resumed;
|
|
721
916
|
} else {
|
|
722
|
-
let invitation = await deps.promptSecret(
|
|
723
|
-
|
|
917
|
+
let invitation = prefetchedInvitation ?? await deps.promptSecret(
|
|
918
|
+
artifactOnlyEnrollment
|
|
919
|
+
? 'Enrollment invitation (single-use; hidden input):'
|
|
920
|
+
: `Enrollment invitation for \`${authority.apiUrl}\` (single-use; hidden input):`,
|
|
724
921
|
);
|
|
725
922
|
if (!invitation) {
|
|
726
|
-
deps.stderr(
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
923
|
+
deps.stderr(artifactOnlyEnrollment
|
|
924
|
+
? 'No enrollment invitation was entered. Ask the server operator for one, then rerun `borg assimilate --enroll`.\n'
|
|
925
|
+
: `No enrollment invitation was entered for ${authority.apiUrl}. ` +
|
|
926
|
+
`Ask the server operator for one, then rerun ${localAssimilateCommand(authority.apiUrl, true, mode)}.\n`);
|
|
730
927
|
return 1;
|
|
731
928
|
}
|
|
732
929
|
try {
|
|
733
|
-
|
|
930
|
+
const artifact = prefetchedArtifact ?? decodeAndVerifyInvitationArtifact(invitation);
|
|
931
|
+
if (args.flags.server !== undefined && authority.apiUrl !== artifact.endpoint) {
|
|
932
|
+
throw new InvitationArtifactEndpointMismatchError(authority.apiUrl, artifact.endpoint);
|
|
933
|
+
}
|
|
934
|
+
authority = { kind: 'server', apiUrl: artifact.endpoint };
|
|
935
|
+
serverAuth = await deps.connectServer(authority.apiUrl, {
|
|
936
|
+
invitation,
|
|
937
|
+
artifact,
|
|
938
|
+
confirmReplacement: async () => strictAffirmative(await deps.prompt(
|
|
939
|
+
`A local enrollment for ${authority.apiUrl} already exists. Replacing it will orphan ` +
|
|
940
|
+
'the first enrolled client. Replace it? [y/N]: ',
|
|
941
|
+
)),
|
|
942
|
+
});
|
|
734
943
|
} finally {
|
|
735
944
|
// Strings cannot be zeroized in JavaScript, but drop this command's
|
|
736
945
|
// reference immediately after the exchange instead of retaining the
|
|
@@ -758,6 +967,11 @@ export async function runAssimilate(
|
|
|
758
967
|
serverTrustIdentity: serverAuth.trustIdentity,
|
|
759
968
|
serverCapabilities: serverAuth.serverCapabilities ?? [],
|
|
760
969
|
};
|
|
970
|
+
if (args.flags.enroll) {
|
|
971
|
+
deps.stderr(
|
|
972
|
+
`This machine (${deps.getHostname()}) is enrolled with Borg server \`${authority.apiUrl}\`.\n`,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
761
975
|
} catch (error) {
|
|
762
976
|
return reportServerFailure(deps, authority.apiUrl, error, args.flags.enroll === true, mode);
|
|
763
977
|
}
|
|
@@ -1492,7 +1706,7 @@ export async function runAssimilate(
|
|
|
1492
1706
|
deps.stderr(
|
|
1493
1707
|
'Borg could not enumerate this repository’s existing worktrees, so it did not risk creating a colliding sibling.\n' +
|
|
1494
1708
|
'Run `git worktree list` from this repository and resolve the reported Git error, then rerun `borg assimilate`.\n' +
|
|
1495
|
-
'A
|
|
1709
|
+
'A local drone reservation was created and remains pending; rerunning after fixing the worktree issue resumes that reservation.\n',
|
|
1496
1710
|
);
|
|
1497
1711
|
return 1;
|
|
1498
1712
|
}
|
|
@@ -1565,13 +1779,13 @@ export async function runAssimilate(
|
|
|
1565
1779
|
? `Git left branch ${residualBranch} without a registered worktree; Borg preserved it.\n`
|
|
1566
1780
|
: '') +
|
|
1567
1781
|
'Run `git worktree list` and `git status` to inspect repository state, resolve the reported Git error, then rerun `borg assimilate`.\n' +
|
|
1568
|
-
'A
|
|
1782
|
+
'A local drone reservation was created and remains pending; rerunning after fixing the worktree issue resumes that reservation.\n',
|
|
1569
1783
|
);
|
|
1570
1784
|
return 1;
|
|
1571
1785
|
}
|
|
1572
1786
|
deps.stderr(
|
|
1573
1787
|
`spawned sibling worktree at ${candidate} on branch ${wtBranch} (${startRef}); ` +
|
|
1574
|
-
`the original dir keeps its active
|
|
1788
|
+
`the original dir keeps its active drone binding — run \`borg reset-local-seat\` there if that binding is stale.\n`
|
|
1575
1789
|
);
|
|
1576
1790
|
deps.chdir(candidate);
|
|
1577
1791
|
deps.stderr(renderWorktreeSteeringNote(candidate, wtBranch, projectRoot));
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
connectLocalBorgServer,
|
|
35
35
|
createLocalBorgServerCube,
|
|
36
36
|
enrollLocalBorgServer,
|
|
37
|
+
enrollLocalBorgServerArtifact,
|
|
37
38
|
probeLocalBorgServer,
|
|
38
39
|
resolveLocalBorgServerRepositoryCube,
|
|
39
40
|
resumeLocalBorgServerEnrollment,
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
setCodexWakeTarget,
|
|
63
64
|
} from './cubes.js';
|
|
64
65
|
import { addProjectSessionStartHook } from './config-utils.js';
|
|
66
|
+
import { findPendingServerEnrollment } from './config.js';
|
|
65
67
|
import { setTerminalTitle as setTitle } from './terminal-title.js';
|
|
66
68
|
import { defaultCliChoiceDeps, resolveCliChoice } from './cli-platform.js';
|
|
67
69
|
import { prepareCodexRemoteLaunch, defaultCodexRemoteDeps } from './codex-remote.js';
|
|
@@ -240,7 +242,19 @@ export function buildDefaultAssimilateDeps(
|
|
|
240
242
|
: null,
|
|
241
243
|
connectServer: async (apiUrl, enrollment) => {
|
|
242
244
|
if (enrollment) {
|
|
245
|
+
if (enrollment.artifact) {
|
|
246
|
+
return enrollLocalBorgServerArtifact(enrollment.artifact, {
|
|
247
|
+
invitation: enrollment.invitation,
|
|
248
|
+
...(enrollment.confirmReplacement === undefined
|
|
249
|
+
? {}
|
|
250
|
+
: { confirmReplacement: enrollment.confirmReplacement }),
|
|
251
|
+
clientName: osHostname(),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
243
254
|
return enrollLocalBorgServer(apiUrl, enrollment.invitation, {
|
|
255
|
+
...(enrollment.confirmReplacement === undefined
|
|
256
|
+
? {}
|
|
257
|
+
: { confirmReplacement: enrollment.confirmReplacement }),
|
|
244
258
|
clientName: osHostname(),
|
|
245
259
|
});
|
|
246
260
|
}
|
|
@@ -250,6 +264,21 @@ export function buildDefaultAssimilateDeps(
|
|
|
250
264
|
resumeLocalBorgServerEnrollment(apiUrl, {
|
|
251
265
|
...(onPending === undefined ? {} : { onPending }),
|
|
252
266
|
}),
|
|
267
|
+
peekPendingServerEnrollment: async () => {
|
|
268
|
+
const pending = await findPendingServerEnrollment();
|
|
269
|
+
return pending === null
|
|
270
|
+
? null
|
|
271
|
+
: { origin: pending.origin, invitation: pending.invitation };
|
|
272
|
+
},
|
|
273
|
+
resumePendingServerEnrollment: async (onPending) =>
|
|
274
|
+
(async () => {
|
|
275
|
+
const pending = await findPendingServerEnrollment();
|
|
276
|
+
if (!pending) return null;
|
|
277
|
+
const result = await resumeLocalBorgServerEnrollment(pending.origin, {
|
|
278
|
+
...(onPending === undefined ? {} : { onPending }),
|
|
279
|
+
});
|
|
280
|
+
return result ? { ...result, apiUrl: pending.origin } : null;
|
|
281
|
+
})(),
|
|
253
282
|
|
|
254
283
|
resolveRepositoryCube: async (apiUrl, token, input, serverTrustIdentity) => {
|
|
255
284
|
if (serverTrustIdentity === undefined) {
|
|
@@ -33,6 +33,7 @@ export function renderAssimilationWelcome(
|
|
|
33
33
|
``,
|
|
34
34
|
`In the launched agent, run \`borg_whoami\` and \`borg_roster\` to verify the seat`,
|
|
35
35
|
`and begin coordinating.`,
|
|
36
|
+
`Launch future sessions in this repository with \`borg\`.`,
|
|
36
37
|
...teammateLines,
|
|
37
38
|
``,
|
|
38
39
|
].join('\n');
|
package/src/claude.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from './reset-local-seat-cmd.js';
|
|
44
44
|
import { parseLaunchAllArgs } from './parse-launch-all-args.js';
|
|
45
45
|
import { unknownSubcommand } from './unknown-subcommand.js';
|
|
46
|
+
import { parseRecoverEnrollmentArgs, runRecoverEnrollment } from './recover-enrollment-cmd.js';
|
|
46
47
|
import { runLaunchAll } from './launch-all-cmd.js';
|
|
47
48
|
import { buildDefaultLaunchAllDeps } from './launch-all-deps.js';
|
|
48
49
|
import { discoverDroneCandidates } from './launch-all-discovery.js';
|
|
@@ -173,6 +174,27 @@ async function main() {
|
|
|
173
174
|
const code = await runResetLocalSeat(parsed.flags, buildDefaultResetLocalSeatDeps());
|
|
174
175
|
process.exit(code);
|
|
175
176
|
}
|
|
177
|
+
if (process.argv[2] === 'recover-enrollment') {
|
|
178
|
+
const parsed = parseRecoverEnrollmentArgs(process.argv.slice(3));
|
|
179
|
+
if (!parsed.ok) {
|
|
180
|
+
if (parsed.error === 'help') {
|
|
181
|
+
process.stdout.write(clientSubcommandHelpText('recover-enrollment', [], getPackageVersion()) ?? '');
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
process.stderr.write(chalk.red(`${consolePrefix()}◼ borg recover-enrollment: ${parsed.error}\n`));
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const prompt = async (message: string): Promise<string> => {
|
|
188
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
189
|
+
try { return await rl.question(message); } finally { rl.close(); }
|
|
190
|
+
};
|
|
191
|
+
const code = await runRecoverEnrollment(parsed.flags, {
|
|
192
|
+
prompt,
|
|
193
|
+
stderr: (line) => process.stderr.write(line),
|
|
194
|
+
stdout: (line) => process.stdout.write(line),
|
|
195
|
+
});
|
|
196
|
+
process.exit(code);
|
|
197
|
+
}
|
|
176
198
|
if (process.argv[2] === 'spawn') {
|
|
177
199
|
// Deprecated; the stub prints a redirect message and exits 2.
|
|
178
200
|
const code = await runSpawn();
|
package/src/cli-help.ts
CHANGED
|
@@ -64,6 +64,7 @@ export function clientSubcommandHelpText(
|
|
|
64
64
|
case 'setup': return setupHelpText(version);
|
|
65
65
|
case 'assimilate': return assimilateHelpText(version);
|
|
66
66
|
case 'reset-local-seat': return resetLocalSeatHelpText(version);
|
|
67
|
+
case 'recover-enrollment': return recoverEnrollmentHelpText(version);
|
|
67
68
|
case 'sync': return syncHelpText(version);
|
|
68
69
|
case 'cleanup': return cleanupHelpText(version);
|
|
69
70
|
case 'launch-all': return launchAllHelpText(version);
|
|
@@ -74,9 +75,8 @@ export function clientSubcommandHelpText(
|
|
|
74
75
|
export function setupNextStepsText(): string {
|
|
75
76
|
return (
|
|
76
77
|
`◼ Next steps:\n` +
|
|
77
|
-
`1. Run \`borg server
|
|
78
|
-
`2.
|
|
79
|
-
`3. In a second terminal, cd into your project and run \`borg assimilate\`.\n`
|
|
78
|
+
`1. Run \`borg server start\` and leave that terminal open.\n` +
|
|
79
|
+
`2. In a second terminal, cd into your project's Git repository and run \`borg assimilate\`.\n`
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -102,6 +102,7 @@ export function topLevelHelpText(version: string): string {
|
|
|
102
102
|
` borg assimilate --worktree <name> Spawn a worktree drone (in ~/.borg/worktrees/<repo>/<name>)\n` +
|
|
103
103
|
` borg server cube init Initialize this repository's cube without creating a drone\n` +
|
|
104
104
|
` borg reset-local-seat Clear ONLY this worktree's saved local seat (offline; after a rejection)\n` +
|
|
105
|
+
` borg recover-enrollment Clear ONLY one failed server enrollment transaction\n` +
|
|
105
106
|
` borg sync [--prune] Sync this worktree's branch to origin/main\n` +
|
|
106
107
|
` borg cleanup [--prune] Report (or --prune) worktrees orphaned by evicted drones\n` +
|
|
107
108
|
` borg launch-all [cube] Launch all drone worktrees of a cube (default: active cube)\n` +
|
|
@@ -114,6 +115,17 @@ export function topLevelHelpText(version: string): string {
|
|
|
114
115
|
);
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
export function recoverEnrollmentHelpText(version: string): string {
|
|
119
|
+
return (
|
|
120
|
+
`borg recover-enrollment (borgmcp ${version}) — clear one failed server enrollment\n\n` +
|
|
121
|
+
`Usage:\n` +
|
|
122
|
+
` borg recover-enrollment [--host <host>] [--yes]\n` +
|
|
123
|
+
` borg recover-enrollment --help\n\n` +
|
|
124
|
+
`This operation clears only the failed enrollment transaction for one server.\n` +
|
|
125
|
+
`It does not touch other server enrollments or accounts. It never requires manual file editing.\n`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
117
129
|
/** Help for the whole-product, npm-owned update journey. */
|
|
118
130
|
export function updateHelpText(version: string): string {
|
|
119
131
|
return (
|
|
@@ -144,6 +156,7 @@ export function serverHelpText(): string {
|
|
|
144
156
|
` status Report verified runtime evidence.\n` +
|
|
145
157
|
` update Verify and activate a local server artifact.\n` +
|
|
146
158
|
` invite Create a single-use invitation in an interactive terminal.\n` +
|
|
159
|
+
` cert-reissue Widen the server certificate to cover another address without replacing the CA.\n` +
|
|
147
160
|
` dashboard View the running local server dashboard.\n` +
|
|
148
161
|
` cube init Initialize this Git repository's cube; does not create a drone.\n\n` +
|
|
149
162
|
`Run borg server <command> --help for server command options.\n`
|