@catalyst-cloud/cli 0.15.2 → 0.15.3

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 (36) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +2 -2
  3. package/dist/cli.js +5 -17
  4. package/dist/oauth.js +1 -1
  5. package/dist/onboard-accounts.js +67 -171
  6. package/dist/onboard-checklist.js +309 -0
  7. package/dist/onboard-existing.js +1 -1
  8. package/dist/onboard-first-ticket.js +199 -0
  9. package/dist/onboard-headless.js +2 -2
  10. package/dist/onboard-housekeeping.js +93 -0
  11. package/dist/onboard-next.js +108 -9
  12. package/dist/onboard-ready.js +61 -0
  13. package/dist/onboard-runtime.js +60 -27
  14. package/dist/onboard-standalone-copy.js +73 -66
  15. package/dist/onboard-ui.js +162 -89
  16. package/dist/onboard-values.js +47 -0
  17. package/dist/onboard.js +49 -13
  18. package/dist/ready-copy.js +87 -0
  19. package/dist/ready.js +1 -57
  20. package/dist/setup-onboard-copy.js +237 -41
  21. package/dist/setup-render.js +73 -16
  22. package/dist/setup.js +6 -3
  23. package/package.json +1 -1
  24. package/skills/catalyst-github/SKILL.md +1 -1
  25. package/skills/catalyst-linear/SKILL.md +1 -1
  26. package/skills/catalyst-onboard/SKILL.md +3 -3
  27. package/skills/catalyst-onboard/references/replacing-a-credential.md +9 -15
  28. package/skills/catalyst-onboard/references/what-a-phase-needs.md +4 -11
  29. package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -1
  30. package/skills/run-this-project/SKILL.md +2 -2
  31. package/skills/unstick/SKILL.md +1 -1
  32. package/skills/what-needs-me/SKILL.md +1 -1
  33. package/skills/what-this-repo-needs/SKILL.md +1 -1
  34. package/skills/whats-happening/SKILL.md +2 -2
  35. package/skills/whats-happening/references/coding-accounts.md +1 -1
  36. package/skills/whats-happening/scripts/snapshot.mjs +1 -1
package/dist/onboard.js CHANGED
@@ -10,7 +10,7 @@ import { onboardJsonView, TIMED_OUT_REASONS } from "./setup-onboard-copy.js";
10
10
  import { standalonePlan, standalonePlanNotes, } from "./onboard-standalone-copy.js";
11
11
  import { CliError, UsageError } from "./errors.js";
12
12
  import { onboardFileSnapshot } from "./onboard-file-snapshot.js";
13
- import { onboardReasonText, onboardTeamKey, savedOnboardBaseUrl, } from "./onboard-next.js";
13
+ import { dispatchStageName, onboardReasonText, onboardTeamKey, savedOnboardBaseUrl, } from "./onboard-next.js";
14
14
  import { parseBootstrapPlan, bootstrapPlanHash, bootstrapPlanLines, } from "./onboard-bootstrap.js";
15
15
  export const ONBOARD_STEPS = [
16
16
  "machine",
@@ -37,6 +37,14 @@ export const ONBOARD_STEPS = [
37
37
  "first-ticket",
38
38
  "ready",
39
39
  ];
40
+ function dailyUpdateChoice(value) {
41
+ const row = object(value);
42
+ if (row?.state === "on" || row?.state === "off")
43
+ return { state: row.state };
44
+ if (row?.state === "skipped" && row.reason === "no_scheduler")
45
+ return { state: "skipped", reason: "no_scheduler" };
46
+ return undefined;
47
+ }
40
48
  export function onboardIdentityLines(identity) {
41
49
  if (!identity)
42
50
  return [
@@ -104,7 +112,8 @@ export const ONBOARD_DEPENDENCIES = {
104
112
  projects: ["linear.team", "github.repos"],
105
113
  accounts: ["signin"],
106
114
  settings: ["projects"],
107
- values: ["settings"],
115
+ // The cloud's required-values check reads the registered repositories, not a local checkout.
116
+ values: ["projects"],
108
117
  capacity: ["projects"],
109
118
  // The join token names the selected team's pool.
110
119
  runner: ["linear.team"],
@@ -127,7 +136,7 @@ const ADMIN_STEPS = new Set([
127
136
  "github.repos",
128
137
  "projects",
129
138
  "settings",
130
- "values",
139
+ // `values` is not here: its check only reads, so a member's run finds missing values too.
131
140
  "capacity",
132
141
  "runner",
133
142
  "first-ticket",
@@ -147,9 +156,9 @@ export const ONBOARD_TITLES = {
147
156
  "github.personal": "Connect your GitHub account",
148
157
  "github.repos": "Choose repositories",
149
158
  projects: "Register your projects",
150
- accounts: "Check coding accounts",
159
+ accounts: "Check AI accounts",
151
160
  settings: "Review repository settings",
152
- values: "Import selected local values",
161
+ values: "Check the repository's values",
153
162
  capacity: "Check runner capacity",
154
163
  runner: "Run Catalyst's work on this machine",
155
164
  daemon: "Check optional local sync",
@@ -251,6 +260,10 @@ function normalizeStep(value, fallbackAt) {
251
260
  "repository",
252
261
  "installation",
253
262
  "ticket",
263
+ // The team's dispatch stage name, as the contract gave it.
264
+ "stage",
265
+ // Admin-owned step ids a member's run found unfinished.
266
+ "admin",
254
267
  "cursor",
255
268
  "lag",
256
269
  "heartbeatAgeMs",
@@ -288,6 +301,8 @@ function normalizeStep(value, fallbackAt) {
288
301
  "org",
289
302
  "url",
290
303
  "actor",
304
+ // CTC-4716: which AI accounts the workspace may add, so the step's text offers only those.
305
+ "aiAccountKinds",
291
306
  ]) {
292
307
  const item = evidence?.[key];
293
308
  if (item === null ||
@@ -323,6 +338,7 @@ export function readOnboardJournal(path, cliVersion = "0.13.1", now = new Date()
323
338
  : typeof value.exitCode === "number"
324
339
  ? value.exitCode
325
340
  : null;
341
+ const dailyUpdate = dailyUpdateChoice(value.dailyUpdate);
326
342
  return {
327
343
  schema: 1,
328
344
  runId: typeof value.runId === "string" ? value.runId : randomUUID(),
@@ -354,6 +370,7 @@ export function readOnboardJournal(path, cliVersion = "0.13.1", now = new Date()
354
370
  ...(typeof value.localSync === "boolean"
355
371
  ? { localSync: value.localSync }
356
372
  : {}),
373
+ ...(dailyUpdate ? { dailyUpdate } : {}),
357
374
  operations: Object.fromEntries(Object.entries(object(value.operations) ?? {}).filter(([key, v]) => ONBOARD_STEPS.includes(key) &&
358
375
  typeof v === "string" &&
359
376
  /^[a-zA-Z0-9_.:-]{1,160}$/.test(v))),
@@ -573,7 +590,8 @@ function printPlan(ctx, journal, identity, options) {
573
590
  for (const line of onboardIdentityLines(identity))
574
591
  ctx.stdout(line);
575
592
  let group;
576
- for (const row of standalonePlan(journal, options?.scope, options?.runner)) {
593
+ // Beside --json and headless output: the numbers JSON actions carry (CTC-4680).
594
+ for (const row of standalonePlan(journal, options?.scope, options?.runner, "json")) {
577
595
  if (row.group !== group) {
578
596
  ctx.stdout("");
579
597
  ctx.stdout(row.group);
@@ -629,14 +647,28 @@ export function stepSatisfied(step) {
629
647
  return (step?.state === "done" ||
630
648
  (step?.state === "skipped" &&
631
649
  (step.id === "legacy" ||
650
+ // Optional: a computer without the daily update still runs work.
651
+ step.id === "housekeeping" ||
632
652
  step.reason === "member_scope" ||
633
653
  (step.id === "daemon" && step.reason === "local_sync_not_selected") ||
634
654
  (step.id === "runner" && step.reason === "runner_not_selected") ||
655
+ (step.id === "first-ticket" && step.reason === "first_ticket_skipped") ||
635
656
  (step.id === "linear.automations" &&
636
657
  step.reason === "automation_management_unavailable"))));
637
658
  }
659
+ /** Optional work this computer or checkout cannot do. Work runs without it, so its wait never holds
660
+ * a complete setup. */
661
+ const OPTIONAL_WAITS = new Set([
662
+ "settings_checkout_unverified",
663
+ "settings_approval_unverified",
664
+ ]);
665
+ /** Satisfied, or an optional wait. What a complete setup needs of every step. */
666
+ export function stepSettled(step) {
667
+ return (stepSatisfied(step) ||
668
+ (step?.state === "waiting" && OPTIONAL_WAITS.has(step.reason ?? "")));
669
+ }
638
670
  /** Steps whose wait does not hold a full run's exit code: work runs without them. A failure still
639
- * fails the run, and the receipt's `complete` still needs them satisfied. Never used for gating. */
671
+ * fails the run, and the receipt's `complete` still needs them settled. Never used for gating. */
640
672
  export const ONBOARD_DEFERRED_STEPS = new Set([
641
673
  "settings",
642
674
  "values",
@@ -677,8 +709,11 @@ export function onboardNextActions(journal, baseUrl, only) {
677
709
  .filter((step) => (!only || step.id === only) && !stepSatisfied(step))
678
710
  .sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id))
679
711
  .map((step) => `${ONBOARD_TITLES[step.id]}: ${onboardStepAction(journal, step, baseUrl)}`);
680
- if (["projects", "accounts", "linear.adopt", "capacity"].every((id) => journalStep(journal, id)?.state === "done"))
681
- lines.push(`Move a ticket in ${onboardTeamKey(journal) ?? "<TEAM KEY>"} to Todo; \`catalyst explain <ticket>\` says why it is or is not starting.`);
712
+ // A waiting first ticket already says to move a ticket, in its own line.
713
+ const firstTicket = journalStep(journal, "first-ticket");
714
+ if (["projects", "accounts", "linear.adopt", "capacity"].every((id) => journalStep(journal, id)?.state === "done") &&
715
+ firstTicket?.state !== "waiting")
716
+ lines.push(`Move a ticket in ${onboardTeamKey(journal) ?? "<TEAM KEY>"} to ${dispatchStageName(firstTicket?.evidence?.stage)}; \`catalyst explain <ticket>\` says why it is or is not starting.`);
682
717
  return lines;
683
718
  }
684
719
  function requireMatchingIdentity(journal, identity) {
@@ -727,9 +762,10 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
727
762
  const only = typeof args.flags.only === "string"
728
763
  ? args.flags.only
729
764
  : undefined;
765
+ // A member's values check reads the selected team, so without --team it waits on the same skip.
730
766
  const memberScopeSkips = (id) => ADMIN_STEPS.has(id) ||
731
- (id === "linear.team" &&
732
- only !== "linear.team" &&
767
+ ((id === "linear.team" || id === "values") &&
768
+ only !== id &&
733
769
  typeof args.flags.team !== "string");
734
770
  const resume = typeof args.flags["resume-from"] === "string"
735
771
  ? args.flags["resume-from"]
@@ -1069,7 +1105,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1069
1105
  journal.complete =
1070
1106
  !only &&
1071
1107
  code === 0 &&
1072
- ONBOARD_STEPS.every((id) => stepSatisfied(journalStep(journal, id)));
1108
+ ONBOARD_STEPS.every((id) => stepSettled(journalStep(journal, id)));
1073
1109
  writeOnboardJournal(statePath, journal);
1074
1110
  if (args.json)
1075
1111
  ctx.stdout(JSON.stringify(journal));
@@ -1442,7 +1478,7 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
1442
1478
  journal.complete =
1443
1479
  !only &&
1444
1480
  code === 0 &&
1445
- ONBOARD_STEPS.every((id) => stepSatisfied(journalStep(journal, id)));
1481
+ ONBOARD_STEPS.every((id) => stepSettled(journalStep(journal, id)));
1446
1482
  const unfinished = journal.steps.filter((step) => !stepSatisfied(step));
1447
1483
  if (!only &&
1448
1484
  !refused &&
@@ -65,6 +65,93 @@ function slotList(slots) {
65
65
  function origin(baseUrl) {
66
66
  return baseUrl.replace(/\/+$/, "");
67
67
  }
68
+ const NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
69
+ const REPOSITORY = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/;
70
+ const identifiers = (value) => Array.isArray(value)
71
+ ? value.filter((name) => typeof name === "string" && NAME.test(name))
72
+ : [];
73
+ const unresolvedFacts = (value) => Array.isArray(value)
74
+ ? value.flatMap((row) => typeof row?.name === "string" && NAME.test(row.name) && identifiers(row.references).length
75
+ ? [{ name: row.name, references: identifiers(row.references) }]
76
+ : [])
77
+ : [];
78
+ /** The declared names a failing check carries, kept in the receipt for the fix line: which variables
79
+ * need a value, which refer to a secret with no value, and the same for the team's other
80
+ * repositories. Identifiers and repository names only. */
81
+ export function valuesFacts(value) {
82
+ const row = (value ?? {});
83
+ return {
84
+ names: identifiers(row.names),
85
+ unresolved: unresolvedFacts(row.unresolved),
86
+ repos: Array.isArray(row.repos)
87
+ ? row.repos.flatMap((note) => typeof note?.repo === "string" && REPOSITORY.test(note.repo)
88
+ ? [{ repo: note.repo, names: identifiers(note.names), unresolved: unresolvedFacts(note.unresolved) }]
89
+ : [])
90
+ : [],
91
+ };
92
+ }
93
+ /** Where the named variables get their values. Names only: nothing here reads or prints a value. */
94
+ function setValuesFix(baseUrl, names) {
95
+ return baseUrl
96
+ ? `set ${names.join(", ")} on the repository's Environment page: open ${origin(baseUrl)}/settings/projects, then the project, then the repository`
97
+ : `set ${names.join(", ")} on the repository's Environment page in Catalyst's project settings`;
98
+ }
99
+ function nameList(v) {
100
+ return Array.isArray(v)
101
+ ? v.filter((n) => typeof n === "string" && n.length > 0)
102
+ : [];
103
+ }
104
+ /** Entries whose name and references are both present; anything malformed is dropped, not guessed. */
105
+ function unresolvedList(v) {
106
+ if (!Array.isArray(v))
107
+ return [];
108
+ return v.flatMap((u) => {
109
+ if (typeof u !== "object" || u === null)
110
+ return [];
111
+ const { name, references } = u;
112
+ const refs = nameList(references);
113
+ return typeof name === "string" && name.length > 0 && refs.length > 0
114
+ ? [{ name, references: refs }]
115
+ : [];
116
+ });
117
+ }
118
+ function unresolvedLine(u) {
119
+ const which = u.references.length === 1 ? "which has" : "which have";
120
+ return `${u.name} refers to ${u.references.join(", ")}, ${which} no value, so Catalyst can't start work there`;
121
+ }
122
+ /** A team check's fix line. A check that carries `names` (CTC-3561: `required_values`, contract
123
+ * 1.24.0) names them and where to set them. CTC-3606: it also names each `unresolved` variable and
124
+ * the reference with no value, and each other repository's missing names from `repos[]`. `unresolved`
125
+ * is a subset of `names` (the variable exists; its reference does not), so those are not told to be
126
+ * "set". Every field is optional so an older cloud still works. Everything printed is a declared
127
+ * identifier or a repository name, never a value, and nothing here reads a value. Any other check
128
+ * keeps the generic line. Shared with setup's values step, which has no web address when the
129
+ * saved login has none. */
130
+ export function valuesFix(base, c) {
131
+ const unresolved = unresolvedList(c.unresolved);
132
+ const unresolvedNames = new Set(unresolved.map((u) => u.name));
133
+ const names = nameList(c.names).filter((n) => !unresolvedNames.has(n));
134
+ const parts = [];
135
+ if (names.length > 0)
136
+ parts.push(setValuesFix(base, names));
137
+ for (const u of unresolved)
138
+ parts.push(unresolvedLine(u));
139
+ for (const note of Array.isArray(c.repos) ? c.repos : []) {
140
+ if (typeof note !== "object" ||
141
+ note === null ||
142
+ typeof note.repo !== "string" ||
143
+ note.repo.length === 0)
144
+ continue;
145
+ const repoUnresolved = unresolvedList(note.unresolved);
146
+ const skip = new Set(repoUnresolved.map((u) => u.name));
147
+ const missing = nameList(note.names).filter((n) => !skip.has(n));
148
+ if (missing.length > 0)
149
+ parts.push(`${note.repo} is missing ${missing.join(", ")}`);
150
+ for (const u of repoUnresolved)
151
+ parts.push(`in ${note.repo}, ${unresolvedLine(u)}`);
152
+ }
153
+ return parts.length === 0 ? null : parts.join(". ");
154
+ }
68
155
  /** The team's own page in Catalyst settings, optionally one of its sub-pages (`map`, `adopt`). */
69
156
  export function teamPage(baseUrl, key, sub) {
70
157
  return `${origin(baseUrl)}/settings/linear-teams/${encodeURIComponent(key)}${sub ? `/${sub}` : ""}`;
package/dist/ready.js CHANGED
@@ -15,7 +15,7 @@ import { FIRST_STAMPED_VERSION } from "./skill-shape.js";
15
15
  import { installedBundleVersion } from "./skills.js";
16
16
  import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
17
17
  import { selectedOnboardTeam } from "./onboard-existing.js";
18
- import { dispatchGateCopy, teamCheckCopy, teamPage, whoFixes } from "./ready-copy.js";
18
+ import { dispatchGateCopy, teamCheckCopy, teamPage, valuesFix, whoFixes } from "./ready-copy.js";
19
19
  import { onboardStatePath, readOnboardJournal } from "./onboard.js";
20
20
  export { semverOlder };
21
21
  /** `ready`'s own wording for the replica. It never recommends adopting the replica — until the
@@ -112,62 +112,6 @@ export function catalystCommandCheck(env, platform = process.platform) {
112
112
  "Keep using catalyst-skills on this machine, or put the npm global bin directory ahead of that one on PATH.",
113
113
  };
114
114
  }
115
- function nameList(v) {
116
- return Array.isArray(v)
117
- ? v.filter((n) => typeof n === "string" && n.length > 0)
118
- : [];
119
- }
120
- /** Entries whose name and references are both present; anything malformed is dropped, not guessed. */
121
- function unresolvedList(v) {
122
- if (!Array.isArray(v))
123
- return [];
124
- return v.flatMap((u) => {
125
- if (typeof u !== "object" || u === null)
126
- return [];
127
- const { name, references } = u;
128
- const refs = nameList(references);
129
- return typeof name === "string" && name.length > 0 && refs.length > 0
130
- ? [{ name, references: refs }]
131
- : [];
132
- });
133
- }
134
- function unresolvedLine(u) {
135
- const which = u.references.length === 1 ? "which has" : "which have";
136
- return `${u.name} refers to ${u.references.join(", ")}, ${which} no value, so Catalyst can't start work there`;
137
- }
138
- /** A team check's fix line. A check that carries `names` (CTC-3561: `required_values`, contract
139
- * 1.24.0) names them and where to set them. CTC-3606: it also names each `unresolved` variable and
140
- * the reference with no value, and each other repository's missing names from `repos[]`. `unresolved`
141
- * is a subset of `names` (the variable exists; its reference does not), so those are not told to be
142
- * "set". Every field is optional so an older cloud still works. Everything printed is a declared
143
- * identifier or a repository name, never a value, and nothing here reads a value. Any other check
144
- * keeps the generic line. */
145
- function valuesFix(base, c) {
146
- const unresolved = unresolvedList(c.unresolved);
147
- const unresolvedNames = new Set(unresolved.map((u) => u.name));
148
- const names = nameList(c.names).filter((n) => !unresolvedNames.has(n));
149
- const parts = [];
150
- if (names.length > 0) {
151
- parts.push(`set ${names.join(", ")} on the repository's Environment page: open ${base}/settings/projects, then the project, then the repository`);
152
- }
153
- for (const u of unresolved)
154
- parts.push(unresolvedLine(u));
155
- for (const note of Array.isArray(c.repos) ? c.repos : []) {
156
- if (typeof note !== "object" ||
157
- note === null ||
158
- typeof note.repo !== "string" ||
159
- note.repo.length === 0)
160
- continue;
161
- const repoUnresolved = unresolvedList(note.unresolved);
162
- const skip = new Set(repoUnresolved.map((u) => u.name));
163
- const missing = nameList(note.names).filter((n) => !skip.has(n));
164
- if (missing.length > 0)
165
- parts.push(`${note.repo} is missing ${missing.join(", ")}`);
166
- for (const u of repoUnresolved)
167
- parts.push(`in ${note.repo}, ${unresolvedLine(u)}`);
168
- }
169
- return parts.length === 0 ? null : parts.join(". ");
170
- }
171
115
  export async function readyReport(ctx, deps) {
172
116
  const checks = [];
173
117
  const facts = deps.runtime ?? detectRuntime();