@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
@@ -1,5 +1,6 @@
1
1
  import { ONBOARD_DEPENDENCIES, ONBOARD_STEPS, ONBOARD_TITLES, } from "./onboard.js";
2
- import { onboardReasonText } from "./onboard-next.js";
2
+ import { dispatchStageName, onboardReasonText } from "./onboard-next.js";
3
+ /** The step numbers `--json` reports. People read SETUP_PART_NUMBERS instead (CTC-4680). */
3
4
  export const SETUP_NUMBERS = {
4
5
  machine: 1,
5
6
  cli: 1,
@@ -23,13 +24,94 @@ export const SETUP_NUMBERS = {
23
24
  values: 16,
24
25
  "first-ticket": 17,
25
26
  };
27
+ /** CTC-4680: setup runs in three parts. The times are estimates until setup logs give medians. */
28
+ export const SETUP_PARTS = {
29
+ 1: {
30
+ title: "This computer",
31
+ estimate: "about 2 min",
32
+ summary: "skills and sign-in",
33
+ done: "This computer is set up.",
34
+ },
35
+ 2: {
36
+ title: "Linear and GitHub",
37
+ estimate: "about 10 min",
38
+ summary: "approvals in your browser",
39
+ done: "Linear and GitHub are connected.",
40
+ },
41
+ 3: {
42
+ title: "Ready for work",
43
+ estimate: "about 5 min",
44
+ summary: "a first ticket",
45
+ done: "Catalyst is ready for work.",
46
+ },
47
+ };
48
+ export const SETUP_PART_OF = {
49
+ machine: 1,
50
+ cli: 1,
51
+ skills: 1,
52
+ legacy: 1,
53
+ signin: 1,
54
+ daemon: 1,
55
+ housekeeping: 1,
56
+ "linear.workspace": 2,
57
+ "linear.personal": 2,
58
+ "linear.team": 2,
59
+ "linear.adopt": 2,
60
+ "linear.automations": 2,
61
+ "github.install": 2,
62
+ "github.personal": 2,
63
+ "github.repos": 3,
64
+ projects: 3,
65
+ accounts: 3,
66
+ capacity: 3,
67
+ runner: 3,
68
+ settings: 3,
69
+ values: 3,
70
+ "first-ticket": 3,
71
+ ready: 3,
72
+ };
73
+ /** What a person reads: each part restarts at 1, in the order setup runs the steps. Steps that
74
+ * share a row (the computer checks, repositories and projects) share a number. */
75
+ export const SETUP_PART_NUMBERS = {
76
+ machine: 1,
77
+ cli: 1,
78
+ skills: 1,
79
+ legacy: 1,
80
+ signin: 2,
81
+ housekeeping: 3,
82
+ "linear.workspace": 1,
83
+ "linear.personal": 2,
84
+ "linear.team": 3,
85
+ "linear.adopt": 4,
86
+ "linear.automations": 5,
87
+ "github.install": 6,
88
+ "github.personal": 7,
89
+ "github.repos": 1,
90
+ projects: 1,
91
+ accounts: 2,
92
+ capacity: 3,
93
+ runner: 4,
94
+ settings: 5,
95
+ values: 6,
96
+ "first-ticket": 7,
97
+ };
98
+ /** CTC-4680: people type `catalyst setup`; `catalyst onboard` stays its alias. What setup draws
99
+ * names setup. `--json` and headless output keep their own words. */
100
+ export function setupCommandWords(text) {
101
+ return text.replace(/\bcatalyst onboard\b/g, "catalyst setup");
102
+ }
103
+ /** "Part 1 of 3 · This computer", with a colon where the output is plain. */
104
+ export function setupPartHeading(part, unicode) {
105
+ return `Part ${part} of 3${unicode ? " ·" : ":"} ${SETUP_PARTS[part].title}`;
106
+ }
26
107
  const TITLES = {
27
108
  "linear.adopt": "Set up the team's workflow",
28
109
  "linear.automations": "Check Linear's PR automations",
29
110
  projects: "Choose repositories",
30
- accounts: "Add a coding account",
111
+ accounts: "Add an AI account",
112
+ housekeeping: "Daily update",
31
113
  capacity: "Check runners",
32
- values: "Review repository settings",
114
+ values: "Check repository values",
33
115
  };
34
116
  const OPTIONAL = new Set([
35
117
  "local_sync_not_selected",
@@ -83,18 +165,23 @@ function succeeded(step) {
83
165
  case "projects":
84
166
  return "repositories are now Catalyst projects";
85
167
  case "accounts":
86
- return `${value(step, "provider") === "claude" ? "Claude" : value(step, "provider") === "codex" ? "Codex" : "a coding account"} can take work`;
168
+ return `${value(step, "provider") === "claude" ? "Claude" : value(step, "provider") === "codex" ? "Codex" : "an AI account"} can take work`;
169
+ case "housekeeping":
170
+ return "scheduled, runs daily";
87
171
  case "capacity":
88
172
  return typeof step.evidence?.remainingUnits === "number"
89
173
  ? `${step.evidence.remainingUnits} runner slots available`
90
174
  : "runners can take work";
91
175
  case "settings":
92
- case "values":
93
176
  return "settings reviewed";
94
- case "first-ticket":
95
- return value(step, "ticketKey")
96
- ? `${value(step, "ticketKey")} started`
177
+ case "values":
178
+ return "values set";
179
+ case "first-ticket": {
180
+ const key = value(step, "ticket") ?? value(step, "ticketKey");
181
+ return key
182
+ ? `${key} moved to ${dispatchStageName(step.evidence?.stage)}. Catalyst picks it up next.`
97
183
  : "first ticket started";
184
+ }
98
185
  case "daemon":
99
186
  return "local sync is running";
100
187
  case "runner":
@@ -114,6 +201,12 @@ function unfinished(step) {
114
201
  return "could not verify the connection's permissions";
115
202
  if (step.reason === "housekeeping_service_unverified")
116
203
  return "no scheduler here. catalyst says when an update is out";
204
+ if (step.reason === "housekeeping_off_chosen")
205
+ return "skipped, as you chose";
206
+ if (step.reason === "housekeeping_no_scheduler")
207
+ return "can't run on this computer";
208
+ if (step.id === "housekeeping")
209
+ return "not scheduled; the install command turns it on";
117
210
  if (step.reason === "local_sync_not_selected")
118
211
  return "off, reads come from the cloud";
119
212
  if (step.reason === "runner_docker_missing")
@@ -122,7 +215,7 @@ function unfinished(step) {
122
215
  return "an admin handles this step";
123
216
  if (step.reason === "automation_management_unavailable")
124
217
  return "could not read them; checked again before work starts";
125
- if (step.reason === "interrupted")
218
+ if (step.reason === "interrupted" || step.reason === "first_ticket_skipped")
126
219
  return "skipped for now";
127
220
  if (TIMED_OUT_REASONS.has(step.reason ?? ""))
128
221
  return "the link timed out";
@@ -134,7 +227,7 @@ function unfinished(step) {
134
227
  case "github.install":
135
228
  return "not installed yet";
136
229
  case "accounts":
137
- return "no coding account ready yet";
230
+ return "no AI account ready yet";
138
231
  case "linear.workspace":
139
232
  case "linear.personal":
140
233
  case "github.personal":
@@ -144,8 +237,13 @@ function unfinished(step) {
144
237
  case "capacity":
145
238
  return "no runner can take work yet";
146
239
  case "settings":
147
- case "values":
148
240
  return "optional; not reviewed yet";
241
+ case "values":
242
+ return step.reason === "required_values_missing"
243
+ ? "a value is missing"
244
+ : "not checked yet";
245
+ case "first-ticket":
246
+ return "no ticket chosen yet";
149
247
  default:
150
248
  return step.state === "skipped" ? "not chosen" : "not done yet";
151
249
  }
@@ -166,31 +264,118 @@ export function setupStepView(step, journal, required = false) {
166
264
  : "act";
167
265
  let outcome = step.state === "done" ? succeeded(step) : unfinished(step);
168
266
  if (mark === "later") {
169
- const numbers = [
170
- ...new Set((ONBOARD_DEPENDENCIES[step.id] ?? [])
171
- .filter((id) => {
267
+ const waitsOn = step.reason === "github_install_pending"
268
+ ? ["github.install"]
269
+ : (ONBOARD_DEPENDENCIES[step.id] ?? []).filter((id) => {
172
270
  const s = journal?.steps.find((s) => s.id === id);
173
271
  return s?.state !== "done" && s?.state !== "skipped";
174
- })
175
- .map((id) => SETUP_NUMBERS[id])
176
- .filter((n) => n !== undefined)),
177
- ];
178
- if (step.reason === "github_install_pending")
179
- numbers.splice(0, numbers.length, SETUP_NUMBERS["github.install"]);
272
+ });
273
+ // Numbers restart in each part: this part's steps come bare, then each other part once.
274
+ const byPart = new Map();
275
+ for (const id of waitsOn) {
276
+ const n = SETUP_PART_NUMBERS[id];
277
+ if (n === undefined)
278
+ continue;
279
+ const p = SETUP_PART_OF[id];
280
+ const list = byPart.get(p) ?? [];
281
+ if (!list.includes(n))
282
+ byPart.set(p, [...list, n]);
283
+ }
284
+ const own = SETUP_PART_OF[step.id];
285
+ const steps = (ns) => {
286
+ const sorted = [...ns].sort((x, y) => x - y);
287
+ const last = sorted.pop();
288
+ return sorted.length
289
+ ? `steps ${sorted.join(", ")} and ${last}`
290
+ : `step ${last}`;
291
+ };
292
+ const groups = [...byPart]
293
+ .sort(([x], [y]) => (x === own ? -1 : y === own ? 1 : x - y))
294
+ .map(([p, ns]) => (p === own ? steps(ns) : `${steps(ns)} of part ${p}`));
180
295
  outcome =
181
296
  step.state === "pending" && !step.reason
182
297
  ? "not checked yet"
183
- : numbers.length
184
- ? `after ${numbers.length === 1 ? "step" : "steps"} ${numbers.join(" and ")}`
298
+ : groups.length
299
+ ? `after ${groups.length > 1 ? `${groups.slice(0, -1).join(", ")}, and ${groups.at(-1)}` : groups[0]}`
185
300
  : "after an earlier step";
186
301
  }
187
302
  return {
188
- number: SETUP_NUMBERS[step.id] ?? 0,
303
+ number: SETUP_PART_NUMBERS[step.id] ?? 0,
189
304
  title: TITLES[step.id] ?? ONBOARD_TITLES[step.id],
190
305
  mark,
191
306
  outcome,
192
307
  };
193
308
  }
309
+ /** How far each part has got, by its numbered rows. A row is done when every step in it is done
310
+ * or skipped, and needs someone when any of its steps does. */
311
+ export function setupPartProgress(journal, scope) {
312
+ const progress = {
313
+ 1: { total: 0, done: 0, needs: 0, failed: 0 },
314
+ 2: { total: 0, done: 0, needs: 0, failed: 0 },
315
+ 3: { total: 0, done: 0, needs: 0, failed: 0 },
316
+ };
317
+ const rows = new Map();
318
+ for (const id of ONBOARD_STEPS) {
319
+ const number = SETUP_PART_NUMBERS[id];
320
+ if (number === undefined || (scope && !scope.includes(id)))
321
+ continue;
322
+ const step = journal.steps.find((s) => s.id === id) ?? {
323
+ id,
324
+ state: "pending",
325
+ };
326
+ const key = `${SETUP_PART_OF[id]}:${number}`;
327
+ rows.set(key, [...(rows.get(key) ?? []), setupStepView(step, journal).mark]);
328
+ }
329
+ for (const [key, marks] of rows) {
330
+ const part = progress[Number(key.split(":")[0])];
331
+ part.total++;
332
+ if (marks.every((m) => m === "done" || m === "skip"))
333
+ part.done++;
334
+ else if (marks.some((m) => m === "act" || m === "fail")) {
335
+ part.needs++;
336
+ if (marks.includes("fail"))
337
+ part.failed++;
338
+ }
339
+ }
340
+ return progress;
341
+ }
342
+ /**
343
+ * The tracker's rows: on the first screen (`plan`, the part about to start), at a boundary
344
+ * (`next`, the part about to start), or at the end. A part with no rows in this run is left out.
345
+ */
346
+ export function setupTrackerRows(progress, moment) {
347
+ const steps = (n) => `${n} ${n === 1 ? "step" : "steps"}`;
348
+ return [1, 2, 3]
349
+ .filter((p) => progress[p].total > 0)
350
+ .map((p) => {
351
+ const { title, estimate, summary } = SETUP_PARTS[p];
352
+ const got = progress[p];
353
+ const row = (mark, note) => ({
354
+ mark,
355
+ number: p,
356
+ title,
357
+ note,
358
+ });
359
+ if (typeof moment === "object" && "plan" in moment)
360
+ return row(p === moment.plan ? "now" : "later", `${estimate} · ${summary}`);
361
+ const finished = got.done === got.total;
362
+ const needsMark = got.failed ? "fail" : "act";
363
+ if (typeof moment === "object") {
364
+ if (p === moment.next)
365
+ return row("now", `next · ${estimate}`);
366
+ if (p > moment.next)
367
+ return row("later", estimate);
368
+ return finished
369
+ ? row("done", "done")
370
+ : row(needsMark, `${steps(got.total - got.done)} not finished`);
371
+ }
372
+ if (finished)
373
+ return row("done", "");
374
+ if (got.needs)
375
+ return row(needsMark, `${steps(got.needs)} ${got.needs === 1 ? "needs" : "need"} someone`);
376
+ return row("later", `${got.done} of ${steps(got.total)} done`);
377
+ });
378
+ }
194
379
  function page(base, path) {
195
380
  try {
196
381
  const url = new URL(path, base);
@@ -206,7 +391,8 @@ function page(base, path) {
206
391
  }
207
392
  function action(step, base, journal) {
208
393
  if (CONNECTION_REPAIR_REASONS.has(step.reason ?? "") ||
209
- step.reason === "github_installation_approval_pending")
394
+ step.reason === "github_installation_approval_pending" ||
395
+ step.reason === "member_scope")
210
396
  return onboardReasonText(step, { baseUrl: base, journal });
211
397
  const link = (path, verb) => {
212
398
  const url = page(base, path);
@@ -229,7 +415,9 @@ function action(step, base, journal) {
229
415
  case "github.personal":
230
416
  return link("/settings/connected-accounts?connect=github", "connect your GitHub account");
231
417
  case "accounts":
232
- return link("/settings/coding-accounts", "add a coding account");
418
+ return step.reason === "ai_account_not_usable"
419
+ ? onboardReasonText(step, { baseUrl: base, journal })
420
+ : link("/settings/coding-accounts", "add an AI account");
233
421
  case "linear.adopt": {
234
422
  const team = journal?.steps.find((row) => row.id === "linear.team" && row.state === "done");
235
423
  const key = team && value(team, "teamKey");
@@ -245,24 +433,30 @@ function action(step, base, journal) {
245
433
  ? onboardReasonText(step, { baseUrl: base, journal })
246
434
  : "Run catalyst onboard --runner to check whether this computer can take work.";
247
435
  case "settings":
248
- case "values":
249
436
  return "Open your repository checkout and review its settings.";
437
+ case "values":
438
+ return onboardReasonText({ ...step, reason: step.reason ?? "required_values_unverified" }, { baseUrl: base, journal });
250
439
  case "linear.team":
251
440
  return "Choose a Linear team when setup checks again.";
252
441
  case "github.repos":
253
442
  case "projects":
254
443
  return "Choose repositories when setup checks again.";
255
444
  case "first-ticket":
256
- return "Choose a first ticket when setup checks again.";
445
+ return onboardReasonText({ ...step, reason: step.reason ?? "first_ticket_choice_required" }, { baseUrl: base, journal });
257
446
  default:
258
447
  return "Try this step again when setup checks again.";
259
448
  }
260
449
  }
261
450
  export function setupFinalScreen(journal, base, paused = false, required = new Set()) {
451
+ // A member's run skips admin steps; readiness names the ones an admin has not finished yet.
452
+ const readyStep = journal.steps.find((step) => step.id === "ready");
453
+ const adminLeft = new Set(readyStep?.reason === "admin_setup_pending" && typeof readyStep.evidence?.admin === "string"
454
+ ? readyStep.evidence.admin.split(",")
455
+ : []);
262
456
  const roots = journal.steps
263
457
  .filter((step) => {
264
458
  const m = setupStepView(step, journal, required.has(step.id)).mark;
265
- return ((m === "act" || m === "fail") &&
459
+ return ((m === "act" || m === "fail" || adminLeft.has(step.id)) &&
266
460
  (ONBOARD_DEPENDENCIES[step.id] ?? []).every((id) => {
267
461
  const parent = journal.steps.find((s) => s.id === id);
268
462
  return parent?.state === "done" || parent?.state === "skipped";
@@ -270,7 +464,7 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
270
464
  })
271
465
  .filter((step) => step.id !== "ready")
272
466
  .sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id));
273
- const folded = (id) => id === "projects" ? "github.repos" : id === "values" ? "settings" : id;
467
+ const folded = (id) => id === "projects" ? "github.repos" : id;
274
468
  const unique = roots.filter((step, i) => roots.findIndex((s) => folded(s.id) === folded(step.id)) === i);
275
469
  const actions = unique
276
470
  .slice(0, 5)
@@ -297,16 +491,17 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
297
491
  : journal.exit === 0
298
492
  ? "Ready for work"
299
493
  : "Not ready for work yet";
300
- const ticket = journal.steps.find((s) => s.id === "first-ticket" && s.state === "done");
301
- const key = ticket && value(ticket, "ticketKey");
494
+ const firstTicket = journal.steps.find((s) => s.id === "first-ticket");
495
+ const ticket = firstTicket?.state === "done" ? firstTicket : undefined;
496
+ const key = ticket && (value(ticket, "ticket") ?? value(ticket, "ticketKey"));
302
497
  const savedTicketUrl = ticket && (value(ticket, "url") ?? value(ticket, "ticketUrl"));
303
498
  const ticketUrl = savedTicketUrl && page(base, savedTicketUrl);
304
499
  const next = paused
305
500
  ? "Next: run catalyst onboard to continue."
306
- : key && ticketUrl
307
- ? `Next: open ${ticketUrl} and follow ${key}.`
501
+ : key
502
+ ? `Follow ${ticketUrl ?? key} in Linear; Catalyst comments there as each phase finishes.`
308
503
  : heading === "Setup complete"
309
- ? "Next: move a ticket to Todo in Linear."
504
+ ? `Next: move a ticket to ${dispatchStageName(firstTicket?.evidence?.stage)} in Linear.`
310
505
  : journal.exit === 0 && actions[0]
311
506
  ? `Next: ${actions[0].text[0].toLowerCase() + actions[0].text.slice(1)}`
312
507
  : actions.length === 1
@@ -317,7 +512,9 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
317
512
  ? "Next: run catalyst onboard after you finish 1 to " +
318
513
  actions.length +
319
514
  "."
320
- : "Next: run catalyst onboard to check again.";
515
+ : readyStep?.reason === "member_team_required"
516
+ ? "Next: run catalyst onboard --team <KEY> to check again."
517
+ : "Next: run catalyst onboard to check again.";
321
518
  return {
322
519
  heading,
323
520
  readinessCause,
@@ -351,8 +548,8 @@ export function setupBrowserInstruction(id, base, opened = false) {
351
548
  ],
352
549
  accounts: [
353
550
  "/settings/coding-accounts",
354
- "add the token with your Claude email",
355
- "Add a coding account",
551
+ "add an API key",
552
+ "Add an API key",
356
553
  ],
357
554
  };
358
555
  const words = copy[id];
@@ -363,9 +560,6 @@ export function setupBrowserInstruction(id, base, opened = false) {
363
560
  ? `Opened your browser. ${words[2]} there.`
364
561
  : `Open this link and ${words[1]}:`,
365
562
  url: page(base, words[0]),
366
- preparation: id === "accounts"
367
- ? "In another terminal, run claude setup-token."
368
- : undefined,
369
563
  };
370
564
  }
371
565
  /** JSON facts use the same reachable actions as the terminal, while retaining the existing journal. */
@@ -412,7 +606,9 @@ export function onboardJsonView(journal, base, paused = false, options = {}) {
412
606
  // CTC-4680: refused by the person's Catalyst role, not by GitHub, so a workspace admin acts.
413
607
  step.reason === "github_installation_admin_required" ||
414
608
  step.reason?.endsWith("_identity_unverified") ||
415
- (action.id === "accounts" && role !== "owner" && role !== "admin");
609
+ ((action.id === "accounts" || action.id === "values") &&
610
+ role !== "owner" &&
611
+ role !== "admin");
416
612
  const url = action.text
417
613
  .match(/https?:\/\/[^\s<>]+/)?.[0]
418
614
  ?.replace(/[.,;:!?]+$/, "");
@@ -60,6 +60,7 @@ const ASCII_MARKS = {
60
60
  later: "[later]",
61
61
  run: "[..]",
62
62
  ask: "[?]",
63
+ now: "[next]",
63
64
  };
64
65
  const UNICODE_MARKS = {
65
66
  done: ["✓", "green"],
@@ -69,7 +70,34 @@ const UNICODE_MARKS = {
69
70
  later: ["·", "gray"],
70
71
  run: ["◐", "cyan"],
71
72
  ask: ["◆", "cyan"],
73
+ now: ["◆", "cyan"],
72
74
  };
75
+ /** CTC-4680: the Pixel Nucleus core, copper on a dark terminal and rust on a light one
76
+ * (catalyst-cloud docs/brand-mark.md), with the nearest 256- and 16-colour background. */
77
+ const CORE = {
78
+ dark: { rgb: "210;142;99", x256: 173, x16: 43 },
79
+ light: { rgb: "169;81;47", x256: 130, x16: 41 },
80
+ };
81
+ /**
82
+ * The mark's 4×4 grid in two terminal lines, each cell 2 columns by half a line. The ring is a
83
+ * glyph in the terminal's own foreground colour; the core is a background colour, so no
84
+ * foreground is ever set and the C reads on light and dark terminals alike. The second line
85
+ * reverses video so the core's half block takes the copper and its other half the ring.
86
+ */
87
+ export function pixelNucleusLines(env) {
88
+ // COLORFGBG is "fg;bg" (rxvt, Konsole, iTerm2); 7 and 15 are the light backgrounds.
89
+ const light = ["7", "15"].includes((env.COLORFGBG ?? "").split(";").at(-1) ?? "");
90
+ const core = light ? CORE.light : CORE.dark;
91
+ const bg = /^(truecolor|24bit)$/i.test(env.COLORTERM ?? "")
92
+ ? `48;2;${core.rgb}`
93
+ : /256/.test(env.TERM ?? "")
94
+ ? `48;5;${core.x256}`
95
+ : String(core.x16);
96
+ return [
97
+ `▄▄\u001b[${bg}m▀▀▀▀\u001b[49m▀▀`,
98
+ `▀▀\u001b[7;${bg}m▀▀▀▀\u001b[27;49m▄▄`,
99
+ ];
100
+ }
73
101
  export function createSetupRenderer(stream, env) {
74
102
  let traits = terminalTraits(stream, env);
75
103
  const style = (format, text) => traits.color ? styleText(format, text, { validateStream: false }) : text;
@@ -85,12 +113,14 @@ export function createSetupRenderer(stream, env) {
85
113
  const visible = (text) => text
86
114
  .replace(/\u001b\]8;;[^\u001b]*\u001b\\/g, "")
87
115
  .replace(/\u001b\[[0-9;]*m/g, "");
88
- const rail = traits.unicode ? "│" : "";
89
- // CTC-4680: details sit 4 columns in (under the rail in Unicode), not under the step title at 6 or
90
- // 11, so the eye doesn't travel across a small screen to read them.
91
- const titleCol = 4;
92
- const outcomeCol = traits.unicode ? 38 : 43;
93
- const prefix = (col) => rail + " ".repeat(col - rail.length);
116
+ // CTC-4680 (round 2): headings and prose at column 2, step rows at 4, and their details 4 more,
117
+ // at 8, so the eye doesn't travel across a small screen to read them. No rail: the same columns
118
+ // on a terminal and in a pipe.
119
+ const textCol = 2;
120
+ const rowCol = 4;
121
+ const titleCol = rowCol + 4;
122
+ const outcomeCol = rowCol + (traits.unicode ? 38 : 43);
123
+ const prefix = (col) => " ".repeat(col);
94
124
  const mark = (kind) => {
95
125
  if (!traits.unicode)
96
126
  return ASCII_MARKS[kind];
@@ -111,9 +141,7 @@ export function createSetupRenderer(stream, env) {
111
141
  .map((line) => {
112
142
  const plain = visible(line);
113
143
  const indent = /^https?:\/\/\S+$/.test(plain) && stringWidth(plain) > width - col
114
- ? traits.unicode
115
- ? 3
116
- : 0
144
+ ? textCol
117
145
  : col;
118
146
  return prefix(indent) + line;
119
147
  });
@@ -122,13 +150,13 @@ export function createSetupRenderer(stream, env) {
122
150
  refresh();
123
151
  const lead = plan
124
152
  ? traits.unicode
125
- ? "│"
153
+ ? " "
126
154
  : " "
127
155
  : traits.unicode
128
156
  ? mark(kind)
129
157
  : mark(kind).padEnd(7);
130
158
  const titleWidth = stringWidth(visible(title));
131
- const label = `${lead}${traits.unicode ? " " : " "}${String(number || "").padStart(2)} ${title}`;
159
+ const label = `${prefix(rowCol)}${lead}${traits.unicode ? " " : " "}${String(number || "").padStart(2)} ${title}`;
132
160
  if (!outcome)
133
161
  return [label];
134
162
  if (width < 80 || titleWidth > 30)
@@ -214,6 +242,7 @@ export function createSetupRenderer(stream, env) {
214
242
  clear();
215
243
  abandon();
216
244
  };
245
+ let branded = false;
217
246
  const r = {
218
247
  get traits() {
219
248
  refresh();
@@ -221,11 +250,31 @@ export function createSetupRenderer(stream, env) {
221
250
  },
222
251
  intro(title) {
223
252
  refresh();
224
- ordinary([traits.unicode ? `┌ ${style("bold", title)}` : title]);
253
+ ordinary([prefix(textCol) + style("bold", title)]);
254
+ },
255
+ brand(program, version) {
256
+ if (branded)
257
+ return;
258
+ branded = true;
259
+ refresh();
260
+ if (!traits.color || traits.columns < 50) {
261
+ ordinary([
262
+ `${prefix(textCol)}Catalyst Cloud ${program}${version ? ` ${version}` : ""}`,
263
+ ]);
264
+ return;
265
+ }
266
+ const [one, two] = pixelNucleusLines(env);
267
+ ordinary([
268
+ `${prefix(textCol)}${one} ${style("bold", "Catalyst Cloud")}`,
269
+ `${prefix(textCol)}${two} ${style("dim", `${program}${version ? ` · ${version}` : ""}`)}`,
270
+ ]);
271
+ },
272
+ tracker(parts) {
273
+ ordinary(parts.flatMap((p) => stepRows(p.mark, p.number, p.title, p.note)));
225
274
  },
226
275
  outro(text) {
227
276
  abandon();
228
- write(traits.unicode ? [`│`, `└ ${text}`] : ["", text]);
277
+ write(["", prefix(textCol) + text]);
229
278
  },
230
279
  plan(number, title, text) {
231
280
  ordinary(stepRows("run", number, title, text, true));
@@ -318,14 +367,22 @@ export function createSetupRenderer(stream, env) {
318
367
  },
319
368
  heading(text) {
320
369
  abandon();
321
- write(traits.unicode ? ["│", `├─ ${style("bold", text)}`] : ["", text]);
370
+ write(["", prefix(textCol) + style("bold", text)]);
322
371
  },
323
372
  line(text) {
324
- ordinary(rows(text, traits.unicode ? 3 : 0));
373
+ ordinary(rows(text, textCol));
374
+ },
375
+ item(label, text) {
376
+ const col = textCol + stringWidth(label);
377
+ const lines = rows(text, col);
378
+ const hang = prefix(col);
379
+ ordinary(lines[0].startsWith(hang)
380
+ ? [prefix(textCol) + label + lines[0].slice(col), ...lines.slice(1)]
381
+ : [prefix(textCol) + label.trimEnd(), ...lines]);
325
382
  },
326
383
  blank() {
327
384
  refresh();
328
- ordinary([rail]);
385
+ ordinary([""]);
329
386
  },
330
387
  link(url) {
331
388
  return traits.links
package/dist/setup.js CHANGED
@@ -19,6 +19,7 @@ import { Readable, Writable } from "node:stream";
19
19
  import { VERB_USAGE } from "./args.js";
20
20
  import { plainSetupLine, SetupEventStream, } from "./setup-events.js";
21
21
  import { createSetupRenderer, } from "./setup-render.js";
22
+ import { setupPartHeading } from "./setup-onboard-copy.js";
22
23
  const MACHINE_TITLES = {
23
24
  1: "Install the catalyst command",
24
25
  2: "Add the Catalyst skills",
@@ -107,7 +108,7 @@ function planRenderer(r) {
107
108
  },
108
109
  more(text) {
109
110
  if (!hidden)
110
- r.line(` ${text}`);
111
+ r.line(` ${text}`);
111
112
  },
112
113
  close() {
113
114
  open = false;
@@ -238,7 +239,9 @@ async function runEngine(engine, args, flags, ctx, deps) {
238
239
  }
239
240
  if (!deps.onboard ||
240
241
  !["What’s left", "What\'s left"].includes(event.text))
241
- r.heading(event.text === "Setting up" ? "This computer" : event.text);
242
+ r.heading(event.text === "Setting up"
243
+ ? setupPartHeading(1, r.traits.unicode)
244
+ : event.text);
242
245
  return;
243
246
  case "plan":
244
247
  plan.plan(event.number, event.title, event.text);
@@ -399,7 +402,7 @@ async function runEngine(engine, args, flags, ctx, deps) {
399
402
  return await deps.onboard(r, flags);
400
403
  }
401
404
  catch {
402
- errors.line("catalyst setup: onboarding could not finish. Run catalyst onboard to try again.");
405
+ errors.line("catalyst setup: onboarding could not finish. Run catalyst setup to try again.");
403
406
  if (flags.json)
404
407
  deps.writeJson(`${JSON.stringify({ schema: "catalyst-install-last-run/1", state: "failed", exitCode: 10, steps: [] })}\n`);
405
408
  return 10;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/cli",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "Set up Catalyst Cloud, check your tickets and manage your account from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,7 +4,7 @@ description: >-
4
4
  Catalyst's GitHub: show a ticket's pull request with its checks, reviews and review threads, say whether it is mergeable under this repository's policy, and explain what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). Use when someone asks "show me the PR", "what are the checks saying", "why hasn't it merged", "what does the queue need", "what does that label mean", or wants the branch and merge conventions Catalyst follows.
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.2 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.3 — written in this repository for customer accounts -->
8
8
 
9
9
  # Catalyst's GitHub
10
10
 
@@ -4,7 +4,7 @@ description: >-
4
4
  Catalyst's view of Linear on the customer's own cloud account. Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the cloud API by default, with local replica reads available as an explicit opt-in, always naming the source; searches tickets, PRs, projects and initiatives; writes comments, card moves, labels and new tickets through the account's agent proxy as the app actor, with every route, stage id, label id and marker read from the account's contract. Knows what a ticket accumulates as Catalyst works it (phase-outcome comments, the document per phase, the agent session, the labels, the single blocks relation, the bookkeeping marker, the eyes acknowledgement). Use when a person says "show me the ticket", "what did Catalyst write on it", "comment on it", "move it", "label it" or "file a ticket". Not for raising a decision for a human (what-needs-me) and not for pull requests (catalyst-github).
5
5
  allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
6
6
  ---
7
- <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.2 — written in this repository for customer accounts -->
7
+ <!-- vendored-from: @catalyst-cloud/catalyst-skills@0.15.3 — written in this repository for customer accounts -->
8
8
 
9
9
  # Catalyst Linear
10
10