@catalyst-cloud/cli 0.14.10 → 0.15.0

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 (50) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +41 -0
  3. package/dist/args.js +34 -2
  4. package/dist/capabilities.js +2 -1
  5. package/dist/cli-update-notice.js +76 -0
  6. package/dist/cli.js +263 -101
  7. package/dist/oauth.js +48 -18
  8. package/dist/onboard-accounts.js +34 -5
  9. package/dist/onboard-automation-management.js +27 -9
  10. package/dist/onboard-capacity.js +10 -1
  11. package/dist/onboard-existing.js +132 -31
  12. package/dist/onboard-github.js +171 -6
  13. package/dist/onboard-headless.js +647 -0
  14. package/dist/onboard-interrupts.js +41 -0
  15. package/dist/onboard-next.js +215 -0
  16. package/dist/onboard-permissions.js +100 -0
  17. package/dist/onboard-personal.js +46 -10
  18. package/dist/onboard-runner-cloud.js +213 -0
  19. package/dist/onboard-runner-egress.js +67 -0
  20. package/dist/onboard-runner.js +780 -0
  21. package/dist/onboard-runtime.js +48 -8
  22. package/dist/onboard-standalone-copy.js +110 -0
  23. package/dist/onboard-team-create.js +267 -0
  24. package/dist/onboard-ui.js +623 -29
  25. package/dist/onboard-workflow.js +311 -168
  26. package/dist/onboard-workspace.js +52 -5
  27. package/dist/onboard.js +359 -212
  28. package/dist/setup-events.js +100 -0
  29. package/dist/setup-onboard-copy.js +438 -0
  30. package/dist/setup-prompt-frame.js +94 -0
  31. package/dist/setup-prompts.js +108 -0
  32. package/dist/setup-render.js +339 -0
  33. package/dist/setup-signin.js +187 -0
  34. package/dist/setup.js +426 -0
  35. package/package.json +7 -3
  36. package/skills/catalyst-github/SKILL.md +1 -1
  37. package/skills/catalyst-linear/SKILL.md +1 -1
  38. package/skills/catalyst-onboard/SKILL.md +3 -2
  39. package/skills/run-this-project/SKILL.md +1 -1
  40. package/skills/unstick/SKILL.md +1 -1
  41. package/skills/what-needs-me/SKILL.md +1 -1
  42. package/skills/what-this-repo-needs/SKILL.md +1 -1
  43. package/skills/whats-happening/SKILL.md +1 -1
  44. package/vendor/README.md +4 -0
  45. package/vendor/self-host/compose.yaml +110 -0
  46. package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.service +14 -0
  47. package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.timer +10 -0
  48. package/vendor/self-host/linux-session-egress/catalyst-session-egress.service +19 -0
  49. package/vendor/self-host/linux-session-egress/session-egress.py +291 -0
  50. package/vendor/self-host/provenance.json +11 -0
@@ -1,11 +1,36 @@
1
+ import { COMPUTER_CHECKS, standalonePlan, standalonePlanNotes, pendingContinuation, } from "./onboard-standalone-copy.js";
2
+ import { trackedPrompt } from "./setup-prompt-frame.js";
3
+ import { createOnboardInterrupts } from "./onboard-interrupts.js";
4
+ import { setupStepView, CONNECTION_REPAIR_REASONS, setupFinalScreen, setupBrowserInstruction, SETUP_NUMBERS, } from "./setup-onboard-copy.js";
1
5
  import { createOnboardProgress, } from "./onboard-progress.js";
2
- import { ONBOARD_STEPS, ONBOARD_TITLES, onboardIdentityLines, onboardNextActions, onboardReadyForWork, onboardStepAction, } from "./onboard.js";
6
+ import { CREATE_TEAM_CHOICE, suggestTeamKey, teamKeyProblem, teamNameProblem, } from "./onboard-team-create.js";
7
+ import { ONBOARD_STEPS, ONBOARD_TITLES, stepSatisfied, onboardIdentityLines, onboardNextActions, onboardReadyForWork, onboardStepAction, } from "./onboard.js";
8
+ import { onboardStepDetail, onboardReasonText } from "./onboard-next.js";
3
9
  export function shouldUseOnboardUi(args, stdoutTty) {
4
10
  return (stdoutTty &&
5
11
  !args.json &&
6
12
  args.flags.yes !== true &&
7
13
  args.flags["dry-run"] !== true);
8
14
  }
15
+ export const STEP_NUMBERS = {
16
+ signin: 4,
17
+ housekeeping: 5,
18
+ "linear.workspace": 6,
19
+ "linear.personal": 7,
20
+ "linear.team": 8,
21
+ "linear.adopt": 9,
22
+ "linear.automations": 10,
23
+ "github.install": 11,
24
+ "github.personal": 12,
25
+ "github.repos": 13,
26
+ projects: 13,
27
+ accounts: 14,
28
+ capacity: 15,
29
+ runner: 15,
30
+ settings: 16,
31
+ values: 16,
32
+ "first-ticket": 17,
33
+ };
9
34
  export function createClackOnboardUi(prompts, streams, deps = {}) {
10
35
  const abort = new AbortController();
11
36
  const options = { ...streams, signal: abort.signal };
@@ -13,50 +38,237 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
13
38
  const spin = deps.progress ??
14
39
  createOnboardProgress(streams.output, abort.signal, () => abort.abort());
15
40
  const signals = deps.signals ?? process;
16
- const interrupt = () => abort.abort();
41
+ const interrupts = createOnboardInterrupts(abort);
42
+ const handlers = {
43
+ SIGINT: () => interrupts.interrupt("SIGINT"),
44
+ SIGTERM: () => interrupts.interrupt("SIGTERM"),
45
+ SIGHUP: () => interrupts.interrupt("SIGHUP"),
46
+ };
17
47
  for (const event of ["SIGINT", "SIGTERM", "SIGHUP"])
18
- signals.on(event, interrupt);
48
+ signals.on(event, handlers[event]);
19
49
  let active = false;
20
- let introduced = false;
50
+ let introduced = deps.introduced === true;
51
+ let currentStep;
52
+ const renderer = deps.renderer;
53
+ const interactive = deps.interactive !== false;
21
54
  let group;
55
+ let browserOpened = true;
56
+ let questionOpen = false;
57
+ let signinShown = false;
58
+ let stagedSigninApproved = false;
59
+ let signinStarted = 0;
60
+ const machineChecks = new Map();
61
+ let machineScope = COMPUTER_CHECKS;
62
+ let machineBlocked = false;
63
+ const standaloneComputer = (id) => !!renderer &&
64
+ !deps.consentGiven &&
65
+ !!id &&
66
+ COMPUTER_CHECKS.some((check) => check === id);
67
+ const hidden = (id) => (!!renderer && id === "ready") ||
68
+ (!!renderer &&
69
+ !!id &&
70
+ ((id === "signin" && stagedSigninApproved) ||
71
+ (deps.consentGiven === true &&
72
+ [
73
+ "machine",
74
+ "cli",
75
+ "skills",
76
+ "legacy",
77
+ "signin",
78
+ "daemon",
79
+ "housekeeping",
80
+ "ready",
81
+ ].includes(id))) &&
82
+ !(id === "signin" && signinShown));
83
+ const title = (id) => setupStepView({ id, state: "running" }).title;
22
84
  const stop = () => {
23
85
  if (active) {
24
- spin.stop();
86
+ if (renderer)
87
+ renderer.suspendLive();
88
+ else
89
+ spin.stop();
25
90
  active = false;
26
91
  }
27
92
  };
28
93
  const message = (text) => {
94
+ if (renderer &&
95
+ (currentStep === "signin" || currentStep === undefined) &&
96
+ !signinShown &&
97
+ /(?:visit:\s*https?:|enter.*(?:code|[A-Z0-9]{4}-[A-Z0-9]{4})|https?:\/\/.*device)/i.test(text)) {
98
+ const reveal = currentStep === undefined || hidden(currentStep);
99
+ if (currentStep === undefined) {
100
+ currentStep = "signin";
101
+ signinStarted = Date.now();
102
+ interrupts.begin("signin");
103
+ }
104
+ signinShown = true;
105
+ if (reveal) {
106
+ group = "This computer";
107
+ renderer.heading(group);
108
+ renderer.begin(4, title("signin"), "signing in…");
109
+ active = true;
110
+ }
111
+ }
112
+ if (hidden(currentStep))
113
+ return;
114
+ if (renderer &&
115
+ currentStep === "linear.adopt" &&
116
+ text.endsWith("already has every state and label"))
117
+ return;
118
+ if (renderer) {
119
+ if (currentStep &&
120
+ setupBrowserInstruction(currentStep, deps.baseUrl?.())) {
121
+ if (/no browser opened|could not open a browser/i.test(text))
122
+ browserOpened = false;
123
+ return;
124
+ }
125
+ if (currentStep === "capacity")
126
+ return;
127
+ renderer.detail(text);
128
+ return;
129
+ }
29
130
  stop();
30
131
  prompts.log.message(text, { output: streams.output });
31
132
  };
32
133
  const start = (text) => {
33
134
  stop();
34
135
  if (!abort.signal.aborted) {
35
- spin.start(text);
136
+ if (hidden(currentStep))
137
+ return;
138
+ if (renderer && currentStep)
139
+ renderer.begin(SETUP_NUMBERS[currentStep] ?? 0, title(currentStep), text === ONBOARD_TITLES[currentStep] ? "checking…" : text);
140
+ else if (renderer)
141
+ renderer.line(text);
142
+ else
143
+ spin.start(text);
144
+ active = true;
145
+ }
146
+ };
147
+ const question = () => {
148
+ if (renderer && currentStep && !questionOpen) {
149
+ stop();
150
+ renderer.begin(SETUP_NUMBERS[currentStep] ?? 0, title(currentStep), "", "ask");
151
+ active = true;
152
+ questionOpen = true;
153
+ }
154
+ };
155
+ const select = async (opts, ownFrame = false) => {
156
+ if (!renderer)
157
+ return prompts.select(opts);
158
+ const detached = ownFrame || !currentStep;
159
+ if (detached) {
160
+ stop();
161
+ renderer.begin(0, "Your choice", "", "ask");
36
162
  active = true;
37
163
  }
164
+ else
165
+ question();
166
+ const result = await trackedPrompt(streams.output, (output) => prompts.select({ ...opts, output }));
167
+ renderer.promptRows(result.rows);
168
+ questionOpen = false;
169
+ if (detached)
170
+ stop();
171
+ return result.answer;
172
+ };
173
+ const text = async (opts) => {
174
+ if (!prompts.text)
175
+ return null;
176
+ if (!renderer)
177
+ return prompts.text(opts);
178
+ question();
179
+ const result = await trackedPrompt(streams.output, (output) => prompts.text({ ...opts, output }));
180
+ renderer.promptRows(result.rows);
181
+ questionOpen = false;
182
+ return result.answer;
38
183
  };
39
- return {
184
+ const multiselect = async (opts) => {
185
+ if (!prompts.multiselect)
186
+ return null;
187
+ if (!renderer)
188
+ return prompts.multiselect(opts);
189
+ question();
190
+ const result = await trackedPrompt(streams.output, (output) => prompts.multiselect({ ...opts, output }));
191
+ renderer.promptRows(result.rows);
192
+ questionOpen = false;
193
+ return result.answer;
194
+ };
195
+ let summaryShown = false;
196
+ const summary = (journal) => {
197
+ const screen = setupFinalScreen(journal, deps.baseUrl?.(), abort.signal.aborted);
198
+ if (!renderer)
199
+ return screen;
200
+ renderer.suspendLive();
201
+ active = false;
202
+ renderer.heading(screen.heading);
203
+ if (screen.paused)
204
+ renderer.line("Your progress is saved.");
205
+ if (screen.readinessCause)
206
+ renderer.line(screen.readinessCause);
207
+ if (screen.actions.length) {
208
+ renderer.line(`${screen.actions.length} ${screen.actions.length === 1 ? "thing needs" : "things need"} you:`);
209
+ screen.actions.forEach((action, i) => renderer.line(`${i + 1}. ${action.text}`));
210
+ if (screen.more)
211
+ renderer.line(`and ${screen.more} more after these`);
212
+ const continuation = pendingContinuation(journal);
213
+ if (continuation)
214
+ renderer.line(continuation);
215
+ }
216
+ else if (screen.heading === "Setup complete")
217
+ renderer.line("Catalyst is ready to work on your team's tickets.");
218
+ if (deps.logPath?.())
219
+ renderer.line(`Full log: ${deps.logPath()}`);
220
+ return screen;
221
+ };
222
+ const ui = {
40
223
  signal: abort.signal,
41
- plan(journal, identity) {
224
+ get stepSignal() {
225
+ return interrupts.signal;
226
+ },
227
+ interactive,
228
+ handlesSignals: true,
229
+ plan(journal, identity, planOptions) {
230
+ if (deps.consentGiven)
231
+ return;
232
+ machineScope = (planOptions?.scope ?? ONBOARD_STEPS).filter((id) => COMPUTER_CHECKS.some((check) => check === id));
233
+ machineChecks.clear();
234
+ machineBlocked = false;
42
235
  stop();
43
236
  if (!introduced) {
44
- prompts.intro("Catalyst setup", { output: streams.output });
237
+ if (renderer)
238
+ renderer.intro("Catalyst setup");
239
+ else
240
+ prompts.intro("Catalyst setup", { output: streams.output });
45
241
  introduced = true;
46
242
  }
47
243
  if (identity !== undefined)
48
244
  for (const line of onboardIdentityLines(identity))
49
245
  message(line);
246
+ if (renderer) {
247
+ renderer.heading("The plan");
248
+ let planGroup;
249
+ for (const row of standalonePlan(journal, planOptions?.scope, planOptions?.runner)) {
250
+ if (row.group !== planGroup) {
251
+ renderer.blank();
252
+ renderer.line(renderer.bold(row.group));
253
+ planGroup = row.group;
254
+ }
255
+ renderer.plan(row.number, row.title, row.detail);
256
+ }
257
+ renderer.line(standalonePlanNotes(planOptions?.localSync ?? journal.localSync === true));
258
+ return;
259
+ }
50
260
  const steps = new Map(journal.steps.map((step) => [step.id, step]));
51
261
  message(ONBOARD_STEPS.map((id) => `· ${ONBOARD_TITLES[id]}${steps.get(id)?.state === "done" ? " (recheck)" : ""}`).join("\n"));
52
262
  message("Use cloud reads by default. Local sync is optional for SQL or sustained local reads.");
53
263
  message("Setup may check one stored Claude account using a one-token provider request. This may use Claude quota. It does not refresh Codex credentials.");
54
264
  },
55
265
  async confirmPlan(localSync, signin = "unavailable") {
266
+ if (deps.consentGiven || !interactive)
267
+ return { proceed: true, localSync };
56
268
  stop();
57
- const answer = await prompts.select({
269
+ const answer = await select({
58
270
  ...options,
59
- message: "Continue with this plan?",
271
+ message: "Continue setup?",
60
272
  initialValue: signin === "required" ? "signin" : localSync ? "local" : "cloud",
61
273
  options: [
62
274
  ...(signin === "required"
@@ -64,7 +276,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
64
276
  : [
65
277
  {
66
278
  value: "cloud",
67
- label: "Continue using cloud reads",
279
+ label: "Yes, continue",
68
280
  hint: "default",
69
281
  },
70
282
  { value: "local", label: "Continue and set up local sync" },
@@ -92,18 +304,35 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
92
304
  ...(answer === "signin" ? { signin: true } : {}),
93
305
  };
94
306
  },
95
- async chooseTeam(teams) {
307
+ async chooseTeam(teams, create) {
96
308
  stop();
97
- if (!teams.length || abort.signal.aborted)
309
+ // Creating needs a name prompt; a port without one offers only the existing teams.
310
+ const offer = prompts.text ? create : undefined;
311
+ if ((!teams.length && !offer?.available) || abort.signal.aborted)
98
312
  return null;
99
- const answer = await prompts.select({
313
+ const answer = await select({
100
314
  ...options,
101
- message: "Which Linear team should this project use?",
102
- initialValue: teams[0].id,
103
- options: teams.map((team) => ({
104
- value: team.id,
105
- label: `${team.name || team.key || team.id}${team.key && team.name ? ` (${team.key})` : ""}`,
106
- })),
315
+ message: renderer
316
+ ? "Which Linear team should Catalyst work in?"
317
+ : "Which Linear team should this project use?",
318
+ initialValue: teams[0]?.id ?? CREATE_TEAM_CHOICE,
319
+ options: [
320
+ ...teams.map((team) => ({
321
+ value: team.id,
322
+ label: `${team.name || team.key || team.id}${team.key && team.name ? ` (${team.key})` : ""}`,
323
+ })),
324
+ ...(offer
325
+ ? [
326
+ {
327
+ value: CREATE_TEAM_CHOICE,
328
+ label: "Create a new Linear team…",
329
+ ...(offer.available
330
+ ? {}
331
+ : { hint: offer.reason, disabled: true }),
332
+ },
333
+ ]
334
+ : []),
335
+ ],
107
336
  });
108
337
  if (prompts.isCancel(answer)) {
109
338
  abort.abort();
@@ -111,13 +340,90 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
111
340
  }
112
341
  return typeof answer === "string" ? answer : null;
113
342
  },
343
+ async confirmWorkflowAdoption(team, lines) {
344
+ stop();
345
+ if (abort.signal.aborted)
346
+ return false;
347
+ if (renderer)
348
+ question();
349
+ message([`Catalyst workflow plan for ${team}:`, ...lines].join("\n"));
350
+ const answer = await select({
351
+ ...options,
352
+ message: renderer
353
+ ? `Apply these changes to ${team}?`
354
+ : `Apply this to ${team}?`,
355
+ initialValue: "apply",
356
+ options: [
357
+ { value: "apply", label: "Yes, apply it" },
358
+ { value: "skip", label: "No, not now" },
359
+ ],
360
+ });
361
+ if (prompts.isCancel(answer)) {
362
+ abort.abort();
363
+ return false;
364
+ }
365
+ return answer === "apply";
366
+ },
367
+ ...(prompts.text
368
+ ? {
369
+ async nameNewTeam(question) {
370
+ stop();
371
+ if (abort.signal.aborted)
372
+ return null;
373
+ const name = await text({
374
+ ...options,
375
+ message: question.problem
376
+ ? `${question.problem}\nName for the new Linear team`
377
+ : "Name for the new Linear team",
378
+ ...(question.name ? { initialValue: question.name } : {}),
379
+ placeholder: "Mobile app",
380
+ validate: teamNameProblem,
381
+ });
382
+ if (prompts.isCancel(name) || typeof name !== "string") {
383
+ abort.abort();
384
+ return null;
385
+ }
386
+ const key = await text({
387
+ ...options,
388
+ message: "Team key (the prefix on its tickets, like MOB-12)",
389
+ initialValue: question.key &&
390
+ (!question.name || question.name === name.trim())
391
+ ? question.key
392
+ : suggestTeamKey(name),
393
+ validate: teamKeyProblem,
394
+ });
395
+ if (prompts.isCancel(key) || typeof key !== "string") {
396
+ abort.abort();
397
+ return null;
398
+ }
399
+ const answer = {
400
+ name: name.trim(),
401
+ key: key.trim().toUpperCase(),
402
+ };
403
+ const confirm = await select({
404
+ ...options,
405
+ message: `Create the Linear team ${answer.name} (${answer.key}) and set up Catalyst's workflow on it?`,
406
+ initialValue: "create",
407
+ options: [
408
+ { value: "create", label: "Create the team" },
409
+ { value: "back", label: "Go back to the team list" },
410
+ ],
411
+ });
412
+ if (prompts.isCancel(confirm)) {
413
+ abort.abort();
414
+ return null;
415
+ }
416
+ return confirm === "create" ? answer : null;
417
+ },
418
+ }
419
+ : {}),
114
420
  ...(prompts.multiselect
115
421
  ? {
116
422
  async chooseRepositories(repositories) {
117
423
  stop();
118
424
  if (!repositories.length || abort.signal.aborted)
119
425
  return null;
120
- const answer = await prompts.multiselect({
426
+ const answer = await multiselect({
121
427
  ...options,
122
428
  message: "Which repositories should use this Linear team?",
123
429
  required: true,
@@ -138,7 +444,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
138
444
  stop();
139
445
  if (!summaries.length || abort.signal.aborted)
140
446
  return "cancel";
141
- const answer = await prompts.select({
447
+ const answer = await select({
142
448
  ...options,
143
449
  message: "Review settings for all selected repositories. Keep private copies of new drafts for review?",
144
450
  initialValue: "keep",
@@ -161,7 +467,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
161
467
  stop();
162
468
  if (!repositories.length || abort.signal.aborted)
163
469
  return null;
164
- const answer = await prompts.select({
470
+ const answer = await select({
165
471
  ...options,
166
472
  message: "Which repository should start this project?",
167
473
  initialValue: `${repositories[0].owner}/${repositories[0].name}`,
@@ -176,7 +482,98 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
176
482
  }
177
483
  return typeof answer === "string" ? answer : null;
178
484
  },
485
+ async chooseRunner() {
486
+ stop();
487
+ if (abort.signal.aborted)
488
+ return null;
489
+ const answer = await select({
490
+ ...options,
491
+ message: "Run Catalyst's work on this machine?",
492
+ initialValue: "no",
493
+ options: [
494
+ {
495
+ value: "no",
496
+ label: "No",
497
+ hint: "default; work runs on the workspace's runner hosts",
498
+ },
499
+ {
500
+ value: "yes",
501
+ label: "Yes, start a Catalyst runner here with Docker",
502
+ },
503
+ ],
504
+ });
505
+ if (prompts.isCancel(answer)) {
506
+ abort.abort();
507
+ return null;
508
+ }
509
+ return answer === "yes";
510
+ },
511
+ stagedSigninEnd(state, cause) {
512
+ if (!renderer)
513
+ return false;
514
+ if (state === "done")
515
+ stagedSigninApproved = true;
516
+ if (currentStep === "signin" && signinShown)
517
+ renderer.step(state === "done" ? "done" : state === "failed" ? "fail" : "act", 4, title("signin"), state === "done"
518
+ ? "approved"
519
+ : state === "failed"
520
+ ? "could not verify this account"
521
+ : "not finished");
522
+ else
523
+ renderer.suspendLive();
524
+ active = false;
525
+ currentStep = undefined;
526
+ signinShown = false;
527
+ if (state !== "done") {
528
+ if (cause)
529
+ renderer.line(cause.charAt(0).toUpperCase() + cause.slice(1));
530
+ renderer.heading(abort.signal.aborted ? "Setup paused" : "Setup is not ready yet");
531
+ if (!cause?.includes("Your saved connection was not changed."))
532
+ renderer.line("Your saved connection was not changed.");
533
+ renderer.outro("Next: run catalyst onboard to sign in again.");
534
+ }
535
+ return true;
536
+ },
179
537
  stepStart(id) {
538
+ interrupts.begin(id);
539
+ browserOpened = true;
540
+ questionOpen = false;
541
+ currentStep = id;
542
+ if (id === "signin") {
543
+ signinShown = false;
544
+ signinStarted = Date.now();
545
+ }
546
+ if (renderer) {
547
+ if (standaloneComputer(id)) {
548
+ if (!machineScope.length)
549
+ return;
550
+ if (machineChecks.size === 0 && !machineBlocked) {
551
+ renderer.heading("This computer");
552
+ group = "This computer";
553
+ renderer.begin(1, "This computer", "checking…");
554
+ active = true;
555
+ }
556
+ return;
557
+ }
558
+ if (hidden(id))
559
+ return;
560
+ if (id === "projects" || id === "values")
561
+ return;
562
+ const next = !deps.consentGiven &&
563
+ ["signin", "daemon", "housekeeping", "ready"].includes(id)
564
+ ? "This computer"
565
+ : id.startsWith("linear.")
566
+ ? "Linear"
567
+ : id.startsWith("github.")
568
+ ? "GitHub"
569
+ : "Work";
570
+ if (next !== group) {
571
+ renderer.heading(next);
572
+ group = next;
573
+ }
574
+ start(ONBOARD_TITLES[id]);
575
+ return;
576
+ }
180
577
  const next = ["machine", "cli", "skills", "legacy"].includes(id)
181
578
  ? "This computer"
182
579
  : id === "signin"
@@ -185,7 +582,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
185
582
  ? "Connections"
186
583
  : ["projects", "accounts", "settings", "values"].includes(id)
187
584
  ? "Project setup"
188
- : ["capacity", "daemon", "housekeeping"].includes(id)
585
+ : ["capacity", "runner", "daemon", "housekeeping"].includes(id)
189
586
  ? "Runner and services"
190
587
  : "Work and readiness";
191
588
  if (next !== group) {
@@ -195,8 +592,62 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
195
592
  start(ONBOARD_TITLES[id]);
196
593
  },
197
594
  stepEnd(step, journal) {
595
+ if (renderer) {
596
+ if (standaloneComputer(step.id)) {
597
+ machineChecks.set(step.id, step);
598
+ if (step.state === "failed" || step.state === "waiting") {
599
+ stop();
600
+ machineBlocked = true;
601
+ const view = setupStepView(step, journal);
602
+ renderer.step(view.mark, view.number, view.title, view.outcome);
603
+ renderer.detail(deps.logPath?.()
604
+ ? "Check the full log for the cause, then try this step again."
605
+ : "Finish the action below, then check again.");
606
+ active = false;
607
+ }
608
+ else if (!machineBlocked &&
609
+ machineScope.length > 0 &&
610
+ machineScope.every((id) => stepSatisfied(machineChecks.get(id)))) {
611
+ renderer.step("done", 1, "This computer", machineScope.length === COMPUTER_CHECKS.length
612
+ ? machineChecks.get("legacy")?.state === "done"
613
+ ? "the command and skills are ready; no earlier install remains, data folders kept"
614
+ : "the command and skills are ready"
615
+ : "the selected checks passed");
616
+ active = false;
617
+ }
618
+ return;
619
+ }
620
+ if (step.id === "ready" || (hidden(step.id) && step.state !== "failed"))
621
+ return;
622
+ if ((step.id === "github.repos" || step.id === "settings") &&
623
+ step.state === "done")
624
+ return;
625
+ if ((step.id === "projects" || step.id === "values") &&
626
+ journal?.steps.find((s) => s.id === (step.id === "projects" ? "github.repos" : "settings"))?.state !== "done")
627
+ return;
628
+ const view = setupStepView(step, journal);
629
+ if (hidden(step.id)) {
630
+ renderer.heading("This computer");
631
+ group = "This computer";
632
+ }
633
+ renderer.step(view.mark, view.number, view.title, view.outcome);
634
+ const granted = deps.verbose ? onboardStepDetail(step) : undefined;
635
+ if (granted)
636
+ renderer.detail(granted);
637
+ if (deps.verbose && CONNECTION_REPAIR_REASONS.has(step.reason ?? ""))
638
+ renderer.detail(onboardReasonText(step, { baseUrl: deps.baseUrl?.(), journal }));
639
+ if (view.mark === "fail")
640
+ renderer.detail(deps.logPath?.()
641
+ ? "Check the full log for the cause, then try this step again."
642
+ : "Finish the action below, then check again.");
643
+ active = false;
644
+ return;
645
+ }
198
646
  stop();
199
- const text = `${ONBOARD_TITLES[step.id]}${step.reason ? `: ${onboardStepAction(journal, step, deps.baseUrl?.())}` : ""}`;
647
+ const detail = step.reason
648
+ ? onboardStepAction(journal, step, deps.baseUrl?.())
649
+ : undefined;
650
+ const text = `${ONBOARD_TITLES[step.id]}${detail ? `: ${detail}` : ""}`;
200
651
  const kind = step.state === "done"
201
652
  ? "info"
202
653
  : step.state === "failed"
@@ -207,9 +658,72 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
207
658
  prompts.log[kind](`${step.state === "done" ? "✓ " : ""}${text}`, {
208
659
  output: streams.output,
209
660
  });
661
+ const granted = deps.verbose ? onboardStepDetail(step) : undefined;
662
+ if (granted)
663
+ prompts.log.message(granted, { output: streams.output });
210
664
  },
211
665
  message,
666
+ async checkAgain(journal) {
667
+ if (!renderer ||
668
+ !interactive ||
669
+ abort.signal.aborted ||
670
+ !setupFinalScreen(journal, deps.baseUrl?.()).actions.length)
671
+ return false;
672
+ active = false;
673
+ summary(journal);
674
+ summaryShown = true;
675
+ const answer = await select({
676
+ ...options,
677
+ message: "Done with these?",
678
+ initialValue: "again",
679
+ options: [
680
+ { value: "again", label: "Check again now" },
681
+ { value: "stop", label: "Stop here" },
682
+ ],
683
+ }, true);
684
+ if (prompts.isCancel(answer)) {
685
+ abort.abort();
686
+ return false;
687
+ }
688
+ if (answer !== "again")
689
+ return false;
690
+ summaryShown = false;
691
+ machineScope = COMPUTER_CHECKS.filter((id) => !stepSatisfied(journal.steps.find((step) => step.id === id)));
692
+ machineChecks.clear();
693
+ machineBlocked = false;
694
+ group = undefined;
695
+ renderer.heading("Checking again");
696
+ return true;
697
+ },
212
698
  finish(journal, only) {
699
+ if (renderer && only) {
700
+ stop();
701
+ renderer.heading(abort.signal.aborted
702
+ ? "Setup paused"
703
+ : journal.exit === 0
704
+ ? "Step complete"
705
+ : "Step needs you");
706
+ if (journal.exit === 0)
707
+ renderer.line(`${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.`);
708
+ else
709
+ for (const action of onboardNextActions(journal, deps.baseUrl?.(), only))
710
+ renderer.line(action);
711
+ renderer.outro("Next: run catalyst onboard");
712
+ return;
713
+ }
714
+ if (renderer) {
715
+ active = false;
716
+ const screen = summaryShown
717
+ ? setupFinalScreen(journal, deps.baseUrl?.(), abort.signal.aborted)
718
+ : summary(journal);
719
+ if (summaryShown && screen.paused) {
720
+ renderer.heading(screen.heading);
721
+ renderer.line("Your progress is saved.");
722
+ }
723
+ summaryShown = false;
724
+ renderer.outro(screen.next);
725
+ return;
726
+ }
213
727
  stop();
214
728
  let text = [
215
729
  "Setup still needs these steps:",
@@ -232,12 +746,80 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
232
746
  prompts.outro(text, { output: streams.output });
233
747
  },
234
748
  async wait(text, run) {
235
- start(text);
749
+ const browser = currentStep &&
750
+ renderer &&
751
+ setupBrowserInstruction(currentStep, deps.baseUrl?.(), currentStep !== "accounts" && browserOpened);
752
+ const signinWait = Boolean(renderer && currentStep === "signin" && signinShown);
753
+ const seconds = signinWait
754
+ ? Math.ceil((deps.signinTimeoutMs ?? 600000) / 1000)
755
+ : 600;
756
+ const started = signinWait ? signinStarted : Date.now();
757
+ const status = () => {
758
+ const remaining = Math.max(0, seconds - Math.floor((Date.now() - started) / 1000));
759
+ return `waiting for you · ${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, "0")} left`;
760
+ };
761
+ if (renderer && browser && currentStep) {
762
+ stop();
763
+ renderer.begin(SETUP_NUMBERS[currentStep] ?? 0, title(currentStep), renderer.traits.unicode
764
+ ? status()
765
+ : "waiting for you, up to 10 minutes");
766
+ if (browser.preparation)
767
+ renderer.detail(browser.preparation);
768
+ renderer.action(browser.instruction);
769
+ if (browser.url)
770
+ renderer.detail(renderer.link(browser.url));
771
+ renderer.detail(renderer.dim(interactive ? "Ctrl-C skips this step." : "Ctrl-C stops setup."));
772
+ active = true;
773
+ }
774
+ else if (renderer && signinWait) {
775
+ if (renderer.traits.unicode) {
776
+ renderer.update(status());
777
+ renderer.detail("Ctrl-C stops setup.");
778
+ }
779
+ else {
780
+ const minutes = Math.max(0, Math.ceil((started + seconds * 1000 - Date.now()) / 60000));
781
+ renderer.detail(`waiting for you${minutes ? `, up to ${minutes} minute${minutes === 1 ? "" : "s"}` : ""}. Ctrl-C stops setup.`);
782
+ }
783
+ }
784
+ else
785
+ start(text);
786
+ const timer = renderer && (browser || signinWait)
787
+ ? renderer.traits.unicode
788
+ ? setInterval(() => renderer.update(status()), 1000)
789
+ : setInterval(() => {
790
+ const minutes = Math.max(0, Math.ceil((seconds * 1000 - (Date.now() - started)) / 60000));
791
+ if (minutes > 0)
792
+ renderer.detail(`Still waiting, ${minutes} minute${minutes === 1 ? "" : "s"} left.`);
793
+ }, 60000)
794
+ : undefined;
795
+ timer?.unref();
796
+ interrupts.waiting(interactive);
236
797
  try {
237
798
  return await run();
238
799
  }
239
800
  finally {
240
- stop();
801
+ if (timer)
802
+ clearInterval(timer);
803
+ interrupts.waiting(false);
804
+ if (renderer &&
805
+ browser &&
806
+ interrupts.signal.aborted &&
807
+ !abort.signal.aborted) {
808
+ renderer.replaceLastDetail(renderer.dim("Press Ctrl-C again to stop setup."));
809
+ await new Promise((resolve) => {
810
+ const done = () => {
811
+ clearTimeout(timeout);
812
+ abort.signal.removeEventListener("abort", done);
813
+ resolve();
814
+ };
815
+ const timeout = setTimeout(done, 2000);
816
+ abort.signal.addEventListener("abort", done, { once: true });
817
+ if (abort.signal.aborted)
818
+ done();
819
+ });
820
+ }
821
+ if (!renderer)
822
+ stop();
241
823
  }
242
824
  },
243
825
  dispose() {
@@ -248,12 +830,24 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
248
830
  abort.abort();
249
831
  try {
250
832
  spin.dispose();
833
+ if (deps.disposeRenderer)
834
+ renderer?.dispose();
251
835
  }
252
836
  finally {
253
837
  for (const event of ["SIGINT", "SIGTERM", "SIGHUP"])
254
- signals.off(event, interrupt);
838
+ signals.off(event, handlers[event]);
255
839
  }
256
840
  }
257
841
  },
258
842
  };
843
+ if (!interactive) {
844
+ delete ui.chooseTeam;
845
+ delete ui.nameNewTeam;
846
+ delete ui.chooseRepositories;
847
+ delete ui.chooseFirstRepository;
848
+ delete ui.reviewSettings;
849
+ delete ui.chooseRunner;
850
+ delete ui.confirmWorkflowAdoption;
851
+ }
852
+ return ui;
259
853
  }