@catalyst-cloud/cli 0.14.10 → 0.15.1
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 +13 -0
- package/README.md +41 -0
- package/dist/args.js +34 -2
- package/dist/capabilities.js +2 -1
- package/dist/cli-update-notice.js +76 -0
- package/dist/cli.js +263 -101
- package/dist/oauth.js +48 -18
- package/dist/onboard-accounts.js +34 -5
- package/dist/onboard-automation-management.js +27 -9
- package/dist/onboard-capacity.js +10 -1
- package/dist/onboard-existing.js +132 -31
- package/dist/onboard-github.js +171 -6
- package/dist/onboard-headless.js +647 -0
- package/dist/onboard-interrupts.js +41 -0
- package/dist/onboard-next.js +215 -0
- package/dist/onboard-permissions.js +100 -0
- package/dist/onboard-personal.js +46 -10
- package/dist/onboard-runner-cloud.js +213 -0
- package/dist/onboard-runner-egress.js +67 -0
- package/dist/onboard-runner.js +784 -0
- package/dist/onboard-runtime.js +48 -8
- package/dist/onboard-standalone-copy.js +110 -0
- package/dist/onboard-team-create.js +267 -0
- package/dist/onboard-ui.js +623 -29
- package/dist/onboard-workflow.js +311 -168
- package/dist/onboard-workspace.js +52 -5
- package/dist/onboard.js +359 -212
- package/dist/setup-events.js +100 -0
- package/dist/setup-onboard-copy.js +438 -0
- package/dist/setup-prompt-frame.js +94 -0
- package/dist/setup-prompts.js +108 -0
- package/dist/setup-render.js +339 -0
- package/dist/setup-signin.js +187 -0
- package/dist/setup.js +426 -0
- package/package.json +7 -3
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +3 -2
- 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 +1 -1
- package/vendor/README.md +4 -0
- package/vendor/self-host/compose.yaml +110 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.service +14 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.timer +10 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress.service +19 -0
- package/vendor/self-host/linux-session-egress/session-egress.py +291 -0
- package/vendor/self-host/provenance.json +11 -0
package/dist/onboard.js
CHANGED
|
@@ -6,6 +6,8 @@ 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
8
|
import { configPathFor } from "./config.js";
|
|
9
|
+
import { onboardJsonView } from "./setup-onboard-copy.js";
|
|
10
|
+
import { standalonePlan, standalonePlanNotes, } from "./onboard-standalone-copy.js";
|
|
9
11
|
import { CliError, UsageError } from "./errors.js";
|
|
10
12
|
import { onboardFileSnapshot } from "./onboard-file-snapshot.js";
|
|
11
13
|
import { onboardReasonText, onboardTeamKey, savedOnboardBaseUrl, } from "./onboard-next.js";
|
|
@@ -16,6 +18,8 @@ export const ONBOARD_STEPS = [
|
|
|
16
18
|
"skills",
|
|
17
19
|
"legacy",
|
|
18
20
|
"signin",
|
|
21
|
+
"daemon",
|
|
22
|
+
"housekeeping",
|
|
19
23
|
"linear.workspace",
|
|
20
24
|
"linear.personal",
|
|
21
25
|
"linear.team",
|
|
@@ -26,11 +30,10 @@ export const ONBOARD_STEPS = [
|
|
|
26
30
|
"github.repos",
|
|
27
31
|
"projects",
|
|
28
32
|
"accounts",
|
|
33
|
+
"capacity",
|
|
34
|
+
"runner",
|
|
29
35
|
"settings",
|
|
30
36
|
"values",
|
|
31
|
-
"capacity",
|
|
32
|
-
"daemon",
|
|
33
|
-
"housekeeping",
|
|
34
37
|
"first-ticket",
|
|
35
38
|
"ready",
|
|
36
39
|
];
|
|
@@ -102,6 +105,8 @@ export const ONBOARD_DEPENDENCIES = {
|
|
|
102
105
|
settings: ["projects"],
|
|
103
106
|
values: ["settings"],
|
|
104
107
|
capacity: ["projects"],
|
|
108
|
+
// The join token names the selected team's pool.
|
|
109
|
+
runner: ["linear.team"],
|
|
105
110
|
daemon: ["signin"],
|
|
106
111
|
// What dispatch needs. Environment approval gates only implement and pr, and the first phase
|
|
107
112
|
// needs no imported values, so repository settings and local sync do not hold a first ticket.
|
|
@@ -123,6 +128,7 @@ const ADMIN_STEPS = new Set([
|
|
|
123
128
|
"settings",
|
|
124
129
|
"values",
|
|
125
130
|
"capacity",
|
|
131
|
+
"runner",
|
|
126
132
|
"first-ticket",
|
|
127
133
|
]);
|
|
128
134
|
export const ONBOARD_TITLES = {
|
|
@@ -144,6 +150,7 @@ export const ONBOARD_TITLES = {
|
|
|
144
150
|
settings: "Review repository settings",
|
|
145
151
|
values: "Import selected local values",
|
|
146
152
|
capacity: "Check runner capacity",
|
|
153
|
+
runner: "Run Catalyst's work on this machine",
|
|
147
154
|
daemon: "Check optional local sync",
|
|
148
155
|
housekeeping: "Schedule the daily update",
|
|
149
156
|
"first-ticket": "Start a first ticket",
|
|
@@ -215,6 +222,8 @@ function normalizeStep(value, fallbackAt) {
|
|
|
215
222
|
if (!state)
|
|
216
223
|
return null;
|
|
217
224
|
const step = { id, state };
|
|
225
|
+
if (state === "failed" && row.refused === true)
|
|
226
|
+
step.refused = true;
|
|
218
227
|
if (typeof row.at === "string")
|
|
219
228
|
step.at = row.at;
|
|
220
229
|
else if (typeof row.updatedAt === "string")
|
|
@@ -263,6 +272,21 @@ function normalizeStep(value, fallbackAt) {
|
|
|
263
272
|
"passed",
|
|
264
273
|
"team",
|
|
265
274
|
"teamKey",
|
|
275
|
+
"selected",
|
|
276
|
+
"hostName",
|
|
277
|
+
"hostId",
|
|
278
|
+
"capacity",
|
|
279
|
+
"failing",
|
|
280
|
+
"image",
|
|
281
|
+
"automations",
|
|
282
|
+
"workspaceSlug",
|
|
283
|
+
// CTC-4629: an outdated connection's grant, scopes, action URL and who takes it.
|
|
284
|
+
"grant",
|
|
285
|
+
"granted",
|
|
286
|
+
"missing",
|
|
287
|
+
"org",
|
|
288
|
+
"url",
|
|
289
|
+
"actor",
|
|
266
290
|
]) {
|
|
267
291
|
const item = evidence?.[key];
|
|
268
292
|
if (item === null ||
|
|
@@ -522,13 +546,13 @@ function freshJournal(ctx, cliVersion, deps) {
|
|
|
522
546
|
}
|
|
523
547
|
/** A refusal before loading state must not reuse or copy an unreadable receipt. */
|
|
524
548
|
export function onboardErrorJournal(ctx, cliVersion, exit) {
|
|
525
|
-
return {
|
|
549
|
+
return onboardJsonView({
|
|
526
550
|
...freshJournal(ctx, cliVersion, {}),
|
|
527
551
|
scope: "onboarding",
|
|
528
552
|
mode: "run",
|
|
529
553
|
exit,
|
|
530
554
|
complete: false,
|
|
531
|
-
};
|
|
555
|
+
}, savedOnboardBaseUrl(ctx.home));
|
|
532
556
|
}
|
|
533
557
|
function planJournal(journal) {
|
|
534
558
|
return {
|
|
@@ -542,17 +566,21 @@ function planJournal(journal) {
|
|
|
542
566
|
}),
|
|
543
567
|
};
|
|
544
568
|
}
|
|
545
|
-
function printPlan(ctx, journal, identity) {
|
|
546
|
-
const value = planJournal(journal);
|
|
569
|
+
function printPlan(ctx, journal, identity, options) {
|
|
547
570
|
ctx.stdout("Catalyst setup plan");
|
|
548
571
|
if (identity !== undefined)
|
|
549
572
|
for (const line of onboardIdentityLines(identity))
|
|
550
573
|
ctx.stdout(line);
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
574
|
+
let group;
|
|
575
|
+
for (const row of standalonePlan(journal, options?.scope, options?.runner)) {
|
|
576
|
+
if (row.group !== group) {
|
|
577
|
+
ctx.stdout("");
|
|
578
|
+
ctx.stdout(row.group);
|
|
579
|
+
group = row.group;
|
|
580
|
+
}
|
|
581
|
+
ctx.stdout(` ${row.number} ${row.title} ${row.detail}`);
|
|
582
|
+
}
|
|
583
|
+
ctx.stdout(standalonePlanNotes(options?.localSync ?? journal.localSync === true));
|
|
556
584
|
}
|
|
557
585
|
async function confirmContinue(ctx, args, deps) {
|
|
558
586
|
if (args.flags.yes === true)
|
|
@@ -591,16 +619,18 @@ function sanitizedResult(id, result, at) {
|
|
|
591
619
|
id,
|
|
592
620
|
...result,
|
|
593
621
|
state: result.state === "refused" ? "failed" : result.state,
|
|
622
|
+
...(result.state === "refused" ? { refused: true } : {}),
|
|
594
623
|
}, at);
|
|
595
624
|
row.at = at;
|
|
596
625
|
return row;
|
|
597
626
|
}
|
|
598
|
-
function stepSatisfied(step) {
|
|
627
|
+
export function stepSatisfied(step) {
|
|
599
628
|
return (step?.state === "done" ||
|
|
600
629
|
(step?.state === "skipped" &&
|
|
601
630
|
(step.id === "legacy" ||
|
|
602
631
|
step.reason === "member_scope" ||
|
|
603
632
|
(step.id === "daemon" && step.reason === "local_sync_not_selected") ||
|
|
633
|
+
(step.id === "runner" && step.reason === "runner_not_selected") ||
|
|
604
634
|
(step.id === "linear.automations" &&
|
|
605
635
|
step.reason === "automation_management_unavailable"))));
|
|
606
636
|
}
|
|
@@ -609,6 +639,8 @@ function stepSatisfied(step) {
|
|
|
609
639
|
export const ONBOARD_DEFERRED_STEPS = new Set([
|
|
610
640
|
"settings",
|
|
611
641
|
"values",
|
|
642
|
+
// Optional: work also runs on the workspace's other runner hosts.
|
|
643
|
+
"runner",
|
|
612
644
|
"housekeeping",
|
|
613
645
|
"first-ticket",
|
|
614
646
|
]);
|
|
@@ -616,7 +648,8 @@ export const ONBOARD_DEFERRED_STEPS = new Set([
|
|
|
616
648
|
* check is that same wait, so readiness waits too instead of reporting a new failure. */
|
|
617
649
|
export function onboardRequiredStepsWaiting(journal) {
|
|
618
650
|
return !!journal?.steps.some((step) => step.id !== "ready" &&
|
|
619
|
-
!ONBOARD_DEFERRED_STEPS.has(step.id)
|
|
651
|
+
(!ONBOARD_DEFERRED_STEPS.has(step.id) ||
|
|
652
|
+
(step.id === "runner" && step.evidence?.selected === true)) &&
|
|
620
653
|
step.state !== "failed" &&
|
|
621
654
|
!stepSatisfied(step));
|
|
622
655
|
}
|
|
@@ -664,8 +697,32 @@ function requireMatchingIdentity(journal, identity) {
|
|
|
664
697
|
export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
665
698
|
if (args.subcommand !== null || args.rest.length > 0)
|
|
666
699
|
throw new UsageError("onboard takes no positional arguments");
|
|
667
|
-
if (args.json ||
|
|
700
|
+
if (args.json ||
|
|
701
|
+
(args.flags.yes === true &&
|
|
702
|
+
!deps.reviewedSetup &&
|
|
703
|
+
deps.ui?.interactive !== false) ||
|
|
704
|
+
args.flags["dry-run"] === true)
|
|
668
705
|
deps = { ...deps, ui: undefined };
|
|
706
|
+
const interactiveUi = Boolean(deps.ui && deps.ui.interactive !== false);
|
|
707
|
+
let activeRunSignal;
|
|
708
|
+
if (args.json) {
|
|
709
|
+
const output = ctx.stdout;
|
|
710
|
+
ctx = {
|
|
711
|
+
...ctx,
|
|
712
|
+
stdout: (line) => {
|
|
713
|
+
const value = JSON.parse(line);
|
|
714
|
+
const paused = Boolean(activeRunSignal?.aborted ||
|
|
715
|
+
deps.signal?.aborted ||
|
|
716
|
+
deps.ui?.signal.aborted);
|
|
717
|
+
output(JSON.stringify(onboardJsonView(value, savedOnboardBaseUrl(ctx.home), paused, {
|
|
718
|
+
requiredSteps: args.flags.runner === true ? ["runner"] : [],
|
|
719
|
+
only: typeof args.flags.only === "string"
|
|
720
|
+
? args.flags.only
|
|
721
|
+
: undefined,
|
|
722
|
+
})));
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|
|
669
726
|
const only = typeof args.flags.only === "string"
|
|
670
727
|
? args.flags.only
|
|
671
728
|
: undefined;
|
|
@@ -680,6 +737,24 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
680
737
|
for (const id of [only, resumeFrom])
|
|
681
738
|
if (id && !ONBOARD_STEPS.includes(id))
|
|
682
739
|
throw new UsageError(`unknown onboarding step: ${id}`);
|
|
740
|
+
const reviewedScope = new Set();
|
|
741
|
+
const includeReviewed = (id) => {
|
|
742
|
+
if (reviewedScope.has(id))
|
|
743
|
+
return;
|
|
744
|
+
reviewedScope.add(id);
|
|
745
|
+
for (const dependency of ONBOARD_DEPENDENCIES[id] ?? [])
|
|
746
|
+
includeReviewed(dependency);
|
|
747
|
+
};
|
|
748
|
+
if (only)
|
|
749
|
+
includeReviewed(only);
|
|
750
|
+
else
|
|
751
|
+
for (const id of ONBOARD_STEPS)
|
|
752
|
+
reviewedScope.add(id);
|
|
753
|
+
const planOptions = (journal) => ({
|
|
754
|
+
runner: args.flags.runner === true,
|
|
755
|
+
localSync: args.flags["local-sync"] === true || journal.localSync === true,
|
|
756
|
+
scope: ONBOARD_STEPS.filter((id) => reviewedScope.has(id)),
|
|
757
|
+
});
|
|
683
758
|
const bootstrap = deps.bootstrap;
|
|
684
759
|
const bootstrapPlan = bootstrap
|
|
685
760
|
? parseBootstrapPlan(bootstrap.plan)
|
|
@@ -744,8 +819,10 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
744
819
|
if (args.json)
|
|
745
820
|
ctx.stdout(JSON.stringify(planJournal(journal)));
|
|
746
821
|
else
|
|
747
|
-
printPlan(ctx, journal);
|
|
822
|
+
printPlan(ctx, journal, undefined, planOptions(journal));
|
|
748
823
|
showBootstrap();
|
|
824
|
+
if (!args.json)
|
|
825
|
+
ctx.stdout(`Next: run catalyst onboard${only ? ` --only ${only}` : ""}`);
|
|
749
826
|
return 0;
|
|
750
827
|
}
|
|
751
828
|
// Check tenant binding before even a local cleanup mutation. Never overwrite a mismatched receipt.
|
|
@@ -760,7 +837,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
760
837
|
const canRenew = error instanceof CliError &&
|
|
761
838
|
error.code === "onboard-login-refresh-required" &&
|
|
762
839
|
deps.stageSignin &&
|
|
763
|
-
(
|
|
840
|
+
(interactiveUi || plainSigninAllowed);
|
|
764
841
|
if (!canRenew) {
|
|
765
842
|
const code = error instanceof CliError ? error.exitCode : EXIT_FAILED;
|
|
766
843
|
ctx.stderr(error instanceof CliError &&
|
|
@@ -772,6 +849,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
772
849
|
...journal,
|
|
773
850
|
mode: "run",
|
|
774
851
|
exit: code,
|
|
852
|
+
errorCode: error instanceof CliError ? error.code : undefined,
|
|
775
853
|
complete: false,
|
|
776
854
|
}));
|
|
777
855
|
return code;
|
|
@@ -794,11 +872,11 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
794
872
|
if (identity)
|
|
795
873
|
requireBootstrapIdentity(identity);
|
|
796
874
|
if (deps.ui)
|
|
797
|
-
deps.ui.plan(journal, identity);
|
|
875
|
+
deps.ui.plan(journal, identity, planOptions(journal));
|
|
798
876
|
else if (!args.json)
|
|
799
|
-
printPlan(ctx, journal, identity);
|
|
877
|
+
printPlan(ctx, journal, identity, planOptions(journal));
|
|
800
878
|
else
|
|
801
|
-
printPlan({ ...ctx, stdout: ctx.stderr }, journal, identity);
|
|
879
|
+
printPlan({ ...ctx, stdout: ctx.stderr }, journal, identity, planOptions(journal));
|
|
802
880
|
showBootstrap();
|
|
803
881
|
let localSync = args.flags["local-sync"] === true || journal.localSync === true;
|
|
804
882
|
let candidate = null;
|
|
@@ -806,7 +884,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
806
884
|
// person and workspace. Do not ask to approve a plan whose identity is still unknown.
|
|
807
885
|
if (!identity &&
|
|
808
886
|
deps.stageSignin &&
|
|
809
|
-
!
|
|
887
|
+
!interactiveUi &&
|
|
810
888
|
personalScope &&
|
|
811
889
|
!plainSigninAllowed) {
|
|
812
890
|
ctx.stderr("Sign in with catalyst login, then run catalyst onboard, or use --yes to allow browser sign-in before the displayed setup plan.");
|
|
@@ -814,9 +892,11 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
814
892
|
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: EXIT_WAITING }));
|
|
815
893
|
return EXIT_WAITING;
|
|
816
894
|
}
|
|
817
|
-
const stageFirst = Boolean(deps.stageSignin && !identity && (
|
|
895
|
+
const stageFirst = Boolean(deps.stageSignin && !identity && (interactiveUi || plainSigninAllowed));
|
|
818
896
|
if (stageFirst) {
|
|
819
|
-
const message =
|
|
897
|
+
const message = deps.reviewedSetup
|
|
898
|
+
? "Sign in in your browser to continue setup."
|
|
899
|
+
: "Sign in in your browser first. Then review your person, workspace and setup plan. Your saved connection stays unchanged until you approve that plan.";
|
|
820
900
|
if (deps.ui)
|
|
821
901
|
deps.ui.message(message);
|
|
822
902
|
else
|
|
@@ -824,7 +904,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
824
904
|
}
|
|
825
905
|
let consent = stageFirst
|
|
826
906
|
? { proceed: false, localSync, signin: true }
|
|
827
|
-
: deps.ui
|
|
907
|
+
: deps.ui && interactiveUi
|
|
828
908
|
? await deps.ui.confirmPlan(localSync, deps.stageSignin
|
|
829
909
|
? identity
|
|
830
910
|
? "saved"
|
|
@@ -859,27 +939,32 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
859
939
|
reviewedIdentity = identityTuple(identity);
|
|
860
940
|
requireMatchingIdentity(journal, identity);
|
|
861
941
|
requireBootstrapIdentity(identity);
|
|
942
|
+
deps.ui?.stagedSigninEnd?.("done");
|
|
862
943
|
if (deps.ui)
|
|
863
|
-
deps.ui.plan(journal, identity);
|
|
944
|
+
deps.ui.plan(journal, identity, planOptions(journal));
|
|
864
945
|
else if (!args.json)
|
|
865
|
-
printPlan(ctx, journal, identity);
|
|
946
|
+
printPlan(ctx, journal, identity, planOptions(journal));
|
|
866
947
|
else
|
|
867
|
-
printPlan({ ...ctx, stdout: ctx.stderr }, journal, identity);
|
|
948
|
+
printPlan({ ...ctx, stdout: ctx.stderr }, journal, identity, planOptions(journal));
|
|
868
949
|
showBootstrap();
|
|
869
|
-
consent =
|
|
870
|
-
|
|
871
|
-
|
|
950
|
+
consent =
|
|
951
|
+
deps.ui && interactiveUi
|
|
952
|
+
? await deps.ui.confirmPlan(localSync, "unavailable")
|
|
953
|
+
: await confirmContinue(ctx, args, deps);
|
|
872
954
|
}
|
|
873
955
|
catch (error) {
|
|
874
956
|
const message = error instanceof CliError
|
|
875
957
|
? error.message
|
|
876
958
|
: "Sign-in could not be verified. Your saved connection was not changed.";
|
|
877
|
-
if (deps.ui)
|
|
878
|
-
deps.ui.message(message);
|
|
879
|
-
else
|
|
880
|
-
ctx.stderr(message);
|
|
881
|
-
deps.ui?.dispose();
|
|
882
959
|
const code = error instanceof CliError ? error.exitCode : EXIT_WAITING;
|
|
960
|
+
const rendered = deps.ui?.stagedSigninEnd?.(code === EXIT_WAITING ? "waiting" : "failed", message);
|
|
961
|
+
if (!rendered) {
|
|
962
|
+
if (deps.ui)
|
|
963
|
+
deps.ui.message(message);
|
|
964
|
+
else
|
|
965
|
+
ctx.stderr(message);
|
|
966
|
+
}
|
|
967
|
+
deps.ui?.dispose();
|
|
883
968
|
if (args.json)
|
|
884
969
|
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: code }));
|
|
885
970
|
return code;
|
|
@@ -888,7 +973,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
888
973
|
if (typeof consent !== "boolean")
|
|
889
974
|
localSync = consent.localSync;
|
|
890
975
|
if (!(typeof consent === "boolean" ? consent : consent.proceed)) {
|
|
891
|
-
const missingConsent = !
|
|
976
|
+
const missingConsent = !interactiveUi &&
|
|
892
977
|
args.flags.yes !== true &&
|
|
893
978
|
(args.json ||
|
|
894
979
|
!(deps.isTty ??
|
|
@@ -913,7 +998,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
913
998
|
throw error;
|
|
914
999
|
ctx.stderr(error.message);
|
|
915
1000
|
if (args.json)
|
|
916
|
-
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: error.exitCode }));
|
|
1001
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: error.exitCode, errorCode: error.code }));
|
|
917
1002
|
return error.exitCode;
|
|
918
1003
|
}
|
|
919
1004
|
const stop = new AbortController();
|
|
@@ -922,6 +1007,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
922
1007
|
...(deps.signal ? [deps.signal] : []),
|
|
923
1008
|
...(deps.ui ? [deps.ui.signal] : []),
|
|
924
1009
|
]);
|
|
1010
|
+
activeRunSignal = signal;
|
|
925
1011
|
const stepCtx = {
|
|
926
1012
|
...ctx,
|
|
927
1013
|
stdout: deps.ui
|
|
@@ -937,17 +1023,33 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
937
1023
|
};
|
|
938
1024
|
let current = null;
|
|
939
1025
|
let mayRecordProgress = candidate === null;
|
|
1026
|
+
const recordStep = (next) => {
|
|
1027
|
+
const prior = journalStep(journal, next.id);
|
|
1028
|
+
// Once a create was sent, generic capability, identity, scope and interruption results
|
|
1029
|
+
// cannot erase the recovery key. Only a verified selection or explicit team evidence can
|
|
1030
|
+
// replace it. This also retains a confirmed team's ID while adoption remains unfinished.
|
|
1031
|
+
if (next.id === "linear.team" &&
|
|
1032
|
+
next.state !== "done" &&
|
|
1033
|
+
!next.evidence?.teamKey &&
|
|
1034
|
+
(prior?.reason === "team_create_unverified" ||
|
|
1035
|
+
prior?.reason === "team_created_not_adopted") &&
|
|
1036
|
+
prior.evidence?.teamKey)
|
|
1037
|
+
setStep(journal, { ...prior, state: "waiting", at: next.at });
|
|
1038
|
+
else
|
|
1039
|
+
setStep(journal, next);
|
|
1040
|
+
};
|
|
1041
|
+
const interruptStep = (id) => recordStep({
|
|
1042
|
+
id,
|
|
1043
|
+
state: "failed",
|
|
1044
|
+
reason: "interrupted",
|
|
1045
|
+
at: isoNow(ctx, deps),
|
|
1046
|
+
});
|
|
940
1047
|
const interrupted = () => {
|
|
941
1048
|
stop.abort();
|
|
942
1049
|
if (!mayRecordProgress)
|
|
943
1050
|
return;
|
|
944
1051
|
if (current)
|
|
945
|
-
|
|
946
|
-
id: current,
|
|
947
|
-
state: "failed",
|
|
948
|
-
reason: "interrupted",
|
|
949
|
-
at: isoNow(ctx, deps),
|
|
950
|
-
});
|
|
1052
|
+
interruptStep(current);
|
|
951
1053
|
journal.exit = EXIT_WAITING;
|
|
952
1054
|
journal.complete = false;
|
|
953
1055
|
try {
|
|
@@ -958,7 +1060,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
958
1060
|
}
|
|
959
1061
|
};
|
|
960
1062
|
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
961
|
-
if (deps.bindSignals !== false)
|
|
1063
|
+
if (deps.bindSignals !== false && deps.ui?.handlesSignals !== true)
|
|
962
1064
|
for (const name of signals)
|
|
963
1065
|
process.on(name, interrupted);
|
|
964
1066
|
const finish = (code) => {
|
|
@@ -975,7 +1077,8 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
975
1077
|
else {
|
|
976
1078
|
if (journal.complete)
|
|
977
1079
|
ctx.stdout("Onboarding complete.");
|
|
978
|
-
else if (onboardReadyForWork(journal, only)
|
|
1080
|
+
else if (onboardReadyForWork(journal, only) &&
|
|
1081
|
+
(deps.requiredSteps ?? []).every((id) => journalStep(journal, id)?.state === "done")) {
|
|
979
1082
|
ctx.stdout("Ready for work.");
|
|
980
1083
|
ctx.stdout("Next, when you want:");
|
|
981
1084
|
for (const line of onboardNextActions(journal, savedOnboardBaseUrl(ctx.home)))
|
|
@@ -984,7 +1087,8 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
984
1087
|
else if (only && code === 0)
|
|
985
1088
|
ctx.stdout(`${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.`);
|
|
986
1089
|
else {
|
|
987
|
-
ctx.stdout(`Setup still needs ${journal.steps.filter((step) => !stepSatisfied(step)
|
|
1090
|
+
ctx.stdout(`Setup still needs ${journal.steps.filter((step) => !stepSatisfied(step) ||
|
|
1091
|
+
(deps.requiredSteps?.includes(step.id) && step.state !== "done")).length} checks.`);
|
|
988
1092
|
for (const line of onboardNextActions(journal, savedOnboardBaseUrl(ctx.home), only))
|
|
989
1093
|
ctx.stdout(line);
|
|
990
1094
|
}
|
|
@@ -1113,7 +1217,6 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
1113
1217
|
identity = refreshed;
|
|
1114
1218
|
}
|
|
1115
1219
|
}
|
|
1116
|
-
let refused = false;
|
|
1117
1220
|
const needed = new Set();
|
|
1118
1221
|
const include = (id) => {
|
|
1119
1222
|
if (needed.has(id))
|
|
@@ -1128,180 +1231,224 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
1128
1231
|
for (const id of ONBOARD_STEPS)
|
|
1129
1232
|
needed.add(id);
|
|
1130
1233
|
const fromIndex = resumeFrom ? ONBOARD_STEPS.indexOf(resumeFrom) : 0;
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
: { state: "skipped", reason: "legacy_not_selected" };
|
|
1170
|
-
adapter = {
|
|
1171
|
-
check: async () => result,
|
|
1172
|
-
act: deps.runStep
|
|
1173
|
-
? async () => (result = await deps.runStep(id, stepCtx))
|
|
1174
|
-
: undefined,
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
if (!adapter) {
|
|
1178
|
-
setStep(journal, {
|
|
1179
|
-
id,
|
|
1180
|
-
state: "waiting",
|
|
1181
|
-
reason: "step_not_available_in_this_release",
|
|
1182
|
-
at: isoNow(ctx, deps),
|
|
1183
|
-
});
|
|
1184
|
-
writeOnboardJournal(statePath, journal);
|
|
1185
|
-
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1186
|
-
continue;
|
|
1187
|
-
}
|
|
1188
|
-
try {
|
|
1189
|
-
let result = await adapter.check(stepCtx, journal, signal);
|
|
1190
|
-
if (result.state === "pending" &&
|
|
1191
|
-
adapter.act &&
|
|
1192
|
-
(!only || id === only) &&
|
|
1193
|
-
ONBOARD_STEPS.indexOf(id) >= fromIndex) {
|
|
1194
|
-
if (deps.identity) {
|
|
1195
|
-
identity = await deps.identity(journal);
|
|
1196
|
-
requireMatchingIdentity(journal, identity);
|
|
1197
|
-
if (identity?.role === "member" && memberScopeSkips(id)) {
|
|
1198
|
-
setStep(journal, {
|
|
1199
|
-
id,
|
|
1200
|
-
state: "skipped",
|
|
1201
|
-
reason: "member_scope",
|
|
1202
|
-
at: isoNow(ctx, deps),
|
|
1203
|
-
});
|
|
1204
|
-
writeOnboardJournal(statePath, journal);
|
|
1205
|
-
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
journal.operations[id] ??= `${journal.runId}:${id}`;
|
|
1210
|
-
setStep(journal, { id, state: "running", at: isoNow(ctx, deps) });
|
|
1211
|
-
journal.exit = null;
|
|
1234
|
+
let rerun = null;
|
|
1235
|
+
for (;;) {
|
|
1236
|
+
let refused = false;
|
|
1237
|
+
for (const id of ONBOARD_STEPS.filter((id) => needed.has(id) && (!rerun || rerun.has(id)))) {
|
|
1238
|
+
if (signal.aborted)
|
|
1239
|
+
return finish(EXIT_WAITING);
|
|
1240
|
+
// No step is running while the session refreshes, so an interrupt here marks none of them.
|
|
1241
|
+
current = null;
|
|
1242
|
+
await deps.beforeStep?.(id);
|
|
1243
|
+
if (signal.aborted)
|
|
1244
|
+
return finish(EXIT_WAITING);
|
|
1245
|
+
current = id;
|
|
1246
|
+
deps.ui?.stepStart(id);
|
|
1247
|
+
const stepSignal = deps.ui?.stepSignal
|
|
1248
|
+
? AbortSignal.any([signal, deps.ui.stepSignal])
|
|
1249
|
+
: signal;
|
|
1250
|
+
// Member onboarding keeps administration outside its scope, with an explicit recorded reason.
|
|
1251
|
+
if (identity?.role === "member" && memberScopeSkips(id)) {
|
|
1252
|
+
recordStep({
|
|
1253
|
+
id,
|
|
1254
|
+
state: "skipped",
|
|
1255
|
+
reason: "member_scope",
|
|
1256
|
+
...(id === "runner" && args.flags.runner === true
|
|
1257
|
+
? { evidence: { selected: true } }
|
|
1258
|
+
: {}),
|
|
1259
|
+
at: isoNow(ctx, deps),
|
|
1260
|
+
});
|
|
1261
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
const missing = (ONBOARD_DEPENDENCIES[id] ?? []).find((parent) => !stepSatisfied(journalStep(journal, parent)));
|
|
1265
|
+
if (missing) {
|
|
1266
|
+
recordStep({
|
|
1267
|
+
id,
|
|
1268
|
+
state: "waiting",
|
|
1269
|
+
reason: "prerequisite_not_ready",
|
|
1270
|
+
at: isoNow(ctx, deps),
|
|
1271
|
+
});
|
|
1212
1272
|
writeOnboardJournal(statePath, journal);
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1273
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
let adapter = deps.adapters?.[id];
|
|
1277
|
+
// Compatibility for the initial legacy adapter, which performs its own before/after checks.
|
|
1278
|
+
if (!adapter && id === "legacy") {
|
|
1279
|
+
let result = deps.runStep
|
|
1280
|
+
? { state: "pending" }
|
|
1281
|
+
: { state: "skipped", reason: "legacy_not_selected" };
|
|
1282
|
+
adapter = {
|
|
1283
|
+
check: async () => result,
|
|
1284
|
+
act: deps.runStep
|
|
1285
|
+
? async () => (result = await deps.runStep(id, stepCtx))
|
|
1286
|
+
: undefined,
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
if (!adapter) {
|
|
1290
|
+
recordStep({
|
|
1291
|
+
id,
|
|
1292
|
+
state: "waiting",
|
|
1293
|
+
reason: "step_not_available_in_this_release",
|
|
1294
|
+
at: isoNow(ctx, deps),
|
|
1295
|
+
});
|
|
1296
|
+
writeOnboardJournal(statePath, journal);
|
|
1297
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
try {
|
|
1301
|
+
let result = await adapter.check(stepCtx, journal, stepSignal);
|
|
1302
|
+
if (result.state === "pending" &&
|
|
1303
|
+
adapter.act &&
|
|
1304
|
+
(!only || id === only) &&
|
|
1305
|
+
ONBOARD_STEPS.indexOf(id) >= fromIndex) {
|
|
1306
|
+
if (deps.identity) {
|
|
1307
|
+
identity = await deps.identity(journal);
|
|
1308
|
+
requireMatchingIdentity(journal, identity);
|
|
1309
|
+
if (identity?.role === "member" && memberScopeSkips(id)) {
|
|
1310
|
+
recordStep({
|
|
1311
|
+
id,
|
|
1312
|
+
state: "skipped",
|
|
1313
|
+
reason: "member_scope",
|
|
1314
|
+
at: isoNow(ctx, deps),
|
|
1315
|
+
});
|
|
1316
|
+
writeOnboardJournal(statePath, journal);
|
|
1317
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
journal.operations[id] ??= `${journal.runId}:${id}`;
|
|
1322
|
+
const prior = journalStep(journal, id);
|
|
1323
|
+
const recoveringTeam = id === "linear.team" &&
|
|
1324
|
+
(prior?.reason === "team_create_unverified" ||
|
|
1325
|
+
prior?.reason === "team_created_not_adopted");
|
|
1326
|
+
recordStep({
|
|
1327
|
+
...(recoveringTeam ? prior : {}),
|
|
1328
|
+
id,
|
|
1329
|
+
state: "running",
|
|
1330
|
+
at: isoNow(ctx, deps),
|
|
1331
|
+
});
|
|
1332
|
+
journal.exit = null;
|
|
1333
|
+
writeOnboardJournal(statePath, journal);
|
|
1334
|
+
const action = await adapter.act(stepCtx, journal, stepSignal);
|
|
1335
|
+
if (action.state === "done") {
|
|
1336
|
+
result = await adapter.check(stepCtx, journal, stepSignal);
|
|
1337
|
+
if (result.state === "pending")
|
|
1338
|
+
result = { state: "waiting", reason: "verification_pending" };
|
|
1339
|
+
}
|
|
1340
|
+
else
|
|
1341
|
+
result = action;
|
|
1342
|
+
if (id === "signin" && result.state === "done" && deps.identity) {
|
|
1343
|
+
identity = await deps.identity(journal);
|
|
1344
|
+
requireMatchingIdentity(journal, identity);
|
|
1345
|
+
if (identity?.role === "member" && memberScopeSkips(id)) {
|
|
1346
|
+
recordStep({
|
|
1347
|
+
id,
|
|
1348
|
+
state: "skipped",
|
|
1349
|
+
reason: "member_scope",
|
|
1350
|
+
at: isoNow(ctx, deps),
|
|
1351
|
+
});
|
|
1352
|
+
writeOnboardJournal(statePath, journal);
|
|
1353
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1218
1357
|
}
|
|
1219
|
-
else
|
|
1220
|
-
result = action;
|
|
1221
1358
|
if (id === "signin" && result.state === "done" && deps.identity) {
|
|
1222
1359
|
identity = await deps.identity(journal);
|
|
1223
1360
|
requireMatchingIdentity(journal, identity);
|
|
1224
|
-
if (identity
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
at: isoNow(ctx, deps),
|
|
1230
|
-
});
|
|
1231
|
-
writeOnboardJournal(statePath, journal);
|
|
1232
|
-
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1233
|
-
continue;
|
|
1234
|
-
}
|
|
1361
|
+
if (!identity)
|
|
1362
|
+
result = { state: "failed", reason: "membership_not_verified" };
|
|
1363
|
+
else
|
|
1364
|
+
for (const line of onboardIdentityLines(identity))
|
|
1365
|
+
stepCtx.stdout(line);
|
|
1235
1366
|
}
|
|
1367
|
+
if (result.state === "refused")
|
|
1368
|
+
refused = true;
|
|
1369
|
+
if (result.state === "pending")
|
|
1370
|
+
result = {
|
|
1371
|
+
...result,
|
|
1372
|
+
state: "waiting",
|
|
1373
|
+
reason: result.reason ?? "action_required",
|
|
1374
|
+
};
|
|
1375
|
+
recordStep(sanitizedResult(id, result, isoNow(ctx, deps)));
|
|
1236
1376
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1377
|
+
catch (error) {
|
|
1378
|
+
if (error instanceof CliError && error.exitCode === EXIT_REFUSED)
|
|
1379
|
+
refused = true;
|
|
1380
|
+
const reason = stepSignal.aborted
|
|
1381
|
+
? "interrupted"
|
|
1382
|
+
: error instanceof CliError &&
|
|
1383
|
+
/^[a-z][a-z0-9_-]{1,63}$/.test(error.code)
|
|
1384
|
+
? error.code.replaceAll("-", "_")
|
|
1385
|
+
: "step_failed";
|
|
1386
|
+
const renewLogin = error instanceof CliError &&
|
|
1387
|
+
error.code === "onboard-login-refresh-required";
|
|
1388
|
+
recordStep({
|
|
1389
|
+
id,
|
|
1390
|
+
state: renewLogin || stepSignal.aborted ? "waiting" : "failed",
|
|
1391
|
+
...(error instanceof CliError && error.exitCode === EXIT_REFUSED && !stepSignal.aborted ? { refused: true } : {}),
|
|
1392
|
+
reason,
|
|
1393
|
+
at: isoNow(ctx, deps),
|
|
1394
|
+
});
|
|
1395
|
+
stepCtx.stderr(renewLogin
|
|
1396
|
+
? "Renew your login with catalyst login, then run catalyst onboard to resume."
|
|
1397
|
+
: `✗ ${ONBOARD_TITLES[id]}: ${reason}. The safe reason is saved; run the same command to resume.`);
|
|
1245
1398
|
}
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const renewLogin = error instanceof CliError &&
|
|
1266
|
-
error.code === "onboard-login-refresh-required";
|
|
1267
|
-
setStep(journal, {
|
|
1268
|
-
id,
|
|
1269
|
-
state: renewLogin ? "waiting" : "failed",
|
|
1270
|
-
reason,
|
|
1271
|
-
at: isoNow(ctx, deps),
|
|
1272
|
-
});
|
|
1273
|
-
stepCtx.stderr(renewLogin
|
|
1274
|
-
? "Renew your login with catalyst login, then run catalyst onboard to resume."
|
|
1275
|
-
: `✗ ${ONBOARD_TITLES[id]}: ${reason}. The safe reason is saved; run the same command to resume.`);
|
|
1399
|
+
if (signal.aborted) {
|
|
1400
|
+
const settled = journalStep(journal, id);
|
|
1401
|
+
if (settled?.state !== "done" && settled?.state !== "failed")
|
|
1402
|
+
recordStep({
|
|
1403
|
+
id,
|
|
1404
|
+
state: "waiting",
|
|
1405
|
+
reason: "interrupted",
|
|
1406
|
+
at: isoNow(ctx, deps),
|
|
1407
|
+
});
|
|
1408
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1409
|
+
return finish(EXIT_WAITING);
|
|
1410
|
+
}
|
|
1411
|
+
deps.ui?.stepEnd(journalStep(journal, id), journal);
|
|
1412
|
+
writeOnboardJournal(statePath, journal);
|
|
1413
|
+
// Later steps share the same login; preserve the actionable cause instead of cascading failures.
|
|
1414
|
+
if (journalStep(journal, id)?.reason?.endsWith("_login_refresh_required"))
|
|
1415
|
+
return finish(EXIT_WAITING);
|
|
1416
|
+
if (refused)
|
|
1417
|
+
break;
|
|
1276
1418
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1419
|
+
current = null;
|
|
1420
|
+
const scope = only ? [only] : [...ONBOARD_STEPS];
|
|
1421
|
+
const requiredStepRefused = scope.some((id) => deps.requiredSteps?.includes(id) && journalStep(journal, id)?.reason === "member_scope");
|
|
1422
|
+
const required = new Set(deps.requiredSteps ?? []);
|
|
1423
|
+
if (args.flags.runner === true || journalStep(journal, "runner")?.evidence?.selected === true)
|
|
1424
|
+
required.add("runner");
|
|
1425
|
+
const code = refused || requiredStepRefused
|
|
1426
|
+
? EXIT_REFUSED
|
|
1427
|
+
: [...needed].some((id) => journalStep(journal, id)?.state === "failed")
|
|
1428
|
+
? EXIT_FAILED
|
|
1429
|
+
: scope.every((id) => (stepSatisfied(journalStep(journal, id)) &&
|
|
1430
|
+
!(required.has(id) && journalStep(journal, id)?.state !== "done")) ||
|
|
1431
|
+
(!only &&
|
|
1432
|
+
ONBOARD_DEFERRED_STEPS.has(id) &&
|
|
1433
|
+
!required.has(id)))
|
|
1434
|
+
? 0
|
|
1435
|
+
: EXIT_WAITING;
|
|
1436
|
+
journal.exit = code;
|
|
1437
|
+
journal.complete =
|
|
1438
|
+
!only &&
|
|
1439
|
+
code === 0 &&
|
|
1440
|
+
ONBOARD_STEPS.every((id) => stepSatisfied(journalStep(journal, id)));
|
|
1441
|
+
const unfinished = journal.steps.filter((step) => !stepSatisfied(step));
|
|
1442
|
+
if (!only &&
|
|
1443
|
+
!refused &&
|
|
1444
|
+
unfinished.length &&
|
|
1445
|
+
!signal.aborted &&
|
|
1446
|
+
(await deps.ui?.checkAgain?.(journal))) {
|
|
1447
|
+
rerun = new Set([...unfinished.map((step) => step.id), "ready"]);
|
|
1448
|
+
continue;
|
|
1285
1449
|
}
|
|
1286
|
-
|
|
1287
|
-
writeOnboardJournal(statePath, journal);
|
|
1288
|
-
// Later steps share the same login; preserve the actionable cause instead of cascading failures.
|
|
1289
|
-
if (journalStep(journal, id)?.reason?.endsWith("_login_refresh_required"))
|
|
1290
|
-
return finish(EXIT_WAITING);
|
|
1291
|
-
if (refused)
|
|
1292
|
-
break;
|
|
1450
|
+
return finish(signal.aborted ? EXIT_WAITING : code);
|
|
1293
1451
|
}
|
|
1294
|
-
current = null;
|
|
1295
|
-
const scope = only ? [only] : [...ONBOARD_STEPS];
|
|
1296
|
-
const code = refused
|
|
1297
|
-
? EXIT_REFUSED
|
|
1298
|
-
: scope.some((id) => journalStep(journal, id)?.state === "failed")
|
|
1299
|
-
? EXIT_FAILED
|
|
1300
|
-
: scope.every((id) => stepSatisfied(journalStep(journal, id)) ||
|
|
1301
|
-
(!only && ONBOARD_DEFERRED_STEPS.has(id)))
|
|
1302
|
-
? 0
|
|
1303
|
-
: EXIT_WAITING;
|
|
1304
|
-
return finish(code);
|
|
1305
1452
|
}
|
|
1306
1453
|
catch (error) {
|
|
1307
1454
|
if (candidate?.accepted && !mayRecordProgress) {
|
|
@@ -1314,13 +1461,13 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
|
1314
1461
|
if (error instanceof CliError) {
|
|
1315
1462
|
ctx.stderr(error.message);
|
|
1316
1463
|
if (args.json)
|
|
1317
|
-
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: error.exitCode }));
|
|
1464
|
+
ctx.stdout(JSON.stringify({ ...planJournal(journal), exit: error.exitCode, errorCode: error.code }));
|
|
1318
1465
|
return error.exitCode;
|
|
1319
1466
|
}
|
|
1320
1467
|
throw error;
|
|
1321
1468
|
}
|
|
1322
1469
|
finally {
|
|
1323
|
-
if (deps.bindSignals !== false)
|
|
1470
|
+
if (deps.bindSignals !== false && deps.ui?.handlesSignals !== true)
|
|
1324
1471
|
for (const name of signals)
|
|
1325
1472
|
process.off(name, interrupted);
|
|
1326
1473
|
releaseLock(lock);
|