@catalyst-cloud/cli 0.14.6 → 0.14.8
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 +14 -0
- package/README.md +7 -5
- package/dist/args.js +68 -18
- package/dist/browser.js +37 -3
- package/dist/cli.js +58 -12
- package/dist/config.js +17 -5
- package/dist/consent-browser.js +58 -0
- 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 +250 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +205 -0
- package/dist/events-cloud-tail.js +146 -0
- package/dist/events-cloud.js +619 -0
- package/dist/events.js +186 -31
- package/dist/oauth.js +129 -34
- 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-github.js +14 -3
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-next.js +139 -0
- package/dist/onboard-personal.js +14 -3
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +146 -37
- package/dist/onboard-runtime.js +108 -13
- 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 +85 -59
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard-workspace.js +14 -3
- package/dist/onboard.js +422 -39
- package/dist/owned-fetch.js +182 -0
- package/dist/pagination.js +13 -1
- package/dist/query.js +104 -31
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/repository-id.js +5 -0
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +4 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/local-replica.md +78 -0
- package/skills/catalyst-github/references/reading-from-the-cloud.md +24 -0
- 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 +5 -3
- package/skills/catalyst-linear/references/local-replica.md +91 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/references/reading-from-the-cloud.md +38 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +6 -4
- package/skills/catalyst-onboard/references/local-replica.md +40 -0
- package/skills/catalyst-onboard/references/local-sync.md +8 -4
- package/skills/catalyst-onboard/scripts/local-sync.mjs +2 -2
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -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 +4 -2
- package/skills/whats-happening/references/local-replica.md +51 -0
- package/skills/whats-happening/references/reading-from-the-cloud.md +21 -0
- 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,11 @@ 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 { onboardReasonText, onboardTeamKey, savedOnboardBaseUrl, } from "./onboard-next.js";
|
|
12
|
+
import { parseBootstrapPlan, bootstrapPlanHash, bootstrapPlanLines, } from "./onboard-bootstrap.js";
|
|
9
13
|
export const ONBOARD_STEPS = [
|
|
10
14
|
"machine",
|
|
11
15
|
"cli",
|
|
@@ -30,6 +34,26 @@ export const ONBOARD_STEPS = [
|
|
|
30
34
|
"first-ticket",
|
|
31
35
|
"ready",
|
|
32
36
|
];
|
|
37
|
+
export function onboardIdentityLines(identity) {
|
|
38
|
+
if (!identity)
|
|
39
|
+
return [
|
|
40
|
+
"Use an existing Catalyst account or accept your invitation before approving sign-in.",
|
|
41
|
+
];
|
|
42
|
+
const label = (text) => text
|
|
43
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ")
|
|
44
|
+
.replace(/\s+/g, " ")
|
|
45
|
+
.trim()
|
|
46
|
+
.slice(0, 180);
|
|
47
|
+
const display = identity.display;
|
|
48
|
+
const person = label(display?.personLabel || identity.membershipId);
|
|
49
|
+
const email = display?.email ? label(display.email) : "";
|
|
50
|
+
const workspace = label(display?.workspaceName || identity.account);
|
|
51
|
+
const slug = display?.workspaceSlug ? label(display.workspaceSlug) : "";
|
|
52
|
+
return [
|
|
53
|
+
`Signed in as ${person}${email ? ` (${email})` : ""}`,
|
|
54
|
+
`Workspace: ${workspace}${slug ? ` (${slug})` : ""} · ${identity.role}`,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
33
57
|
const EXIT_FAILED = 10;
|
|
34
58
|
const EXIT_WAITING = 11;
|
|
35
59
|
const EXIT_REFUSED = 12;
|
|
@@ -79,15 +103,14 @@ export const ONBOARD_DEPENDENCIES = {
|
|
|
79
103
|
values: ["settings"],
|
|
80
104
|
capacity: ["projects"],
|
|
81
105
|
daemon: ["signin"],
|
|
106
|
+
// What dispatch needs. Environment approval gates only implement and pr, and the first phase
|
|
107
|
+
// needs no imported values, so repository settings and local sync do not hold a first ticket.
|
|
82
108
|
"first-ticket": [
|
|
83
109
|
"projects",
|
|
84
110
|
"linear.adopt",
|
|
85
111
|
"linear.automations",
|
|
86
112
|
"accounts",
|
|
87
|
-
"settings",
|
|
88
|
-
"values",
|
|
89
113
|
"capacity",
|
|
90
|
-
"daemon",
|
|
91
114
|
],
|
|
92
115
|
};
|
|
93
116
|
const ADMIN_STEPS = new Set([
|
|
@@ -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,44 @@ 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"))));
|
|
606
|
+
}
|
|
607
|
+
/** Steps whose wait does not hold a full run's exit code: work runs without them. A failure still
|
|
608
|
+
* fails the run, and the receipt's `complete` still needs them satisfied. Never used for gating. */
|
|
609
|
+
export const ONBOARD_DEFERRED_STEPS = new Set([
|
|
610
|
+
"settings",
|
|
611
|
+
"values",
|
|
612
|
+
"housekeeping",
|
|
613
|
+
"first-ticket",
|
|
614
|
+
]);
|
|
615
|
+
/** A full run that exited 0 with deferred steps still open: ready for work, not complete. */
|
|
616
|
+
export function onboardReadyForWork(journal, only) {
|
|
617
|
+
return !only && journal.exit === 0 && !journal.complete;
|
|
618
|
+
}
|
|
619
|
+
/** A recorded step's next action; a prerequisite wait names the first unfinished step it needs. */
|
|
620
|
+
export function onboardStepAction(journal, step, baseUrl) {
|
|
621
|
+
if (!step.reason)
|
|
622
|
+
return "Not checked yet. Run catalyst onboard.";
|
|
623
|
+
const waitsFor = journal &&
|
|
624
|
+
(ONBOARD_DEPENDENCIES[step.id] ?? []).find((parent) => !stepSatisfied(journalStep(journal, parent)));
|
|
625
|
+
return onboardReasonText(step, {
|
|
626
|
+
baseUrl,
|
|
627
|
+
journal,
|
|
628
|
+
...(waitsFor ? { waitsFor: ONBOARD_TITLES[waitsFor] } : {}),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
/** "<title>: <action>" for each recorded step still blocking onboarding, then the first-ticket hint
|
|
632
|
+
* once the steps a ticket needs are done. Display only: completeness and exits stay with the engine. */
|
|
633
|
+
export function onboardNextActions(journal, baseUrl, only) {
|
|
634
|
+
const lines = journal.steps
|
|
635
|
+
.filter((step) => (!only || step.id === only) && !stepSatisfied(step))
|
|
636
|
+
.sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id))
|
|
637
|
+
.map((step) => `${ONBOARD_TITLES[step.id]}: ${onboardStepAction(journal, step, baseUrl)}`);
|
|
638
|
+
if (["projects", "accounts", "linear.adopt", "capacity"].every((id) => journalStep(journal, id)?.state === "done"))
|
|
639
|
+
lines.push(`Move a ticket in ${onboardTeamKey(journal) ?? "<TEAM KEY>"} to Todo; \`catalyst explain <ticket>\` says why it is or is not starting.`);
|
|
640
|
+
return lines;
|
|
573
641
|
}
|
|
574
642
|
function requireMatchingIdentity(journal, identity) {
|
|
575
643
|
const account = journal.account ?? journal.tenant;
|
|
@@ -604,14 +672,72 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
604
672
|
for (const id of [only, resumeFrom])
|
|
605
673
|
if (id && !ONBOARD_STEPS.includes(id))
|
|
606
674
|
throw new UsageError(`unknown onboarding step: ${id}`);
|
|
675
|
+
const bootstrap = deps.bootstrap;
|
|
676
|
+
const bootstrapPlan = bootstrap
|
|
677
|
+
? parseBootstrapPlan(bootstrap.plan)
|
|
678
|
+
: undefined;
|
|
679
|
+
const reviewedBootstrapHash = bootstrapPlan
|
|
680
|
+
? bootstrapPlanHash(bootstrapPlan)
|
|
681
|
+
: undefined;
|
|
682
|
+
if (bootstrapPlan &&
|
|
683
|
+
(only ||
|
|
684
|
+
resumeFrom ||
|
|
685
|
+
bootstrapPlan.home !== ctx.home ||
|
|
686
|
+
bootstrapPlan.statePath !== onboardStatePath(ctx.home, ctx.env)))
|
|
687
|
+
throw new CliError("The staged installation belongs to another setup path. Run setup again.", "onboard-bootstrap-binding", EXIT_REFUSED);
|
|
688
|
+
const requireBootstrapIdentity = (identity) => {
|
|
689
|
+
if (bootstrapPlan &&
|
|
690
|
+
(!identity || identity.baseUrl !== bootstrapPlan.origin))
|
|
691
|
+
throw new CliError("The staged installation and verified person use different clouds. Run setup again.", "onboard-bootstrap-binding", EXIT_REFUSED);
|
|
692
|
+
};
|
|
693
|
+
const showBootstrap = () => {
|
|
694
|
+
if (!bootstrapPlan)
|
|
695
|
+
return;
|
|
696
|
+
for (const line of bootstrapPlanLines(bootstrapPlan)) {
|
|
697
|
+
if (deps.ui)
|
|
698
|
+
deps.ui.message(line);
|
|
699
|
+
else
|
|
700
|
+
ctx.stderr(line);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
const recheckBootstrap = async () => {
|
|
704
|
+
if (!bootstrap || !bootstrapPlan)
|
|
705
|
+
return;
|
|
706
|
+
if (bootstrapPlanHash(bootstrap.plan) !== reviewedBootstrapHash)
|
|
707
|
+
throw new CliError("The installation plan changed during review. Run setup again.", "onboard-bootstrap-changed", EXIT_WAITING);
|
|
708
|
+
await bootstrap.recheck();
|
|
709
|
+
if (bootstrapPlanHash(bootstrap.plan) !== reviewedBootstrapHash)
|
|
710
|
+
throw new CliError("The installation plan changed during review. Run setup again.", "onboard-bootstrap-changed", EXIT_WAITING);
|
|
711
|
+
};
|
|
607
712
|
const statePath = onboardStatePath(ctx.home, ctx.env);
|
|
713
|
+
const reviewedReceipt = deps.stageSignin
|
|
714
|
+
? onboardFileSnapshot(statePath)
|
|
715
|
+
: undefined;
|
|
716
|
+
const scopeNeedsSignin = (id) => id === "signin" || (ONBOARD_DEPENDENCIES[id] ?? []).some(scopeNeedsSignin);
|
|
717
|
+
const personalScope = !only || scopeNeedsSignin(only);
|
|
718
|
+
const plainSigninAllowed = personalScope &&
|
|
719
|
+
(args.flags.yes === true ||
|
|
720
|
+
(!args.json &&
|
|
721
|
+
(deps.isTty ??
|
|
722
|
+
(() => Boolean(process.stdin.isTTY && process.stdout.isTTY)))()));
|
|
608
723
|
let journal = readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)()) ??
|
|
609
724
|
freshJournal(ctx, cliVersion, deps);
|
|
725
|
+
const originalBinding = {
|
|
726
|
+
account: journal.account,
|
|
727
|
+
tenant: journal.tenant,
|
|
728
|
+
membershipId: journal.membershipId,
|
|
729
|
+
baseUrl: journal.baseUrl,
|
|
730
|
+
};
|
|
731
|
+
const wasBound = Boolean(journal.account ||
|
|
732
|
+
journal.tenant ||
|
|
733
|
+
journal.membershipId ||
|
|
734
|
+
journal.baseUrl);
|
|
610
735
|
if (args.flags["dry-run"] === true) {
|
|
611
736
|
if (args.json)
|
|
612
737
|
ctx.stdout(JSON.stringify(planJournal(journal)));
|
|
613
738
|
else
|
|
614
739
|
printPlan(ctx, journal);
|
|
740
|
+
showBootstrap();
|
|
615
741
|
return 0;
|
|
616
742
|
}
|
|
617
743
|
// Check tenant binding before even a local cleanup mutation. Never overwrite a mismatched receipt.
|
|
@@ -623,36 +749,153 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
623
749
|
}
|
|
624
750
|
}
|
|
625
751
|
catch (error) {
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
752
|
+
const canRenew = error instanceof CliError &&
|
|
753
|
+
error.code === "onboard-login-refresh-required" &&
|
|
754
|
+
deps.stageSignin &&
|
|
755
|
+
(deps.ui || plainSigninAllowed);
|
|
756
|
+
if (!canRenew) {
|
|
757
|
+
const code = error instanceof CliError ? error.exitCode : EXIT_FAILED;
|
|
758
|
+
ctx.stderr(error instanceof CliError &&
|
|
759
|
+
error.code === "onboard-login-refresh-required"
|
|
760
|
+
? "Renew your login with catalyst login, then run catalyst onboard. Setup was not changed."
|
|
761
|
+
: "Could not verify your Catalyst membership. Setup was not changed. Sign in with the original account to resume.");
|
|
762
|
+
if (args.json)
|
|
763
|
+
ctx.stdout(JSON.stringify({
|
|
764
|
+
...journal,
|
|
765
|
+
mode: "run",
|
|
766
|
+
exit: code,
|
|
767
|
+
complete: false,
|
|
768
|
+
}));
|
|
769
|
+
return code;
|
|
770
|
+
}
|
|
771
|
+
const renewalMessage = "Your saved login needs renewal. Sign in before reviewing this plan.";
|
|
772
|
+
if (deps.ui)
|
|
773
|
+
deps.ui.message(renewalMessage);
|
|
774
|
+
else
|
|
775
|
+
ctx.stderr(renewalMessage);
|
|
636
776
|
}
|
|
777
|
+
const identityTuple = (value) => value
|
|
778
|
+
? Object.freeze({
|
|
779
|
+
account: value.account,
|
|
780
|
+
membershipId: value.membershipId,
|
|
781
|
+
baseUrl: value.baseUrl,
|
|
782
|
+
role: value.role,
|
|
783
|
+
})
|
|
784
|
+
: null;
|
|
785
|
+
let reviewedIdentity = identityTuple(identity);
|
|
786
|
+
if (identity)
|
|
787
|
+
requireBootstrapIdentity(identity);
|
|
637
788
|
if (deps.ui)
|
|
638
|
-
deps.ui.plan(journal);
|
|
789
|
+
deps.ui.plan(journal, identity);
|
|
639
790
|
else if (!args.json)
|
|
640
|
-
printPlan(ctx, journal);
|
|
641
|
-
|
|
791
|
+
printPlan(ctx, journal, identity);
|
|
792
|
+
else
|
|
793
|
+
printPlan({ ...ctx, stdout: ctx.stderr }, journal, identity);
|
|
794
|
+
showBootstrap();
|
|
642
795
|
let localSync = args.flags["local-sync"] === true || journal.localSync === true;
|
|
643
|
-
|
|
644
|
-
|
|
796
|
+
let candidate = null;
|
|
797
|
+
// A fresh interactive sign-in needs browser approval, then one review of the verified
|
|
798
|
+
// person and workspace. Do not ask to approve a plan whose identity is still unknown.
|
|
799
|
+
if (!identity &&
|
|
800
|
+
deps.stageSignin &&
|
|
801
|
+
!deps.ui &&
|
|
802
|
+
personalScope &&
|
|
803
|
+
!plainSigninAllowed) {
|
|
804
|
+
ctx.stderr("Sign in with catalyst login, then run catalyst onboard, or use --yes to allow browser sign-in before the displayed setup plan.");
|
|
805
|
+
if (args.json)
|
|
806
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: EXIT_WAITING }));
|
|
807
|
+
return EXIT_WAITING;
|
|
808
|
+
}
|
|
809
|
+
const stageFirst = Boolean(deps.stageSignin && !identity && (deps.ui || plainSigninAllowed));
|
|
810
|
+
if (stageFirst) {
|
|
811
|
+
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.";
|
|
812
|
+
if (deps.ui)
|
|
813
|
+
deps.ui.message(message);
|
|
814
|
+
else
|
|
815
|
+
ctx.stderr(message);
|
|
816
|
+
}
|
|
817
|
+
let consent = stageFirst
|
|
818
|
+
? { proceed: false, localSync, signin: true }
|
|
645
819
|
: deps.ui
|
|
646
|
-
? await deps.ui.confirmPlan(localSync
|
|
820
|
+
? await deps.ui.confirmPlan(localSync, deps.stageSignin
|
|
821
|
+
? identity
|
|
822
|
+
? "saved"
|
|
823
|
+
: "required"
|
|
824
|
+
: "unavailable")
|
|
647
825
|
: await confirmContinue(ctx, args, deps);
|
|
826
|
+
if (typeof consent !== "boolean" && consent.signin) {
|
|
827
|
+
if (!deps.stageSignin)
|
|
828
|
+
throw new Error("onboard_signin_stage_unavailable");
|
|
829
|
+
try {
|
|
830
|
+
const stagingSignals = [
|
|
831
|
+
...(deps.ui ? [deps.ui.signal] : []),
|
|
832
|
+
...(deps.signal ? [deps.signal] : []),
|
|
833
|
+
];
|
|
834
|
+
const stagingSignal = stagingSignals.length > 1
|
|
835
|
+
? AbortSignal.any(stagingSignals)
|
|
836
|
+
: stagingSignals[0];
|
|
837
|
+
if (stagingSignal?.aborted)
|
|
838
|
+
throw new CliError("Sign-in was cancelled. Your saved connection was not changed.", "onboard-signin-cancelled", EXIT_WAITING);
|
|
839
|
+
candidate = await deps.stageSignin(stagingSignal);
|
|
840
|
+
// A saved receipt may renew only its existing identity. A fresh preview can change it.
|
|
841
|
+
if (wasBound)
|
|
842
|
+
requireMatchingIdentity({ ...journal, ...originalBinding }, candidate.identity);
|
|
843
|
+
else
|
|
844
|
+
Object.assign(journal, {
|
|
845
|
+
account: undefined,
|
|
846
|
+
tenant: null,
|
|
847
|
+
membershipId: undefined,
|
|
848
|
+
baseUrl: undefined,
|
|
849
|
+
});
|
|
850
|
+
identity = candidate.identity;
|
|
851
|
+
reviewedIdentity = identityTuple(identity);
|
|
852
|
+
requireMatchingIdentity(journal, identity);
|
|
853
|
+
requireBootstrapIdentity(identity);
|
|
854
|
+
if (deps.ui)
|
|
855
|
+
deps.ui.plan(journal, identity);
|
|
856
|
+
else if (!args.json)
|
|
857
|
+
printPlan(ctx, journal, identity);
|
|
858
|
+
else
|
|
859
|
+
printPlan({ ...ctx, stdout: ctx.stderr }, journal, identity);
|
|
860
|
+
showBootstrap();
|
|
861
|
+
consent = deps.ui
|
|
862
|
+
? await deps.ui.confirmPlan(localSync, "unavailable")
|
|
863
|
+
: await confirmContinue(ctx, args, deps);
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
const message = error instanceof CliError
|
|
867
|
+
? error.message
|
|
868
|
+
: "Sign-in could not be verified. Your saved connection was not changed.";
|
|
869
|
+
if (deps.ui)
|
|
870
|
+
deps.ui.message(message);
|
|
871
|
+
else
|
|
872
|
+
ctx.stderr(message);
|
|
873
|
+
deps.ui?.dispose();
|
|
874
|
+
const code = error instanceof CliError ? error.exitCode : EXIT_WAITING;
|
|
875
|
+
if (args.json)
|
|
876
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: code }));
|
|
877
|
+
return code;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
648
880
|
if (typeof consent !== "boolean")
|
|
649
881
|
localSync = consent.localSync;
|
|
650
882
|
if (!(typeof consent === "boolean" ? consent : consent.proceed)) {
|
|
651
|
-
|
|
883
|
+
const missingConsent = !deps.ui &&
|
|
884
|
+
args.flags.yes !== true &&
|
|
885
|
+
(args.json ||
|
|
886
|
+
!(deps.isTty ??
|
|
887
|
+
(() => Boolean(process.stdin.isTTY && process.stdout.isTTY)))());
|
|
888
|
+
const code = deps.ui?.signal.aborted || missingConsent ? EXIT_WAITING : 0;
|
|
889
|
+
ctx.stderr(missingConsent
|
|
890
|
+
? "Setup is waiting for your approval. Run with --yes to accept the displayed plan."
|
|
891
|
+
: "Nothing was changed. Run the same command when ready.");
|
|
652
892
|
if (args.json)
|
|
653
|
-
ctx.stdout(JSON.stringify({ ...journal, complete: false }));
|
|
654
|
-
return
|
|
893
|
+
ctx.stdout(JSON.stringify({ ...journal, exit: code, complete: false }));
|
|
894
|
+
return code;
|
|
655
895
|
}
|
|
896
|
+
if (bootstrap)
|
|
897
|
+
requireBootstrapIdentity(identity);
|
|
898
|
+
await recheckBootstrap();
|
|
656
899
|
let lock;
|
|
657
900
|
try {
|
|
658
901
|
lock = acquireLock(ctx, deps);
|
|
@@ -685,8 +928,11 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
685
928
|
})),
|
|
686
929
|
};
|
|
687
930
|
let current = null;
|
|
931
|
+
let mayRecordProgress = candidate === null;
|
|
688
932
|
const interrupted = () => {
|
|
689
933
|
stop.abort();
|
|
934
|
+
if (!mayRecordProgress)
|
|
935
|
+
return;
|
|
690
936
|
if (current)
|
|
691
937
|
setStep(journal, {
|
|
692
938
|
id: current,
|
|
@@ -709,7 +955,10 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
709
955
|
process.on(name, interrupted);
|
|
710
956
|
const finish = (code) => {
|
|
711
957
|
journal.exit = code;
|
|
712
|
-
journal.complete =
|
|
958
|
+
journal.complete =
|
|
959
|
+
!only &&
|
|
960
|
+
code === 0 &&
|
|
961
|
+
ONBOARD_STEPS.every((id) => stepSatisfied(journalStep(journal, id)));
|
|
713
962
|
writeOnboardJournal(statePath, journal);
|
|
714
963
|
if (args.json)
|
|
715
964
|
ctx.stdout(JSON.stringify(journal));
|
|
@@ -718,15 +967,54 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
718
967
|
else {
|
|
719
968
|
if (journal.complete)
|
|
720
969
|
ctx.stdout("Onboarding complete.");
|
|
970
|
+
else if (onboardReadyForWork(journal, only)) {
|
|
971
|
+
ctx.stdout("Ready for work.");
|
|
972
|
+
ctx.stdout("Next, when you want:");
|
|
973
|
+
for (const line of onboardNextActions(journal, savedOnboardBaseUrl(ctx.home)))
|
|
974
|
+
ctx.stdout(line);
|
|
975
|
+
}
|
|
721
976
|
else if (only && code === 0)
|
|
722
977
|
ctx.stdout(`${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.`);
|
|
723
|
-
else
|
|
978
|
+
else {
|
|
724
979
|
ctx.stdout(`Setup still needs ${journal.steps.filter((step) => !stepSatisfied(step)).length} checks.`);
|
|
725
|
-
|
|
980
|
+
for (const line of onboardNextActions(journal, savedOnboardBaseUrl(ctx.home), only))
|
|
981
|
+
ctx.stdout(line);
|
|
982
|
+
}
|
|
983
|
+
ctx.stdout(bootstrap && journalStep(journal, "machine")?.state !== "done"
|
|
984
|
+
? "resume: run the original setup command"
|
|
985
|
+
: "resume: catalyst onboard");
|
|
726
986
|
}
|
|
727
987
|
return code;
|
|
728
988
|
};
|
|
729
989
|
try {
|
|
990
|
+
await recheckBootstrap();
|
|
991
|
+
if (candidate) {
|
|
992
|
+
// Re-read while holding the receipt lock before accepting the staged connection. Config
|
|
993
|
+
// and receipt publication are ordered, not a promised transaction against other writers.
|
|
994
|
+
if (onboardFileSnapshot(statePath) !== reviewedReceipt)
|
|
995
|
+
throw new CliError("Your setup record changed during review. Run catalyst onboard again.", "onboard-receipt-changed", EXIT_WAITING);
|
|
996
|
+
const currentReceipt = readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)());
|
|
997
|
+
if (currentReceipt)
|
|
998
|
+
requireMatchingIdentity(currentReceipt, candidate.identity);
|
|
999
|
+
try {
|
|
1000
|
+
await candidate.accept(signal, () => {
|
|
1001
|
+
if (onboardFileSnapshot(statePath) !== reviewedReceipt)
|
|
1002
|
+
throw new CliError("Your setup record changed during sign-in. Run catalyst onboard again.", "onboard-receipt-changed", EXIT_WAITING);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
ctx.stderr(candidate.accepted
|
|
1007
|
+
? "Your connection was accepted, but setup could not be recorded. Run catalyst onboard to resume."
|
|
1008
|
+
: error instanceof CliError
|
|
1009
|
+
? error.message
|
|
1010
|
+
: "Sign-in was not accepted. Your saved connection was not changed.");
|
|
1011
|
+
if (args.json)
|
|
1012
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: EXIT_WAITING }));
|
|
1013
|
+
return EXIT_WAITING;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (candidate && onboardFileSnapshot(statePath) !== reviewedReceipt)
|
|
1017
|
+
throw new CliError("Your setup record changed during sign-in. Run catalyst onboard again.", "onboard-receipt-changed", EXIT_WAITING);
|
|
730
1018
|
journal =
|
|
731
1019
|
readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)()) ??
|
|
732
1020
|
journal;
|
|
@@ -741,6 +1029,82 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
741
1029
|
setStep(journal, { id, state: "pending" });
|
|
742
1030
|
journal.steps.sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id));
|
|
743
1031
|
journal.operations ??= {};
|
|
1032
|
+
if (candidate) {
|
|
1033
|
+
writeOnboardJournal(statePath, journal);
|
|
1034
|
+
mayRecordProgress = true;
|
|
1035
|
+
}
|
|
1036
|
+
if (bootstrap && bootstrapPlan && reviewedBootstrapHash) {
|
|
1037
|
+
if (!identity || identity.baseUrl !== bootstrapPlan.origin)
|
|
1038
|
+
throw new CliError("The staged installation and verified person use different clouds. Run setup again.", "onboard-bootstrap-binding", EXIT_REFUSED);
|
|
1039
|
+
if (signal.aborted)
|
|
1040
|
+
return finish(EXIT_WAITING);
|
|
1041
|
+
await recheckBootstrap();
|
|
1042
|
+
if (!deps.identity || !reviewedIdentity)
|
|
1043
|
+
throw new CliError("The verified person could not be checked before installation. Run setup again.", "onboard-bootstrap-identity", EXIT_WAITING);
|
|
1044
|
+
const beforeConfig = onboardFileSnapshot(configPathFor(ctx.home));
|
|
1045
|
+
const freshIdentity = await deps.identity(journal);
|
|
1046
|
+
if (!freshIdentity ||
|
|
1047
|
+
beforeConfig !== onboardFileSnapshot(configPathFor(ctx.home)) ||
|
|
1048
|
+
freshIdentity.account !== reviewedIdentity.account ||
|
|
1049
|
+
freshIdentity.membershipId !== reviewedIdentity.membershipId ||
|
|
1050
|
+
freshIdentity.baseUrl !== reviewedIdentity.baseUrl ||
|
|
1051
|
+
freshIdentity.role !== reviewedIdentity.role)
|
|
1052
|
+
throw new CliError("Your person or workspace changed during review. Run setup again.", "onboard-bootstrap-identity", EXIT_REFUSED);
|
|
1053
|
+
requireMatchingIdentity(journal, freshIdentity);
|
|
1054
|
+
identity = freshIdentity;
|
|
1055
|
+
if (signal.aborted)
|
|
1056
|
+
return finish(EXIT_WAITING);
|
|
1057
|
+
bootstrap.assertCurrent();
|
|
1058
|
+
// The capability stays in native memory; the child protocol must transport it over a private FD.
|
|
1059
|
+
// The continuation contract joins owned children before returning, so finally cannot release early.
|
|
1060
|
+
current = "machine";
|
|
1061
|
+
setStep(journal, {
|
|
1062
|
+
id: "machine",
|
|
1063
|
+
state: "running",
|
|
1064
|
+
at: isoNow(ctx, deps),
|
|
1065
|
+
});
|
|
1066
|
+
writeOnboardJournal(statePath, journal);
|
|
1067
|
+
try {
|
|
1068
|
+
await bootstrap.continue(Object.freeze({
|
|
1069
|
+
plan: bootstrapPlan,
|
|
1070
|
+
planHash: reviewedBootstrapHash,
|
|
1071
|
+
account: identity.account,
|
|
1072
|
+
person: identity.membershipId,
|
|
1073
|
+
origin: identity.baseUrl,
|
|
1074
|
+
role: identity.role,
|
|
1075
|
+
localSync,
|
|
1076
|
+
runId: journal.runId,
|
|
1077
|
+
lockPath: lock.path,
|
|
1078
|
+
ownerPid: lock.owner.pid,
|
|
1079
|
+
ownerToken: lock.owner.token,
|
|
1080
|
+
}), signal);
|
|
1081
|
+
}
|
|
1082
|
+
catch {
|
|
1083
|
+
setStep(journal, {
|
|
1084
|
+
id: "machine",
|
|
1085
|
+
state: signal.aborted ? "waiting" : "failed",
|
|
1086
|
+
reason: signal.aborted ? "interrupted" : "bootstrap_install_failed",
|
|
1087
|
+
at: isoNow(ctx, deps),
|
|
1088
|
+
});
|
|
1089
|
+
ctx.stderr("Installation stopped. Completed files remain. Run the original setup command to resume.");
|
|
1090
|
+
return finish(signal.aborted ? EXIT_WAITING : EXIT_FAILED);
|
|
1091
|
+
}
|
|
1092
|
+
current = null;
|
|
1093
|
+
if (signal.aborted) {
|
|
1094
|
+
setStep(journal, {
|
|
1095
|
+
id: "machine",
|
|
1096
|
+
state: "waiting",
|
|
1097
|
+
reason: "interrupted",
|
|
1098
|
+
at: isoNow(ctx, deps),
|
|
1099
|
+
});
|
|
1100
|
+
return finish(EXIT_WAITING);
|
|
1101
|
+
}
|
|
1102
|
+
if (deps.identity) {
|
|
1103
|
+
const refreshed = await deps.identity(journal);
|
|
1104
|
+
requireMatchingIdentity(journal, refreshed);
|
|
1105
|
+
identity = refreshed;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
744
1108
|
let refused = false;
|
|
745
1109
|
const needed = new Set();
|
|
746
1110
|
const include = (id) => {
|
|
@@ -769,7 +1133,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
769
1133
|
reason: "member_scope",
|
|
770
1134
|
at: isoNow(ctx, deps),
|
|
771
1135
|
});
|
|
772
|
-
deps.ui?.stepEnd(journalStep(journal, id));
|
|
1136
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
773
1137
|
continue;
|
|
774
1138
|
}
|
|
775
1139
|
const missing = (ONBOARD_DEPENDENCIES[id] ?? []).find((parent) => !stepSatisfied(journalStep(journal, parent)));
|
|
@@ -781,7 +1145,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
781
1145
|
at: isoNow(ctx, deps),
|
|
782
1146
|
});
|
|
783
1147
|
writeOnboardJournal(statePath, journal);
|
|
784
|
-
deps.ui?.stepEnd(journalStep(journal, id));
|
|
1148
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
785
1149
|
continue;
|
|
786
1150
|
}
|
|
787
1151
|
let adapter = deps.adapters?.[id];
|
|
@@ -805,7 +1169,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
805
1169
|
at: isoNow(ctx, deps),
|
|
806
1170
|
});
|
|
807
1171
|
writeOnboardJournal(statePath, journal);
|
|
808
|
-
deps.ui?.stepEnd(journalStep(journal, id));
|
|
1172
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
809
1173
|
continue;
|
|
810
1174
|
}
|
|
811
1175
|
try {
|
|
@@ -825,7 +1189,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
825
1189
|
at: isoNow(ctx, deps),
|
|
826
1190
|
});
|
|
827
1191
|
writeOnboardJournal(statePath, journal);
|
|
828
|
-
deps.ui?.stepEnd(journalStep(journal, id));
|
|
1192
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
829
1193
|
continue;
|
|
830
1194
|
}
|
|
831
1195
|
}
|
|
@@ -852,7 +1216,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
852
1216
|
at: isoNow(ctx, deps),
|
|
853
1217
|
});
|
|
854
1218
|
writeOnboardJournal(statePath, journal);
|
|
855
|
-
deps.ui?.stepEnd(journalStep(journal, id));
|
|
1219
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
856
1220
|
continue;
|
|
857
1221
|
}
|
|
858
1222
|
}
|
|
@@ -862,6 +1226,9 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
862
1226
|
requireMatchingIdentity(journal, identity);
|
|
863
1227
|
if (!identity)
|
|
864
1228
|
result = { state: "failed", reason: "membership_not_verified" };
|
|
1229
|
+
else
|
|
1230
|
+
for (const line of onboardIdentityLines(identity))
|
|
1231
|
+
stepCtx.stdout(line);
|
|
865
1232
|
}
|
|
866
1233
|
if (result.state === "refused")
|
|
867
1234
|
refused = true;
|
|
@@ -882,13 +1249,17 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
882
1249
|
/^[a-z][a-z0-9_-]{1,63}$/.test(error.code)
|
|
883
1250
|
? error.code.replaceAll("-", "_")
|
|
884
1251
|
: "step_failed";
|
|
1252
|
+
const renewLogin = error instanceof CliError &&
|
|
1253
|
+
error.code === "onboard-login-refresh-required";
|
|
885
1254
|
setStep(journal, {
|
|
886
1255
|
id,
|
|
887
|
-
state: "failed",
|
|
1256
|
+
state: renewLogin ? "waiting" : "failed",
|
|
888
1257
|
reason,
|
|
889
1258
|
at: isoNow(ctx, deps),
|
|
890
1259
|
});
|
|
891
|
-
stepCtx.stderr(
|
|
1260
|
+
stepCtx.stderr(renewLogin
|
|
1261
|
+
? "Renew your login with catalyst login, then run catalyst onboard to resume."
|
|
1262
|
+
: `✗ ${ONBOARD_TITLES[id]}: ${reason}. The safe reason is saved; run the same command to resume.`);
|
|
892
1263
|
}
|
|
893
1264
|
if (signal.aborted) {
|
|
894
1265
|
setStep(journal, {
|
|
@@ -899,8 +1270,11 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
899
1270
|
});
|
|
900
1271
|
return finish(EXIT_WAITING);
|
|
901
1272
|
}
|
|
902
|
-
deps.ui?.stepEnd(journalStep(journal, id));
|
|
1273
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
903
1274
|
writeOnboardJournal(statePath, journal);
|
|
1275
|
+
// Later steps share the same login; preserve the actionable cause instead of cascading failures.
|
|
1276
|
+
if (journalStep(journal, id)?.reason?.endsWith("_login_refresh_required"))
|
|
1277
|
+
return finish(EXIT_WAITING);
|
|
904
1278
|
if (refused)
|
|
905
1279
|
break;
|
|
906
1280
|
}
|
|
@@ -910,15 +1284,24 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
910
1284
|
? EXIT_REFUSED
|
|
911
1285
|
: scope.some((id) => journalStep(journal, id)?.state === "failed")
|
|
912
1286
|
? EXIT_FAILED
|
|
913
|
-
: scope.every((id) => stepSatisfied(journalStep(journal, id))
|
|
1287
|
+
: scope.every((id) => stepSatisfied(journalStep(journal, id)) ||
|
|
1288
|
+
(!only && ONBOARD_DEFERRED_STEPS.has(id)))
|
|
914
1289
|
? 0
|
|
915
1290
|
: EXIT_WAITING;
|
|
916
1291
|
return finish(code);
|
|
917
1292
|
}
|
|
918
1293
|
catch (error) {
|
|
1294
|
+
if (candidate?.accepted && !mayRecordProgress) {
|
|
1295
|
+
ctx.stderr("Your connection was accepted, but setup could not be recorded. Run catalyst onboard to resume.");
|
|
1296
|
+
if (args.json)
|
|
1297
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: EXIT_WAITING }));
|
|
1298
|
+
return EXIT_WAITING;
|
|
1299
|
+
}
|
|
919
1300
|
// Identity can change while the lock is held; stop before the next action.
|
|
920
1301
|
if (error instanceof CliError) {
|
|
921
1302
|
ctx.stderr(error.message);
|
|
1303
|
+
if (args.json)
|
|
1304
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: error.exitCode }));
|
|
922
1305
|
return error.exitCode;
|
|
923
1306
|
}
|
|
924
1307
|
throw error;
|