@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/dist/onboard-next.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { nativeEgressInstallCommands } from "./onboard-runner-egress.js";
|
|
2
|
-
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
2
|
+
import { INSTALL_COMMAND, loadConfig, normalizeBaseUrl } from "./config.js";
|
|
3
3
|
import { githubInstallationPage } from "./onboard-permissions.js";
|
|
4
|
+
import { valuesFacts, valuesFix } from "./ready-copy.js";
|
|
4
5
|
/** What a person reads for a step that is not done. Each line ends in an action they can take. */
|
|
5
6
|
const REASONS = {
|
|
6
|
-
account_identity_unverified: "Your login changed while
|
|
7
|
-
account_inventory_unavailable: "
|
|
8
|
-
account_inventory_unverified: "The server did not return a fresh supported
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
account_identity_unverified: "Your login changed while AI accounts were checked. Run catalyst onboard again to verify your person and workspace.",
|
|
8
|
+
account_inventory_unavailable: "AI accounts could not be read. Run catalyst onboard again to retry.",
|
|
9
|
+
account_inventory_unverified: "The server did not return a fresh supported AI account list. Run catalyst onboard after the server update.",
|
|
10
|
+
connect_checklist_refused: "Catalyst refused to show this login the Connect accounts page's status. Sign in again with catalyst login, then run catalyst onboard.",
|
|
11
|
+
connect_checklist_identity_unverified: "Your login changed while setup was checking the Connect accounts page. Run catalyst onboard again to verify your person and workspace.",
|
|
12
|
+
coding_account_inactive: "The AI account named by --coding-account is turned off. Turn it on in the workspace's AI account settings, then run catalyst onboard.",
|
|
13
|
+
coding_account_ended: "The AI account named by --coding-account was removed or its access has ended. Name another one with --coding-account, or add one in the workspace's AI account settings.",
|
|
14
|
+
coding_account_needs_login: "The AI account named by --coding-account needs its key added again. Replace it in the workspace's AI account settings, then run catalyst onboard.",
|
|
15
|
+
coding_account_quarantined: "The AI account named by --coding-account is held after repeated failures. Check it in the workspace's AI account settings, then run catalyst onboard.",
|
|
16
|
+
coding_account_walled: "The AI account named by --coding-account has used its usage limit for now. Check when it resets, then run catalyst onboard again.",
|
|
17
|
+
coding_account_not_found: "The AI account named by --coding-account is not in this workspace. Run catalyst accounts to list the slots, then run catalyst onboard with one of them.",
|
|
18
|
+
account_validation_admin_required: "An administrator must check the workspace's AI account access.",
|
|
11
19
|
account_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
|
|
12
20
|
account_validation_unavailable: "The provider check could not finish. Run catalyst onboard again to retry.",
|
|
13
21
|
account_validation_unverified: "The server did not return a fresh provider check. Run catalyst onboard after the server update.",
|
|
14
|
-
account_provider_access_unverified: "
|
|
22
|
+
account_provider_access_unverified: "AI account access is not freshly verified. Check the account in the workspace's AI account settings, then resume setup.",
|
|
15
23
|
codex_provider_access_unverified: "Codex credentials are stored, but provider access is not freshly verified. Setup did not refresh or rotate the Codex login.",
|
|
16
24
|
account_provider_walled: "Claude's usage limit is spent for now. Check the account's reset time, then run catalyst onboard again.",
|
|
17
|
-
account_provider_rejected: "Claude refused the stored login. Replace it in the workspace's
|
|
25
|
+
account_provider_rejected: "Claude refused the stored login. Replace it in the workspace's AI account settings, then resume setup.",
|
|
18
26
|
personal_consent_handoff: "The browser approval link could not be verified. Run catalyst onboard to try again.",
|
|
19
27
|
personal_identity_refused: "Your current login no longer matches this setup. Resume with the original workspace and person.",
|
|
20
28
|
personal_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
|
|
@@ -44,6 +52,7 @@ const REASONS = {
|
|
|
44
52
|
local_sync_not_selected: "Using cloud reads. Local sync was not selected.",
|
|
45
53
|
local_sync_capability_unavailable: "Local sync was selected but could not be verified.",
|
|
46
54
|
member_scope: "Your workspace administrator handles this step.",
|
|
55
|
+
member_team_required: "Setup needs your Linear team to check what Catalyst can work on. Run catalyst setup again with --team <KEY>, using your team's key from Linear.",
|
|
47
56
|
onboarding_checks_pending: "Some required checks are still unverified.",
|
|
48
57
|
interrupted: "Setup paused. Run the same command to resume.",
|
|
49
58
|
signin_timeout: "Sign-in timed out. Run catalyst onboard to try again.",
|
|
@@ -138,7 +147,15 @@ const REASONS = {
|
|
|
138
147
|
linear_workspace_permissions_unverified: "The workspace's Linear connection works, but its granted scopes could not be checked. Run catalyst onboard to try again.",
|
|
139
148
|
linear_personal_scope_outdated: "Your personal Linear connection is missing scopes this version needs. Re-authorize it from Connected accounts, then run catalyst onboard.",
|
|
140
149
|
personal_permissions_unverified: "Your personal connection's granted scopes could not be checked. Run catalyst onboard to try again.",
|
|
150
|
+
required_values_unverified: "Catalyst has not read the repository's settings yet. Run catalyst setup again in a few minutes.",
|
|
151
|
+
first_ticket_team_unverified: "Setup could not confirm the selected Linear team. Run catalyst setup again.",
|
|
152
|
+
first_ticket_dispatch_unmapped: "Catalyst does not know which of the team's stages starts its work yet. Set up the team's Catalyst workflow, then run catalyst setup again.",
|
|
141
153
|
housekeeping_service_unverified: "The daily update needs a user service manager, launchd on macOS or systemd --user on Linux. Setup cannot schedule it on this computer. It is optional, and work does not depend on it.",
|
|
154
|
+
housekeeping_off_chosen: "The daily update is off, as you chose when you installed. It is optional. catalyst says when an update is out.",
|
|
155
|
+
housekeeping_no_scheduler: "The daily update can't run on this computer: it needs launchd on macOS or systemd --user on Linux. It is optional. catalyst says when an update is out.",
|
|
156
|
+
housekeeping_not_loaded: "The daily update is installed but not turned on. Run launchctl load ~/Library/LaunchAgents/dev.catalystcloud.housekeeping.plist to turn it on.",
|
|
157
|
+
housekeeping_not_enabled: "The daily update is installed but not turned on. Run systemctl --user enable --now catalyst-housekeeping.timer to turn it on.",
|
|
158
|
+
housekeeping_not_scheduled: `The daily update is not scheduled on this computer. Run the installer again to schedule it: ${INSTALL_COMMAND} -s -- --housekeeping yes`,
|
|
142
159
|
};
|
|
143
160
|
/** The saved login's web address, or nothing when the config is missing, unreadable or not http(s). */
|
|
144
161
|
export function savedOnboardBaseUrl(home) {
|
|
@@ -261,6 +278,26 @@ export function onboardStepDetail(step) {
|
|
|
261
278
|
: undefined;
|
|
262
279
|
return granted ? `granted ${granted}` : undefined;
|
|
263
280
|
}
|
|
281
|
+
/** The dispatch stage's name as the contract gave it, or what it does when unknown. */
|
|
282
|
+
export function dispatchStageName(value) {
|
|
283
|
+
return typeof value === "string" && /^[\p{L}\p{N}][\p{L}\p{N} ._&'()-]{0,59}$/u.test(value)
|
|
284
|
+
? value
|
|
285
|
+
: "the stage that starts Catalyst's work";
|
|
286
|
+
}
|
|
287
|
+
function firstTicketText(reason, key, stage) {
|
|
288
|
+
const move = `move one of ${key ? `${key}'s` : "your team's"} tickets to ${dispatchStageName(stage)} in Linear`;
|
|
289
|
+
switch (reason) {
|
|
290
|
+
case "first_ticket_choice_required":
|
|
291
|
+
return `${move.charAt(0).toUpperCase()}${move.slice(1)}. Catalyst picks it up next.`;
|
|
292
|
+
case "first_ticket_move_refused":
|
|
293
|
+
return `Catalyst could not move the ticket for you. Ask a Catalyst owner or admin to check its Linear connection, or ${move} yourself.`;
|
|
294
|
+
case "first_ticket_unavailable":
|
|
295
|
+
return `Setup could not read or move the team's tickets. Run catalyst setup to try again, or ${move} yourself.`;
|
|
296
|
+
case "first_ticket_unverified":
|
|
297
|
+
return `Setup could not confirm which ticket moved. Check ${dispatchStageName(stage)} in Linear before choosing again.`;
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
264
301
|
/** One step's next action. `waitsFor` names the unfinished step a prerequisite wait is blocked on. */
|
|
265
302
|
export function onboardReasonText(step, context = {}) {
|
|
266
303
|
const reason = step.reason ?? "";
|
|
@@ -284,6 +321,36 @@ export function onboardReasonText(step, context = {}) {
|
|
|
284
321
|
if (step.id === "github.install" &&
|
|
285
322
|
(reason === "github_installation_admin_required" || reason === "member_scope"))
|
|
286
323
|
return `Only an owner or admin of your Catalyst workspace can install Catalyst on GitHub. Ask one to open ${context.baseUrl ? `${normalizeBaseUrl(context.baseUrl)}/settings/connections` : "Catalyst settings, Integrations,"} and install it there.`;
|
|
324
|
+
if (reason === "admin_setup_pending") {
|
|
325
|
+
const words = {
|
|
326
|
+
"linear.workspace": "connect Linear",
|
|
327
|
+
"github.install": "install Catalyst on GitHub",
|
|
328
|
+
"linear.adopt": "set up the team's Catalyst workflow",
|
|
329
|
+
};
|
|
330
|
+
const left = String(step.evidence?.admin ?? "")
|
|
331
|
+
.split(",")
|
|
332
|
+
.flatMap((id) => (words[id] ? [words[id]] : []));
|
|
333
|
+
const list = left.length > 1 ? `${left.slice(0, -1).join(", ")} and ${left.at(-1)}` : (left[0] ?? "finish setup");
|
|
334
|
+
return `A Catalyst owner or admin still has to ${list}. Ask one to open ${context.baseUrl ? `${normalizeBaseUrl(context.baseUrl)}/settings/connections` : "Catalyst settings"}, then run catalyst setup.`;
|
|
335
|
+
}
|
|
336
|
+
// A member is told which admin action is left and where an admin takes it.
|
|
337
|
+
if (reason === "member_scope") {
|
|
338
|
+
const key = onboardTeamKey(context.journal);
|
|
339
|
+
const adminOnly = {
|
|
340
|
+
"linear.workspace": ["connect Linear", "/settings/connections", " and connect it there"],
|
|
341
|
+
"linear.adopt": [
|
|
342
|
+
"set up the team's Catalyst workflow",
|
|
343
|
+
key ? `/settings/linear-teams/${encodeURIComponent(key)}/adopt` : "/settings/linear-teams",
|
|
344
|
+
"",
|
|
345
|
+
],
|
|
346
|
+
};
|
|
347
|
+
const admin = adminOnly[step.id];
|
|
348
|
+
if (admin) {
|
|
349
|
+
const [what, path, rest] = admin;
|
|
350
|
+
const url = context.baseUrl ? `${normalizeBaseUrl(context.baseUrl)}${path}` : "Catalyst settings";
|
|
351
|
+
return `Only a Catalyst owner or admin can ${what}. Ask one to open ${url}${rest}.`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
287
354
|
// An install request can sit on GitHub for good (a wrong organization, or an owner who said no),
|
|
288
355
|
// so the step always keeps a way to install it directly.
|
|
289
356
|
if (reason === "github_installation_approval_pending" && context.baseUrl)
|
|
@@ -302,11 +369,22 @@ export function onboardReasonText(step, context = {}) {
|
|
|
302
369
|
if (team)
|
|
303
370
|
return `Setup could not confirm whether Linear created the team ${team}. Check Linear for it. If it is there: ${adopt(team)} If it is not, run catalyst onboard to create it again.`;
|
|
304
371
|
}
|
|
372
|
+
if (reason === "ai_account_not_usable") {
|
|
373
|
+
const where = context.baseUrl
|
|
374
|
+
? `at ${normalizeBaseUrl(context.baseUrl)}/settings/coding-accounts`
|
|
375
|
+
: "in the web app on the Settings → AI accounts page";
|
|
376
|
+
return `No AI account in this workspace can take work yet. A workspace owner or administrator checks them ${where}: one may need its key replaced, or may have reached its usage limit. Then run catalyst onboard.`;
|
|
377
|
+
}
|
|
305
378
|
if (reason === "account_enrollment_required") {
|
|
306
379
|
const where = context.baseUrl
|
|
307
380
|
? `at ${normalizeBaseUrl(context.baseUrl)}/settings/coding-accounts, the Settings → AI accounts page`
|
|
308
381
|
: "in the web app on the Settings → AI accounts page";
|
|
309
|
-
|
|
382
|
+
// CTC-4716: subscriptions are offered only to a workspace the cloud enables for them.
|
|
383
|
+
const kinds = step.evidence?.aiAccountKinds;
|
|
384
|
+
const what = typeof kinds === "string" && kinds.split(",").includes("subscription")
|
|
385
|
+
? "an API key, billed per token, or a subscription, a fixed monthly plan"
|
|
386
|
+
: "an API key, billed per token";
|
|
387
|
+
return `No AI account is added yet. A workspace owner or administrator adds one ${where}: ${what}. Then run catalyst onboard.`;
|
|
310
388
|
}
|
|
311
389
|
if (reason === "automation_management_unavailable") {
|
|
312
390
|
const conflicts = typeof step.evidence?.automations === "string" &&
|
|
@@ -357,6 +435,27 @@ export function onboardReasonText(step, context = {}) {
|
|
|
357
435
|
}
|
|
358
436
|
if (reason === "runner_admission_operator")
|
|
359
437
|
return `The admission policy for team ${onboardTeamKey(context.journal) ?? "<TEAM KEY>"} needs a Catalyst operator to check it. Setup changes only restricted admission and preserves existing policy fields. Ask Catalyst support to confirm a compatible policy, then run the same setup command.`;
|
|
438
|
+
if (reason === "required_values_missing") {
|
|
439
|
+
let facts;
|
|
440
|
+
try {
|
|
441
|
+
facts = JSON.parse(String(step.evidence?.requiredValues ?? "null"));
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
facts = null;
|
|
445
|
+
}
|
|
446
|
+
const fix = valuesFix(context.baseUrl, valuesFacts(facts)) ??
|
|
447
|
+
valuesFix(context.baseUrl, { names: ["the missing values"] });
|
|
448
|
+
// A member cannot set a value; the line says who does, and still never prints one.
|
|
449
|
+
const member = context.journal?.steps.find((row) => row.id === "signin")?.evidence?.role === "member";
|
|
450
|
+
if (member)
|
|
451
|
+
return fix.startsWith("set ")
|
|
452
|
+
? `Ask a Catalyst owner or admin to ${fix}. Then run catalyst setup.`
|
|
453
|
+
: `${fix.charAt(0).toUpperCase()}${fix.slice(1)}. Ask a Catalyst owner or admin to fix it, then run catalyst setup.`;
|
|
454
|
+
return `${fix.charAt(0).toUpperCase()}${fix.slice(1)}. Then run catalyst setup.`;
|
|
455
|
+
}
|
|
456
|
+
const firstTicket = firstTicketText(reason, onboardTeamKey(context.journal), step.evidence?.stage);
|
|
457
|
+
if (step.id === "first-ticket" && firstTicket)
|
|
458
|
+
return firstTicket;
|
|
360
459
|
if (reason === "prerequisite_not_ready" && context.waitsFor)
|
|
361
460
|
return `Runs after "${context.waitsFor}".`;
|
|
362
461
|
const permission = permissionText(step, context.baseUrl);
|
package/dist/onboard-ready.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from "./config.js";
|
|
2
2
|
import { loadContract } from "./contract.js";
|
|
3
|
+
import { ONBOARD_DEFERRED_STEPS, onboardRequiredStepsWaiting, } from "./onboard.js";
|
|
3
4
|
const evidenceNames = new Set(["account", "membershipId", "project", "team", "repository", "count", "checkedAt", "expiresAt", "revision", "lag", "cursor", "heartbeatAgeMs", "supervised", "available", "validated", "phaseStarted", "linearComment", "fleetActivity"]);
|
|
4
5
|
const slug = (value) => value && /^[a-z][a-z0-9_]{1,63}$/.test(value) ? value : undefined;
|
|
5
6
|
function cleanCheck(check) {
|
|
@@ -32,6 +33,66 @@ export async function onboardingReadyReport(ctx, deps) {
|
|
|
32
33
|
: required.length === 0 || required.some(check => check.state === "unknown") || work.state === "unknown" ? "unknown" : "complete";
|
|
33
34
|
return { schema: 1, readMode: deps.localSync ? "local" : "cloud", state, checks, work };
|
|
34
35
|
}
|
|
36
|
+
/** Team check outcomes that do not hold setup's readiness, as `check:reason`: the cloud's four
|
|
37
|
+
* WAITING_PAIRS (nothing has happened yet before a first ticket: a webhook delivery, a write, a host
|
|
38
|
+
* attaching, a reviewer's first answer), plus two of setup's own. A missing value is the values
|
|
39
|
+
* step's action; a reference with no value is not here, because the checkout refuses it before any
|
|
40
|
+
* work starts. A team with no reviewer still runs. */
|
|
41
|
+
const READY_EXEMPT = new Set([
|
|
42
|
+
"webhook_covers_team:delivery_window_empty",
|
|
43
|
+
"writes_land:no_write_observed",
|
|
44
|
+
"hosts_current:no_host_connected",
|
|
45
|
+
"reviewer_answering:reviewer_not_yet_answered",
|
|
46
|
+
"required_values:required_value_missing",
|
|
47
|
+
"reviewer_configured:no_reviewer_configured",
|
|
48
|
+
]);
|
|
49
|
+
/** Admin-owned steps a member's run skips, and the team checks that show an admin finished them. A
|
|
50
|
+
* member's skip is not a finished setup: readiness holds until these checks pass. */
|
|
51
|
+
const ADMIN_OWNED = [
|
|
52
|
+
["linear.workspace", ["oauth_scope", "token_live"]],
|
|
53
|
+
["github.install", ["github_app_installed"]],
|
|
54
|
+
["linear.adopt", ["mapping_total", "mapped_states_exist"]],
|
|
55
|
+
];
|
|
56
|
+
/** The setup step's verdict. Every required step must be satisfied, and every required check must
|
|
57
|
+
* pass or be one of the exempt outcomes above. A failing check fails; anything else unread (a stale
|
|
58
|
+
* or unverified team, a check the cloud could not read, the cloud itself) waits. Work nobody can
|
|
59
|
+
* observe yet does not hold it. */
|
|
60
|
+
export function onboardReadyStep(report, journal) {
|
|
61
|
+
const evidence = {
|
|
62
|
+
checks: report.checks.length,
|
|
63
|
+
passed: report.checks.filter((check) => check.state === "pass").length,
|
|
64
|
+
};
|
|
65
|
+
const failedStep = (journal?.steps ?? []).some((step) => step.id !== "ready" &&
|
|
66
|
+
!ONBOARD_DEFERRED_STEPS.has(step.id) &&
|
|
67
|
+
step.state === "failed");
|
|
68
|
+
if (onboardRequiredStepsWaiting(journal) || failedStep)
|
|
69
|
+
return { state: "waiting", reason: "onboarding_checks_pending", evidence };
|
|
70
|
+
const skipped = (id) => journal?.steps.some((step) => step.id === id && step.state === "skipped" && step.reason === "member_scope");
|
|
71
|
+
// A member's plain run has no team, so nothing can show what an admin finished.
|
|
72
|
+
if (skipped("linear.team"))
|
|
73
|
+
return { state: "waiting", reason: "member_team_required", evidence };
|
|
74
|
+
// Only a check read from a fresh team (`team.<id>.<check>`) that fails counts. An absent, stale or
|
|
75
|
+
// unknown one (an outage, a workflow just adopted) says nothing about the admin step, so it falls
|
|
76
|
+
// through to the waits below.
|
|
77
|
+
const read = new Map(report.checks
|
|
78
|
+
.filter((check) => /^team\.[^.]+\.[a-z0-9_]+$/.test(check.id))
|
|
79
|
+
.map((check) => [check.id.split(".")[2], check.state]));
|
|
80
|
+
const adminLeft = ADMIN_OWNED.filter(([id, checks]) => skipped(id) && checks.some((check) => read.get(check) === "fail")).map(([id]) => id);
|
|
81
|
+
if (adminLeft.length)
|
|
82
|
+
return { state: "waiting", reason: "admin_setup_pending", evidence: { ...evidence, admin: adminLeft.join(",") } };
|
|
83
|
+
const open = report.checks.filter((check) => {
|
|
84
|
+
if (!check.required || check.state === "pass")
|
|
85
|
+
return false;
|
|
86
|
+
// `team.<id>.<check>` is one team check; `team.<id>` alone is the team's whole check list.
|
|
87
|
+
const parts = check.id.split(".");
|
|
88
|
+
return !(parts[0] === "team" && parts.length === 3 && READY_EXEMPT.has(`${parts[2]}:${check.reason}`));
|
|
89
|
+
});
|
|
90
|
+
if (open.some((check) => check.state === "fail"))
|
|
91
|
+
return { state: "failed", reason: "onboarding_checks_pending", evidence };
|
|
92
|
+
if (open.length)
|
|
93
|
+
return { state: "waiting", reason: "onboarding_checks_pending", evidence };
|
|
94
|
+
return { state: "done", evidence };
|
|
95
|
+
}
|
|
35
96
|
/** The current contract exposes checked project setup, but not fresh starter-ticket proof. */
|
|
36
97
|
export async function observeCloudOnboarding(ctx, scope = {}) {
|
|
37
98
|
const cfg = loadConfig(ctx.home);
|
package/dist/onboard-runtime.js
CHANGED
|
@@ -15,7 +15,11 @@ import { onboardRunnerAdapter } from "./onboard-runner.js";
|
|
|
15
15
|
import { onboardAutomationManagementAdapter } from "./onboard-automation-management.js";
|
|
16
16
|
import { onboardWorkflowVerificationAdapter } from "./onboard-workflow.js";
|
|
17
17
|
import { onboardAccountsAdapter } from "./onboard-accounts.js";
|
|
18
|
+
import { onboardFirstTicketAdapter } from "./onboard-first-ticket.js";
|
|
19
|
+
import { onboardValuesAdapter } from "./onboard-values.js";
|
|
18
20
|
import { githubInstallationAdapter } from "./onboard-github.js";
|
|
21
|
+
import { connectChecklistAdapter, } from "./onboard-checklist.js";
|
|
22
|
+
import { onboardHousekeepingAdapter, } from "./onboard-housekeeping.js";
|
|
19
23
|
import { guardOnboardCapabilities, } from "./onboard-capabilities.js";
|
|
20
24
|
import { onboardSettingsAdapter, } from "./onboard-settings.js";
|
|
21
25
|
import { TIMED_OUT_REASONS } from "./setup-onboard-copy.js";
|
|
@@ -147,9 +151,21 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
147
151
|
// CTC-4680 round 5: whether the step now acting ran a browser wait, so a timed-out wait can be
|
|
148
152
|
// re-offered with a fresh link instead of moving on to steps that need it.
|
|
149
153
|
let waited = false;
|
|
150
|
-
const waitFor = (message, run) => {
|
|
154
|
+
const waitFor = (message, run, page) => {
|
|
151
155
|
waited = true;
|
|
152
|
-
return hooks.ui.wait(message, run);
|
|
156
|
+
return hooks.ui.wait(message, run, page);
|
|
157
|
+
};
|
|
158
|
+
// CTC-4680: the Connect accounts page opens once per run, whichever step needs it first.
|
|
159
|
+
const connectPage = { openedBy: null };
|
|
160
|
+
const checklistOptions = {
|
|
161
|
+
openBrowser: hooks.openBrowser ?? openConsentBrowser,
|
|
162
|
+
wait: interactiveWait
|
|
163
|
+
? (message, work, page) => waitFor(message, work, page)
|
|
164
|
+
: undefined,
|
|
165
|
+
note: (text) => hooks.ui?.note ? hooks.ui.note(text) : hooks.ui ? hooks.ui.message(text) : ctx.stderr(text),
|
|
166
|
+
sleep: hooks.sleep,
|
|
167
|
+
page: connectPage,
|
|
168
|
+
cache: { last: null },
|
|
153
169
|
};
|
|
154
170
|
const personalAdapter = (provider) => personalConsentAdapter({
|
|
155
171
|
provider,
|
|
@@ -237,12 +253,17 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
237
253
|
: undefined,
|
|
238
254
|
sleep: hooks.sleep,
|
|
239
255
|
}),
|
|
240
|
-
"github.install":
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
256
|
+
"github.install": connectChecklistAdapter({
|
|
257
|
+
...checklistOptions,
|
|
258
|
+
step: "github.install",
|
|
259
|
+
item: "github-app",
|
|
260
|
+
fallback: githubInstallationAdapter({
|
|
261
|
+
openBrowser: hooks.openBrowser ?? openConsentBrowser,
|
|
262
|
+
wait: interactiveWait
|
|
263
|
+
? (message, work) => waitFor(message, work)
|
|
264
|
+
: undefined,
|
|
265
|
+
sleep: hooks.sleep,
|
|
266
|
+
}),
|
|
246
267
|
}),
|
|
247
268
|
settings: onboardSettingsAdapter({
|
|
248
269
|
...hooks.settings,
|
|
@@ -262,21 +283,23 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
262
283
|
: undefined,
|
|
263
284
|
message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
|
|
264
285
|
}),
|
|
265
|
-
accounts:
|
|
266
|
-
...
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
286
|
+
accounts: connectChecklistAdapter({
|
|
287
|
+
...checklistOptions,
|
|
288
|
+
step: "accounts",
|
|
289
|
+
item: "ai-account",
|
|
290
|
+
// A named account is checked on its own: the checklist answers for the whole workspace.
|
|
291
|
+
enabled: typeof args.flags["coding-account"] !== "string",
|
|
292
|
+
fallback: onboardAccountsAdapter({
|
|
293
|
+
...(interactiveWait
|
|
294
|
+
? {
|
|
295
|
+
waitForAccount: (work) => waitFor("Waiting for an AI account", work),
|
|
296
|
+
sleep: hooks.sleep,
|
|
297
|
+
}
|
|
298
|
+
: {}),
|
|
299
|
+
...(typeof args.flags["coding-account"] === "string"
|
|
300
|
+
? { slot: args.flags["coding-account"] }
|
|
301
|
+
: {}),
|
|
302
|
+
}),
|
|
280
303
|
}),
|
|
281
304
|
capacity: onboardCapacityAdapter({
|
|
282
305
|
message: (text) => (hooks.ui ? hooks.ui.message(text) : ctx.stderr(text)),
|
|
@@ -315,9 +338,14 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
315
338
|
evidence: { provider: "cloud" },
|
|
316
339
|
},
|
|
317
340
|
},
|
|
318
|
-
housekeeping:
|
|
319
|
-
|
|
320
|
-
|
|
341
|
+
housekeeping: onboardHousekeepingAdapter(hooks.scheduler),
|
|
342
|
+
values: onboardValuesAdapter(),
|
|
343
|
+
// Only a person at a terminal picks a ticket; --yes, JSON and headless runs leave it waiting.
|
|
344
|
+
"first-ticket": onboardFirstTicketAdapter(interactiveWait && hooks.ui?.chooseFirstTicket
|
|
345
|
+
? {
|
|
346
|
+
choose: (tickets, where) => hooks.ui.chooseFirstTicket(tickets.map((ticket) => ({ ...ticket })), { ...where }),
|
|
347
|
+
}
|
|
348
|
+
: {}),
|
|
321
349
|
ready: { check: hooks.ready },
|
|
322
350
|
};
|
|
323
351
|
const get = (path) => ({ method: "GET", path });
|
|
@@ -377,6 +405,10 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
377
405
|
check: [get("/api/v1/repos"), get("/api/v1/agent/contract")],
|
|
378
406
|
fallback: "connections",
|
|
379
407
|
},
|
|
408
|
+
values: {
|
|
409
|
+
check: [get("/api/v1/agent/contract")],
|
|
410
|
+
fallback: "connections",
|
|
411
|
+
},
|
|
380
412
|
ready: {
|
|
381
413
|
check: [get("/api/v1/agent/contract")],
|
|
382
414
|
fallback: "connections",
|
|
@@ -409,7 +441,8 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
409
441
|
const timedOut = result.state === "waiting" &&
|
|
410
442
|
(TIMED_OUT_REASONS.has(result.reason ?? "") ||
|
|
411
443
|
(step === "accounts" &&
|
|
412
|
-
result.reason === "account_enrollment_required"
|
|
444
|
+
(result.reason === "account_enrollment_required" ||
|
|
445
|
+
result.reason === "ai_account_not_usable")));
|
|
413
446
|
if (!timedOut || !waited || signal?.aborted || hooks.ui?.signal.aborted)
|
|
414
447
|
return result;
|
|
415
448
|
if (!(await retry(step)))
|
|
@@ -1,60 +1,36 @@
|
|
|
1
|
-
import { setupStepView, SETUP_NUMBERS } from "./setup-onboard-copy.js";
|
|
1
|
+
import { setupPartHeading, setupStepView, SETUP_NUMBERS, SETUP_PART_NUMBERS, SETUP_PART_OF, } from "./setup-onboard-copy.js";
|
|
2
2
|
export const COMPUTER_CHECKS = ["machine", "cli", "skills", "legacy"];
|
|
3
3
|
const PLAN = [
|
|
4
4
|
["On this computer", "signin", "you approve once in your browser"],
|
|
5
|
-
[
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"schedule the daily update, where supported",
|
|
9
|
-
],
|
|
10
|
-
[
|
|
11
|
-
"In Linear and GitHub",
|
|
12
|
-
"linear.workspace",
|
|
13
|
-
"an admin connects the workspace",
|
|
14
|
-
],
|
|
15
|
-
[
|
|
16
|
-
"In Linear and GitHub",
|
|
17
|
-
"linear.personal",
|
|
18
|
-
"you connect your own Linear account",
|
|
19
|
-
],
|
|
5
|
+
["On this computer", "housekeeping", "schedule the daily update"],
|
|
6
|
+
["In Linear and GitHub", "linear.workspace", "an admin connects the workspace"],
|
|
7
|
+
["In Linear and GitHub", "linear.personal", "you connect your own Linear account"],
|
|
20
8
|
["In Linear and GitHub", "linear.team", "you choose the team"],
|
|
21
|
-
[
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
],
|
|
26
|
-
[
|
|
27
|
-
"In Linear and GitHub",
|
|
28
|
-
"linear.automations",
|
|
29
|
-
"check how pull requests change issue states",
|
|
30
|
-
],
|
|
31
|
-
[
|
|
32
|
-
"In Linear and GitHub",
|
|
33
|
-
"github.install",
|
|
34
|
-
"an admin chooses access on GitHub",
|
|
35
|
-
],
|
|
36
|
-
[
|
|
37
|
-
"In Linear and GitHub",
|
|
38
|
-
"github.personal",
|
|
39
|
-
"you connect your own GitHub account",
|
|
40
|
-
],
|
|
41
|
-
[
|
|
42
|
-
"In Linear and GitHub",
|
|
43
|
-
"github.repos",
|
|
44
|
-
"you choose where Catalyst can work",
|
|
45
|
-
],
|
|
46
|
-
["Work", "accounts", "check a coding account"],
|
|
9
|
+
["In Linear and GitHub", "linear.adopt", "review any states and labels to add"],
|
|
10
|
+
["In Linear and GitHub", "linear.automations", "check how pull requests change issue states"],
|
|
11
|
+
["In Linear and GitHub", "github.install", "an admin chooses access on GitHub"],
|
|
12
|
+
["In Linear and GitHub", "github.personal", "you connect your own GitHub account"],
|
|
13
|
+
["In Linear and GitHub", "github.repos", "you choose where Catalyst can work"],
|
|
14
|
+
["Work", "accounts", "check an AI account"],
|
|
47
15
|
["Work", "capacity", "check which runners can take work"],
|
|
48
16
|
["Work", "settings", "you review settings before saving them"],
|
|
17
|
+
["Work", "values", "check every value the repositories need has one"],
|
|
49
18
|
["Work", "first-ticket", "you choose a ticket to start"],
|
|
50
19
|
];
|
|
51
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The plan's rows. `parts` is what the terminal draws: part headings, numbers restarting in each
|
|
22
|
+
* part. `json` is the plain-text plan an agent reads beside `--json` (dry run, --json on stderr,
|
|
23
|
+
* headless): the older groups and the numbers JSON `actions[].number` reports (CTC-4680).
|
|
24
|
+
*/
|
|
25
|
+
export function standalonePlan(journal, scope, runnerSelected = false, numbering = "parts") {
|
|
26
|
+
const json = numbering === "json";
|
|
52
27
|
const includes = (id) => !scope || scope.includes(id);
|
|
53
28
|
const steps = new Map(journal.steps.map((step) => [step.id, step]));
|
|
54
29
|
const rows = [];
|
|
55
30
|
if (COMPUTER_CHECKS.some((id) => includes(id)))
|
|
56
31
|
rows.push({
|
|
57
|
-
group: "On this computer",
|
|
32
|
+
group: json ? "On this computer" : setupPartHeading(1, false),
|
|
33
|
+
part: 1,
|
|
58
34
|
number: 1,
|
|
59
35
|
title: "Check this computer",
|
|
60
36
|
detail: includes("legacy")
|
|
@@ -62,9 +38,31 @@ export function standalonePlan(journal, scope, runnerSelected = false) {
|
|
|
62
38
|
: "check the selected machine requirements",
|
|
63
39
|
});
|
|
64
40
|
for (const [group, id, detail] of PLAN) {
|
|
41
|
+
// The terminal gives this computer's runner its own row after "Check runners". The plain-text
|
|
42
|
+
// plan keeps it folded into that row, under the number JSON reports for both.
|
|
43
|
+
if (!json && id === "capacity") {
|
|
44
|
+
const row = (step, text) => rows.push({
|
|
45
|
+
group: setupPartHeading(3, false),
|
|
46
|
+
part: 3,
|
|
47
|
+
number: SETUP_PART_NUMBERS[step],
|
|
48
|
+
title: setupStepView({ id: step, state: "pending" }).title,
|
|
49
|
+
detail: steps.get(step)?.state === "done" ? `check again; ${text}` : text,
|
|
50
|
+
});
|
|
51
|
+
if (includes("capacity"))
|
|
52
|
+
row("capacity", detail);
|
|
53
|
+
if (includes("runner"))
|
|
54
|
+
row("runner", runnerSelected
|
|
55
|
+
? "start a Catalyst runner here with Docker"
|
|
56
|
+
: "optional; this computer takes work only if you choose it");
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// The terminal gives repository values their own row; the plain-text plan keeps them folded
|
|
60
|
+
// into the settings row, under the number JSON reports for both.
|
|
61
|
+
if (json && id === "values")
|
|
62
|
+
continue;
|
|
65
63
|
if (!includes(id) &&
|
|
66
64
|
!(id === "capacity" && includes("runner")) &&
|
|
67
|
-
!(id === "settings" && includes("values")))
|
|
65
|
+
!(json && id === "settings" && includes("values")))
|
|
68
66
|
continue;
|
|
69
67
|
const reviewedDetail = id === "capacity" && includes("runner") && runnerSelected
|
|
70
68
|
? "start a Catalyst runner here with Docker"
|
|
@@ -72,8 +70,9 @@ export function standalonePlan(journal, scope, runnerSelected = false) {
|
|
|
72
70
|
? "review the optional runner on this machine; start only if selected"
|
|
73
71
|
: detail;
|
|
74
72
|
rows.push({
|
|
75
|
-
group,
|
|
76
|
-
|
|
73
|
+
group: json ? group : setupPartHeading(SETUP_PART_OF[id], false),
|
|
74
|
+
part: SETUP_PART_OF[id],
|
|
75
|
+
number: (json ? SETUP_NUMBERS : SETUP_PART_NUMBERS)[id],
|
|
77
76
|
title: setupStepView({ id, state: "pending" }).title,
|
|
78
77
|
detail: steps.get(id)?.state === "done"
|
|
79
78
|
? `check again; ${reviewedDetail}`
|
|
@@ -82,29 +81,37 @@ export function standalonePlan(journal, scope, runnerSelected = false) {
|
|
|
82
81
|
}
|
|
83
82
|
return rows;
|
|
84
83
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
84
|
+
/** What setup does next on its own, in the order it runs: steps not finished and not already listed
|
|
85
|
+
* as something a person must do (`listed`). A step setup never reached because it paused is here. */
|
|
86
|
+
const CONTINUES = [
|
|
87
|
+
[["linear.workspace"], "connects your Linear workspace"],
|
|
88
|
+
[["linear.personal"], "connects your Linear account"],
|
|
89
|
+
[["linear.team"], "chooses a Linear team"],
|
|
90
|
+
[["linear.adopt"], "sets up the team's workflow"],
|
|
91
|
+
[["linear.automations"], "checks Linear's pull request automations"],
|
|
92
|
+
[["github.install"], "installs Catalyst on GitHub"],
|
|
93
|
+
[["github.personal"], "connects your GitHub account"],
|
|
94
|
+
[["github.repos", "projects"], "chooses repositories"],
|
|
95
|
+
[["accounts"], "checks an AI account"],
|
|
96
|
+
[["capacity", "runner"], "checks runners"],
|
|
97
|
+
[["settings"], "reviews repository settings"],
|
|
98
|
+
[["values"], "checks repository values"],
|
|
99
|
+
[["first-ticket"], "starts a first ticket"],
|
|
100
|
+
];
|
|
101
|
+
export function pendingContinuation(journal, listed = new Set()) {
|
|
102
|
+
const unfinished = (ids) => !ids.some((id) => listed.has(id)) &&
|
|
103
|
+
ids.some((id) => {
|
|
104
|
+
const step = journal.steps.find((step) => step.id === id);
|
|
105
|
+
return (step &&
|
|
106
|
+
step.state !== "done" &&
|
|
107
|
+
setupStepView(step, journal).mark !== "skip");
|
|
108
|
+
});
|
|
109
|
+
const next = CONTINUES.filter(([ids]) => unfinished(ids)).map(([, words]) => words);
|
|
103
110
|
if (!next.length)
|
|
104
111
|
return null;
|
|
105
112
|
const last = next.pop();
|
|
106
113
|
return `Then setup ${next.length ? `${next.join(", ")} and ` : ""}${last}.`;
|
|
107
114
|
}
|
|
108
115
|
export function standalonePlanNotes(localSync) {
|
|
109
|
-
return `${localSync ? "Local sync is selected." : "Local sync stays off, so Catalyst reads from the cloud."}
|
|
116
|
+
return `${localSync ? "Local sync is selected." : "Local sync stays off, so Catalyst reads from the cloud."} Setup reads your AI accounts without sending them a request. Completed steps are checked again before work starts. Your code, git settings and other skills stay as they are.`;
|
|
110
117
|
}
|