@catalyst-cloud/cli 0.14.0 → 0.14.2
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 +14 -0
- package/README.md +14 -0
- package/dist/args.js +8 -3
- package/dist/cli.js +94 -37
- package/dist/contract.js +5 -0
- package/dist/env/parse-env.js +40 -4
- package/dist/env/selected-import.js +249 -0
- package/dist/legacy.js +16 -17
- package/dist/oauth.js +57 -6
- package/dist/onboard-existing.js +169 -0
- package/dist/onboard-progress.js +70 -0
- package/dist/onboard-ready.js +67 -0
- package/dist/onboard-repositories.js +160 -0
- package/dist/onboard-runtime.js +153 -0
- package/dist/onboard-signin.js +43 -0
- package/dist/onboard-ui.js +155 -0
- package/dist/onboard.js +151 -240
- package/dist/ready.js +28 -1
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +18 -5
- package/skills/catalyst-onboard/references/the-one-path.md +1 -1
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +2 -2
- package/skills/run-this-project/SKILL.md +1 -1
- 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 +1 -1
package/dist/onboard.js
CHANGED
|
@@ -2,9 +2,8 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
5
|
import { dirname, isAbsolute, join } from "node:path";
|
|
7
|
-
import { parseMachinePaths, resolveCatalystPath
|
|
6
|
+
import { parseMachinePaths, resolveCatalystPath } from "../vendor/paths/index.js";
|
|
8
7
|
import { machinePathsFile } from "../vendor/paths/node.js";
|
|
9
8
|
import { CliError, UsageError } from "./errors.js";
|
|
10
9
|
export const ONBOARD_STEPS = [
|
|
@@ -19,6 +18,7 @@ export const ONBOARD_STEPS = [
|
|
|
19
18
|
"linear.adopt",
|
|
20
19
|
"linear.automations",
|
|
21
20
|
"github.install",
|
|
21
|
+
"github.personal",
|
|
22
22
|
"github.repos",
|
|
23
23
|
"projects",
|
|
24
24
|
"accounts",
|
|
@@ -41,15 +41,21 @@ export function onboardStateRoot(home, env = {}) {
|
|
|
41
41
|
throw new CliError("bootstrap state directory must be absolute", "onboard-state-path", EXIT_REFUSED);
|
|
42
42
|
return bootstrap;
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
try {
|
|
45
|
+
const pathsFile = machinePathsFile({ env: { ...env, HOME: home } });
|
|
46
|
+
const machine = pathsFile && existsSync(pathsFile)
|
|
47
|
+
? parseMachinePaths(JSON.parse(readFileSync(pathsFile, "utf8"))) : undefined;
|
|
48
|
+
if (env.CATALYST_PATHS_FILE && !machine)
|
|
49
|
+
throw new CliError("the selected machine paths file is missing", "onboard-state-path", EXIT_REFUSED);
|
|
50
|
+
if (env.CATALYST_STATE_DIR !== undefined || machine)
|
|
51
|
+
return resolveCatalystPath("state", { env, machine });
|
|
52
|
+
return join(env.XDG_STATE_HOME ?? join(home, ".local", "state"), "catalyst");
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof CliError)
|
|
56
|
+
throw error;
|
|
57
|
+
throw new CliError("the selected setup paths are invalid; keep the file and correct it before resuming", "onboard-state-path", EXIT_REFUSED);
|
|
58
|
+
}
|
|
53
59
|
}
|
|
54
60
|
export function onboardStatePath(home, env = {}) {
|
|
55
61
|
return join(onboardStateRoot(home, env), "install", "last-run.json");
|
|
@@ -58,63 +64,29 @@ export function onboardLockPath(home, env = {}) {
|
|
|
58
64
|
return join(onboardStateRoot(home, env), "install.lock");
|
|
59
65
|
}
|
|
60
66
|
export const ONBOARD_DEPENDENCIES = {
|
|
61
|
-
"linear.workspace": ["signin"],
|
|
62
|
-
"linear.personal": ["signin", "linear.workspace"],
|
|
67
|
+
"linear.workspace": ["signin"], "linear.personal": ["signin", "linear.workspace"],
|
|
63
68
|
"linear.team": ["signin", "linear.workspace", "linear.personal"],
|
|
64
|
-
"linear.adopt": ["linear.team"],
|
|
65
|
-
"
|
|
66
|
-
"github.
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
accounts: ["signin"],
|
|
70
|
-
settings: ["projects"],
|
|
71
|
-
values: ["settings"],
|
|
72
|
-
capacity: ["projects"],
|
|
73
|
-
daemon: ["signin"],
|
|
74
|
-
"first-ticket": [
|
|
75
|
-
"projects",
|
|
76
|
-
"accounts",
|
|
77
|
-
"settings",
|
|
78
|
-
"values",
|
|
79
|
-
"capacity",
|
|
80
|
-
"daemon",
|
|
81
|
-
],
|
|
69
|
+
"linear.adopt": ["linear.team"], "linear.automations": ["linear.team", "linear.personal"],
|
|
70
|
+
"github.install": ["signin"], "github.personal": ["signin"], "github.repos": ["github.install", "linear.team"],
|
|
71
|
+
projects: ["linear.team", "github.repos"], accounts: ["signin"], settings: ["projects"],
|
|
72
|
+
values: ["settings"], capacity: ["projects"], daemon: ["signin"],
|
|
73
|
+
"first-ticket": ["projects", "linear.adopt", "linear.automations", "accounts", "settings", "values", "capacity", "daemon"],
|
|
82
74
|
};
|
|
83
75
|
const ADMIN_STEPS = new Set([
|
|
84
|
-
"linear.workspace",
|
|
85
|
-
"
|
|
86
|
-
"linear.adopt",
|
|
87
|
-
"linear.automations",
|
|
88
|
-
"github.install",
|
|
89
|
-
"github.repos",
|
|
90
|
-
"projects",
|
|
91
|
-
"settings",
|
|
92
|
-
"values",
|
|
93
|
-
"capacity",
|
|
94
|
-
"first-ticket",
|
|
76
|
+
"linear.workspace", "linear.adopt", "linear.automations", "github.install",
|
|
77
|
+
"github.repos", "projects", "settings", "values", "capacity", "first-ticket",
|
|
95
78
|
]);
|
|
96
79
|
export const ONBOARD_TITLES = {
|
|
97
|
-
machine: "Check this computer",
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"github.install": "Install Catalyst on GitHub",
|
|
108
|
-
"github.repos": "Choose repositories",
|
|
109
|
-
projects: "Register your projects",
|
|
110
|
-
accounts: "Check coding accounts",
|
|
111
|
-
settings: "Review repository settings",
|
|
112
|
-
values: "Import selected local values",
|
|
113
|
-
capacity: "Check runner capacity",
|
|
114
|
-
daemon: "Start background sync",
|
|
115
|
-
housekeeping: "Schedule the daily update",
|
|
116
|
-
"first-ticket": "Start a first ticket",
|
|
117
|
-
ready: "Check onboarding readiness",
|
|
80
|
+
machine: "Check this computer", cli: "Check the catalyst command", skills: "Check the Catalyst skills",
|
|
81
|
+
legacy: "Remove earlier Catalyst installs", signin: "Sign in to Catalyst",
|
|
82
|
+
"linear.workspace": "Connect your Linear workspace", "linear.personal": "Connect your Linear account",
|
|
83
|
+
"linear.team": "Choose a Linear team", "linear.adopt": "Apply the Catalyst workflow",
|
|
84
|
+
"linear.automations": "Check Linear's pull request automations", "github.install": "Install Catalyst on GitHub",
|
|
85
|
+
"github.personal": "Connect your GitHub account",
|
|
86
|
+
"github.repos": "Choose repositories", projects: "Register your projects", accounts: "Check coding accounts",
|
|
87
|
+
settings: "Review repository settings", values: "Import selected local values", capacity: "Check runner capacity",
|
|
88
|
+
daemon: "Check optional local sync", housekeeping: "Schedule the daily update",
|
|
89
|
+
"first-ticket": "Start a first ticket", ready: "Check onboarding readiness",
|
|
118
90
|
};
|
|
119
91
|
function isoNow(ctx, deps) {
|
|
120
92
|
return (deps.now ?? ctx.now)().toISOString();
|
|
@@ -123,13 +95,16 @@ function safeRead(path) {
|
|
|
123
95
|
try {
|
|
124
96
|
if (lstatSync(path).isSymbolicLink())
|
|
125
97
|
throw new CliError(`the saved setup record at ${path} cannot be a symbolic link`, "onboard-state-symlink", EXIT_REFUSED);
|
|
126
|
-
|
|
98
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
99
|
+
if (value === null)
|
|
100
|
+
throw new CliError("the saved setup record has an unknown shape; keep it and move it aside before trying again", "onboard-state-shape", EXIT_REFUSED);
|
|
101
|
+
return value;
|
|
127
102
|
}
|
|
128
103
|
catch (err) {
|
|
129
104
|
if (err instanceof CliError)
|
|
130
105
|
throw err;
|
|
131
106
|
if (object(err)?.code === "ENOENT")
|
|
132
|
-
return
|
|
107
|
+
return null;
|
|
133
108
|
throw new CliError(`the saved setup record at ${path} is not valid JSON; keep it and move it aside before trying again`, "onboard-state-corrupt", EXIT_REFUSED);
|
|
134
109
|
}
|
|
135
110
|
}
|
|
@@ -197,33 +172,10 @@ function normalizeStep(value, fallbackAt) {
|
|
|
197
172
|
"found",
|
|
198
173
|
"remaining",
|
|
199
174
|
"count",
|
|
200
|
-
"version",
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
"project",
|
|
205
|
-
"repository",
|
|
206
|
-
"ticket",
|
|
207
|
-
"cursor",
|
|
208
|
-
"lag",
|
|
209
|
-
"heartbeatAgeMs",
|
|
210
|
-
"supervised",
|
|
211
|
-
"streaming",
|
|
212
|
-
"scheduled",
|
|
213
|
-
"validated",
|
|
214
|
-
"available",
|
|
215
|
-
"phaseStarted",
|
|
216
|
-
"linearComment",
|
|
217
|
-
"fleetActivity",
|
|
218
|
-
"requiredValues",
|
|
219
|
-
"paused",
|
|
220
|
-
"revision",
|
|
221
|
-
"hash",
|
|
222
|
-
"path",
|
|
223
|
-
"provider",
|
|
224
|
-
"scope",
|
|
225
|
-
"checks",
|
|
226
|
-
"passed",
|
|
175
|
+
"version", "account", "membershipId", "checkedAt", "project", "repository", "ticket",
|
|
176
|
+
"cursor", "lag", "heartbeatAgeMs", "supervised", "streaming", "scheduled", "validated",
|
|
177
|
+
"available", "phaseStarted", "linearComment", "fleetActivity", "requiredValues", "paused",
|
|
178
|
+
"revision", "hash", "path", "provider", "scope", "checks", "passed", "team", "teamKey",
|
|
227
179
|
]) {
|
|
228
180
|
const item = evidence?.[key];
|
|
229
181
|
if (item === null ||
|
|
@@ -237,9 +189,9 @@ function normalizeStep(value, fallbackAt) {
|
|
|
237
189
|
return step;
|
|
238
190
|
}
|
|
239
191
|
/** Reads both the M1 journal and the 0.13.4 install-script receipt without discarding completed steps. */
|
|
240
|
-
export function readOnboardJournal(path, cliVersion = "0.
|
|
192
|
+
export function readOnboardJournal(path, cliVersion = "0.13.1", now = new Date()) {
|
|
241
193
|
const raw = safeRead(path);
|
|
242
|
-
if (raw ===
|
|
194
|
+
if (raw === null)
|
|
243
195
|
return null;
|
|
244
196
|
const value = object(raw);
|
|
245
197
|
if (!value || !Array.isArray(value.steps))
|
|
@@ -274,22 +226,13 @@ export function readOnboardJournal(path, cliVersion = "0.14.0", now = new Date()
|
|
|
274
226
|
: exit,
|
|
275
227
|
steps: [...deduped.values()],
|
|
276
228
|
...(typeof value.account === "string" ? { account: value.account } : {}),
|
|
277
|
-
...(typeof value.membershipId === "string"
|
|
278
|
-
? { membershipId: value.membershipId }
|
|
279
|
-
: {}),
|
|
229
|
+
...(typeof value.membershipId === "string" ? { membershipId: value.membershipId } : {}),
|
|
280
230
|
...(typeof value.baseUrl === "string" ? { baseUrl: value.baseUrl } : {}),
|
|
281
|
-
...(value.scope === "step" || value.scope === "onboarding"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
...(value.
|
|
285
|
-
|
|
286
|
-
: {}),
|
|
287
|
-
...(typeof value.complete === "boolean"
|
|
288
|
-
? { complete: value.complete }
|
|
289
|
-
: {}),
|
|
290
|
-
operations: Object.fromEntries(Object.entries(object(value.operations) ?? {}).filter(([key, v]) => ONBOARD_STEPS.includes(key) &&
|
|
291
|
-
typeof v === "string" &&
|
|
292
|
-
/^[a-zA-Z0-9_.:-]{1,160}$/.test(v))),
|
|
231
|
+
...(value.scope === "step" || value.scope === "onboarding" ? { scope: value.scope } : {}),
|
|
232
|
+
...(value.mode === "plan" || value.mode === "run" ? { mode: value.mode } : {}),
|
|
233
|
+
...(typeof value.complete === "boolean" ? { complete: value.complete } : {}),
|
|
234
|
+
...(typeof value.localSync === "boolean" ? { localSync: value.localSync } : {}),
|
|
235
|
+
operations: Object.fromEntries(Object.entries(object(value.operations) ?? {}).filter(([key, v]) => ONBOARD_STEPS.includes(key) && typeof v === "string" && /^[a-zA-Z0-9_.:-]{1,160}$/.test(v))),
|
|
293
236
|
changes: Array.isArray(value.changes)
|
|
294
237
|
? value.changes.flatMap((change) => {
|
|
295
238
|
const row = object(change);
|
|
@@ -335,20 +278,11 @@ export function writeOnboardJournal(path, journal) {
|
|
|
335
278
|
function defaultAlive(pid) {
|
|
336
279
|
try {
|
|
337
280
|
process.kill(pid, 0);
|
|
281
|
+
return true;
|
|
338
282
|
}
|
|
339
|
-
catch (
|
|
340
|
-
return object(
|
|
283
|
+
catch (error) {
|
|
284
|
+
return object(error)?.code !== "ESRCH";
|
|
341
285
|
}
|
|
342
|
-
if (pid === process.pid)
|
|
343
|
-
return true;
|
|
344
|
-
const probe = spawnSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
345
|
-
encoding: "utf8",
|
|
346
|
-
timeout: 2_000,
|
|
347
|
-
});
|
|
348
|
-
if (probe.status !== 0)
|
|
349
|
-
return true;
|
|
350
|
-
const command = probe.stdout.trim();
|
|
351
|
-
return command.includes("catalyst") && command.includes("onboard");
|
|
352
286
|
}
|
|
353
287
|
function readLockOwner(path) {
|
|
354
288
|
try {
|
|
@@ -375,12 +309,14 @@ function removeStaleLock(lockPath, owner) {
|
|
|
375
309
|
if (!before.isDirectory() || before.isSymbolicLink())
|
|
376
310
|
return false;
|
|
377
311
|
const current = readLockOwner(lockPath);
|
|
378
|
-
if (current?.pid !== owner
|
|
312
|
+
if ((current?.pid ?? null) !== (owner?.pid ?? null) ||
|
|
313
|
+
(current?.token ?? "") !== (owner?.token ?? ""))
|
|
379
314
|
return false;
|
|
380
|
-
const stalePath = `${lockPath}.stale-${owner
|
|
315
|
+
const stalePath = `${lockPath}.stale-${owner?.pid ?? "unknown"}-${randomBytes(4).toString("hex")}`;
|
|
381
316
|
renameSync(lockPath, stalePath);
|
|
382
317
|
const moved = readLockOwner(stalePath);
|
|
383
|
-
if (moved?.pid !== owner
|
|
318
|
+
if ((moved?.pid ?? null) !== (owner?.pid ?? null) ||
|
|
319
|
+
(moved?.token ?? "") !== (owner?.token ?? "")) {
|
|
384
320
|
try {
|
|
385
321
|
renameSync(stalePath, lockPath);
|
|
386
322
|
}
|
|
@@ -448,11 +384,7 @@ function acquireLock(ctx, deps) {
|
|
|
448
384
|
const handoff = ctx.env.CATALYST_INSTALL_LOCK_TOKEN;
|
|
449
385
|
if (current?.pid === pid && current.token !== "" && current.token === handoff)
|
|
450
386
|
return { path, owner: current };
|
|
451
|
-
const ownerIsAlive = current
|
|
452
|
-
? false
|
|
453
|
-
: current
|
|
454
|
-
? (deps.isProcessAlive ?? defaultAlive)(current.pid)
|
|
455
|
-
: false;
|
|
387
|
+
const ownerIsAlive = current ? (deps.isProcessAlive ?? defaultAlive)(current.pid) : false;
|
|
456
388
|
if (current && ownerIsAlive)
|
|
457
389
|
throw new CliError(`setup is already running (pid ${current.pid}); wait for it to finish, then run the same command`, "onboard-locked", EXIT_FAILED);
|
|
458
390
|
if (!current)
|
|
@@ -487,6 +419,10 @@ function freshJournal(ctx, cliVersion, deps) {
|
|
|
487
419
|
changes: [],
|
|
488
420
|
};
|
|
489
421
|
}
|
|
422
|
+
/** A refusal before loading state must not reuse or copy an unreadable receipt. */
|
|
423
|
+
export function onboardErrorJournal(ctx, cliVersion, exit) {
|
|
424
|
+
return { ...freshJournal(ctx, cliVersion, {}), scope: "onboarding", mode: "run", exit, complete: false };
|
|
425
|
+
}
|
|
490
426
|
function planJournal(journal) {
|
|
491
427
|
return {
|
|
492
428
|
...journal,
|
|
@@ -536,18 +472,14 @@ async function confirmContinue(ctx, args, deps) {
|
|
|
536
472
|
}
|
|
537
473
|
}
|
|
538
474
|
function sanitizedResult(id, result, at) {
|
|
539
|
-
const row = normalizeStep({
|
|
540
|
-
id,
|
|
541
|
-
...result,
|
|
542
|
-
state: result.state === "refused" ? "failed" : result.state,
|
|
543
|
-
}, at);
|
|
475
|
+
const row = normalizeStep({ id, ...result, state: result.state === "refused" ? "failed" : result.state }, at);
|
|
544
476
|
row.at = at;
|
|
545
477
|
return row;
|
|
546
478
|
}
|
|
547
479
|
function stepSatisfied(step) {
|
|
548
|
-
return
|
|
549
|
-
(step
|
|
550
|
-
(step.id === "
|
|
480
|
+
return step?.state === "done" || (step?.state === "skipped" &&
|
|
481
|
+
(step.id === "legacy" || step.reason === "member_scope" ||
|
|
482
|
+
(step.id === "daemon" && step.reason === "local_sync_not_selected")));
|
|
551
483
|
}
|
|
552
484
|
function requireMatchingIdentity(journal, identity) {
|
|
553
485
|
const account = journal.account ?? journal.tenant;
|
|
@@ -563,22 +495,21 @@ function requireMatchingIdentity(journal, identity) {
|
|
|
563
495
|
}
|
|
564
496
|
}
|
|
565
497
|
/** The receipt records progress; adapters verify the current state before any resumed action. */
|
|
566
|
-
export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.
|
|
498
|
+
export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.13.1") {
|
|
567
499
|
if (args.subcommand !== null || args.rest.length > 0)
|
|
568
500
|
throw new UsageError("onboard takes no positional arguments");
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
501
|
+
if (args.json || args.flags.yes === true || args.flags["dry-run"] === true)
|
|
502
|
+
deps = { ...deps, ui: undefined };
|
|
503
|
+
const only = typeof args.flags.only === "string" ? args.flags.only : undefined;
|
|
504
|
+
const memberScopeSkips = (id) => ADMIN_STEPS.has(id) ||
|
|
505
|
+
(id === "linear.team" && only !== "linear.team" && typeof args.flags.team !== "string");
|
|
506
|
+
const resume = typeof args.flags["resume-from"] === "string" ? args.flags["resume-from"] : undefined;
|
|
575
507
|
const resumeFrom = resume === "install" ? "signin" : resume;
|
|
576
508
|
for (const id of [only, resumeFrom])
|
|
577
509
|
if (id && !ONBOARD_STEPS.includes(id))
|
|
578
510
|
throw new UsageError(`unknown onboarding step: ${id}`);
|
|
579
511
|
const statePath = onboardStatePath(ctx.home, ctx.env);
|
|
580
|
-
let journal = readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)()) ??
|
|
581
|
-
freshJournal(ctx, cliVersion, deps);
|
|
512
|
+
let journal = readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)()) ?? freshJournal(ctx, cliVersion, deps);
|
|
582
513
|
if (args.flags["dry-run"] === true) {
|
|
583
514
|
if (args.json)
|
|
584
515
|
ctx.stdout(JSON.stringify(planJournal(journal)));
|
|
@@ -590,26 +521,31 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.14.0") {
|
|
|
590
521
|
let identity = null;
|
|
591
522
|
try {
|
|
592
523
|
if (deps.identity) {
|
|
593
|
-
identity = await deps.identity();
|
|
524
|
+
identity = await deps.identity(journal);
|
|
594
525
|
requireMatchingIdentity(journal, identity);
|
|
595
526
|
}
|
|
596
527
|
}
|
|
597
528
|
catch (error) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
return EXIT_FAILED;
|
|
529
|
+
const code = error instanceof CliError ? error.exitCode : EXIT_FAILED;
|
|
530
|
+
ctx.stderr("Could not verify your Catalyst membership. Setup was not changed. Sign in with the original account to resume.");
|
|
531
|
+
if (args.json)
|
|
532
|
+
ctx.stdout(JSON.stringify({ ...journal, mode: "run", exit: code, complete: false }));
|
|
533
|
+
return code;
|
|
604
534
|
}
|
|
605
|
-
if (
|
|
535
|
+
if (deps.ui)
|
|
536
|
+
deps.ui.plan(journal);
|
|
537
|
+
else if (!args.json)
|
|
606
538
|
printPlan(ctx, journal);
|
|
607
539
|
const inheritedConsent = resume === "install" && Boolean(ctx.env.CATALYST_INSTALL_LOCK_TOKEN);
|
|
608
|
-
|
|
540
|
+
let localSync = args.flags["local-sync"] === true || journal.localSync === true;
|
|
541
|
+
const consent = inheritedConsent ? true : deps.ui ? await deps.ui.confirmPlan(localSync) : await confirmContinue(ctx, args, deps);
|
|
542
|
+
if (typeof consent !== "boolean")
|
|
543
|
+
localSync = consent.localSync;
|
|
544
|
+
if (!(typeof consent === "boolean" ? consent : consent.proceed)) {
|
|
609
545
|
ctx.stderr("Nothing was changed. Run the same command when ready.");
|
|
610
546
|
if (args.json)
|
|
611
547
|
ctx.stdout(JSON.stringify({ ...journal, complete: false }));
|
|
612
|
-
return 0;
|
|
548
|
+
return deps.ui?.signal.aborted ? EXIT_WAITING : 0;
|
|
613
549
|
}
|
|
614
550
|
let lock;
|
|
615
551
|
try {
|
|
@@ -624,35 +560,23 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.14.0") {
|
|
|
624
560
|
return error.exitCode;
|
|
625
561
|
}
|
|
626
562
|
const stop = new AbortController();
|
|
627
|
-
const signal = deps.signal
|
|
628
|
-
|
|
629
|
-
:
|
|
630
|
-
const stepCtx = {
|
|
631
|
-
...ctx,
|
|
632
|
-
stdout: args.json ? ctx.stderr : ctx.stdout,
|
|
563
|
+
const signal = AbortSignal.any([stop.signal, ...(deps.signal ? [deps.signal] : []), ...(deps.ui ? [deps.ui.signal] : [])]);
|
|
564
|
+
const stepCtx = { ...ctx, stdout: deps.ui ? (line) => deps.ui.message(line) : args.json ? ctx.stderr : ctx.stdout,
|
|
565
|
+
stderr: deps.ui ? (line) => deps.ui.message(line) : ctx.stderr,
|
|
633
566
|
fetch: ((input, init) => ctx.fetch(input, {
|
|
634
|
-
...init,
|
|
635
|
-
|
|
636
|
-
})),
|
|
637
|
-
};
|
|
567
|
+
...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal,
|
|
568
|
+
})) };
|
|
638
569
|
let current = null;
|
|
639
570
|
const interrupted = () => {
|
|
640
571
|
stop.abort();
|
|
641
572
|
if (current)
|
|
642
|
-
setStep(journal, {
|
|
643
|
-
id: current,
|
|
644
|
-
state: "failed",
|
|
645
|
-
reason: "interrupted",
|
|
646
|
-
at: isoNow(ctx, deps),
|
|
647
|
-
});
|
|
573
|
+
setStep(journal, { id: current, state: "failed", reason: "interrupted", at: isoNow(ctx, deps) });
|
|
648
574
|
journal.exit = EXIT_WAITING;
|
|
649
575
|
journal.complete = false;
|
|
650
576
|
try {
|
|
651
577
|
writeOnboardJournal(statePath, journal);
|
|
652
578
|
}
|
|
653
|
-
catch {
|
|
654
|
-
/* keep the last atomic receipt */
|
|
655
|
-
}
|
|
579
|
+
catch { /* keep the last atomic receipt */ }
|
|
656
580
|
};
|
|
657
581
|
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
658
582
|
if (deps.bindSignals !== false)
|
|
@@ -664,26 +588,27 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.14.0") {
|
|
|
664
588
|
writeOnboardJournal(statePath, journal);
|
|
665
589
|
if (args.json)
|
|
666
590
|
ctx.stdout(JSON.stringify(journal));
|
|
591
|
+
else if (deps.ui)
|
|
592
|
+
deps.ui.finish(journal, only);
|
|
667
593
|
else {
|
|
668
594
|
if (journal.complete)
|
|
669
595
|
ctx.stdout("Onboarding complete.");
|
|
670
596
|
else if (only && code === 0)
|
|
671
597
|
ctx.stdout(`${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.`);
|
|
672
598
|
else
|
|
673
|
-
ctx.stdout(`Setup still needs ${journal.steps.filter(
|
|
599
|
+
ctx.stdout(`Setup still needs ${journal.steps.filter(step => !stepSatisfied(step)).length} checks.`);
|
|
674
600
|
ctx.stdout("resume: catalyst onboard");
|
|
675
601
|
}
|
|
676
602
|
return code;
|
|
677
603
|
};
|
|
678
604
|
try {
|
|
679
|
-
journal =
|
|
680
|
-
readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)()) ??
|
|
681
|
-
journal;
|
|
605
|
+
journal = readOnboardJournal(statePath, cliVersion, (deps.now ?? ctx.now)()) ?? journal;
|
|
682
606
|
if (deps.identity)
|
|
683
607
|
requireMatchingIdentity(journal, identity);
|
|
684
608
|
journal.cli = cliVersion;
|
|
685
609
|
journal.scope = only ? "step" : "onboarding";
|
|
686
610
|
journal.mode = "run";
|
|
611
|
+
journal.localSync = localSync;
|
|
687
612
|
for (const id of ONBOARD_STEPS)
|
|
688
613
|
if (!journalStep(journal, id))
|
|
689
614
|
setStep(journal, { id, state: "pending" });
|
|
@@ -704,119 +629,104 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.14.0") {
|
|
|
704
629
|
for (const id of ONBOARD_STEPS)
|
|
705
630
|
needed.add(id);
|
|
706
631
|
const fromIndex = resumeFrom ? ONBOARD_STEPS.indexOf(resumeFrom) : 0;
|
|
707
|
-
for (const id of ONBOARD_STEPS.filter(
|
|
632
|
+
for (const id of ONBOARD_STEPS.filter(id => needed.has(id))) {
|
|
708
633
|
if (signal.aborted)
|
|
709
634
|
return finish(EXIT_WAITING);
|
|
710
635
|
current = id;
|
|
636
|
+
deps.ui?.stepStart(id);
|
|
711
637
|
// Member onboarding keeps administration outside its scope, with an explicit recorded reason.
|
|
712
|
-
if (identity?.role === "member" &&
|
|
713
|
-
setStep(journal, {
|
|
714
|
-
|
|
715
|
-
state: "skipped",
|
|
716
|
-
reason: "member_scope",
|
|
717
|
-
at: isoNow(ctx, deps),
|
|
718
|
-
});
|
|
638
|
+
if (identity?.role === "member" && memberScopeSkips(id)) {
|
|
639
|
+
setStep(journal, { id, state: "skipped", reason: "member_scope", at: isoNow(ctx, deps) });
|
|
640
|
+
deps.ui?.stepEnd(journalStep(journal, id));
|
|
719
641
|
continue;
|
|
720
642
|
}
|
|
721
|
-
const missing = (ONBOARD_DEPENDENCIES[id] ?? []).find(
|
|
643
|
+
const missing = (ONBOARD_DEPENDENCIES[id] ?? []).find(parent => !stepSatisfied(journalStep(journal, parent)));
|
|
722
644
|
if (missing) {
|
|
723
|
-
setStep(journal, {
|
|
724
|
-
id,
|
|
725
|
-
state: "waiting",
|
|
726
|
-
reason: "prerequisite_not_ready",
|
|
727
|
-
at: isoNow(ctx, deps),
|
|
728
|
-
});
|
|
645
|
+
setStep(journal, { id, state: "waiting", reason: "prerequisite_not_ready", at: isoNow(ctx, deps) });
|
|
729
646
|
writeOnboardJournal(statePath, journal);
|
|
647
|
+
deps.ui?.stepEnd(journalStep(journal, id));
|
|
730
648
|
continue;
|
|
731
649
|
}
|
|
732
650
|
let adapter = deps.adapters?.[id];
|
|
733
651
|
// Compatibility for the initial legacy adapter, which performs its own before/after checks.
|
|
734
652
|
if (!adapter && id === "legacy") {
|
|
735
|
-
let result = deps.runStep
|
|
736
|
-
|
|
737
|
-
: { state: "skipped", reason: "legacy_not_selected" };
|
|
738
|
-
adapter = {
|
|
739
|
-
check: async () => result,
|
|
740
|
-
act: deps.runStep
|
|
741
|
-
? async () => (result = await deps.runStep(id, stepCtx))
|
|
742
|
-
: undefined,
|
|
743
|
-
};
|
|
653
|
+
let result = deps.runStep ? { state: "pending" } : { state: "skipped", reason: "legacy_not_selected" };
|
|
654
|
+
adapter = { check: async () => result, act: deps.runStep ? async () => result = await deps.runStep(id, stepCtx) : undefined };
|
|
744
655
|
}
|
|
745
656
|
if (!adapter) {
|
|
746
|
-
setStep(journal, {
|
|
747
|
-
id,
|
|
748
|
-
state: "waiting",
|
|
749
|
-
reason: "step_not_available_in_this_release",
|
|
750
|
-
at: isoNow(ctx, deps),
|
|
751
|
-
});
|
|
657
|
+
setStep(journal, { id, state: "waiting", reason: "step_not_available_in_this_release", at: isoNow(ctx, deps) });
|
|
752
658
|
writeOnboardJournal(statePath, journal);
|
|
659
|
+
deps.ui?.stepEnd(journalStep(journal, id));
|
|
753
660
|
continue;
|
|
754
661
|
}
|
|
755
662
|
try {
|
|
756
|
-
let result = await adapter.check(stepCtx, journal);
|
|
757
|
-
if (result.state === "pending" &&
|
|
758
|
-
adapter.act &&
|
|
759
|
-
(!only || id === only) &&
|
|
760
|
-
ONBOARD_STEPS.indexOf(id) >= fromIndex) {
|
|
663
|
+
let result = await adapter.check(stepCtx, journal, signal);
|
|
664
|
+
if (result.state === "pending" && adapter.act && (!only || id === only) && ONBOARD_STEPS.indexOf(id) >= fromIndex) {
|
|
761
665
|
if (deps.identity) {
|
|
762
|
-
identity = await deps.identity();
|
|
666
|
+
identity = await deps.identity(journal);
|
|
763
667
|
requireMatchingIdentity(journal, identity);
|
|
668
|
+
if (identity?.role === "member" && memberScopeSkips(id)) {
|
|
669
|
+
setStep(journal, { id, state: "skipped", reason: "member_scope", at: isoNow(ctx, deps) });
|
|
670
|
+
writeOnboardJournal(statePath, journal);
|
|
671
|
+
deps.ui?.stepEnd(journalStep(journal, id));
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
764
674
|
}
|
|
765
675
|
journal.operations[id] ??= `${journal.runId}:${id}`;
|
|
766
676
|
setStep(journal, { id, state: "running", at: isoNow(ctx, deps) });
|
|
767
677
|
journal.exit = null;
|
|
768
678
|
writeOnboardJournal(statePath, journal);
|
|
769
|
-
const action = await adapter.act(stepCtx, journal);
|
|
679
|
+
const action = await adapter.act(stepCtx, journal, signal);
|
|
770
680
|
if (action.state === "done") {
|
|
771
|
-
result = await adapter.check(stepCtx, journal);
|
|
681
|
+
result = await adapter.check(stepCtx, journal, signal);
|
|
772
682
|
if (result.state === "pending")
|
|
773
683
|
result = { state: "waiting", reason: "verification_pending" };
|
|
774
684
|
}
|
|
775
685
|
else
|
|
776
686
|
result = action;
|
|
777
687
|
if (id === "signin" && result.state === "done" && deps.identity) {
|
|
778
|
-
identity = await deps.identity();
|
|
688
|
+
identity = await deps.identity(journal);
|
|
779
689
|
requireMatchingIdentity(journal, identity);
|
|
690
|
+
if (identity?.role === "member" && memberScopeSkips(id)) {
|
|
691
|
+
setStep(journal, { id, state: "skipped", reason: "member_scope", at: isoNow(ctx, deps) });
|
|
692
|
+
writeOnboardJournal(statePath, journal);
|
|
693
|
+
deps.ui?.stepEnd(journalStep(journal, id));
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
780
696
|
}
|
|
781
697
|
}
|
|
698
|
+
if (id === "signin" && result.state === "done" && deps.identity) {
|
|
699
|
+
identity = await deps.identity(journal);
|
|
700
|
+
requireMatchingIdentity(journal, identity);
|
|
701
|
+
if (!identity)
|
|
702
|
+
result = { state: "failed", reason: "membership_not_verified" };
|
|
703
|
+
}
|
|
782
704
|
if (result.state === "refused")
|
|
783
705
|
refused = true;
|
|
784
706
|
if (result.state === "pending")
|
|
785
|
-
result = {
|
|
786
|
-
...result,
|
|
787
|
-
state: "waiting",
|
|
788
|
-
reason: result.reason ?? "action_required",
|
|
789
|
-
};
|
|
707
|
+
result = { ...result, state: "waiting", reason: result.reason ?? "action_required" };
|
|
790
708
|
setStep(journal, sanitizedResult(id, result, isoNow(ctx, deps)));
|
|
791
709
|
}
|
|
792
710
|
catch (error) {
|
|
793
711
|
if (error instanceof CliError && error.exitCode === EXIT_REFUSED)
|
|
794
712
|
refused = true;
|
|
795
|
-
const reason = error instanceof CliError &&
|
|
796
|
-
|
|
797
|
-
? error.code.replaceAll("-", "_")
|
|
798
|
-
: "step_failed";
|
|
799
|
-
setStep(journal, {
|
|
800
|
-
id,
|
|
801
|
-
state: "failed",
|
|
802
|
-
reason,
|
|
803
|
-
at: isoNow(ctx, deps),
|
|
804
|
-
});
|
|
713
|
+
const reason = signal.aborted ? "interrupted" : error instanceof CliError && /^[a-z][a-z0-9_-]{1,63}$/.test(error.code) ? error.code.replaceAll("-", "_") : "step_failed";
|
|
714
|
+
setStep(journal, { id, state: "failed", reason, at: isoNow(ctx, deps) });
|
|
805
715
|
stepCtx.stderr(`✗ ${ONBOARD_TITLES[id]}: ${reason}. The safe reason is saved; run the same command to resume.`);
|
|
806
716
|
}
|
|
717
|
+
if (signal.aborted) {
|
|
718
|
+
setStep(journal, { id, state: "failed", reason: "interrupted", at: isoNow(ctx, deps) });
|
|
719
|
+
return finish(EXIT_WAITING);
|
|
720
|
+
}
|
|
721
|
+
deps.ui?.stepEnd(journalStep(journal, id));
|
|
807
722
|
writeOnboardJournal(statePath, journal);
|
|
808
723
|
if (refused)
|
|
809
724
|
break;
|
|
810
725
|
}
|
|
811
726
|
current = null;
|
|
812
727
|
const scope = only ? [only] : [...ONBOARD_STEPS];
|
|
813
|
-
const code = refused
|
|
814
|
-
?
|
|
815
|
-
: scope.some((id) => journalStep(journal, id)?.state === "failed")
|
|
816
|
-
? EXIT_FAILED
|
|
817
|
-
: scope.every((id) => stepSatisfied(journalStep(journal, id)))
|
|
818
|
-
? 0
|
|
819
|
-
: EXIT_WAITING;
|
|
728
|
+
const code = refused ? EXIT_REFUSED : scope.some(id => journalStep(journal, id)?.state === "failed") ? EXIT_FAILED
|
|
729
|
+
: scope.every(id => stepSatisfied(journalStep(journal, id))) ? 0 : EXIT_WAITING;
|
|
820
730
|
return finish(code);
|
|
821
731
|
}
|
|
822
732
|
catch (error) {
|
|
@@ -832,5 +742,6 @@ export async function cmdOnboard(args, ctx, deps = {}, cliVersion = "0.14.0") {
|
|
|
832
742
|
for (const name of signals)
|
|
833
743
|
process.off(name, interrupted);
|
|
834
744
|
releaseLock(lock);
|
|
745
|
+
deps.ui?.dispose();
|
|
835
746
|
}
|
|
836
747
|
}
|