@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.
- package/CHANGELOG.md +9 -0
- package/README.md +41 -0
- package/dist/args.js +34 -2
- package/dist/capabilities.js +2 -1
- package/dist/cli-update-notice.js +76 -0
- package/dist/cli.js +263 -101
- package/dist/oauth.js +48 -18
- package/dist/onboard-accounts.js +34 -5
- package/dist/onboard-automation-management.js +27 -9
- package/dist/onboard-capacity.js +10 -1
- package/dist/onboard-existing.js +132 -31
- package/dist/onboard-github.js +171 -6
- package/dist/onboard-headless.js +647 -0
- package/dist/onboard-interrupts.js +41 -0
- package/dist/onboard-next.js +215 -0
- package/dist/onboard-permissions.js +100 -0
- package/dist/onboard-personal.js +46 -10
- package/dist/onboard-runner-cloud.js +213 -0
- package/dist/onboard-runner-egress.js +67 -0
- package/dist/onboard-runner.js +780 -0
- package/dist/onboard-runtime.js +48 -8
- package/dist/onboard-standalone-copy.js +110 -0
- package/dist/onboard-team-create.js +267 -0
- package/dist/onboard-ui.js +623 -29
- package/dist/onboard-workflow.js +311 -168
- package/dist/onboard-workspace.js +52 -5
- package/dist/onboard.js +359 -212
- package/dist/setup-events.js +100 -0
- package/dist/setup-onboard-copy.js +438 -0
- package/dist/setup-prompt-frame.js +94 -0
- package/dist/setup-prompts.js +108 -0
- package/dist/setup-render.js +339 -0
- package/dist/setup-signin.js +187 -0
- package/dist/setup.js +426 -0
- package/package.json +7 -3
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +3 -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/vendor/README.md +4 -0
- package/vendor/self-host/compose.yaml +110 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.service +14 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.timer +10 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress.service +19 -0
- package/vendor/self-host/linux-session-egress/session-egress.py +291 -0
- package/vendor/self-host/provenance.json +11 -0
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// cli.ts — the `catalyst` dispatcher (npm: @catalyst-cloud/cli). Verbs live in
|
|
2
2
|
// their own modules; this file keeps every export the 0.1 tests and the bin import.
|
|
3
|
+
import { createSetupRenderer } from "./setup-render.js";
|
|
3
4
|
import { existsSync, openSync, closeSync } from "node:fs";
|
|
4
5
|
import { ReadStream as TerminalInput } from "node:tty";
|
|
6
|
+
import { Writable } from "node:stream";
|
|
5
7
|
import { parseArgs, positionals, verbHelp } from "./args.js";
|
|
6
8
|
import { CONFIG_MODE, DEFAULT_BASE_URL, LEGACY_PACKAGE_NAME, PACKAGE_NAME, cliPath, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, formatMode, installerOwnsSkills, INSTALL_COMMAND, loadConfig, modernCliPath, normalizeBaseUrl, readManifest, requireConfig, saveConfig, writeConfig, } from "./config.js";
|
|
7
9
|
import { loadContract, pickPath } from "./contract.js";
|
|
@@ -14,12 +16,16 @@ const ONBOARD_STEP_MIN_MS = 11 * 60_000;
|
|
|
14
16
|
import { openBrowserChecked as defaultOpenBrowser } from "./browser.js";
|
|
15
17
|
import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning, } from "./execution.js";
|
|
16
18
|
import { cmdQuery } from "./query.js";
|
|
19
|
+
import { maybePrintPublishedUpdateNotice } from "./cli-update-notice.js";
|
|
17
20
|
import { cmdReady } from "./ready.js";
|
|
18
21
|
import { cmdReplica } from "./replica.js";
|
|
19
22
|
import { cmdRuntime } from "./runtime-verb.js";
|
|
20
23
|
import { cmdEvents } from "./events.js";
|
|
21
24
|
import { stageOnboardLogin } from "./onboard-login-candidate.js";
|
|
22
25
|
import { stdinIsTty } from "./prompt.js";
|
|
26
|
+
import { cmdSetup, SETUP_USAGE } from "./setup.js";
|
|
27
|
+
import { clackSetupAsk, clackSetupSpinner, openSetupTerminal, setupOnboardStreams, } from "./setup-prompts.js";
|
|
28
|
+
import { keepInstalledCliPath, runSetupSignin } from "./setup-signin.js";
|
|
23
29
|
import { FIRST_STAMPED_VERSION, PROVENANCE_MARKER, parseProvenanceVersion, } from "./skill-shape.js";
|
|
24
30
|
import { installedBundleVersion, installSkills, parseChangelogEntry, readChangelog, resolveSkillsDir, skillsSourceDir, updateNoticeLine, } from "./skills.js";
|
|
25
31
|
import { cmdWatch } from "./watch.js";
|
|
@@ -41,6 +47,7 @@ import { cmdConnections } from "./connections.js";
|
|
|
41
47
|
import { cmdOnboard, onboardErrorJournal, onboardRequiredStepsWaiting, } from "./onboard.js";
|
|
42
48
|
import { createClackOnboardUi, shouldUseOnboardUi, } from "./onboard-ui.js";
|
|
43
49
|
import { createOnboardRuntime } from "./onboard-runtime.js";
|
|
50
|
+
import { argvRequestsHeadless, headlessOnboardDeps, headlessUsageRefusal, onboardHeadlessRequested, runOnboardHeadless, } from "./onboard-headless.js";
|
|
44
51
|
import { savedOnboardBaseUrl } from "./onboard-next.js";
|
|
45
52
|
import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
|
|
46
53
|
import { selectedOnboardTeam } from "./onboard-existing.js";
|
|
@@ -69,7 +76,8 @@ export function usageText() {
|
|
|
69
76
|
" catalyst login --key <personal-key> [--base-url <url>] (or CATALYST_CLOUD_TOKEN, for a key)",
|
|
70
77
|
" catalyst join ... (deprecated alias of login; removed in the next minor version)",
|
|
71
78
|
" catalyst install [--skills-dir <dir>] [--force] (repair path; your agent's own command installs the skills)",
|
|
72
|
-
" catalyst onboard [--resume-from <step>] [--only <step>] [--yes] [--dry-run] [--json]",
|
|
79
|
+
" catalyst onboard [--resume-from <step>] [--only <step>] [--runner|--no-runner] [--yes] [--dry-run] [--json]",
|
|
80
|
+
` catalyst ${SETUP_USAGE}`,
|
|
73
81
|
" catalyst status | notice | me | ready | accounts",
|
|
74
82
|
" catalyst mcp add|list|remove (vault references only)",
|
|
75
83
|
" catalyst contract [--refresh] [--path <a.b.c>]",
|
|
@@ -199,6 +207,10 @@ function maybePrintUpdateNotice(args, ctx) {
|
|
|
199
207
|
saveConfig(ctx.home, cfg);
|
|
200
208
|
}
|
|
201
209
|
export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
210
|
+
// CTC-4625: setup passes the install options through to its engine untouched, so they never meet
|
|
211
|
+
// this CLI's own flag tables.
|
|
212
|
+
if (argv[0] === "setup")
|
|
213
|
+
return runSetup(argv.slice(1), ctx, deps);
|
|
202
214
|
let args;
|
|
203
215
|
try {
|
|
204
216
|
args = parseArgs(argv.map((a, i) => (i === 0 && VERB_ALIASES[a] ? VERB_ALIASES[a] : a)));
|
|
@@ -206,11 +218,18 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
206
218
|
args.command = VERB_ALIASES[args.command];
|
|
207
219
|
}
|
|
208
220
|
catch (err) {
|
|
221
|
+
if (argvRequestsHeadless(argv, ctx.env))
|
|
222
|
+
return headlessUsageRefusal(ctx, err instanceof Error ? err.message : String(err), argv.includes("--json"), readManifest().version);
|
|
209
223
|
ctx.stderr(err instanceof Error ? err.message : String(err));
|
|
210
224
|
ctx.stderr(usageText());
|
|
211
225
|
return 1;
|
|
212
226
|
}
|
|
213
227
|
const manifest = readManifest();
|
|
228
|
+
if (args.json &&
|
|
229
|
+
(args.help || args.version) &&
|
|
230
|
+
args.command === "onboard" &&
|
|
231
|
+
onboardHeadlessRequested(args, ctx.env))
|
|
232
|
+
return headlessUsageRefusal(ctx, "--help and --version print text, not the headless document; run them without --json", true, manifest.version);
|
|
214
233
|
if (args.version) {
|
|
215
234
|
ctx.stdout(`${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
|
|
216
235
|
return 0;
|
|
@@ -223,6 +242,12 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
223
242
|
ctx.stdout(verbHelp(args.command));
|
|
224
243
|
return 0;
|
|
225
244
|
}
|
|
245
|
+
await maybePrintPublishedUpdateNotice(ctx, {
|
|
246
|
+
interactive: deps.isTty?.() ?? process.stdout.isTTY === true,
|
|
247
|
+
json: args.json,
|
|
248
|
+
command: args.command,
|
|
249
|
+
version: manifest.version,
|
|
250
|
+
});
|
|
226
251
|
try {
|
|
227
252
|
if (args.command !== "onboard") {
|
|
228
253
|
migrateLegacyCliPath(ctx);
|
|
@@ -234,116 +259,178 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
234
259
|
case "install":
|
|
235
260
|
return cmdInstall(args, ctx);
|
|
236
261
|
case "onboard": {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
262
|
+
// Setup may already own signal handlers; release them before any headless early exit.
|
|
263
|
+
if (onboardHeadlessRequested(args, ctx.env))
|
|
264
|
+
deps.setupUi?.dispose();
|
|
265
|
+
for (const flag of ["key-file"])
|
|
266
|
+
if (args.flags[flag] !== undefined && !onboardHeadlessRequested(args, ctx.env))
|
|
267
|
+
throw new UsageError(`--${flag} needs --headless`);
|
|
268
|
+
if (typeof args.flags.runner === "string" && !onboardHeadlessRequested(args, ctx.env))
|
|
269
|
+
throw new UsageError("--runner yes|no needs --headless; use --runner or --no-runner interactively");
|
|
270
|
+
// Headless runs drop the terminal UI, browser sign-in and every browser action (CTC-4633).
|
|
271
|
+
const runOnboard = async (args, ctx, headless) => {
|
|
272
|
+
let ui = headless ? undefined : deps.setupUi;
|
|
273
|
+
let terminal;
|
|
274
|
+
let input = process.stdin;
|
|
275
|
+
const originalRaw = Boolean(input.isRaw);
|
|
276
|
+
if (!headless && !ui && shouldUseOnboardUi(args, Boolean(process.stdout.isTTY))) {
|
|
277
|
+
if (!input.isTTY) {
|
|
278
|
+
try {
|
|
279
|
+
const fd = openSync("/dev/tty", "r");
|
|
280
|
+
try {
|
|
281
|
+
terminal = new TerminalInput(fd);
|
|
282
|
+
input = terminal;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
closeSync(fd);
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
ctx.stderr("No controlling terminal is available; using the displayed defaults.");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (input.isTTY) {
|
|
245
294
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
295
|
+
ui = createClackOnboardUi(await import("@clack/prompts"), { input, output: process.stdout }, {
|
|
296
|
+
baseUrl: () => savedOnboardBaseUrl(ctx.home),
|
|
297
|
+
verbose: args.flags.verbose === true,
|
|
298
|
+
renderer: createSetupRenderer(process.stdout, ctx.env),
|
|
299
|
+
disposeRenderer: true,
|
|
300
|
+
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
301
|
+
});
|
|
248
302
|
}
|
|
249
303
|
catch (error) {
|
|
250
|
-
|
|
304
|
+
terminal?.destroy();
|
|
251
305
|
throw error;
|
|
252
306
|
}
|
|
253
307
|
}
|
|
254
|
-
catch {
|
|
255
|
-
ctx.stderr("No controlling terminal is available; using the displayed defaults.");
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (input.isTTY) {
|
|
259
|
-
try {
|
|
260
|
-
ui = createClackOnboardUi(await import("@clack/prompts"), { input, output: process.stdout }, { baseUrl: () => savedOnboardBaseUrl(ctx.home) });
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
terminal?.destroy();
|
|
264
|
-
throw error;
|
|
265
|
-
}
|
|
266
308
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
309
|
+
if (!headless && !ui && !args.json && args.flags["dry-run"] !== true) {
|
|
310
|
+
const output = process.stdout.isTTY === true
|
|
311
|
+
? process.stdout
|
|
312
|
+
: new Writable({
|
|
313
|
+
write(chunk, _encoding, callback) {
|
|
314
|
+
ctx.stdout(chunk.toString().replace(/\n$/, ""));
|
|
315
|
+
callback();
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
ui = createClackOnboardUi(await import("@clack/prompts"), { input: process.stdin, output }, {
|
|
319
|
+
baseUrl: () => savedOnboardBaseUrl(ctx.home),
|
|
320
|
+
renderer: createSetupRenderer(output, ctx.env),
|
|
321
|
+
disposeRenderer: true,
|
|
280
322
|
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
},
|
|
296
|
-
});
|
|
323
|
+
interactive: false,
|
|
324
|
+
verbose: args.flags.verbose === true,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// Start with a full access token: a session saved long ago would otherwise expire part way
|
|
328
|
+
// through, or send the person back through the device sign-in. A dry run changes nothing.
|
|
329
|
+
if (args.flags["dry-run"] !== true)
|
|
330
|
+
await refreshSessionIfShort(ctx, ONBOARD_SESSION_MIN_MS);
|
|
331
|
+
const jsonLines = [];
|
|
332
|
+
const onboardCtx = args.json
|
|
333
|
+
? {
|
|
334
|
+
...ctx,
|
|
335
|
+
stdout: (line) => {
|
|
336
|
+
jsonLines.push(line);
|
|
297
337
|
},
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
teamIds: team ? [team] : undefined,
|
|
310
|
-
localSync: journal?.localSync ?? args.flags["local-sync"] === true,
|
|
311
|
-
observe: observeCloudOnboarding,
|
|
312
|
-
});
|
|
313
|
-
const failed = report.checks.some((check) => check.required && check.state === "fail") && !onboardRequiredStepsWaiting(journal);
|
|
314
|
-
return {
|
|
315
|
-
state: report.state === "complete"
|
|
316
|
-
? "done"
|
|
317
|
-
: failed
|
|
318
|
-
? "failed"
|
|
319
|
-
: "waiting",
|
|
320
|
-
reason: report.state === "complete"
|
|
321
|
-
? undefined
|
|
322
|
-
: "onboarding_checks_pending",
|
|
323
|
-
evidence: {
|
|
324
|
-
checks: report.checks.length,
|
|
325
|
-
passed: report.checks.filter((check) => check.state === "pass").length,
|
|
326
|
-
},
|
|
327
|
-
};
|
|
338
|
+
}
|
|
339
|
+
: ctx;
|
|
340
|
+
let code;
|
|
341
|
+
try {
|
|
342
|
+
code = await cmdOnboard(args, onboardCtx, ((value) => (headless ? headlessOnboardDeps(value, headless, args.flags.runner === true) : value))({
|
|
343
|
+
...(deps.setupApproved
|
|
344
|
+
? { reviewedSetup: true, confirm: async () => true }
|
|
345
|
+
: {}),
|
|
346
|
+
beforeStep: async () => {
|
|
347
|
+
if (args.flags["dry-run"] !== true)
|
|
348
|
+
await refreshSessionIfShort(ctx, ONBOARD_STEP_MIN_MS);
|
|
328
349
|
},
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
350
|
+
...createOnboardRuntime(args, ctx, {
|
|
351
|
+
ui,
|
|
352
|
+
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
353
|
+
stageSignin: (signal) => {
|
|
354
|
+
if (args.key || ctx.env.CATALYST_CLOUD_TOKEN)
|
|
355
|
+
throw new CliError("Connect your personal key with catalyst login, then run catalyst onboard. Setup will not replace a supplied key with browser sign-in.", "onboard-person-required", 12);
|
|
356
|
+
return stageOnboardLogin({
|
|
357
|
+
...ctx,
|
|
358
|
+
stdout: ui
|
|
359
|
+
? (line) => ui.message(line)
|
|
360
|
+
: ctx.stderr,
|
|
361
|
+
}, {
|
|
362
|
+
baseUrl: args.baseUrl,
|
|
363
|
+
signal,
|
|
364
|
+
timeoutMs: deps.onboardSigninTimeoutMs,
|
|
365
|
+
device: {
|
|
366
|
+
isTty: deps.isTty ?? (() => Boolean(process.stdout.isTTY)),
|
|
367
|
+
openBrowser: deps.openBrowser ?? defaultOpenBrowser,
|
|
368
|
+
sleep: deps.sleep,
|
|
369
|
+
waitForApproval: ui
|
|
370
|
+
? (run) => ui.wait("Waiting for Catalyst approval", run)
|
|
371
|
+
: undefined,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
login: (stepCtx, signal) => cmdLogin({ ...args, command: "login", flags: {}, json: false }, stepCtx, {
|
|
376
|
+
...deps,
|
|
377
|
+
signal,
|
|
378
|
+
isTty: () => true,
|
|
379
|
+
waitForApproval: ui
|
|
380
|
+
? (run) => ui.wait("Waiting for Catalyst approval", run)
|
|
381
|
+
: undefined,
|
|
382
|
+
}, true),
|
|
383
|
+
ready: async (stepCtx, journal) => {
|
|
384
|
+
const team = selectedOnboardTeam(journal);
|
|
385
|
+
const report = await onboardingReadyReport(stepCtx, {
|
|
386
|
+
teamIds: team ? [team] : undefined,
|
|
387
|
+
localSync: journal?.localSync ?? args.flags["local-sync"] === true,
|
|
388
|
+
observe: observeCloudOnboarding,
|
|
389
|
+
});
|
|
390
|
+
const failed = report.checks.some((check) => check.required && check.state === "fail") && !onboardRequiredStepsWaiting(journal);
|
|
391
|
+
return {
|
|
392
|
+
state: report.state === "complete"
|
|
393
|
+
? "done"
|
|
394
|
+
: failed
|
|
395
|
+
? "failed"
|
|
396
|
+
: "waiting",
|
|
397
|
+
reason: report.state === "complete"
|
|
398
|
+
? undefined
|
|
399
|
+
: "onboarding_checks_pending",
|
|
400
|
+
evidence: {
|
|
401
|
+
checks: report.checks.length,
|
|
402
|
+
passed: report.checks.filter((check) => check.state === "pass").length,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
legacy: deps.legacy,
|
|
407
|
+
openBrowser: deps.openBrowser,
|
|
408
|
+
sleep: deps.sleep,
|
|
409
|
+
skillNames: CUSTOMER_SKILLS,
|
|
410
|
+
}),
|
|
411
|
+
...(deps.onboardBootstrap
|
|
412
|
+
? { bootstrap: deps.onboardBootstrap }
|
|
413
|
+
: {}),
|
|
414
|
+
...(deps.isTty ? { isTty: deps.isTty } : {}),
|
|
415
|
+
...(deps.signal ? { signal: deps.signal } : {}),
|
|
416
|
+
}), readManifest().version);
|
|
417
|
+
}
|
|
418
|
+
finally {
|
|
419
|
+
ui?.dispose();
|
|
420
|
+
if (input.isTTY && input.isRaw !== originalRaw)
|
|
421
|
+
input.setRawMode(originalRaw);
|
|
422
|
+
terminal?.destroy();
|
|
423
|
+
}
|
|
424
|
+
for (const line of jsonLines)
|
|
425
|
+
ctx.stdout(line);
|
|
426
|
+
return code;
|
|
427
|
+
};
|
|
428
|
+
if (!onboardHeadlessRequested(args, ctx.env))
|
|
429
|
+
return await runOnboard(args, ctx, null);
|
|
430
|
+
return await runOnboardHeadless(args, ctx, {
|
|
431
|
+
login: (key, baseUrl, loginCtx) => cmdLogin({ ...args, command: "login", key, baseUrl, flags: {}, json: false }, loginCtx, { ...deps, isTty: () => false }, true),
|
|
432
|
+
onboard: (onboardArgs, onboardCtx, tracker) => runOnboard(onboardArgs, onboardCtx, tracker),
|
|
433
|
+
}, readManifest().version);
|
|
347
434
|
}
|
|
348
435
|
case "notice":
|
|
349
436
|
return 0;
|
|
@@ -435,6 +522,11 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
435
522
|
ctx.stderr(`catalyst: ${err.message}`);
|
|
436
523
|
return 2;
|
|
437
524
|
}
|
|
525
|
+
if (args.command === "onboard" && args.json) {
|
|
526
|
+
ctx.stdout(JSON.stringify(onboardErrorJournal(ctx, manifest.version, 10)));
|
|
527
|
+
ctx.stderr("catalyst: onboarding could not finish. Run catalyst onboard to try again.");
|
|
528
|
+
return 10;
|
|
529
|
+
}
|
|
438
530
|
throw err;
|
|
439
531
|
}
|
|
440
532
|
}
|
|
@@ -472,6 +564,74 @@ const VERB_HELP_KNOWN = Object.fromEntries([
|
|
|
472
564
|
"legacy",
|
|
473
565
|
"onboard",
|
|
474
566
|
].map((v) => [v, true]));
|
|
567
|
+
/** CTC-4625: `catalyst setup`, wired to this terminal, @clack/prompts and the keyless login. */
|
|
568
|
+
async function runSetup(argv, ctx, deps) {
|
|
569
|
+
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
570
|
+
ctx.stdout(verbHelp("setup"));
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
const clack = await import("@clack/prompts");
|
|
574
|
+
const terminal = openSetupTerminal(process.stdout);
|
|
575
|
+
const spinner = clackSetupSpinner(clack, Boolean(process.stdin.isTTY));
|
|
576
|
+
try {
|
|
577
|
+
return await cmdSetup(argv, ctx, {
|
|
578
|
+
stdout: process.stdout,
|
|
579
|
+
stderr: process.stderr,
|
|
580
|
+
writeJson: (chunk) => process.stdout.write(chunk),
|
|
581
|
+
interactive: terminal !== null,
|
|
582
|
+
...(terminal ? { ask: clackSetupAsk(clack, terminal) } : {}),
|
|
583
|
+
spinner,
|
|
584
|
+
onboard: async (renderer, flags) => {
|
|
585
|
+
// This returns through the dispatcher without launching another CLI or frame.
|
|
586
|
+
const onboardArgs = [
|
|
587
|
+
"onboard",
|
|
588
|
+
...(flags.json ? ["--json"] : []),
|
|
589
|
+
...(flags.verbose ? ["--verbose"] : []),
|
|
590
|
+
...(flags.localSync ? ["--local-sync"] : []),
|
|
591
|
+
...(flags.yes || flags.json || !terminal ? ["--yes"] : []),
|
|
592
|
+
];
|
|
593
|
+
let ui;
|
|
594
|
+
if (!flags.json) {
|
|
595
|
+
ui = createClackOnboardUi(clack, setupOnboardStreams(terminal), {
|
|
596
|
+
baseUrl: () => savedOnboardBaseUrl(ctx.home),
|
|
597
|
+
renderer,
|
|
598
|
+
logPath: () => flags.logPath,
|
|
599
|
+
verbose: flags.verbose,
|
|
600
|
+
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
601
|
+
introduced: true,
|
|
602
|
+
consentGiven: true,
|
|
603
|
+
interactive: terminal !== null && !flags.yes,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return main(onboardArgs, ctx, {
|
|
607
|
+
...deps,
|
|
608
|
+
setupUi: ui,
|
|
609
|
+
setupApproved: true,
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
signin: (seconds, r, stop, spin) => runSetupSignin(ctx, r, seconds, {
|
|
613
|
+
interrupted: stop,
|
|
614
|
+
spinner: spin,
|
|
615
|
+
login: async (loginCtx, login) => {
|
|
616
|
+
const code = await cmdLogin(parseArgs(["login"]), loginCtx, {
|
|
617
|
+
...deps,
|
|
618
|
+
isTty: deps.isTty ?? (() => terminal !== null),
|
|
619
|
+
signal: login.signal,
|
|
620
|
+
waitForApproval: login.waitForApproval,
|
|
621
|
+
presentCode: login.present,
|
|
622
|
+
presentBrowser: login.presentBrowser,
|
|
623
|
+
});
|
|
624
|
+
if (code === 0)
|
|
625
|
+
keepInstalledCliPath(ctx, cliPath());
|
|
626
|
+
return code;
|
|
627
|
+
},
|
|
628
|
+
}),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
finally {
|
|
632
|
+
terminal?.close();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
475
635
|
async function cmdLogin(args, ctx, deps, personalOnly = false) {
|
|
476
636
|
const checkCancelled = () => {
|
|
477
637
|
if (deps.signal?.aborted)
|
|
@@ -492,6 +652,8 @@ async function cmdLogin(args, ctx, deps, personalOnly = false) {
|
|
|
492
652
|
sleep: deps.sleep,
|
|
493
653
|
signal: deps.signal,
|
|
494
654
|
waitForApproval: deps.waitForApproval,
|
|
655
|
+
present: deps.presentCode,
|
|
656
|
+
presentBrowser: deps.presentBrowser,
|
|
495
657
|
})).accessToken;
|
|
496
658
|
checkCancelled();
|
|
497
659
|
const me = await fetchMe(baseUrl, bearer, ctx.fetch);
|
package/dist/oauth.js
CHANGED
|
@@ -126,27 +126,45 @@ export async function deviceFlowLogin(ctx, baseUrl, deps = {}) {
|
|
|
126
126
|
cancelled();
|
|
127
127
|
const auth = await deviceAuthorize(ctx, discovery);
|
|
128
128
|
cancelled();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
browserOpened =
|
|
142
|
-
|
|
129
|
+
const opensBrowser = round === 1 ? (deps.isTty ?? (() => false))() : browserOpened;
|
|
130
|
+
if (deps.present) {
|
|
131
|
+
deps.present({
|
|
132
|
+
verificationUri: auth.verification_uri,
|
|
133
|
+
userCode: auth.user_code,
|
|
134
|
+
...(auth.verification_uri_complete
|
|
135
|
+
? { completeUri: auth.verification_uri_complete }
|
|
136
|
+
: {}),
|
|
137
|
+
round,
|
|
138
|
+
rounds: MAX_DEVICE_CODES,
|
|
139
|
+
});
|
|
140
|
+
if (opensBrowser) {
|
|
141
|
+
browserOpened = await tryOpenBrowser(deps, auth);
|
|
142
|
+
deps.presentBrowser?.(browserOpened);
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
if (round > 1)
|
|
147
|
+
ctx.stdout(`That code expired. Here is a new one (${round} of ${MAX_DEVICE_CODES}):`);
|
|
148
|
+
ctx.stdout("");
|
|
149
|
+
ctx.stdout(`To connect this machine, visit: ${auth.verification_uri}`);
|
|
150
|
+
ctx.stdout(`and enter the code: ${auth.user_code}`);
|
|
151
|
+
if (auth.verification_uri_complete)
|
|
152
|
+
ctx.stdout(`Or open this link, which fills the code in: ${auth.verification_uri_complete}`);
|
|
153
|
+
if (opensBrowser) {
|
|
154
|
+
try {
|
|
155
|
+
if (!deps.openBrowser)
|
|
156
|
+
throw new Error("browser-unavailable");
|
|
157
|
+
await deps.openBrowser(auth.verification_uri_complete ?? auth.verification_uri);
|
|
158
|
+
browserOpened = true;
|
|
159
|
+
ctx.stdout("Opened your browser to that page — approve there, or use the code above.");
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// a browser that will not open is not a failure; the code and URL still work
|
|
163
|
+
ctx.stdout("Open the link above in a browser.");
|
|
164
|
+
}
|
|
147
165
|
}
|
|
166
|
+
ctx.stdout("Waiting for you to approve… (Ctrl-C to cancel)");
|
|
148
167
|
}
|
|
149
|
-
ctx.stdout("Waiting for you to approve… (Ctrl-C to cancel)");
|
|
150
168
|
const poll = () => pollDeviceCode(ctx, discovery, auth, deps.sleep, deps.signal);
|
|
151
169
|
const tokens = deps.waitForApproval
|
|
152
170
|
? await deps.waitForApproval(poll)
|
|
@@ -159,6 +177,18 @@ export async function deviceFlowLogin(ctx, baseUrl, deps = {}) {
|
|
|
159
177
|
// unchanged from before CTC-2136; only the line is new, and its wording is Ryan's.
|
|
160
178
|
throw new CliError(`The sign-in code expired ${MAX_DEVICE_CODES} times. Run the same command again when you are ready to approve.`, "login-expired");
|
|
161
179
|
}
|
|
180
|
+
/** Opens the browser at the code's page; false when it would not open (never a failure). */
|
|
181
|
+
async function tryOpenBrowser(deps, auth) {
|
|
182
|
+
try {
|
|
183
|
+
if (!deps.openBrowser)
|
|
184
|
+
throw new Error("browser-unavailable");
|
|
185
|
+
await deps.openBrowser(auth.verification_uri_complete ?? auth.verification_uri);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
162
192
|
/**
|
|
163
193
|
* Poll one device code until it yields a token pair or expires. Terminal refusals (`access_denied`,
|
|
164
194
|
* a non-transient error) throw; expiry, by the server's `expired_token` or the code's own
|
package/dist/onboard-accounts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
2
2
|
import { readExistingOnboardJson } from "./onboard-existing.js";
|
|
3
|
+
import { pollConsent } from "./onboard-consent.js";
|
|
3
4
|
import { verifyOnboardRoutes } from "./onboard-capabilities.js";
|
|
4
5
|
const listPath = "/api/v1/coding-accounts";
|
|
5
6
|
const validatePath = "/api/v1/coding-accounts/:slot/validate";
|
|
@@ -99,7 +100,8 @@ export function onboardAccountsAdapter(input = {}) {
|
|
|
99
100
|
let proof = null;
|
|
100
101
|
let attempted = false;
|
|
101
102
|
const candidate = (slots, now) => slots
|
|
102
|
-
.filter((row) => row.
|
|
103
|
+
.filter((row) => (input.slot === undefined || row.accountSlot === input.slot) &&
|
|
104
|
+
row.provider === "claude" &&
|
|
103
105
|
row.declaredState === "active" &&
|
|
104
106
|
row.ownedByMe &&
|
|
105
107
|
!row.needsCredential &&
|
|
@@ -139,7 +141,13 @@ export function onboardAccountsAdapter(input = {}) {
|
|
|
139
141
|
};
|
|
140
142
|
proof = null;
|
|
141
143
|
if (!live.slots.length)
|
|
142
|
-
return
|
|
144
|
+
return input.waitForAccount &&
|
|
145
|
+
["owner", "admin"].includes(live.identity.role)
|
|
146
|
+
? { state: "pending" }
|
|
147
|
+
: waiting("account_enrollment_required");
|
|
148
|
+
if (input.slot !== undefined &&
|
|
149
|
+
!live.slots.some((row) => row.accountSlot === input.slot))
|
|
150
|
+
return waiting("coding_account_not_found");
|
|
143
151
|
if (!selected)
|
|
144
152
|
return waiting(live.slots.some((row) => row.provider === "codex")
|
|
145
153
|
? "codex_provider_access_unverified"
|
|
@@ -156,9 +164,30 @@ export function onboardAccountsAdapter(input = {}) {
|
|
|
156
164
|
act: async (ctx, journal, external) => {
|
|
157
165
|
if (attempted)
|
|
158
166
|
return waiting("account_provider_access_unverified");
|
|
159
|
-
const
|
|
160
|
-
if ("reason" in
|
|
161
|
-
return waiting(
|
|
167
|
+
const initial = await inventory(ctx, journal, external);
|
|
168
|
+
if ("reason" in initial)
|
|
169
|
+
return waiting(initial.reason);
|
|
170
|
+
let live = initial;
|
|
171
|
+
if (!live.slots.length &&
|
|
172
|
+
input.waitForAccount &&
|
|
173
|
+
["owner", "admin"].includes(live.identity.role)) {
|
|
174
|
+
const result = await input.waitForAccount(() => pollConsent({
|
|
175
|
+
timeoutMs: input.accountWaitMs,
|
|
176
|
+
signal: external,
|
|
177
|
+
sleep: input.sleep,
|
|
178
|
+
readStatus: async (signal) => {
|
|
179
|
+
const current = await inventory(ctx, journal, signal);
|
|
180
|
+
if ("reason" in current)
|
|
181
|
+
return { outcome: "waiting", reason: current.reason };
|
|
182
|
+
live = current;
|
|
183
|
+
return { outcome: current.slots.length ? "connected" : "absent" };
|
|
184
|
+
},
|
|
185
|
+
}));
|
|
186
|
+
if (result.state !== "done")
|
|
187
|
+
return waiting(result.reason === "consent_timeout"
|
|
188
|
+
? "account_enrollment_required"
|
|
189
|
+
: (result.reason ?? "account_enrollment_required"));
|
|
190
|
+
}
|
|
162
191
|
const selected = candidate(live.slots, ctx.now().getTime());
|
|
163
192
|
if (!selected)
|
|
164
193
|
return waiting("account_provider_access_unverified");
|