@catalyst-cloud/cli 0.15.2 → 0.15.4
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 +13 -0
- package/README.md +4 -2
- package/dist/args.js +5 -1
- package/dist/cli.js +44 -20
- package/dist/oauth.js +1 -1
- package/dist/onboard-accounts.js +67 -171
- package/dist/onboard-checklist.js +310 -0
- package/dist/onboard-existing.js +1 -1
- package/dist/onboard-first-ticket.js +199 -0
- package/dist/onboard-headless.js +2 -2
- package/dist/onboard-housekeeping.js +93 -0
- package/dist/onboard-next.js +108 -9
- package/dist/onboard-ready.js +61 -0
- package/dist/onboard-runtime.js +60 -27
- package/dist/onboard-standalone-copy.js +73 -66
- package/dist/onboard-ui.js +278 -105
- package/dist/onboard-values.js +47 -0
- package/dist/onboard.js +63 -18
- package/dist/ready-copy.js +87 -0
- package/dist/ready.js +1 -57
- package/dist/setup-brand.js +202 -0
- package/dist/setup-onboard-copy.js +314 -42
- package/dist/setup-render.js +99 -17
- package/dist/setup.js +6 -3
- package/dist/terminal-background.js +220 -0
- package/package.json +1 -1
- 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 +1 -1
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.4
|
|
4
|
+
|
|
5
|
+
- Setup shows a colour mark and clearer progress, with a plain display when your terminal needs it.
|
|
6
|
+
- Setup separates your next steps from tasks waiting on an owner or admin, and reports when it stops waiting for an account connection.
|
|
7
|
+
- Setup wraps long text by words and keeps numbered steps in order.
|
|
8
|
+
|
|
9
|
+
## 0.15.3
|
|
10
|
+
|
|
11
|
+
- Setup can finish once the steps needed to run work are ready. It lists optional next steps and keeps your first-ticket choice when you resume.
|
|
12
|
+
- Setup shows one progress display with numbered steps and the reason a step needs attention.
|
|
13
|
+
- When the Connect accounts page is available, setup uses it to finish account connections. Any usable AI account counts without a test request.
|
|
14
|
+
- Setup offers API keys by default. Subscription account options appear only when your workspace enables them.
|
|
15
|
+
|
|
3
16
|
## 0.15.2
|
|
4
17
|
|
|
5
18
|
- `catalyst setup` is the command to set up Catalyst. It runs the same steps as `catalyst onboard`, which still works.
|
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ The command is `catalyst`. `catalyst-skills` still works as a deprecated alias,
|
|
|
111
111
|
|
|
112
112
|
## What this is
|
|
113
113
|
|
|
114
|
-
A set of skills that let your coding agent run your own Catalyst Cloud account (https://staging.catalystcloud.dev) from your seat: what is happening, what needs you, and what to do about it. They read your account through the Catalyst Cloud SDK, write to it through the account's agent proxy, and never compose a URL or run a tool of their own; every read, write and
|
|
114
|
+
A set of skills that let your coding agent run your own Catalyst Cloud account (https://staging.catalystcloud.dev) from your seat: what is happening, what needs you, and what to do about it. They read your account through the Catalyst Cloud SDK, write to it through the account's agent proxy, and never compose a URL or run a tool of their own; every read, write and live watch is a `catalyst` verb with `--help`. Every skill is plain Markdown under `skills/<name>/SKILL.md` in this repository, and your own login — a keyless device-code session, or a personal key — is the only credential. Your agent acts as you: the asks it raises and the comments it posts carry your name, and "what needs me" means you.
|
|
115
115
|
|
|
116
116
|
## What connecting does
|
|
117
117
|
|
|
@@ -133,6 +133,8 @@ catalyst onboard
|
|
|
133
133
|
catalyst ready --onboarding --json
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
`catalyst setup` opens with the Catalyst Cloud mark in your terminal's colours. It asks the terminal whether its background is light or dark; set `CATALYST_THEME=light` or `CATALYST_THEME=dark` when it guesses wrong. `--no-color` or `NO_COLOR` prints plain text without the mark.
|
|
137
|
+
|
|
136
138
|
The dry run writes no files. `--only <step>` runs one step and reports its scope separately from full onboarding. Exit 10 means a failed step, 11 means waiting, and 12 means a guard refused the action. The private receipt is `install/last-run.json` under your machine's Catalyst state directory; credentials stay in the login config. A fake HOME reports old services and keeps them intact.
|
|
137
139
|
|
|
138
140
|
### Headless setup (CI, image builds, agent VMs)
|
|
@@ -231,7 +233,7 @@ Some of these only read, and some write: a comment, a card move, an ask, a relea
|
|
|
231
233
|
|
|
232
234
|
## What a key cannot see yet
|
|
233
235
|
|
|
234
|
-
Your personal key reads everything the skills need — tickets, pull requests, the eligibility explainer, the dispatch queue, fleet activity, per-ticket execution history (`catalyst explain --history <ticket>`: phase attempts, remediation rounds, park state) and coding-account status (`catalyst accounts`: provider, declared and observed state,
|
|
236
|
+
Your personal key reads everything the skills need — tickets, pull requests, the eligibility explainer, the dispatch queue, fleet activity, per-ticket execution history (`catalyst explain --history <ticket>`: phase attempts, remediation rounds, park state) and coding-account status (`catalyst accounts`: provider, declared and observed state, usage limits, walls, quarantine — never a credential; enrolling or pausing one is `<your cloud>/settings/coding-accounts`). It also releases a parked or held ticket once its cause is fixed: `catalyst release <ticket> --because <what changed>` (the `unstick` skill runs it), recorded against your name and shown in the ticket's history. And it declares what your containers need: `catalyst environment` reads the account-wide declaration, `environment propose --file <path> --approve` proposes and approves it in one compare-and-set, and the values behind the names stay in the cloud — reading needs any active seat, proposing and approving need an admin or owner one. An admin or owner can put a repository's values in from the terminal. `catalyst secret import .env --repo owner/name` stores every name in the file and lists the declared names that still have no value. `catalyst secret set NAME --repo owner/name --command 'op read op://Vault/item/field'` runs the command on your machine and stores its output; with no `--command` it reads the value from stdin, or asks for it without echoing. No value is ever printed, and the cloud's audit records the command, not its output. That is the account's own declaration — a repository's own `.catalyst/catalyst.toml`, which `catalyst-onboard` already reports on, is a separate thing. `catalyst env inventory` lists the environment variable names a repository needs — never a value; `env check` checks the TOML syntax and environment variable table in `.catalyst/catalyst.toml` offline; `env migrate [catalyst.env.json]` prints a TOML environment table from a legacy declaration, with all names optional and values omitted. These commands need no login (the `what-this-repo-needs` skill walks through them). Two things it cannot do, and the skills say so by name rather than guess:
|
|
235
237
|
|
|
236
238
|
- Read pull-request labels or the reviewer's reaction. The mirror does not carry them; GitHub's own page does.
|
|
237
239
|
- Compute a flow number — cycle time, throughput, or how long pull requests have been open. Nothing serves those yet, so say they are not computed rather than counting something else and calling it that.
|
package/dist/args.js
CHANGED
|
@@ -64,6 +64,10 @@ export const FLAG_TABLES = {
|
|
|
64
64
|
value: false,
|
|
65
65
|
help: "do not run Catalyst's work on this machine, and do not ask",
|
|
66
66
|
},
|
|
67
|
+
"no-color": {
|
|
68
|
+
value: false,
|
|
69
|
+
help: "print setup in plain text, without colour or the Catalyst Cloud mark",
|
|
70
|
+
},
|
|
67
71
|
"resume-from": {
|
|
68
72
|
value: true,
|
|
69
73
|
help: "resume at a verified onboarding step",
|
|
@@ -459,7 +463,7 @@ export const FLAG_TABLES = {
|
|
|
459
463
|
export const VERB_USAGE = {
|
|
460
464
|
setup: "setup [onboard options] (sets up Catalyst step by step; same as catalyst onboard) | setup --engine <file> --engine-sha256 <hex> [-- <install options>] (run by install.sh; install.sh --help lists the options)",
|
|
461
465
|
login: "login [--base-url <url>] [--start-replica] (keyless; or --key <personal-key> / CATALYST_CLOUD_TOKEN)",
|
|
462
|
-
onboard: "onboard [--team <ID|key>] [--repo <owner/name>]... [--coding-account <slot>] [--resume-from <step>] [--only <step>] [--local-sync] [--runner|--no-runner] [--yes] [--dry-run] [--json] | onboard --headless [--key-file <path>] [--team ...] [--repo ...]... [--coding-account ...] [--runner yes|no] [--json]",
|
|
466
|
+
onboard: "onboard [--team <ID|key>] [--repo <owner/name>]... [--coding-account <slot>] [--resume-from <step>] [--only <step>] [--local-sync] [--runner|--no-runner] [--no-color] [--yes] [--dry-run] [--json] | onboard --headless [--key-file <path>] [--team ...] [--repo ...]... [--coding-account ...] [--runner yes|no] [--json]",
|
|
463
467
|
install: "install [--skills-dir <dir>] [--force]",
|
|
464
468
|
status: "status",
|
|
465
469
|
notice: "notice",
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
+
import { createSetupRenderer, setupBanner, } from "./setup-render.js";
|
|
4
4
|
import { existsSync, openSync, closeSync } from "node:fs";
|
|
5
5
|
import { ReadStream as TerminalInput } from "node:tty";
|
|
6
6
|
import { Writable } from "node:stream";
|
|
@@ -44,12 +44,12 @@ import { cmdRepo } from "./repo.js";
|
|
|
44
44
|
import { cmdLegacy } from "./legacy.js";
|
|
45
45
|
import { cmdIdentity } from "./identity.js";
|
|
46
46
|
import { cmdConnections } from "./connections.js";
|
|
47
|
-
import { cmdOnboard, onboardErrorJournal,
|
|
47
|
+
import { cmdOnboard, onboardErrorJournal, } from "./onboard.js";
|
|
48
48
|
import { createClackOnboardUi, shouldUseOnboardUi, } from "./onboard-ui.js";
|
|
49
49
|
import { createOnboardRuntime } from "./onboard-runtime.js";
|
|
50
50
|
import { argvRequestsHeadless, headlessOnboardDeps, headlessUsageRefusal, onboardHeadlessRequested, runOnboardHeadless, } from "./onboard-headless.js";
|
|
51
51
|
import { savedOnboardBaseUrl } from "./onboard-next.js";
|
|
52
|
-
import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
|
|
52
|
+
import { onboardingReadyReport, onboardReadyStep, observeCloudOnboarding, } from "./onboard-ready.js";
|
|
53
53
|
import { selectedOnboardTeam } from "./onboard-existing.js";
|
|
54
54
|
export { CONFIG_MODE, DEFAULT_BASE_URL, FIRST_STAMPED_VERSION, LEGACY_PACKAGE_NAME, PACKAGE_NAME, PROVENANCE_MARKER, CliError, MeError, UsageError, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, fetchMe, formatMode, installedBundleVersion, installSkills, loadConfig, modernCliPath, normalizeBaseUrl, parseArgs, parseChangelogEntry, parseProvenanceVersion, readChangelog, readManifest, resolveSkillsDir, saveConfig, skillsSourceDir, updateNoticeLine, writeConfig, };
|
|
55
55
|
export const CUSTOMER_SKILLS = [
|
|
@@ -210,6 +210,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
210
210
|
// CTC-4625: setup passes the install options through to its engine untouched, so they never meet
|
|
211
211
|
// this CLI's own flag tables. CTC-4680: without --engine, `setup` is the name a person types for
|
|
212
212
|
// the onboard flow, so it runs exactly that.
|
|
213
|
+
const viaSetup = drawsMark(argv);
|
|
213
214
|
if (argv[0] === "setup") {
|
|
214
215
|
const rest = argv.slice(1);
|
|
215
216
|
const own = rest.indexOf("--") === -1 ? rest : rest.slice(0, rest.indexOf("--"));
|
|
@@ -278,6 +279,9 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
278
279
|
// Headless runs drop the terminal UI, browser sign-in and every browser action (CTC-4633).
|
|
279
280
|
const runOnboard = async (args, ctx, headless) => {
|
|
280
281
|
let ui = headless ? undefined : deps.setupUi;
|
|
282
|
+
// --no-color reaches the renderer as NO_COLOR, so the banner and every step row go plain.
|
|
283
|
+
const noColor = args.flags["no-color"] === true;
|
|
284
|
+
const rendererEnv = noColor ? { ...ctx.env, NO_COLOR: "1" } : ctx.env;
|
|
281
285
|
let terminal;
|
|
282
286
|
let input = process.stdin;
|
|
283
287
|
const originalRaw = Boolean(input.isRaw);
|
|
@@ -300,12 +304,21 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
300
304
|
}
|
|
301
305
|
if (input.isTTY) {
|
|
302
306
|
try {
|
|
307
|
+
const banner = await setupBanner({
|
|
308
|
+
setup: viaSetup,
|
|
309
|
+
env: rendererEnv,
|
|
310
|
+
input,
|
|
311
|
+
stdout: process.stdout,
|
|
312
|
+
stream: bannerStream(ctx),
|
|
313
|
+
noColor,
|
|
314
|
+
});
|
|
303
315
|
ui = createClackOnboardUi(await import("@clack/prompts"), { input, output: process.stdout }, {
|
|
304
316
|
baseUrl: () => savedOnboardBaseUrl(ctx.home),
|
|
305
317
|
verbose: args.flags.verbose === true,
|
|
306
|
-
renderer: createSetupRenderer(process.stdout,
|
|
318
|
+
renderer: createSetupRenderer(process.stdout, rendererEnv, banner),
|
|
307
319
|
disposeRenderer: true,
|
|
308
320
|
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
321
|
+
version: manifest.version,
|
|
309
322
|
});
|
|
310
323
|
}
|
|
311
324
|
catch (error) {
|
|
@@ -325,9 +338,17 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
325
338
|
});
|
|
326
339
|
ui = createClackOnboardUi(await import("@clack/prompts"), { input: process.stdin, output }, {
|
|
327
340
|
baseUrl: () => savedOnboardBaseUrl(ctx.home),
|
|
328
|
-
renderer: createSetupRenderer(output,
|
|
341
|
+
renderer: createSetupRenderer(output, rendererEnv, await setupBanner({
|
|
342
|
+
setup: viaSetup,
|
|
343
|
+
env: rendererEnv,
|
|
344
|
+
input: process.stdin,
|
|
345
|
+
stdout: process.stdout,
|
|
346
|
+
stream: bannerStream(ctx),
|
|
347
|
+
noColor,
|
|
348
|
+
})),
|
|
329
349
|
disposeRenderer: true,
|
|
330
350
|
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
351
|
+
version: manifest.version,
|
|
331
352
|
interactive: false,
|
|
332
353
|
verbose: args.flags.verbose === true,
|
|
333
354
|
});
|
|
@@ -395,21 +416,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
395
416
|
localSync: journal?.localSync ?? args.flags["local-sync"] === true,
|
|
396
417
|
observe: observeCloudOnboarding,
|
|
397
418
|
});
|
|
398
|
-
|
|
399
|
-
return {
|
|
400
|
-
state: report.state === "complete"
|
|
401
|
-
? "done"
|
|
402
|
-
: failed
|
|
403
|
-
? "failed"
|
|
404
|
-
: "waiting",
|
|
405
|
-
reason: report.state === "complete"
|
|
406
|
-
? undefined
|
|
407
|
-
: "onboarding_checks_pending",
|
|
408
|
-
evidence: {
|
|
409
|
-
checks: report.checks.length,
|
|
410
|
-
passed: report.checks.filter((check) => check.state === "pass").length,
|
|
411
|
-
},
|
|
412
|
-
};
|
|
419
|
+
return onboardReadyStep(report, journal);
|
|
413
420
|
},
|
|
414
421
|
legacy: deps.legacy,
|
|
415
422
|
openBrowser: deps.openBrowser,
|
|
@@ -572,6 +579,23 @@ const VERB_HELP_KNOWN = Object.fromEntries([
|
|
|
572
579
|
"legacy",
|
|
573
580
|
"onboard",
|
|
574
581
|
].map((v) => [v, true]));
|
|
582
|
+
/** CTC-4680: the Pixel Nucleus mark belongs to `catalyst setup` alone. `setup` runs the onboard
|
|
583
|
+
* flow, so this is read from the verb typed, before setup becomes onboard. */
|
|
584
|
+
export function drawsMark(argv) {
|
|
585
|
+
return argv[0] === "setup";
|
|
586
|
+
}
|
|
587
|
+
/** CTC-4680: the banner goes to stderr, through ctx when stderr is not a terminal. */
|
|
588
|
+
function bannerStream(ctx) {
|
|
589
|
+
if (process.stderr.isTTY === true)
|
|
590
|
+
return process.stderr;
|
|
591
|
+
return {
|
|
592
|
+
write(chunk) {
|
|
593
|
+
for (const line of chunk.replace(/\n$/, "").split("\n"))
|
|
594
|
+
ctx.stderr(line);
|
|
595
|
+
return true;
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
}
|
|
575
599
|
/** CTC-4625: `catalyst setup`, wired to this terminal, @clack/prompts and the keyless login. */
|
|
576
600
|
async function runSetup(argv, ctx, deps) {
|
|
577
601
|
if (argv[0] === "--help" || argv[0] === "-h") {
|
package/dist/oauth.js
CHANGED
|
@@ -156,7 +156,7 @@ export async function deviceFlowLogin(ctx, baseUrl, deps = {}) {
|
|
|
156
156
|
throw new Error("browser-unavailable");
|
|
157
157
|
await deps.openBrowser(auth.verification_uri_complete ?? auth.verification_uri);
|
|
158
158
|
browserOpened = true;
|
|
159
|
-
ctx.stdout("Opened your browser to that page
|
|
159
|
+
ctx.stdout("Opened your browser to that page. Approve there, or use the code above.");
|
|
160
160
|
}
|
|
161
161
|
catch {
|
|
162
162
|
// a browser that will not open is not a failure; the code and URL still work
|
package/dist/onboard-accounts.js
CHANGED
|
@@ -3,7 +3,6 @@ import { readExistingOnboardJson } from "./onboard-existing.js";
|
|
|
3
3
|
import { pollConsent } from "./onboard-consent.js";
|
|
4
4
|
import { verifyOnboardRoutes } from "./onboard-capabilities.js";
|
|
5
5
|
const listPath = "/api/v1/coding-accounts";
|
|
6
|
-
const validatePath = "/api/v1/coding-accounts/:slot/validate";
|
|
7
6
|
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
8
7
|
? value
|
|
9
8
|
: null;
|
|
@@ -94,195 +93,92 @@ async function inventory(ctx, journal, signal) {
|
|
|
94
93
|
}
|
|
95
94
|
return { slots, identity };
|
|
96
95
|
}
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
96
|
+
/** Why one account cannot take work, or null when it can. The Connect page's AI-account rule
|
|
97
|
+
* (the server's isUsableCodingAccount): any provider, any member's account, and a canceled
|
|
98
|
+
* subscription keeps working until its paid access ends. */
|
|
99
|
+
function unusableCause(row, now) {
|
|
100
|
+
if (row.declaredState !== "active")
|
|
101
|
+
return "coding_account_inactive";
|
|
102
|
+
if (row.revokedAtMs !== null ||
|
|
103
|
+
(row.accessEndsAtMs !== null && row.accessEndsAtMs <= now))
|
|
104
|
+
return "coding_account_ended";
|
|
105
|
+
if (row.needsCredential)
|
|
106
|
+
return "coding_account_needs_login";
|
|
107
|
+
if (row.quarantined)
|
|
108
|
+
return "coding_account_quarantined";
|
|
109
|
+
if (row.walled)
|
|
110
|
+
return "coding_account_walled";
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
export function usableAiAccount(row, now) {
|
|
114
|
+
return unusableCause(row, now) === null;
|
|
115
|
+
}
|
|
116
|
+
/** CTC-4680: the step is done when the workspace has at least one usable AI account, from any
|
|
117
|
+
* provider. With --coding-account, that named account is the one that must be usable. */
|
|
99
118
|
export function onboardAccountsAdapter(input = {}) {
|
|
100
|
-
|
|
101
|
-
let attempted = false;
|
|
102
|
-
const candidate = (slots, now) => slots
|
|
119
|
+
const usable = (slots, now) => slots
|
|
103
120
|
.filter((row) => (input.slot === undefined || row.accountSlot === input.slot) &&
|
|
104
|
-
row
|
|
105
|
-
row.declaredState === "active" &&
|
|
106
|
-
row.ownedByMe &&
|
|
107
|
-
!row.needsCredential &&
|
|
108
|
-
!row.walled &&
|
|
109
|
-
!row.quarantined &&
|
|
110
|
-
row.renewalStatus !== "canceled" &&
|
|
111
|
-
row.revokedAtMs === null &&
|
|
112
|
-
(row.accessEndsAtMs === null || row.accessEndsAtMs > now))
|
|
121
|
+
usableAiAccount(row, now))
|
|
113
122
|
.sort((a, b) => a.accountSlot.localeCompare(b.accountSlot))[0];
|
|
114
123
|
const check = async (ctx, journal, signal) => {
|
|
115
124
|
const live = await inventory(ctx, journal, signal);
|
|
116
125
|
if ("reason" in live)
|
|
117
126
|
return waiting(live.reason);
|
|
118
|
-
const selected =
|
|
119
|
-
if (
|
|
120
|
-
equal(live.identity, proof) &&
|
|
121
|
-
live.slots.some((row) => row.accountSlot === proof.slot &&
|
|
122
|
-
row.provider === proof.provider &&
|
|
123
|
-
row.declaredState === "active" &&
|
|
124
|
-
row.ownedByMe &&
|
|
125
|
-
!row.needsCredential &&
|
|
126
|
-
!row.walled &&
|
|
127
|
-
!row.quarantined &&
|
|
128
|
-
row.renewalStatus !== "canceled" &&
|
|
129
|
-
row.revokedAtMs === null &&
|
|
130
|
-
(row.accessEndsAtMs === null ||
|
|
131
|
-
row.accessEndsAtMs > ctx.now().getTime())) &&
|
|
132
|
-
proof.checkedAtMs <= ctx.now().getTime() + 5000 &&
|
|
133
|
-
proof.checkedAtMs >= ctx.now().getTime() - 30000)
|
|
127
|
+
const selected = usable(live.slots, ctx.now().getTime());
|
|
128
|
+
if (selected)
|
|
134
129
|
return {
|
|
135
130
|
state: "done",
|
|
136
131
|
evidence: {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
checkedAt: new Date(proof.checkedAtMs).toISOString(),
|
|
132
|
+
provider: selected.provider,
|
|
133
|
+
checkedAt: ctx.now().toISOString(),
|
|
140
134
|
},
|
|
141
135
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
?
|
|
147
|
-
:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
? "codex_provider_access_unverified"
|
|
154
|
-
: "account_provider_access_unverified");
|
|
155
|
-
if (attempted)
|
|
156
|
-
return waiting("account_provider_access_unverified");
|
|
157
|
-
const cfg = loadConfig(ctx.home);
|
|
158
|
-
if (!cfg?.user || !["owner", "admin"].includes(cfg.user.role))
|
|
159
|
-
return waiting("account_validation_admin_required");
|
|
160
|
-
return { state: "pending" };
|
|
136
|
+
if (input.slot !== undefined) {
|
|
137
|
+
const named = live.slots.find((row) => row.accountSlot === input.slot);
|
|
138
|
+
// A named account speaks for itself: other accounts in the workspace may be fine.
|
|
139
|
+
return waiting(named
|
|
140
|
+
? (unusableCause(named, ctx.now().getTime()) ?? "coding_account_not_found")
|
|
141
|
+
: "coding_account_not_found");
|
|
142
|
+
}
|
|
143
|
+
if (input.waitForAccount &&
|
|
144
|
+
["owner", "admin"].includes(live.identity.role))
|
|
145
|
+
return { state: "pending" };
|
|
146
|
+
return waiting(live.slots.length ? "ai_account_not_usable" : "account_enrollment_required");
|
|
161
147
|
};
|
|
162
148
|
return {
|
|
163
149
|
check,
|
|
164
150
|
act: async (ctx, journal, external) => {
|
|
165
|
-
if (attempted)
|
|
166
|
-
return waiting("account_provider_access_unverified");
|
|
167
151
|
const initial = await inventory(ctx, journal, external);
|
|
168
152
|
if ("reason" in initial)
|
|
169
153
|
return waiting(initial.reason);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!equal(binding(ctx, journal), live.identity) ||
|
|
199
|
-
!cfg?.user ||
|
|
200
|
-
!["owner", "admin"].includes(cfg.user.role))
|
|
201
|
-
return waiting("account_identity_unverified");
|
|
202
|
-
const expiry = cfg.auth
|
|
203
|
-
? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime()
|
|
204
|
-
: 0;
|
|
205
|
-
const bearer = cfg.key ||
|
|
206
|
-
(cfg.auth && Number.isFinite(expiry) && expiry > 30000
|
|
207
|
-
? cfg.auth.accessToken
|
|
208
|
-
: undefined);
|
|
209
|
-
if (!bearer)
|
|
210
|
-
return waiting("account_login_refresh_required");
|
|
211
|
-
input.message?.("Checking one stored Claude account with a one-token provider request. This may use Claude quota. Codex credentials are not refreshed. Phase eligibility is checked separately.");
|
|
212
|
-
// A display callback must not be able to switch the identity used for this request.
|
|
213
|
-
if (!equal(binding(ctx, journal), live.identity))
|
|
214
|
-
return waiting("account_identity_unverified");
|
|
215
|
-
const deadline = new AbortController();
|
|
216
|
-
const signal = external
|
|
217
|
-
? AbortSignal.any([external, deadline.signal])
|
|
218
|
-
: deadline.signal;
|
|
219
|
-
const timer = setTimeout(() => deadline.abort(), 15000);
|
|
220
|
-
let remove = () => { };
|
|
221
|
-
try {
|
|
222
|
-
return await new Promise((resolve) => {
|
|
223
|
-
const stopped = () => resolve(waiting(external?.aborted
|
|
224
|
-
? "interrupted"
|
|
225
|
-
: "account_validation_unavailable"));
|
|
226
|
-
if (signal.aborted) {
|
|
227
|
-
stopped();
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
signal.addEventListener("abort", stopped, { once: true });
|
|
231
|
-
remove = () => signal.removeEventListener("abort", stopped);
|
|
232
|
-
Promise.resolve()
|
|
233
|
-
.then(async () => {
|
|
234
|
-
if (signal.aborted)
|
|
235
|
-
return waiting("interrupted");
|
|
236
|
-
// UI callbacks can queue a config change before this deferred send executes.
|
|
237
|
-
if (!equal(binding(ctx, journal), live.identity))
|
|
238
|
-
return waiting("account_identity_unverified");
|
|
239
|
-
attempted = true;
|
|
240
|
-
const response = await ctx.fetch(`${support.origin}/api/v1/coding-accounts/${encodeURIComponent(selected.accountSlot)}/validate`, {
|
|
241
|
-
method: "POST",
|
|
242
|
-
redirect: "error",
|
|
243
|
-
signal,
|
|
244
|
-
headers: {
|
|
245
|
-
authorization: `Bearer ${bearer}`,
|
|
246
|
-
accept: "application/json",
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
if (response.status !== 200)
|
|
250
|
-
return waiting("account_validation_unavailable");
|
|
251
|
-
const body = object(await response.json());
|
|
252
|
-
if (signal.aborted ||
|
|
253
|
-
!equal(binding(ctx, journal), live.identity))
|
|
254
|
-
return waiting("account_identity_unverified");
|
|
255
|
-
const now = ctx.now().getTime();
|
|
256
|
-
if (!body ||
|
|
257
|
-
body.provider !== selected.provider ||
|
|
258
|
-
typeof body.checkedAtMs !== "number" ||
|
|
259
|
-
!Number.isSafeInteger(body.checkedAtMs) ||
|
|
260
|
-
body.checkedAtMs > now + 5000 ||
|
|
261
|
-
body.checkedAtMs < now - 30000 ||
|
|
262
|
-
!["working", "walled", "rejected", "inconclusive"].includes(String(body.result)))
|
|
263
|
-
return waiting("account_validation_unverified");
|
|
264
|
-
if (body.result !== "working")
|
|
265
|
-
return waiting(body.result === "walled"
|
|
266
|
-
? "account_provider_walled"
|
|
267
|
-
: body.result === "rejected"
|
|
268
|
-
? "account_provider_rejected"
|
|
269
|
-
: "account_provider_access_unverified");
|
|
270
|
-
proof = {
|
|
271
|
-
...live.identity,
|
|
272
|
-
slot: selected.accountSlot,
|
|
273
|
-
provider: selected.provider,
|
|
274
|
-
checkedAtMs: body.checkedAtMs,
|
|
275
|
-
};
|
|
276
|
-
input.message?.("Claude provider access was verified. This does not reserve a runner or prove that a phase can start.");
|
|
277
|
-
return check(ctx, journal, signal);
|
|
278
|
-
})
|
|
279
|
-
.then((result) => (signal.aborted ? stopped() : resolve(result)), () => stopped());
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
finally {
|
|
283
|
-
clearTimeout(timer);
|
|
284
|
-
remove();
|
|
285
|
-
}
|
|
154
|
+
if (!input.waitForAccount ||
|
|
155
|
+
!["owner", "admin"].includes(initial.identity.role))
|
|
156
|
+
return check(ctx, journal, external);
|
|
157
|
+
let enrolled = initial.slots.length > 0;
|
|
158
|
+
const result = await input.waitForAccount(() => pollConsent({
|
|
159
|
+
timeoutMs: input.accountWaitMs,
|
|
160
|
+
signal: external,
|
|
161
|
+
sleep: input.sleep,
|
|
162
|
+
readStatus: async (signal) => {
|
|
163
|
+
const current = await inventory(ctx, journal, signal);
|
|
164
|
+
if ("reason" in current)
|
|
165
|
+
return { outcome: "waiting", reason: current.reason };
|
|
166
|
+
enrolled = current.slots.length > 0;
|
|
167
|
+
return {
|
|
168
|
+
outcome: usable(current.slots, ctx.now().getTime())
|
|
169
|
+
? "connected"
|
|
170
|
+
: "absent",
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
}));
|
|
174
|
+
if (result.state === "done")
|
|
175
|
+
return check(ctx, journal, external);
|
|
176
|
+
// A wait that ran out holds the step and offers another try (round 5).
|
|
177
|
+
return waiting(result.reason !== "consent_timeout"
|
|
178
|
+
? (result.reason ?? "account_enrollment_required")
|
|
179
|
+
: enrolled
|
|
180
|
+
? "ai_account_not_usable"
|
|
181
|
+
: "account_enrollment_required");
|
|
286
182
|
},
|
|
287
183
|
};
|
|
288
184
|
}
|