@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +5 -3
  3. package/dist/args.js +51 -13
  4. package/dist/cli.js +58 -10
  5. package/dist/config.js +16 -3
  6. package/dist/daemon-authority-lifetime.js +171 -0
  7. package/dist/daemon-authority.js +216 -0
  8. package/dist/daemon-cache.js +348 -0
  9. package/dist/daemon-store.js +370 -0
  10. package/dist/daemon-supervisor.js +460 -0
  11. package/dist/env/onboard-draft-store.js +243 -0
  12. package/dist/env/onboard-draft.js +33 -0
  13. package/dist/env/selected-import-writer.js +163 -0
  14. package/dist/env/settings-authority.js +203 -0
  15. package/dist/events-cloud-tail.js +134 -0
  16. package/dist/events-cloud.js +500 -0
  17. package/dist/events.js +13 -4
  18. package/dist/legacy.js +145 -32
  19. package/dist/oauth.js +125 -33
  20. package/dist/onboard-accounts.js +259 -0
  21. package/dist/onboard-automation-management.js +15 -0
  22. package/dist/onboard-bootstrap.js +162 -0
  23. package/dist/onboard-capabilities.js +15 -7
  24. package/dist/onboard-capacity.js +226 -0
  25. package/dist/onboard-existing.js +106 -32
  26. package/dist/onboard-file-snapshot.js +45 -0
  27. package/dist/onboard-login-candidate.js +261 -0
  28. package/dist/onboard-projects.js +350 -0
  29. package/dist/onboard-repositories.js +147 -37
  30. package/dist/onboard-runtime.js +104 -9
  31. package/dist/onboard-settings-context.js +141 -0
  32. package/dist/onboard-settings-import.js +127 -0
  33. package/dist/onboard-settings.js +234 -0
  34. package/dist/onboard-ui.js +99 -11
  35. package/dist/onboard-workflow.js +316 -0
  36. package/dist/onboard.js +359 -27
  37. package/dist/owned-fetch.js +182 -0
  38. package/dist/query.js +77 -27
  39. package/dist/ready.js +143 -35
  40. package/dist/replica.js +11 -4
  41. package/dist/sdk.js +33 -0
  42. package/dist/watch.js +2 -2
  43. package/package.json +3 -2
  44. package/skills/catalyst-github/SKILL.md +2 -2
  45. package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
  46. package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
  47. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
  48. package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
  49. package/skills/catalyst-linear/SKILL.md +3 -3
  50. package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
  51. package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
  52. package/skills/catalyst-onboard/SKILL.md +5 -4
  53. package/skills/catalyst-onboard/references/local-sync.md +3 -1
  54. package/skills/run-this-project/SKILL.md +1 -1
  55. package/skills/unstick/SKILL.md +1 -1
  56. package/skills/what-needs-me/SKILL.md +1 -1
  57. package/skills/what-this-repo-needs/SKILL.md +1 -1
  58. package/skills/whats-happening/SKILL.md +2 -2
  59. package/skills/whats-happening/references/status-reply.md +1 -1
  60. package/skills/whats-happening/scripts/snapshot.mjs +104 -26
  61. package/vendor/settings/index.d.ts +4 -0
  62. package/vendor/settings/index.js +2937 -0
  63. 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 (!(deps.isTty ?? (() => Boolean(process.stdin.isTTY && process.stdout.isTTY)))()) {
533
- ctx.stderr("No terminal is attached; using the default answer Yes. Pass --dry-run to inspect without changes.");
534
- return true;
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 code = error instanceof CliError ? error.exitCode : EXIT_FAILED;
627
- ctx.stderr("Could not verify your Catalyst membership. Setup was not changed. Sign in with the original account to resume.");
628
- if (args.json)
629
- ctx.stdout(JSON.stringify({
630
- ...journal,
631
- mode: "run",
632
- exit: code,
633
- complete: false,
634
- }));
635
- return code;
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
- const inheritedConsent = resume === "install" && Boolean(ctx.env.CATALYST_INSTALL_LOCK_TOKEN);
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
- const consent = inheritedConsent
644
- ? true
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
- ctx.stderr("Nothing was changed. Run the same command when ready.");
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 deps.ui?.signal.aborted ? EXIT_WAITING : 0;
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("resume: catalyst onboard");
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(`✗ ${ONBOARD_TITLES[id]}: ${reason}. The safe reason is saved; run the same command to resume.`);
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;