@catalyst-cloud/cli 0.14.5 → 0.14.7
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/CHANGELOG.md +9 -0
- package/README.md +5 -3
- package/dist/args.js +51 -13
- package/dist/cli.js +58 -10
- package/dist/config.js +16 -3
- package/dist/daemon-authority-lifetime.js +171 -0
- package/dist/daemon-authority.js +216 -0
- package/dist/daemon-cache.js +348 -0
- package/dist/daemon-store.js +370 -0
- package/dist/daemon-supervisor.js +460 -0
- package/dist/env/onboard-draft-store.js +243 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +203 -0
- package/dist/events-cloud-tail.js +134 -0
- package/dist/events-cloud.js +500 -0
- package/dist/events.js +13 -4
- package/dist/legacy.js +145 -32
- package/dist/oauth.js +125 -33
- package/dist/onboard-accounts.js +259 -0
- package/dist/onboard-automation-management.js +15 -0
- package/dist/onboard-bootstrap.js +162 -0
- package/dist/onboard-capabilities.js +15 -7
- package/dist/onboard-capacity.js +226 -0
- package/dist/onboard-existing.js +106 -32
- package/dist/onboard-file-snapshot.js +45 -0
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +147 -37
- package/dist/onboard-runtime.js +104 -9
- package/dist/onboard-settings-context.js +141 -0
- package/dist/onboard-settings-import.js +127 -0
- package/dist/onboard-settings.js +234 -0
- package/dist/onboard-ui.js +99 -11
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard.js +359 -27
- package/dist/owned-fetch.js +182 -0
- package/dist/query.js +77 -27
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +2 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
- package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
- package/skills/catalyst-linear/SKILL.md +3 -3
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +5 -4
- package/skills/catalyst-onboard/references/local-sync.md +3 -1
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +2 -2
- package/skills/whats-happening/references/status-reply.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +104 -26
- package/vendor/settings/index.d.ts +4 -0
- package/vendor/settings/index.js +2937 -0
- package/vendor/settings/provenance.json +148 -0
package/dist/onboard.js
CHANGED
|
@@ -5,7 +5,10 @@ import { createInterface } from "node:readline/promises";
|
|
|
5
5
|
import { dirname, isAbsolute, join } from "node:path";
|
|
6
6
|
import { parseMachinePaths, resolveCatalystPath, } from "../vendor/paths/index.js";
|
|
7
7
|
import { machinePathsFile } from "../vendor/paths/node.js";
|
|
8
|
+
import { configPathFor } from "./config.js";
|
|
8
9
|
import { CliError, UsageError } from "./errors.js";
|
|
10
|
+
import { onboardFileSnapshot } from "./onboard-file-snapshot.js";
|
|
11
|
+
import { parseBootstrapPlan, bootstrapPlanHash, bootstrapPlanLines, } from "./onboard-bootstrap.js";
|
|
9
12
|
export const ONBOARD_STEPS = [
|
|
10
13
|
"machine",
|
|
11
14
|
"cli",
|
|
@@ -30,6 +33,26 @@ export const ONBOARD_STEPS = [
|
|
|
30
33
|
"first-ticket",
|
|
31
34
|
"ready",
|
|
32
35
|
];
|
|
36
|
+
export function onboardIdentityLines(identity) {
|
|
37
|
+
if (!identity)
|
|
38
|
+
return [
|
|
39
|
+
"Use an existing Catalyst account or accept your invitation before approving sign-in.",
|
|
40
|
+
];
|
|
41
|
+
const label = (text) => text
|
|
42
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ")
|
|
43
|
+
.replace(/\s+/g, " ")
|
|
44
|
+
.trim()
|
|
45
|
+
.slice(0, 180);
|
|
46
|
+
const display = identity.display;
|
|
47
|
+
const person = label(display?.personLabel || identity.membershipId);
|
|
48
|
+
const email = display?.email ? label(display.email) : "";
|
|
49
|
+
const workspace = label(display?.workspaceName || identity.account);
|
|
50
|
+
const slug = display?.workspaceSlug ? label(display.workspaceSlug) : "";
|
|
51
|
+
return [
|
|
52
|
+
`Signed in as ${person}${email ? ` (${email})` : ""}`,
|
|
53
|
+
`Workspace: ${workspace}${slug ? ` (${slug})` : ""} · ${identity.role}`,
|
|
54
|
+
];
|
|
55
|
+
}
|
|
33
56
|
const EXIT_FAILED = 10;
|
|
34
57
|
const EXIT_WAITING = 11;
|
|
35
58
|
const EXIT_REFUSED = 12;
|
|
@@ -519,19 +542,27 @@ function planJournal(journal) {
|
|
|
519
542
|
}),
|
|
520
543
|
};
|
|
521
544
|
}
|
|
522
|
-
function printPlan(ctx, journal) {
|
|
545
|
+
function printPlan(ctx, journal, identity) {
|
|
523
546
|
const value = planJournal(journal);
|
|
524
547
|
ctx.stdout("Catalyst setup plan");
|
|
548
|
+
if (identity !== undefined)
|
|
549
|
+
for (const line of onboardIdentityLines(identity))
|
|
550
|
+
ctx.stdout(line);
|
|
525
551
|
for (const step of value.steps)
|
|
526
552
|
ctx.stdout(` ${step.state === "done" ? "✓" : "·"} ${ONBOARD_TITLES[step.id]}`);
|
|
553
|
+
ctx.stdout("Use cloud reads by default. Local sync is optional for SQL or sustained local reads.");
|
|
554
|
+
ctx.stdout("Setup may check one stored Claude account using a one-token provider request. This may use Claude quota. It does not refresh Codex credentials.");
|
|
527
555
|
ctx.stdout(`Next: ${value.steps.find((step) => step.state === "pending")?.id ?? "ready"}`);
|
|
528
556
|
}
|
|
529
557
|
async function confirmContinue(ctx, args, deps) {
|
|
530
558
|
if (args.flags.yes === true)
|
|
531
559
|
return true;
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
560
|
+
if (args.json ||
|
|
561
|
+
!(deps.isTty ?? (() => Boolean(process.stdin.isTTY && process.stdout.isTTY)))()) {
|
|
562
|
+
ctx.stderr(args.json
|
|
563
|
+
? "JSON mode needs --yes to accept the displayed setup plan. Pass --dry-run to inspect it."
|
|
564
|
+
: "No terminal is attached. Pass --yes to accept the displayed setup plan, or --dry-run to inspect it.");
|
|
565
|
+
return false;
|
|
535
566
|
}
|
|
536
567
|
if (deps.confirm)
|
|
537
568
|
return deps.confirm("Continue? [Y/n] ");
|
|
@@ -569,7 +600,9 @@ function stepSatisfied(step) {
|
|
|
569
600
|
(step?.state === "skipped" &&
|
|
570
601
|
(step.id === "legacy" ||
|
|
571
602
|
step.reason === "member_scope" ||
|
|
572
|
-
(step.id === "daemon" && step.reason === "local_sync_not_selected")
|
|
603
|
+
(step.id === "daemon" && step.reason === "local_sync_not_selected") ||
|
|
604
|
+
(step.id === "linear.automations" &&
|
|
605
|
+
step.reason === "automation_management_unavailable"))));
|
|
573
606
|
}
|
|
574
607
|
function requireMatchingIdentity(journal, identity) {
|
|
575
608
|
const account = journal.account ?? journal.tenant;
|
|
@@ -604,14 +637,72 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
604
637
|
for (const id of [only, resumeFrom])
|
|
605
638
|
if (id && !ONBOARD_STEPS.includes(id))
|
|
606
639
|
throw new UsageError(`unknown onboarding step: ${id}`);
|
|
640
|
+
const bootstrap = deps.bootstrap;
|
|
641
|
+
const bootstrapPlan = bootstrap
|
|
642
|
+
? parseBootstrapPlan(bootstrap.plan)
|
|
643
|
+
: undefined;
|
|
644
|
+
const reviewedBootstrapHash = bootstrapPlan
|
|
645
|
+
? bootstrapPlanHash(bootstrapPlan)
|
|
646
|
+
: undefined;
|
|
647
|
+
if (bootstrapPlan &&
|
|
648
|
+
(only ||
|
|
649
|
+
resumeFrom ||
|
|
650
|
+
bootstrapPlan.home !== ctx.home ||
|
|
651
|
+
bootstrapPlan.statePath !== onboardStatePath(ctx.home, ctx.env)))
|
|
652
|
+
throw new CliError("The staged installation belongs to another setup path. Run setup again.", "onboard-bootstrap-binding", EXIT_REFUSED);
|
|
653
|
+
const requireBootstrapIdentity = (identity) => {
|
|
654
|
+
if (bootstrapPlan &&
|
|
655
|
+
(!identity || identity.baseUrl !== bootstrapPlan.origin))
|
|
656
|
+
throw new CliError("The staged installation and verified person use different clouds. Run setup again.", "onboard-bootstrap-binding", EXIT_REFUSED);
|
|
657
|
+
};
|
|
658
|
+
const showBootstrap = () => {
|
|
659
|
+
if (!bootstrapPlan)
|
|
660
|
+
return;
|
|
661
|
+
for (const line of bootstrapPlanLines(bootstrapPlan)) {
|
|
662
|
+
if (deps.ui)
|
|
663
|
+
deps.ui.message(line);
|
|
664
|
+
else
|
|
665
|
+
ctx.stderr(line);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
const recheckBootstrap = async () => {
|
|
669
|
+
if (!bootstrap || !bootstrapPlan)
|
|
670
|
+
return;
|
|
671
|
+
if (bootstrapPlanHash(bootstrap.plan) !== reviewedBootstrapHash)
|
|
672
|
+
throw new CliError("The installation plan changed during review. Run setup again.", "onboard-bootstrap-changed", EXIT_WAITING);
|
|
673
|
+
await bootstrap.recheck();
|
|
674
|
+
if (bootstrapPlanHash(bootstrap.plan) !== reviewedBootstrapHash)
|
|
675
|
+
throw new CliError("The installation plan changed during review. Run setup again.", "onboard-bootstrap-changed", EXIT_WAITING);
|
|
676
|
+
};
|
|
607
677
|
const statePath = onboardStatePath(ctx.home, ctx.env);
|
|
678
|
+
const reviewedReceipt = deps.stageSignin
|
|
679
|
+
? onboardFileSnapshot(statePath)
|
|
680
|
+
: undefined;
|
|
681
|
+
const scopeNeedsSignin = (id) => id === "signin" || (ONBOARD_DEPENDENCIES[id] ?? []).some(scopeNeedsSignin);
|
|
682
|
+
const personalScope = !only || scopeNeedsSignin(only);
|
|
683
|
+
const plainSigninAllowed = personalScope &&
|
|
684
|
+
(args.flags.yes === true ||
|
|
685
|
+
(!args.json &&
|
|
686
|
+
(deps.isTty ??
|
|
687
|
+
(() => Boolean(process.stdin.isTTY && process.stdout.isTTY)))()));
|
|
608
688
|
let journal = readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)()) ??
|
|
609
689
|
freshJournal(ctx, cliVersion, deps);
|
|
690
|
+
const originalBinding = {
|
|
691
|
+
account: journal.account,
|
|
692
|
+
tenant: journal.tenant,
|
|
693
|
+
membershipId: journal.membershipId,
|
|
694
|
+
baseUrl: journal.baseUrl,
|
|
695
|
+
};
|
|
696
|
+
const wasBound = Boolean(journal.account ||
|
|
697
|
+
journal.tenant ||
|
|
698
|
+
journal.membershipId ||
|
|
699
|
+
journal.baseUrl);
|
|
610
700
|
if (args.flags["dry-run"] === true) {
|
|
611
701
|
if (args.json)
|
|
612
702
|
ctx.stdout(JSON.stringify(planJournal(journal)));
|
|
613
703
|
else
|
|
614
704
|
printPlan(ctx, journal);
|
|
705
|
+
showBootstrap();
|
|
615
706
|
return 0;
|
|
616
707
|
}
|
|
617
708
|
// Check tenant binding before even a local cleanup mutation. Never overwrite a mismatched receipt.
|
|
@@ -623,36 +714,153 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
623
714
|
}
|
|
624
715
|
}
|
|
625
716
|
catch (error) {
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
717
|
+
const canRenew = error instanceof CliError &&
|
|
718
|
+
error.code === "onboard-login-refresh-required" &&
|
|
719
|
+
deps.stageSignin &&
|
|
720
|
+
(deps.ui || plainSigninAllowed);
|
|
721
|
+
if (!canRenew) {
|
|
722
|
+
const code = error instanceof CliError ? error.exitCode : EXIT_FAILED;
|
|
723
|
+
ctx.stderr(error instanceof CliError &&
|
|
724
|
+
error.code === "onboard-login-refresh-required"
|
|
725
|
+
? "Renew your login with catalyst login, then run catalyst onboard. Setup was not changed."
|
|
726
|
+
: "Could not verify your Catalyst membership. Setup was not changed. Sign in with the original account to resume.");
|
|
727
|
+
if (args.json)
|
|
728
|
+
ctx.stdout(JSON.stringify({
|
|
729
|
+
...journal,
|
|
730
|
+
mode: "run",
|
|
731
|
+
exit: code,
|
|
732
|
+
complete: false,
|
|
733
|
+
}));
|
|
734
|
+
return code;
|
|
735
|
+
}
|
|
736
|
+
const renewalMessage = "Your saved login needs renewal. Sign in before reviewing this plan.";
|
|
737
|
+
if (deps.ui)
|
|
738
|
+
deps.ui.message(renewalMessage);
|
|
739
|
+
else
|
|
740
|
+
ctx.stderr(renewalMessage);
|
|
636
741
|
}
|
|
742
|
+
const identityTuple = (value) => value
|
|
743
|
+
? Object.freeze({
|
|
744
|
+
account: value.account,
|
|
745
|
+
membershipId: value.membershipId,
|
|
746
|
+
baseUrl: value.baseUrl,
|
|
747
|
+
role: value.role,
|
|
748
|
+
})
|
|
749
|
+
: null;
|
|
750
|
+
let reviewedIdentity = identityTuple(identity);
|
|
751
|
+
if (identity)
|
|
752
|
+
requireBootstrapIdentity(identity);
|
|
637
753
|
if (deps.ui)
|
|
638
|
-
deps.ui.plan(journal);
|
|
754
|
+
deps.ui.plan(journal, identity);
|
|
639
755
|
else if (!args.json)
|
|
640
|
-
printPlan(ctx, journal);
|
|
641
|
-
|
|
756
|
+
printPlan(ctx, journal, identity);
|
|
757
|
+
else
|
|
758
|
+
printPlan({ ...ctx, stdout: ctx.stderr }, journal, identity);
|
|
759
|
+
showBootstrap();
|
|
642
760
|
let localSync = args.flags["local-sync"] === true || journal.localSync === true;
|
|
643
|
-
|
|
644
|
-
|
|
761
|
+
let candidate = null;
|
|
762
|
+
// A fresh interactive sign-in needs browser approval, then one review of the verified
|
|
763
|
+
// person and workspace. Do not ask to approve a plan whose identity is still unknown.
|
|
764
|
+
if (!identity &&
|
|
765
|
+
deps.stageSignin &&
|
|
766
|
+
!deps.ui &&
|
|
767
|
+
personalScope &&
|
|
768
|
+
!plainSigninAllowed) {
|
|
769
|
+
ctx.stderr("Sign in with catalyst login, then run catalyst onboard, or use --yes to allow browser sign-in before the displayed setup plan.");
|
|
770
|
+
if (args.json)
|
|
771
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: EXIT_WAITING }));
|
|
772
|
+
return EXIT_WAITING;
|
|
773
|
+
}
|
|
774
|
+
const stageFirst = Boolean(deps.stageSignin && !identity && (deps.ui || plainSigninAllowed));
|
|
775
|
+
if (stageFirst) {
|
|
776
|
+
const message = "Sign in in your browser first. Then review your person, workspace and setup plan. Your saved connection stays unchanged until you approve that plan.";
|
|
777
|
+
if (deps.ui)
|
|
778
|
+
deps.ui.message(message);
|
|
779
|
+
else
|
|
780
|
+
ctx.stderr(message);
|
|
781
|
+
}
|
|
782
|
+
let consent = stageFirst
|
|
783
|
+
? { proceed: false, localSync, signin: true }
|
|
645
784
|
: deps.ui
|
|
646
|
-
? await deps.ui.confirmPlan(localSync
|
|
785
|
+
? await deps.ui.confirmPlan(localSync, deps.stageSignin
|
|
786
|
+
? identity
|
|
787
|
+
? "saved"
|
|
788
|
+
: "required"
|
|
789
|
+
: "unavailable")
|
|
647
790
|
: await confirmContinue(ctx, args, deps);
|
|
791
|
+
if (typeof consent !== "boolean" && consent.signin) {
|
|
792
|
+
if (!deps.stageSignin)
|
|
793
|
+
throw new Error("onboard_signin_stage_unavailable");
|
|
794
|
+
try {
|
|
795
|
+
const stagingSignals = [
|
|
796
|
+
...(deps.ui ? [deps.ui.signal] : []),
|
|
797
|
+
...(deps.signal ? [deps.signal] : []),
|
|
798
|
+
];
|
|
799
|
+
const stagingSignal = stagingSignals.length > 1
|
|
800
|
+
? AbortSignal.any(stagingSignals)
|
|
801
|
+
: stagingSignals[0];
|
|
802
|
+
if (stagingSignal?.aborted)
|
|
803
|
+
throw new CliError("Sign-in was cancelled. Your saved connection was not changed.", "onboard-signin-cancelled", EXIT_WAITING);
|
|
804
|
+
candidate = await deps.stageSignin(stagingSignal);
|
|
805
|
+
// A saved receipt may renew only its existing identity. A fresh preview can change it.
|
|
806
|
+
if (wasBound)
|
|
807
|
+
requireMatchingIdentity({ ...journal, ...originalBinding }, candidate.identity);
|
|
808
|
+
else
|
|
809
|
+
Object.assign(journal, {
|
|
810
|
+
account: undefined,
|
|
811
|
+
tenant: null,
|
|
812
|
+
membershipId: undefined,
|
|
813
|
+
baseUrl: undefined,
|
|
814
|
+
});
|
|
815
|
+
identity = candidate.identity;
|
|
816
|
+
reviewedIdentity = identityTuple(identity);
|
|
817
|
+
requireMatchingIdentity(journal, identity);
|
|
818
|
+
requireBootstrapIdentity(identity);
|
|
819
|
+
if (deps.ui)
|
|
820
|
+
deps.ui.plan(journal, identity);
|
|
821
|
+
else if (!args.json)
|
|
822
|
+
printPlan(ctx, journal, identity);
|
|
823
|
+
else
|
|
824
|
+
printPlan({ ...ctx, stdout: ctx.stderr }, journal, identity);
|
|
825
|
+
showBootstrap();
|
|
826
|
+
consent = deps.ui
|
|
827
|
+
? await deps.ui.confirmPlan(localSync, "unavailable")
|
|
828
|
+
: await confirmContinue(ctx, args, deps);
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
const message = error instanceof CliError
|
|
832
|
+
? error.message
|
|
833
|
+
: "Sign-in could not be verified. Your saved connection was not changed.";
|
|
834
|
+
if (deps.ui)
|
|
835
|
+
deps.ui.message(message);
|
|
836
|
+
else
|
|
837
|
+
ctx.stderr(message);
|
|
838
|
+
deps.ui?.dispose();
|
|
839
|
+
const code = error instanceof CliError ? error.exitCode : EXIT_WAITING;
|
|
840
|
+
if (args.json)
|
|
841
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: code }));
|
|
842
|
+
return code;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
648
845
|
if (typeof consent !== "boolean")
|
|
649
846
|
localSync = consent.localSync;
|
|
650
847
|
if (!(typeof consent === "boolean" ? consent : consent.proceed)) {
|
|
651
|
-
|
|
848
|
+
const missingConsent = !deps.ui &&
|
|
849
|
+
args.flags.yes !== true &&
|
|
850
|
+
(args.json ||
|
|
851
|
+
!(deps.isTty ??
|
|
852
|
+
(() => Boolean(process.stdin.isTTY && process.stdout.isTTY)))());
|
|
853
|
+
const code = deps.ui?.signal.aborted || missingConsent ? EXIT_WAITING : 0;
|
|
854
|
+
ctx.stderr(missingConsent
|
|
855
|
+
? "Setup is waiting for your approval. Run with --yes to accept the displayed plan."
|
|
856
|
+
: "Nothing was changed. Run the same command when ready.");
|
|
652
857
|
if (args.json)
|
|
653
|
-
ctx.stdout(JSON.stringify({ ...journal, complete: false }));
|
|
654
|
-
return
|
|
858
|
+
ctx.stdout(JSON.stringify({ ...journal, exit: code, complete: false }));
|
|
859
|
+
return code;
|
|
655
860
|
}
|
|
861
|
+
if (bootstrap)
|
|
862
|
+
requireBootstrapIdentity(identity);
|
|
863
|
+
await recheckBootstrap();
|
|
656
864
|
let lock;
|
|
657
865
|
try {
|
|
658
866
|
lock = acquireLock(ctx, deps);
|
|
@@ -685,8 +893,11 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
685
893
|
})),
|
|
686
894
|
};
|
|
687
895
|
let current = null;
|
|
896
|
+
let mayRecordProgress = candidate === null;
|
|
688
897
|
const interrupted = () => {
|
|
689
898
|
stop.abort();
|
|
899
|
+
if (!mayRecordProgress)
|
|
900
|
+
return;
|
|
690
901
|
if (current)
|
|
691
902
|
setStep(journal, {
|
|
692
903
|
id: current,
|
|
@@ -722,11 +933,41 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
722
933
|
ctx.stdout(`${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.`);
|
|
723
934
|
else
|
|
724
935
|
ctx.stdout(`Setup still needs ${journal.steps.filter((step) => !stepSatisfied(step)).length} checks.`);
|
|
725
|
-
ctx.stdout("
|
|
936
|
+
ctx.stdout(bootstrap && journalStep(journal, "machine")?.state !== "done"
|
|
937
|
+
? "resume: run the original setup command"
|
|
938
|
+
: "resume: catalyst onboard");
|
|
726
939
|
}
|
|
727
940
|
return code;
|
|
728
941
|
};
|
|
729
942
|
try {
|
|
943
|
+
await recheckBootstrap();
|
|
944
|
+
if (candidate) {
|
|
945
|
+
// Re-read while holding the receipt lock before accepting the staged connection. Config
|
|
946
|
+
// and receipt publication are ordered, not a promised transaction against other writers.
|
|
947
|
+
if (onboardFileSnapshot(statePath) !== reviewedReceipt)
|
|
948
|
+
throw new CliError("Your setup record changed during review. Run catalyst onboard again.", "onboard-receipt-changed", EXIT_WAITING);
|
|
949
|
+
const currentReceipt = readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)());
|
|
950
|
+
if (currentReceipt)
|
|
951
|
+
requireMatchingIdentity(currentReceipt, candidate.identity);
|
|
952
|
+
try {
|
|
953
|
+
await candidate.accept(signal, () => {
|
|
954
|
+
if (onboardFileSnapshot(statePath) !== reviewedReceipt)
|
|
955
|
+
throw new CliError("Your setup record changed during sign-in. Run catalyst onboard again.", "onboard-receipt-changed", EXIT_WAITING);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
ctx.stderr(candidate.accepted
|
|
960
|
+
? "Your connection was accepted, but setup could not be recorded. Run catalyst onboard to resume."
|
|
961
|
+
: error instanceof CliError
|
|
962
|
+
? error.message
|
|
963
|
+
: "Sign-in was not accepted. Your saved connection was not changed.");
|
|
964
|
+
if (args.json)
|
|
965
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: EXIT_WAITING }));
|
|
966
|
+
return EXIT_WAITING;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (candidate && onboardFileSnapshot(statePath) !== reviewedReceipt)
|
|
970
|
+
throw new CliError("Your setup record changed during sign-in. Run catalyst onboard again.", "onboard-receipt-changed", EXIT_WAITING);
|
|
730
971
|
journal =
|
|
731
972
|
readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)()) ??
|
|
732
973
|
journal;
|
|
@@ -741,6 +982,82 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
741
982
|
setStep(journal, { id, state: "pending" });
|
|
742
983
|
journal.steps.sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id));
|
|
743
984
|
journal.operations ??= {};
|
|
985
|
+
if (candidate) {
|
|
986
|
+
writeOnboardJournal(statePath, journal);
|
|
987
|
+
mayRecordProgress = true;
|
|
988
|
+
}
|
|
989
|
+
if (bootstrap && bootstrapPlan && reviewedBootstrapHash) {
|
|
990
|
+
if (!identity || identity.baseUrl !== bootstrapPlan.origin)
|
|
991
|
+
throw new CliError("The staged installation and verified person use different clouds. Run setup again.", "onboard-bootstrap-binding", EXIT_REFUSED);
|
|
992
|
+
if (signal.aborted)
|
|
993
|
+
return finish(EXIT_WAITING);
|
|
994
|
+
await recheckBootstrap();
|
|
995
|
+
if (!deps.identity || !reviewedIdentity)
|
|
996
|
+
throw new CliError("The verified person could not be checked before installation. Run setup again.", "onboard-bootstrap-identity", EXIT_WAITING);
|
|
997
|
+
const beforeConfig = onboardFileSnapshot(configPathFor(ctx.home));
|
|
998
|
+
const freshIdentity = await deps.identity(journal);
|
|
999
|
+
if (!freshIdentity ||
|
|
1000
|
+
beforeConfig !== onboardFileSnapshot(configPathFor(ctx.home)) ||
|
|
1001
|
+
freshIdentity.account !== reviewedIdentity.account ||
|
|
1002
|
+
freshIdentity.membershipId !== reviewedIdentity.membershipId ||
|
|
1003
|
+
freshIdentity.baseUrl !== reviewedIdentity.baseUrl ||
|
|
1004
|
+
freshIdentity.role !== reviewedIdentity.role)
|
|
1005
|
+
throw new CliError("Your person or workspace changed during review. Run setup again.", "onboard-bootstrap-identity", EXIT_REFUSED);
|
|
1006
|
+
requireMatchingIdentity(journal, freshIdentity);
|
|
1007
|
+
identity = freshIdentity;
|
|
1008
|
+
if (signal.aborted)
|
|
1009
|
+
return finish(EXIT_WAITING);
|
|
1010
|
+
bootstrap.assertCurrent();
|
|
1011
|
+
// The capability stays in native memory; the child protocol must transport it over a private FD.
|
|
1012
|
+
// The continuation contract joins owned children before returning, so finally cannot release early.
|
|
1013
|
+
current = "machine";
|
|
1014
|
+
setStep(journal, {
|
|
1015
|
+
id: "machine",
|
|
1016
|
+
state: "running",
|
|
1017
|
+
at: isoNow(ctx, deps),
|
|
1018
|
+
});
|
|
1019
|
+
writeOnboardJournal(statePath, journal);
|
|
1020
|
+
try {
|
|
1021
|
+
await bootstrap.continue(Object.freeze({
|
|
1022
|
+
plan: bootstrapPlan,
|
|
1023
|
+
planHash: reviewedBootstrapHash,
|
|
1024
|
+
account: identity.account,
|
|
1025
|
+
person: identity.membershipId,
|
|
1026
|
+
origin: identity.baseUrl,
|
|
1027
|
+
role: identity.role,
|
|
1028
|
+
localSync,
|
|
1029
|
+
runId: journal.runId,
|
|
1030
|
+
lockPath: lock.path,
|
|
1031
|
+
ownerPid: lock.owner.pid,
|
|
1032
|
+
ownerToken: lock.owner.token,
|
|
1033
|
+
}), signal);
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
setStep(journal, {
|
|
1037
|
+
id: "machine",
|
|
1038
|
+
state: signal.aborted ? "waiting" : "failed",
|
|
1039
|
+
reason: signal.aborted ? "interrupted" : "bootstrap_install_failed",
|
|
1040
|
+
at: isoNow(ctx, deps),
|
|
1041
|
+
});
|
|
1042
|
+
ctx.stderr("Installation stopped. Completed files remain. Run the original setup command to resume.");
|
|
1043
|
+
return finish(signal.aborted ? EXIT_WAITING : EXIT_FAILED);
|
|
1044
|
+
}
|
|
1045
|
+
current = null;
|
|
1046
|
+
if (signal.aborted) {
|
|
1047
|
+
setStep(journal, {
|
|
1048
|
+
id: "machine",
|
|
1049
|
+
state: "waiting",
|
|
1050
|
+
reason: "interrupted",
|
|
1051
|
+
at: isoNow(ctx, deps),
|
|
1052
|
+
});
|
|
1053
|
+
return finish(EXIT_WAITING);
|
|
1054
|
+
}
|
|
1055
|
+
if (deps.identity) {
|
|
1056
|
+
const refreshed = await deps.identity(journal);
|
|
1057
|
+
requireMatchingIdentity(journal, refreshed);
|
|
1058
|
+
identity = refreshed;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
744
1061
|
let refused = false;
|
|
745
1062
|
const needed = new Set();
|
|
746
1063
|
const include = (id) => {
|
|
@@ -862,6 +1179,9 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
862
1179
|
requireMatchingIdentity(journal, identity);
|
|
863
1180
|
if (!identity)
|
|
864
1181
|
result = { state: "failed", reason: "membership_not_verified" };
|
|
1182
|
+
else
|
|
1183
|
+
for (const line of onboardIdentityLines(identity))
|
|
1184
|
+
stepCtx.stdout(line);
|
|
865
1185
|
}
|
|
866
1186
|
if (result.state === "refused")
|
|
867
1187
|
refused = true;
|
|
@@ -882,13 +1202,17 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
882
1202
|
/^[a-z][a-z0-9_-]{1,63}$/.test(error.code)
|
|
883
1203
|
? error.code.replaceAll("-", "_")
|
|
884
1204
|
: "step_failed";
|
|
1205
|
+
const renewLogin = error instanceof CliError &&
|
|
1206
|
+
error.code === "onboard-login-refresh-required";
|
|
885
1207
|
setStep(journal, {
|
|
886
1208
|
id,
|
|
887
|
-
state: "failed",
|
|
1209
|
+
state: renewLogin ? "waiting" : "failed",
|
|
888
1210
|
reason,
|
|
889
1211
|
at: isoNow(ctx, deps),
|
|
890
1212
|
});
|
|
891
|
-
stepCtx.stderr(
|
|
1213
|
+
stepCtx.stderr(renewLogin
|
|
1214
|
+
? "Renew your login with catalyst login, then run catalyst onboard to resume."
|
|
1215
|
+
: `✗ ${ONBOARD_TITLES[id]}: ${reason}. The safe reason is saved; run the same command to resume.`);
|
|
892
1216
|
}
|
|
893
1217
|
if (signal.aborted) {
|
|
894
1218
|
setStep(journal, {
|
|
@@ -916,9 +1240,17 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
916
1240
|
return finish(code);
|
|
917
1241
|
}
|
|
918
1242
|
catch (error) {
|
|
1243
|
+
if (candidate?.accepted && !mayRecordProgress) {
|
|
1244
|
+
ctx.stderr("Your connection was accepted, but setup could not be recorded. Run catalyst onboard to resume.");
|
|
1245
|
+
if (args.json)
|
|
1246
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: EXIT_WAITING }));
|
|
1247
|
+
return EXIT_WAITING;
|
|
1248
|
+
}
|
|
919
1249
|
// Identity can change while the lock is held; stop before the next action.
|
|
920
1250
|
if (error instanceof CliError) {
|
|
921
1251
|
ctx.stderr(error.message);
|
|
1252
|
+
if (args.json)
|
|
1253
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: error.exitCode }));
|
|
922
1254
|
return error.exitCode;
|
|
923
1255
|
}
|
|
924
1256
|
throw error;
|