@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.3
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- Setup shows one progress display with numbered steps and the reason a step needs attention.
|
|
7
|
+
- When the Connect accounts page is available, setup uses it to finish account connections. Any usable AI account counts without a test request.
|
|
8
|
+
- Setup offers API keys by default. Subscription account options appear only when your workspace enables them.
|
|
9
|
+
|
|
10
|
+
## 0.15.2
|
|
11
|
+
|
|
12
|
+
- `catalyst setup` is the command to set up Catalyst. It runs the same steps as `catalyst onboard`, which still works.
|
|
13
|
+
- When a browser link times out, setup says so and asks whether you are ready to try again. Yes gives a new link; stopping pauses setup without moving on to steps that depend on it.
|
|
14
|
+
- The GitHub link opens GitHub's install page directly. An install request already waiting on an organization owner is not requested again, and setup still shows the link to install it yourself.
|
|
15
|
+
- Setup asks whether to run work on this machine only after Catalyst is installed on GitHub.
|
|
16
|
+
- Sign-in says which Catalyst workspace you joined, without internal account numbers. `catalyst ready` describes each team problem in plain words, with the page to fix it and who can fix it.
|
|
17
|
+
|
|
3
18
|
## 0.15.1
|
|
4
19
|
|
|
5
20
|
- Runner setup accepts verified personal team admission before a repository has been mapped to that team. Older cloud servers keep their existing admission check.
|
package/README.md
CHANGED
|
@@ -4,13 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Set up Catalyst Cloud and manage your account from the command line.
|
|
8
|
+
|
|
9
|
+
Check ticket progress and see which decisions need your attention.
|
|
10
|
+
|
|
11
|
+
Install: `npm install -g @catalyst-cloud/cli`
|
|
12
|
+
|
|
13
|
+
[CLI documentation](https://github.com/coalesce-labs/catalyst-cloud-skills#readme)
|
|
14
|
+
|
|
15
|
+
Run the installer, then run `catalyst onboard`:
|
|
8
16
|
|
|
9
17
|
```sh
|
|
10
18
|
curl -fsSL https://staging.catalystcloud.dev/install.sh | sh
|
|
19
|
+
catalyst onboard
|
|
11
20
|
```
|
|
12
21
|
|
|
13
|
-
The installer puts the CLI and both skill packs on this machine and connects it with one browser approval.
|
|
22
|
+
The installer puts the CLI and both skill packs on this machine and connects it with one browser approval. Run `catalyst onboard` to finish setup from the command line. You can also use the onboarding skill in your coding agent after that. Everything else on this page is the reference.
|
|
14
23
|
|
|
15
24
|
This repository supplies skills for setting up and operating your Catalyst Cloud account. A coding workstation also uses [`coalesce-labs/catalyst-dev-skills`](https://github.com/coalesce-labs/catalyst-dev-skills) for research, planning, implementation, review, and shipping.
|
|
16
25
|
|
|
@@ -102,7 +111,7 @@ The command is `catalyst`. `catalyst-skills` still works as a deprecated alias,
|
|
|
102
111
|
|
|
103
112
|
## What this is
|
|
104
113
|
|
|
105
|
-
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.
|
|
106
115
|
|
|
107
116
|
## What connecting does
|
|
108
117
|
|
|
@@ -222,7 +231,7 @@ Some of these only read, and some write: a comment, a card move, an ask, a relea
|
|
|
222
231
|
|
|
223
232
|
## What a key cannot see yet
|
|
224
233
|
|
|
225
|
-
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,
|
|
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, 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:
|
|
226
235
|
|
|
227
236
|
- Read pull-request labels or the reviewer's reaction. The mirror does not carry them; GitHub's own page does.
|
|
228
237
|
- 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
|
@@ -457,7 +457,7 @@ export const FLAG_TABLES = {
|
|
|
457
457
|
},
|
|
458
458
|
};
|
|
459
459
|
export const VERB_USAGE = {
|
|
460
|
-
setup: "setup --engine <file> --engine-sha256 <hex> [-- <install options>] (run by install.sh; install.sh --help lists the options)",
|
|
460
|
+
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
461
|
login: "login [--base-url <url>] [--start-replica] (keyless; or --key <personal-key> / CATALYST_CLOUD_TOKEN)",
|
|
462
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]",
|
|
463
463
|
install: "install [--skills-dir <dir>] [--force]",
|
package/dist/capabilities.js
CHANGED
|
@@ -5,7 +5,9 @@ const agent = (method, name) => ({ method, path: `/api/v1/agent/${name}` });
|
|
|
5
5
|
* the person, the workspace, the project, the repository, then work. A new verb is a new row here
|
|
6
6
|
* in the same change, or `catalyst capabilities` lies to every guide that reads it. */
|
|
7
7
|
export const CAPABILITIES = [
|
|
8
|
-
|
|
8
|
+
// CTC-4680: plain `catalyst setup` runs the onboard flow, so install.sh may hand off to it. install.sh
|
|
9
|
+
// reads this flag rather than `setup --help`, which already exits 0 on 0.15.x.
|
|
10
|
+
{ verb: "setup", does: "set up Catalyst step by step, or show install.sh's plan, progress and sign-in in this terminal", needs: "member", routes: [], since: "0.15.0", engineProtocol: 1, bootstrapHandoff: true },
|
|
9
11
|
{ verb: "onboard", does: "run and resume Catalyst setup with a saved step record", needs: "member", routes: [], since: "0.14.0", bootstrapHandoff: true, headless: true },
|
|
10
12
|
{ verb: "status", does: "say whether this machine is connected, as whom, and to which cloud", needs: "member", routes: [], since: "0.1.0" },
|
|
11
13
|
{ verb: "me", does: "read the person's seat, role and Linear identity", needs: "member", routes: [], since: "0.1.0" },
|
package/dist/cli.js
CHANGED
|
@@ -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 = [
|
|
@@ -69,7 +69,7 @@ const VERB_ALIASES = { join: "login" };
|
|
|
69
69
|
const HUMAN_VERBS = new Set(["login", "install", "status", "notice"]);
|
|
70
70
|
export function usageText() {
|
|
71
71
|
return [
|
|
72
|
-
`${PACKAGE_NAME} — the Catalyst Cloud customer CLI: connect to your
|
|
72
|
+
`${PACKAGE_NAME} — the Catalyst Cloud customer CLI: connect to your Catalyst workspace, read through the SDK, write through the agent proxy`,
|
|
73
73
|
"",
|
|
74
74
|
"Usage:",
|
|
75
75
|
" catalyst login [--base-url <url>] [--start-replica] (keyless: logs you in as yourself)",
|
|
@@ -96,7 +96,7 @@ export function usageText() {
|
|
|
96
96
|
" catalyst var set <NAME> | var import <file> [--repo <owner/name>] [--names NAME[,NAME...]] (plain environment variables)",
|
|
97
97
|
" catalyst team <list|check|map|adopt|migrate|checklist> ...",
|
|
98
98
|
" catalyst capabilities [--json] (what this CLI can do, the role each verb needs, and whether this cloud serves it)",
|
|
99
|
-
" catalyst project list [--json] (all
|
|
99
|
+
" catalyst project list [--json] (all workspace projects, including those with no stage mapping)",
|
|
100
100
|
" catalyst project wip-limit <get|set <n>|set default> [--team K] (a project's new-start WIP limit; members read, owners and admins set)",
|
|
101
101
|
" catalyst repo <agents-block <path> [--write]|agent-setup <path> [--apply] [--with-check]> (a checkout's AGENTS.md block and portable agent layout; working tree only)",
|
|
102
102
|
" catalyst legacy [--remove [--data] [--yes]] (leftovers of the old local Catalyst runtime: list them; remove them only on a yes)",
|
|
@@ -110,8 +110,8 @@ export function usageText() {
|
|
|
110
110
|
"--key (or set CATALYST_CLOUD_TOKEN) to use a personal or account key instead. The base URL defaults",
|
|
111
111
|
`to CATALYST_CLOUD_BASE_URL or ${DEFAULT_BASE_URL}.`,
|
|
112
112
|
"",
|
|
113
|
-
"login
|
|
114
|
-
"(0600) holding your login session (or key) and this CLI's path, and caches the
|
|
113
|
+
"login finds your Catalyst workspace, writes ~/.config/catalyst-cloud/customer.json",
|
|
114
|
+
"(0600) holding your login session (or key) and this CLI's path, and caches the workspace contract",
|
|
115
115
|
"beside it. A keyless session's short-lived token refreshes silently on every request. login does",
|
|
116
116
|
"not install skills: your agent's own install command does that, and `install` is only here to",
|
|
117
117
|
"repair a copy this package made.",
|
|
@@ -208,9 +208,15 @@ function maybePrintUpdateNotice(args, ctx) {
|
|
|
208
208
|
}
|
|
209
209
|
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
|
-
// this CLI's own flag tables.
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
// this CLI's own flag tables. CTC-4680: without --engine, `setup` is the name a person types for
|
|
212
|
+
// the onboard flow, so it runs exactly that.
|
|
213
|
+
if (argv[0] === "setup") {
|
|
214
|
+
const rest = argv.slice(1);
|
|
215
|
+
const own = rest.indexOf("--") === -1 ? rest : rest.slice(0, rest.indexOf("--"));
|
|
216
|
+
if (own.includes("--engine") || own.includes("--help") || own.includes("-h"))
|
|
217
|
+
return runSetup(rest, ctx, deps);
|
|
218
|
+
argv = ["onboard", ...rest];
|
|
219
|
+
}
|
|
214
220
|
let args;
|
|
215
221
|
try {
|
|
216
222
|
args = parseArgs(argv.map((a, i) => (i === 0 && VERB_ALIASES[a] ? VERB_ALIASES[a] : a)));
|
|
@@ -231,7 +237,9 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
231
237
|
onboardHeadlessRequested(args, ctx.env))
|
|
232
238
|
return headlessUsageRefusal(ctx, "--help and --version print text, not the headless document; run them without --json", true, manifest.version);
|
|
233
239
|
if (args.version) {
|
|
234
|
-
ctx.stdout(
|
|
240
|
+
ctx.stdout(
|
|
241
|
+
// Machine-read: install.sh and catalyst-cloud's onboarding harness match this exact line.
|
|
242
|
+
`${PACKAGE_NAME} ${manifest.version} (tenant contract range: ${manifest.tenantContractRange})`);
|
|
235
243
|
return 0;
|
|
236
244
|
}
|
|
237
245
|
if (args.command === null) {
|
|
@@ -298,6 +306,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
298
306
|
renderer: createSetupRenderer(process.stdout, ctx.env),
|
|
299
307
|
disposeRenderer: true,
|
|
300
308
|
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
309
|
+
version: manifest.version,
|
|
301
310
|
});
|
|
302
311
|
}
|
|
303
312
|
catch (error) {
|
|
@@ -320,6 +329,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
320
329
|
renderer: createSetupRenderer(output, ctx.env),
|
|
321
330
|
disposeRenderer: true,
|
|
322
331
|
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
332
|
+
version: manifest.version,
|
|
323
333
|
interactive: false,
|
|
324
334
|
verbose: args.flags.verbose === true,
|
|
325
335
|
});
|
|
@@ -387,21 +397,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
387
397
|
localSync: journal?.localSync ?? args.flags["local-sync"] === true,
|
|
388
398
|
observe: observeCloudOnboarding,
|
|
389
399
|
});
|
|
390
|
-
|
|
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
|
-
};
|
|
400
|
+
return onboardReadyStep(report, journal);
|
|
405
401
|
},
|
|
406
402
|
legacy: deps.legacy,
|
|
407
403
|
openBrowser: deps.openBrowser,
|
|
@@ -687,26 +683,25 @@ async function cmdLogin(args, ctx, deps, personalOnly = false) {
|
|
|
687
683
|
};
|
|
688
684
|
checkCancelled();
|
|
689
685
|
const written = writeConfig(ctx.home, config);
|
|
690
|
-
ctx.stdout(`
|
|
686
|
+
ctx.stdout(`Catalyst workspace: ${me.name} (${me.slug})`);
|
|
691
687
|
if (me.user) {
|
|
692
|
-
ctx.stdout(`
|
|
688
|
+
ctx.stdout(`Signed in to Catalyst as ${me.user.label} (${me.user.role})${me.user.linearUserId
|
|
693
689
|
? ""
|
|
694
690
|
: ' — your Linear identity is not matched yet, so "what needs me" will show everyone\'s asks until an admin matches it in Settings'}`);
|
|
695
691
|
}
|
|
696
692
|
else {
|
|
697
|
-
ctx.stderr(`[catalyst] this is
|
|
693
|
+
ctx.stderr(`[catalyst] this is your Catalyst workspace's shared key (a host credential), not your own. The skills work, but nothing your agent writes will carry your name, and "what needs me" cannot mean you. Make a personal key at Settings → API keys and sign in with that.`);
|
|
698
694
|
}
|
|
699
695
|
ctx.stdout(`Config written to ${written.path} (mode ${formatMode(written.mode)}, holds your ${auth ? "login session" : "key"} and the CLI path)${written.mode === CONFIG_MODE
|
|
700
696
|
? ""
|
|
701
697
|
: ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
|
|
702
|
-
ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
|
|
703
698
|
try {
|
|
704
699
|
const loaded = await loadContract(ctx, config, {
|
|
705
700
|
refresh: true,
|
|
706
701
|
signal: deps.signal,
|
|
707
702
|
});
|
|
708
703
|
checkCancelled();
|
|
709
|
-
ctx.stdout(`
|
|
704
|
+
ctx.stdout(`Saved your workspace settings (version ${loaded.doc.contractVersion}) to ${loaded.path}`);
|
|
710
705
|
}
|
|
711
706
|
catch (err) {
|
|
712
707
|
checkCancelled();
|
package/dist/consent-browser.js
CHANGED
|
@@ -54,5 +54,7 @@ export const openConsentBrowser = createConsentBrowserOpener();
|
|
|
54
54
|
/** The web page that finishes a consent step when no browser opens here. The signed link stays private. */
|
|
55
55
|
export function finishOnTheWeb(baseUrl, page, action) {
|
|
56
56
|
const origin = baseUrl.replace(/\/+$/, "");
|
|
57
|
-
|
|
57
|
+
// CTC-4680: the GitHub install has a route that goes straight to GitHub; Integrations starts nothing.
|
|
58
|
+
const path = page === "github-install" ? "/connect/github/start" : `/settings/${page}`;
|
|
59
|
+
return `No browser opened on this computer. In a browser signed in to Catalyst, open ${origin}${path} and ${action}. Setup keeps waiting here.`;
|
|
58
60
|
}
|
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
|
}
|