@catalyst-cloud/cli 0.15.1 → 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.
- package/CHANGELOG.md +15 -0
- package/README.md +13 -4
- package/dist/args.js +1 -1
- package/dist/capabilities.js +3 -1
- package/dist/cli.js +25 -30
- package/dist/consent-browser.js +3 -1
- package/dist/oauth.js +1 -1
- package/dist/onboard-accounts.js +67 -171
- package/dist/onboard-checklist.js +309 -0
- package/dist/onboard-existing.js +1 -1
- package/dist/onboard-first-ticket.js +199 -0
- package/dist/onboard-github.js +13 -1
- package/dist/onboard-headless.js +6 -4
- package/dist/onboard-housekeeping.js +93 -0
- package/dist/onboard-next.js +124 -13
- package/dist/onboard-ready.js +61 -0
- package/dist/onboard-runtime.js +117 -26
- package/dist/onboard-standalone-copy.js +73 -66
- package/dist/onboard-ui.js +189 -81
- package/dist/onboard-values.js +47 -0
- package/dist/onboard.js +59 -18
- package/dist/ready-copy.js +325 -0
- package/dist/ready.js +35 -98
- package/dist/setup-onboard-copy.js +262 -41
- package/dist/setup-render.js +73 -14
- package/dist/setup.js +6 -3
- package/package.json +2 -2
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +3 -3
- package/skills/catalyst-onboard/references/replacing-a-credential.md +9 -15
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +4 -11
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +3 -2
- package/skills/run-this-project/SKILL.md +2 -2
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +2 -2
- package/skills/whats-happening/references/coding-accounts.md +1 -1
- 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
|
|
111
|
+
accounts: "Add an AI account",
|
|
112
|
+
housekeeping: "Daily update",
|
|
31
113
|
capacity: "Check runners",
|
|
32
|
-
values: "
|
|
114
|
+
values: "Check repository values",
|
|
33
115
|
};
|
|
34
116
|
const OPTIONAL = new Set([
|
|
35
117
|
"local_sync_not_selected",
|
|
@@ -47,6 +129,16 @@ export const CONNECTION_REPAIR_REASONS = new Set([
|
|
|
47
129
|
"github_app_permissions_unverified", "github_app_repository_access_unverified",
|
|
48
130
|
"linear_workspace_permissions_unverified", "personal_permissions_unverified",
|
|
49
131
|
]);
|
|
132
|
+
/** CTC-4680: the signed-in browser route that goes straight to GitHub's install page. The settings
|
|
133
|
+
* page it replaces opened Integrations and started nothing. */
|
|
134
|
+
export const GITHUB_INSTALL_START = "/connect/github/start";
|
|
135
|
+
/** CTC-4680 round 5: a browser wait that ran its whole window without the person finishing. */
|
|
136
|
+
export const TIMED_OUT_REASONS = new Set([
|
|
137
|
+
"consent_timeout",
|
|
138
|
+
"workspace_browser_unavailable",
|
|
139
|
+
"personal_browser_unavailable",
|
|
140
|
+
"github_installation_browser_unavailable",
|
|
141
|
+
]);
|
|
50
142
|
const clean = (s) => s.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 300);
|
|
51
143
|
function value(step, key) {
|
|
52
144
|
const v = step.evidence?.[key];
|
|
@@ -73,18 +165,23 @@ function succeeded(step) {
|
|
|
73
165
|
case "projects":
|
|
74
166
|
return "repositories are now Catalyst projects";
|
|
75
167
|
case "accounts":
|
|
76
|
-
return `${value(step, "provider") === "claude" ? "Claude" : value(step, "provider") === "codex" ? "Codex" : "
|
|
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";
|
|
77
171
|
case "capacity":
|
|
78
172
|
return typeof step.evidence?.remainingUnits === "number"
|
|
79
173
|
? `${step.evidence.remainingUnits} runner slots available`
|
|
80
174
|
: "runners can take work";
|
|
81
175
|
case "settings":
|
|
82
|
-
case "values":
|
|
83
176
|
return "settings reviewed";
|
|
84
|
-
case "
|
|
85
|
-
return
|
|
86
|
-
|
|
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.`
|
|
87
183
|
: "first ticket started";
|
|
184
|
+
}
|
|
88
185
|
case "daemon":
|
|
89
186
|
return "local sync is running";
|
|
90
187
|
case "runner":
|
|
@@ -104,6 +201,12 @@ function unfinished(step) {
|
|
|
104
201
|
return "could not verify the connection's permissions";
|
|
105
202
|
if (step.reason === "housekeeping_service_unverified")
|
|
106
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";
|
|
107
210
|
if (step.reason === "local_sync_not_selected")
|
|
108
211
|
return "off, reads come from the cloud";
|
|
109
212
|
if (step.reason === "runner_docker_missing")
|
|
@@ -112,15 +215,19 @@ function unfinished(step) {
|
|
|
112
215
|
return "an admin handles this step";
|
|
113
216
|
if (step.reason === "automation_management_unavailable")
|
|
114
217
|
return "could not read them; checked again before work starts";
|
|
115
|
-
if (step.reason === "interrupted")
|
|
218
|
+
if (step.reason === "interrupted" || step.reason === "first_ticket_skipped")
|
|
116
219
|
return "skipped for now";
|
|
220
|
+
if (TIMED_OUT_REASONS.has(step.reason ?? ""))
|
|
221
|
+
return "the link timed out";
|
|
222
|
+
if (step.reason === "github_installation_approval_pending")
|
|
223
|
+
return "waiting for an owner of your GitHub organization";
|
|
117
224
|
if (step.state === "failed")
|
|
118
225
|
return "could not finish this step";
|
|
119
226
|
switch (step.id) {
|
|
120
227
|
case "github.install":
|
|
121
228
|
return "not installed yet";
|
|
122
229
|
case "accounts":
|
|
123
|
-
return "no
|
|
230
|
+
return "no AI account ready yet";
|
|
124
231
|
case "linear.workspace":
|
|
125
232
|
case "linear.personal":
|
|
126
233
|
case "github.personal":
|
|
@@ -130,8 +237,13 @@ function unfinished(step) {
|
|
|
130
237
|
case "capacity":
|
|
131
238
|
return "no runner can take work yet";
|
|
132
239
|
case "settings":
|
|
133
|
-
case "values":
|
|
134
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";
|
|
135
247
|
default:
|
|
136
248
|
return step.state === "skipped" ? "not chosen" : "not done yet";
|
|
137
249
|
}
|
|
@@ -143,6 +255,7 @@ export function setupStepView(step, journal, required = false) {
|
|
|
143
255
|
: step.state === "failed"
|
|
144
256
|
? "fail"
|
|
145
257
|
: step.reason === "prerequisite_not_ready" ||
|
|
258
|
+
step.reason === "github_install_pending" ||
|
|
146
259
|
(step.state === "pending" && !step.reason)
|
|
147
260
|
? "later"
|
|
148
261
|
: !required &&
|
|
@@ -151,29 +264,118 @@ export function setupStepView(step, journal, required = false) {
|
|
|
151
264
|
: "act";
|
|
152
265
|
let outcome = step.state === "done" ? succeeded(step) : unfinished(step);
|
|
153
266
|
if (mark === "later") {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
267
|
+
const waitsOn = step.reason === "github_install_pending"
|
|
268
|
+
? ["github.install"]
|
|
269
|
+
: (ONBOARD_DEPENDENCIES[step.id] ?? []).filter((id) => {
|
|
157
270
|
const s = journal?.steps.find((s) => s.id === id);
|
|
158
271
|
return s?.state !== "done" && s?.state !== "skipped";
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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}`));
|
|
163
295
|
outcome =
|
|
164
296
|
step.state === "pending" && !step.reason
|
|
165
297
|
? "not checked yet"
|
|
166
|
-
:
|
|
167
|
-
? `after ${
|
|
298
|
+
: groups.length
|
|
299
|
+
? `after ${groups.length > 1 ? `${groups.slice(0, -1).join(", ")}, and ${groups.at(-1)}` : groups[0]}`
|
|
168
300
|
: "after an earlier step";
|
|
169
301
|
}
|
|
170
302
|
return {
|
|
171
|
-
number:
|
|
303
|
+
number: SETUP_PART_NUMBERS[step.id] ?? 0,
|
|
172
304
|
title: TITLES[step.id] ?? ONBOARD_TITLES[step.id],
|
|
173
305
|
mark,
|
|
174
306
|
outcome,
|
|
175
307
|
};
|
|
176
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
|
+
}
|
|
177
379
|
function page(base, path) {
|
|
178
380
|
try {
|
|
179
381
|
const url = new URL(path, base);
|
|
@@ -188,7 +390,9 @@ function page(base, path) {
|
|
|
188
390
|
}
|
|
189
391
|
}
|
|
190
392
|
function action(step, base, journal) {
|
|
191
|
-
if (CONNECTION_REPAIR_REASONS.has(step.reason ?? "")
|
|
393
|
+
if (CONNECTION_REPAIR_REASONS.has(step.reason ?? "") ||
|
|
394
|
+
step.reason === "github_installation_approval_pending" ||
|
|
395
|
+
step.reason === "member_scope")
|
|
192
396
|
return onboardReasonText(step, { baseUrl: base, journal });
|
|
193
397
|
const link = (path, verb) => {
|
|
194
398
|
const url = page(base, path);
|
|
@@ -202,11 +406,18 @@ function action(step, base, journal) {
|
|
|
202
406
|
case "linear.personal":
|
|
203
407
|
return link("/settings/connected-accounts?connect=linear", "connect your Linear account");
|
|
204
408
|
case "github.install":
|
|
205
|
-
|
|
409
|
+
// The start route answers 403 to anyone but an owner or admin, so a member gets the
|
|
410
|
+
// Integrations page and who to ask instead.
|
|
411
|
+
if (step.reason === "github_installation_admin_required" ||
|
|
412
|
+
step.reason === "member_scope")
|
|
413
|
+
return onboardReasonText(step, { baseUrl: base, journal });
|
|
414
|
+
return link(GITHUB_INSTALL_START, "install Catalyst on your GitHub organization");
|
|
206
415
|
case "github.personal":
|
|
207
416
|
return link("/settings/connected-accounts?connect=github", "connect your GitHub account");
|
|
208
417
|
case "accounts":
|
|
209
|
-
return
|
|
418
|
+
return step.reason === "ai_account_not_usable"
|
|
419
|
+
? onboardReasonText(step, { baseUrl: base, journal })
|
|
420
|
+
: link("/settings/coding-accounts", "add an AI account");
|
|
210
421
|
case "linear.adopt": {
|
|
211
422
|
const team = journal?.steps.find((row) => row.id === "linear.team" && row.state === "done");
|
|
212
423
|
const key = team && value(team, "teamKey");
|
|
@@ -222,24 +433,30 @@ function action(step, base, journal) {
|
|
|
222
433
|
? onboardReasonText(step, { baseUrl: base, journal })
|
|
223
434
|
: "Run catalyst onboard --runner to check whether this computer can take work.";
|
|
224
435
|
case "settings":
|
|
225
|
-
case "values":
|
|
226
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 });
|
|
227
439
|
case "linear.team":
|
|
228
440
|
return "Choose a Linear team when setup checks again.";
|
|
229
441
|
case "github.repos":
|
|
230
442
|
case "projects":
|
|
231
443
|
return "Choose repositories when setup checks again.";
|
|
232
444
|
case "first-ticket":
|
|
233
|
-
return "
|
|
445
|
+
return onboardReasonText({ ...step, reason: step.reason ?? "first_ticket_choice_required" }, { baseUrl: base, journal });
|
|
234
446
|
default:
|
|
235
447
|
return "Try this step again when setup checks again.";
|
|
236
448
|
}
|
|
237
449
|
}
|
|
238
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
|
+
: []);
|
|
239
456
|
const roots = journal.steps
|
|
240
457
|
.filter((step) => {
|
|
241
458
|
const m = setupStepView(step, journal, required.has(step.id)).mark;
|
|
242
|
-
return ((m === "act" || m === "fail") &&
|
|
459
|
+
return ((m === "act" || m === "fail" || adminLeft.has(step.id)) &&
|
|
243
460
|
(ONBOARD_DEPENDENCIES[step.id] ?? []).every((id) => {
|
|
244
461
|
const parent = journal.steps.find((s) => s.id === id);
|
|
245
462
|
return parent?.state === "done" || parent?.state === "skipped";
|
|
@@ -247,7 +464,7 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
|
|
|
247
464
|
})
|
|
248
465
|
.filter((step) => step.id !== "ready")
|
|
249
466
|
.sort((a, b) => ONBOARD_STEPS.indexOf(a.id) - ONBOARD_STEPS.indexOf(b.id));
|
|
250
|
-
const folded = (id) => id === "projects" ? "github.repos" : id
|
|
467
|
+
const folded = (id) => id === "projects" ? "github.repos" : id;
|
|
251
468
|
const unique = roots.filter((step, i) => roots.findIndex((s) => folded(s.id) === folded(step.id)) === i);
|
|
252
469
|
const actions = unique
|
|
253
470
|
.slice(0, 5)
|
|
@@ -274,16 +491,17 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
|
|
|
274
491
|
: journal.exit === 0
|
|
275
492
|
? "Ready for work"
|
|
276
493
|
: "Not ready for work yet";
|
|
277
|
-
const
|
|
278
|
-
const
|
|
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"));
|
|
279
497
|
const savedTicketUrl = ticket && (value(ticket, "url") ?? value(ticket, "ticketUrl"));
|
|
280
498
|
const ticketUrl = savedTicketUrl && page(base, savedTicketUrl);
|
|
281
499
|
const next = paused
|
|
282
500
|
? "Next: run catalyst onboard to continue."
|
|
283
|
-
: key
|
|
284
|
-
? `
|
|
501
|
+
: key
|
|
502
|
+
? `Follow ${ticketUrl ?? key} in Linear; Catalyst comments there as each phase finishes.`
|
|
285
503
|
: heading === "Setup complete"
|
|
286
|
-
?
|
|
504
|
+
? `Next: move a ticket to ${dispatchStageName(firstTicket?.evidence?.stage)} in Linear.`
|
|
287
505
|
: journal.exit === 0 && actions[0]
|
|
288
506
|
? `Next: ${actions[0].text[0].toLowerCase() + actions[0].text.slice(1)}`
|
|
289
507
|
: actions.length === 1
|
|
@@ -294,7 +512,9 @@ export function setupFinalScreen(journal, base, paused = false, required = new S
|
|
|
294
512
|
? "Next: run catalyst onboard after you finish 1 to " +
|
|
295
513
|
actions.length +
|
|
296
514
|
"."
|
|
297
|
-
:
|
|
515
|
+
: readyStep?.reason === "member_team_required"
|
|
516
|
+
? "Next: run catalyst onboard --team <KEY> to check again."
|
|
517
|
+
: "Next: run catalyst onboard to check again.";
|
|
298
518
|
return {
|
|
299
519
|
heading,
|
|
300
520
|
readinessCause,
|
|
@@ -317,7 +537,7 @@ export function setupBrowserInstruction(id, base, opened = false) {
|
|
|
317
537
|
"Connect your Linear account",
|
|
318
538
|
],
|
|
319
539
|
"github.install": [
|
|
320
|
-
|
|
540
|
+
GITHUB_INSTALL_START,
|
|
321
541
|
"install Catalyst on your GitHub organization",
|
|
322
542
|
"Install Catalyst on your GitHub organization",
|
|
323
543
|
],
|
|
@@ -328,8 +548,8 @@ export function setupBrowserInstruction(id, base, opened = false) {
|
|
|
328
548
|
],
|
|
329
549
|
accounts: [
|
|
330
550
|
"/settings/coding-accounts",
|
|
331
|
-
"add
|
|
332
|
-
"Add
|
|
551
|
+
"add an API key",
|
|
552
|
+
"Add an API key",
|
|
333
553
|
],
|
|
334
554
|
};
|
|
335
555
|
const words = copy[id];
|
|
@@ -340,9 +560,6 @@ export function setupBrowserInstruction(id, base, opened = false) {
|
|
|
340
560
|
? `Opened your browser. ${words[2]} there.`
|
|
341
561
|
: `Open this link and ${words[1]}:`,
|
|
342
562
|
url: page(base, words[0]),
|
|
343
|
-
preparation: id === "accounts"
|
|
344
|
-
? "In another terminal, run claude setup-token."
|
|
345
|
-
: undefined,
|
|
346
563
|
};
|
|
347
564
|
}
|
|
348
565
|
/** JSON facts use the same reachable actions as the terminal, while retaining the existing journal. */
|
|
@@ -386,8 +603,12 @@ export function onboardJsonView(journal, base, paused = false, options = {}) {
|
|
|
386
603
|
const step = view.steps.find((step) => step.id === action.id);
|
|
387
604
|
const role = view.steps.find((step) => step.id === "signin" && step.state === "done")?.evidence?.role;
|
|
388
605
|
const admin = step.reason === "member_scope" ||
|
|
606
|
+
// CTC-4680: refused by the person's Catalyst role, not by GitHub, so a workspace admin acts.
|
|
607
|
+
step.reason === "github_installation_admin_required" ||
|
|
389
608
|
step.reason?.endsWith("_identity_unverified") ||
|
|
390
|
-
(action.id === "accounts"
|
|
609
|
+
((action.id === "accounts" || action.id === "values") &&
|
|
610
|
+
role !== "owner" &&
|
|
611
|
+
role !== "admin");
|
|
391
612
|
const url = action.text
|
|
392
613
|
.match(/https?:\/\/[^\s<>]+/)?.[0]
|
|
393
614
|
?.replace(/[.,;:!?]+$/, "");
|
package/dist/setup-render.js
CHANGED
|
@@ -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,10 +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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
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);
|
|
92
124
|
const mark = (kind) => {
|
|
93
125
|
if (!traits.unicode)
|
|
94
126
|
return ASCII_MARKS[kind];
|
|
@@ -109,9 +141,7 @@ export function createSetupRenderer(stream, env) {
|
|
|
109
141
|
.map((line) => {
|
|
110
142
|
const plain = visible(line);
|
|
111
143
|
const indent = /^https?:\/\/\S+$/.test(plain) && stringWidth(plain) > width - col
|
|
112
|
-
?
|
|
113
|
-
? 3
|
|
114
|
-
: 0
|
|
144
|
+
? textCol
|
|
115
145
|
: col;
|
|
116
146
|
return prefix(indent) + line;
|
|
117
147
|
});
|
|
@@ -120,13 +150,13 @@ export function createSetupRenderer(stream, env) {
|
|
|
120
150
|
refresh();
|
|
121
151
|
const lead = plan
|
|
122
152
|
? traits.unicode
|
|
123
|
-
? "
|
|
153
|
+
? " "
|
|
124
154
|
: " "
|
|
125
155
|
: traits.unicode
|
|
126
156
|
? mark(kind)
|
|
127
157
|
: mark(kind).padEnd(7);
|
|
128
158
|
const titleWidth = stringWidth(visible(title));
|
|
129
|
-
const label = `${lead}${traits.unicode ? " " : " "}${String(number || "").padStart(2)} ${title}`;
|
|
159
|
+
const label = `${prefix(rowCol)}${lead}${traits.unicode ? " " : " "}${String(number || "").padStart(2)} ${title}`;
|
|
130
160
|
if (!outcome)
|
|
131
161
|
return [label];
|
|
132
162
|
if (width < 80 || titleWidth > 30)
|
|
@@ -212,6 +242,7 @@ export function createSetupRenderer(stream, env) {
|
|
|
212
242
|
clear();
|
|
213
243
|
abandon();
|
|
214
244
|
};
|
|
245
|
+
let branded = false;
|
|
215
246
|
const r = {
|
|
216
247
|
get traits() {
|
|
217
248
|
refresh();
|
|
@@ -219,11 +250,31 @@ export function createSetupRenderer(stream, env) {
|
|
|
219
250
|
},
|
|
220
251
|
intro(title) {
|
|
221
252
|
refresh();
|
|
222
|
-
ordinary([
|
|
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)));
|
|
223
274
|
},
|
|
224
275
|
outro(text) {
|
|
225
276
|
abandon();
|
|
226
|
-
write(
|
|
277
|
+
write(["", prefix(textCol) + text]);
|
|
227
278
|
},
|
|
228
279
|
plan(number, title, text) {
|
|
229
280
|
ordinary(stepRows("run", number, title, text, true));
|
|
@@ -316,14 +367,22 @@ export function createSetupRenderer(stream, env) {
|
|
|
316
367
|
},
|
|
317
368
|
heading(text) {
|
|
318
369
|
abandon();
|
|
319
|
-
write(
|
|
370
|
+
write(["", prefix(textCol) + style("bold", text)]);
|
|
320
371
|
},
|
|
321
372
|
line(text) {
|
|
322
|
-
ordinary(rows(text,
|
|
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]);
|
|
323
382
|
},
|
|
324
383
|
blank() {
|
|
325
384
|
refresh();
|
|
326
|
-
ordinary([
|
|
385
|
+
ordinary([""]);
|
|
327
386
|
},
|
|
328
387
|
link(url) {
|
|
329
388
|
return traits.links
|