@catalyst-cloud/cli 0.14.9 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +41 -0
- package/dist/args.js +34 -2
- package/dist/capabilities.js +2 -1
- package/dist/cli-update-notice.js +76 -0
- package/dist/cli.js +267 -94
- package/dist/oauth.js +80 -18
- package/dist/onboard-accounts.js +34 -5
- package/dist/onboard-automation-management.js +27 -9
- package/dist/onboard-capacity.js +10 -1
- package/dist/onboard-existing.js +132 -31
- package/dist/onboard-github.js +171 -6
- package/dist/onboard-headless.js +647 -0
- package/dist/onboard-interrupts.js +41 -0
- package/dist/onboard-next.js +215 -0
- package/dist/onboard-permissions.js +100 -0
- package/dist/onboard-personal.js +46 -10
- package/dist/onboard-runner-cloud.js +213 -0
- package/dist/onboard-runner-egress.js +67 -0
- package/dist/onboard-runner.js +780 -0
- package/dist/onboard-runtime.js +48 -8
- package/dist/onboard-standalone-copy.js +110 -0
- package/dist/onboard-team-create.js +267 -0
- package/dist/onboard-ui.js +623 -29
- package/dist/onboard-workflow.js +311 -168
- package/dist/onboard-workspace.js +52 -5
- package/dist/onboard.js +359 -207
- package/dist/setup-events.js +100 -0
- package/dist/setup-onboard-copy.js +438 -0
- package/dist/setup-prompt-frame.js +94 -0
- package/dist/setup-prompts.js +108 -0
- package/dist/setup-render.js +339 -0
- package/dist/setup-signin.js +187 -0
- package/dist/setup.js +426 -0
- package/package.json +7 -3
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +3 -2
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +1 -1
- package/vendor/README.md +4 -0
- package/vendor/self-host/compose.yaml +110 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.service +14 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress-attest.timer +10 -0
- package/vendor/self-host/linux-session-egress/catalyst-session-egress.service +19 -0
- package/vendor/self-host/linux-session-egress/session-egress.py +291 -0
- package/vendor/self-host/provenance.json +11 -0
package/dist/onboard-next.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { nativeEgressInstallCommands } from "./onboard-runner-egress.js";
|
|
1
2
|
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
3
|
+
import { githubInstallationPage } from "./onboard-permissions.js";
|
|
2
4
|
/** What a person reads for a step that is not done. Each line ends in an action they can take. */
|
|
3
5
|
const REASONS = {
|
|
4
6
|
account_identity_unverified: "Your login changed while coding accounts were checked. Run catalyst onboard again to verify your person and workspace.",
|
|
5
7
|
account_inventory_unavailable: "Coding accounts could not be read. Run catalyst onboard again to retry.",
|
|
6
8
|
account_inventory_unverified: "The server did not return a fresh supported coding-account list. Run catalyst onboard after the server update.",
|
|
9
|
+
coding_account_not_found: "The coding account named by --coding-account is not enrolled in this workspace. Run catalyst accounts to list the slots, then run catalyst onboard with one of them.",
|
|
7
10
|
account_validation_admin_required: "An administrator must check the workspace's coding-account access.",
|
|
8
11
|
account_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
|
|
9
12
|
account_validation_unavailable: "The provider check could not finish. Run catalyst onboard again to retry.",
|
|
@@ -32,6 +35,7 @@ const REASONS = {
|
|
|
32
35
|
github_installation_admin_required: "A workspace owner or administrator approves the GitHub App installation.",
|
|
33
36
|
github_installation_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
|
|
34
37
|
github_installation_browser_unavailable: "This computer could not open a browser. Install the GitHub App from this workspace’s Integrations page in your browser, then run catalyst onboard to resume.",
|
|
38
|
+
skills_install_unverified: "The Catalyst skills are not in this user's skills folder. Run catalyst install, then run catalyst onboard.",
|
|
35
39
|
step_not_available_in_this_release: "This setup step is not available yet.",
|
|
36
40
|
prerequisite_not_ready: "Waiting for an earlier setup step.",
|
|
37
41
|
local_sync_not_selected: "Using cloud reads. Local sync was not selected.",
|
|
@@ -54,6 +58,10 @@ const REASONS = {
|
|
|
54
58
|
project_login_refresh_required: "Renew your login with catalyst onboard, then review the plan before creating this project.",
|
|
55
59
|
project_create_unverified: "Project creation could not be confirmed. Run catalyst onboard to check for the project before trying again.",
|
|
56
60
|
team_read_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
|
|
61
|
+
team_create_unavailable: "The Linear team could not be created, and nothing was created. Run catalyst onboard to try again, or create the team in Linear and pick it.",
|
|
62
|
+
team_create_unverified: "Setup could not confirm whether the Linear team was created. Check Linear for it before creating it again.",
|
|
63
|
+
team_create_grant_required: "Your own Linear connection is missing or has lapsed. Reconnect it in Settings → Connected accounts, then run catalyst onboard. Nothing was created.",
|
|
64
|
+
team_create_identity_unverified: "Your login changed while setup was creating a team. Resume with the original workspace and person.",
|
|
57
65
|
team_choice_required: "Choose an existing team with catalyst onboard --team <team ID or key>.",
|
|
58
66
|
team_selection_unverified: "Your selected Linear team could not be verified. Run catalyst onboard --team <team ID or key> to choose again.",
|
|
59
67
|
repository_choice_required: "Choose repositories with catalyst onboard --repo <owner/name>. Repeat --repo to choose more than one.",
|
|
@@ -71,13 +79,62 @@ const REASONS = {
|
|
|
71
79
|
settings_draft_storage_timeout: "Keeping your settings draft took too long. Run catalyst onboard to retry.",
|
|
72
80
|
settings_approval_unverified: "Settings still need review and approval; kept drafts do not count. Settings are optional for a first ticket. To finish them later, run catalyst onboard from the repository's checkout.",
|
|
73
81
|
repository_selection_unverified: "Your selected repositories could not be verified. Run catalyst onboard --repo <owner/name> to choose again.",
|
|
82
|
+
workflow_adoption_declined: "Skipped for now. Run catalyst onboard to review the changes again.",
|
|
74
83
|
workflow_identity_unverified: "Setup could not confirm the signed-in workspace, person or role for this team. Run catalyst onboard again as a workspace owner or administrator.",
|
|
75
84
|
workflow_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
|
|
76
85
|
workflow_mapping_changed: "The team's workflow changed while setup checked it. Run catalyst onboard again.",
|
|
86
|
+
workflow_admin_required: "Applying the Catalyst workflow needs a workspace owner or administrator. Ask one to run catalyst onboard for this team.",
|
|
87
|
+
workflow_plan_changed: "The team's Linear workflow changed while setup was applying it. Run catalyst onboard again to see the new plan.",
|
|
88
|
+
verification_pending: "Setup made the change but could not confirm it yet. Run catalyst onboard again to check.",
|
|
89
|
+
automations_compatible: "Nothing to change.",
|
|
77
90
|
workflow_unavailable: "Setup could not read the team's workflow. Run catalyst onboard again to retry.",
|
|
78
91
|
capacity_admission_unverified: "No runner is allowed to take work for this workspace yet. Enroll a self-hosted runner host, or ask Catalyst to enable cloud runners for it. Then run catalyst onboard.",
|
|
79
92
|
capacity_currently_full: "Every runner slot for this repository is in use. Run catalyst onboard again after current work finishes.",
|
|
80
93
|
capacity_unavailable: "Setup could not read runner capacity. Run catalyst onboard again to retry.",
|
|
94
|
+
runner_not_selected: "This setup does not start a runner on this machine. To start one, run catalyst onboard --runner.",
|
|
95
|
+
runner_docker_missing: "A runner needs Docker with Compose (Docker Desktop, OrbStack or Docker Engine), and setup could not reach it. Start Docker, or install it, then run catalyst onboard --runner.",
|
|
96
|
+
runner_identity_unverified: "A workspace owner or administrator enrolls a runner. Sign in as one with catalyst login, then run catalyst onboard --runner.",
|
|
97
|
+
runner_context_unverified: "Setup could not confirm the selected Linear team. Run catalyst onboard again.",
|
|
98
|
+
runner_directory_unavailable: "Setup could not prepare the runner folder in Catalyst's state directory. Check its permissions, then run catalyst onboard again.",
|
|
99
|
+
runner_image_unpinned: "Each runner image must be pinned as name@sha256:<digest>, and Catalyst's runner image is not public yet. Ask Catalyst support to load it here and set CATALYST_RUNNER_IMAGE to the reference they give you. A CATALYST_SUPERVISOR_IMAGE or CATALYST_WATCHDOG_IMAGE override takes the same form. Then run catalyst onboard --runner.",
|
|
100
|
+
runner_image_emulated: "A runner image here is built for another processor, and Catalyst does not run work under emulation. Ask Catalyst support for the image built for this processor, then run catalyst onboard again.",
|
|
101
|
+
runner_session_network_misshaped: "A Docker network named catalyst-session-v1 exists with the wrong settings. Remove it with docker network rm catalyst-session-v1 when nothing uses it, then run catalyst onboard again.",
|
|
102
|
+
runner_session_network_failed: "Docker could not create the catalyst-session-v1 network. Check that Docker is running, then run catalyst onboard again.",
|
|
103
|
+
runner_docker_socket_unreadable: "Setup could not read the group of the Docker socket at /var/run/docker.sock. Check that Docker is running, then run catalyst onboard again.",
|
|
104
|
+
runner_engine_unsupported: "Runner setup needs local Docker Desktop or OrbStack on Mac, or native Linux Docker at /var/run/docker.sock. Select a supported local engine, then run catalyst onboard --runner.",
|
|
105
|
+
runner_enrollment_unavailable: "Setup could not read this workspace's runner hosts. Run catalyst onboard again to retry.",
|
|
106
|
+
runner_enrolled_for_other_team: "This runner is enrolled for another Linear team. Keep using that team, or ask your administrator to revoke the old enrollment before setting up this machine for the selected team.",
|
|
107
|
+
runner_enrollment_stale: "This machine still holds the credential of a runner enrollment that was revoked. Stop the runner with docker compose -p catalyst-host down, remove the credential with docker volume rm catalyst-host_host-credential (this also removes the organization key), then run catalyst onboard --runner.",
|
|
108
|
+
runner_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
|
|
109
|
+
runner_join_token_unavailable: "Catalyst did not issue a join token for this machine. Run catalyst onboard again to retry.",
|
|
110
|
+
runner_session_network_unavailable: "Docker could not inspect the runner session network. Check engine access and run the same setup command again. Setup has not created a replacement network.",
|
|
111
|
+
runner_capability_unavailable: "Catalyst could not read runner setup support. Run the same setup command again.",
|
|
112
|
+
runner_capability_unverified: "Catalyst returned runner setup information this CLI cannot verify. Ask Catalyst support to check the server contract, then run the same setup command.",
|
|
113
|
+
runner_org_key_journal_unavailable: "Setup cannot safely use its saved runner credential request. Keep the request file and ask Catalyst support to inspect it.",
|
|
114
|
+
runner_org_key_recovery_required: "The previous runner key request may already have issued a key. Ask your workspace administrator to revoke that saved request and explicitly restart it. Setup will not mint another key automatically.",
|
|
115
|
+
runner_org_key_reconciliation_pending: "The previous runner key request is still uncertain. Run the same setup command to check its saved request again.",
|
|
116
|
+
runner_org_key_response_unverified: "Setup could not verify the runner key's tenant and scopes. Ask your workspace administrator to inspect the saved request.",
|
|
117
|
+
runner_cloud_unavailable: "Catalyst did not complete the runner setup request. Run the same setup command to check its saved request before continuing.",
|
|
118
|
+
runner_admission_disabled: "The selected team does not admit runner work. Run the same setup command with runner selection to explicitly enable its restricted admission policy.",
|
|
119
|
+
runner_org_key_file_invalid: "CATALYST_RUNNER_ORG_KEY_FILE must name a regular file that holds one organization key. Fix the file, then run catalyst onboard again.",
|
|
120
|
+
runner_org_key_invalid: "The runner account key is invalid, belongs to another workspace, or lacks mirror read, write and feed permissions. Supply a scoped key for this workspace with CATALYST_RUNNER_ORG_KEY_FILE, then run catalyst onboard --runner.",
|
|
121
|
+
runner_org_key_unavailable: "The runner account key could not be checked with Catalyst. Run catalyst onboard --runner to retry.",
|
|
122
|
+
runner_org_key_write_failed: "Setup could not place the organization key on the runner. Check that Docker is running, then run catalyst onboard again.",
|
|
123
|
+
runner_compose_failed: "Docker Compose could not start the runner. Run docker compose -p catalyst-host logs to see why, then run catalyst onboard again.",
|
|
124
|
+
runner_compose_not_running: "The runner's supervisor is not running. Run docker compose -p catalyst-host logs supervisor to see why, then run catalyst onboard again.",
|
|
125
|
+
runner_enrollment_unverified: "The runner started but has not enrolled yet. Run docker compose -p catalyst-host logs supervisor to see why, or run catalyst onboard again in a minute.",
|
|
126
|
+
runner_needs_runner_flag: "This machine was set up to run Catalyst's work, but its runner is not running or not enrolled. To bring it up, run catalyst onboard --runner.",
|
|
127
|
+
runner_capability_pending: "The runner enrolled but has not reported its capacity yet. Run catalyst onboard again in a minute.",
|
|
128
|
+
runner_admission_unverified: "Setup could not read whether the team's pool admits runner hosts. Run catalyst onboard again to retry.",
|
|
129
|
+
github_app_permissions_outdated: "GitHub App needs updated permissions. An organization owner on GitHub accepts the pending request on the App's installation page, then run catalyst onboard.",
|
|
130
|
+
github_app_repository_missing: "The GitHub App installation cannot reach a repository a project registers. An organization owner on GitHub adds it under the installation's Repository access, then run catalyst onboard.",
|
|
131
|
+
github_app_repository_not_installed: "No GitHub App installation can reach a repository a project registers. A workspace owner or administrator installs the GitHub App on its owner: run catalyst onboard.",
|
|
132
|
+
github_app_permissions_unverified: "The GitHub App installation works, but its granted permissions could not be checked. Run catalyst onboard to try again.",
|
|
133
|
+
github_app_repository_access_unverified: "Whether the GitHub App reaches every registered repository could not be checked. Run catalyst onboard to try again.",
|
|
134
|
+
linear_workspace_scope_outdated: "The workspace's Linear connection (the Catalyst app) is missing scopes this version needs. A workspace owner or administrator re-authorizes it from the workspace's Integrations page, then run catalyst onboard.",
|
|
135
|
+
linear_workspace_permissions_unverified: "The workspace's Linear connection works, but its granted scopes could not be checked. Run catalyst onboard to try again.",
|
|
136
|
+
linear_personal_scope_outdated: "Your personal Linear connection is missing scopes this version needs. Re-authorize it from Connected accounts, then run catalyst onboard.",
|
|
137
|
+
personal_permissions_unverified: "Your personal connection's granted scopes could not be checked. Run catalyst onboard to try again.",
|
|
81
138
|
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.",
|
|
82
139
|
};
|
|
83
140
|
/** The saved login's web address, or nothing when the config is missing, unreadable or not http(s). */
|
|
@@ -115,17 +172,145 @@ export function onboardTeamKey(journal) {
|
|
|
115
172
|
? key
|
|
116
173
|
: undefined;
|
|
117
174
|
}
|
|
175
|
+
/** Receipt evidence printed inside a sentence: names, scopes and permission labels only. */
|
|
176
|
+
function evidenceText(value) {
|
|
177
|
+
return typeof value === "string" &&
|
|
178
|
+
/^[A-Za-z0-9 ._:;/(),-]{1,2000}$/.test(value)
|
|
179
|
+
? value
|
|
180
|
+
: undefined;
|
|
181
|
+
}
|
|
182
|
+
const LINEAR_START = {
|
|
183
|
+
linear_workspace_scope_outdated: "/settings/connections",
|
|
184
|
+
linear_personal_scope_outdated: "/connect/linear/personal/start",
|
|
185
|
+
};
|
|
186
|
+
/** A receipt URL, printed only when it is the page this reason's action lives on: the exact
|
|
187
|
+
* installation's page on github.com, or this cloud's own OAuth start for the grant in question. */
|
|
188
|
+
function actionUrl(step, baseUrl) {
|
|
189
|
+
const href = webUrl(step.evidence?.url);
|
|
190
|
+
if (!href)
|
|
191
|
+
return undefined;
|
|
192
|
+
const url = new URL(href);
|
|
193
|
+
if (url.protocol !== "https:" ||
|
|
194
|
+
url.hash ||
|
|
195
|
+
(url.search &&
|
|
196
|
+
!(step.reason === "linear_workspace_scope_outdated" &&
|
|
197
|
+
url.pathname === "/settings/connections" &&
|
|
198
|
+
url.search === "?reauthorize=linear")))
|
|
199
|
+
return undefined;
|
|
200
|
+
const evidence = step.evidence ?? {};
|
|
201
|
+
if (step.reason === "github_app_permissions_outdated" ||
|
|
202
|
+
step.reason === "github_app_repository_missing")
|
|
203
|
+
return githubInstallationPage(url, typeof evidence.installation === "string" ? evidence.installation : "", typeof evidence.org === "string" ? evidence.org : null, step.reason === "github_app_permissions_outdated")
|
|
204
|
+
? href
|
|
205
|
+
: undefined;
|
|
206
|
+
const base = baseUrl ? webUrl(baseUrl) : undefined;
|
|
207
|
+
return base &&
|
|
208
|
+
new URL(base).origin === url.origin &&
|
|
209
|
+
url.pathname === LINEAR_START[step.reason ?? ""] &&
|
|
210
|
+
(step.reason !== "linear_workspace_scope_outdated" ||
|
|
211
|
+
url.search === "?reauthorize=linear")
|
|
212
|
+
? href
|
|
213
|
+
: undefined;
|
|
214
|
+
}
|
|
215
|
+
/** CTC-4629: the outdated-connection lines, built from the evidence the adapters recorded. */
|
|
216
|
+
function permissionText(step, baseUrl) {
|
|
217
|
+
const evidence = step.evidence ?? {};
|
|
218
|
+
const url = actionUrl(step, baseUrl);
|
|
219
|
+
const missing = evidenceText(evidence.missing);
|
|
220
|
+
const org = evidenceText(evidence.org);
|
|
221
|
+
const repository = evidenceText(evidence.repository);
|
|
222
|
+
const github = evidence.actor === "github-account-owner"
|
|
223
|
+
? `The owner of the ${org ?? "installing"} GitHub account`
|
|
224
|
+
: "An organization owner on GitHub";
|
|
225
|
+
switch (step.reason) {
|
|
226
|
+
case "github_app_permissions_outdated":
|
|
227
|
+
if (!url)
|
|
228
|
+
return undefined;
|
|
229
|
+
return `GitHub App needs updated permissions${org ? ` on ${org}` : ""}${missing ? `: ${missing}` : ""}. ${github} reviews and accepts the request at ${url}, then run catalyst onboard.`;
|
|
230
|
+
case "github_app_repository_missing":
|
|
231
|
+
if (!url || !repository)
|
|
232
|
+
return undefined;
|
|
233
|
+
return `The GitHub App installation${org ? ` on ${org}` : ""} cannot reach ${repository}, which a project registers. ${github} adds it under Repository access at ${url}, then run catalyst onboard.`;
|
|
234
|
+
case "github_app_repository_not_installed":
|
|
235
|
+
if (!repository)
|
|
236
|
+
return undefined;
|
|
237
|
+
return `No GitHub App installation can reach ${repository}, which a project registers. A workspace owner or administrator installs the GitHub App on ${org ?? repository.split("/")[0]}: run catalyst onboard.`;
|
|
238
|
+
case "linear_workspace_scope_outdated":
|
|
239
|
+
if (!url)
|
|
240
|
+
return undefined;
|
|
241
|
+
return `The workspace's Linear connection (the Catalyst app) is missing scopes this version needs${missing ? `: ${missing}` : ""}. A workspace owner or administrator re-authorizes it at ${url}, then run catalyst onboard.`;
|
|
242
|
+
case "linear_personal_scope_outdated":
|
|
243
|
+
if (!url)
|
|
244
|
+
return undefined;
|
|
245
|
+
return `Your personal Linear connection is missing scopes this version needs${missing ? `: ${missing}` : ""}. Re-authorize it at ${url}, then run catalyst onboard.`;
|
|
246
|
+
}
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
/** A done step's detail line: what a connection was granted, when the check recorded it. */
|
|
250
|
+
export function onboardStepDetail(step) {
|
|
251
|
+
const value = step.state === "done" ? step.evidence?.granted : undefined;
|
|
252
|
+
const granted = typeof value === "string" &&
|
|
253
|
+
value.length <= 512_000 &&
|
|
254
|
+
/^[A-Za-z0-9 ._:;/(),-]+$/.test(value)
|
|
255
|
+
? value.length <= 2_000
|
|
256
|
+
? value
|
|
257
|
+
: `${value.slice(0, 1_950)} (more permissions in the setup record)`
|
|
258
|
+
: undefined;
|
|
259
|
+
return granted ? `granted ${granted}` : undefined;
|
|
260
|
+
}
|
|
118
261
|
/** One step's next action. `waitsFor` names the unfinished step a prerequisite wait is blocked on. */
|
|
119
262
|
export function onboardReasonText(step, context = {}) {
|
|
120
263
|
const reason = step.reason ?? "";
|
|
264
|
+
if ([
|
|
265
|
+
"workspace_approval_required",
|
|
266
|
+
"personal_approval_required",
|
|
267
|
+
"github_installation_approval_required",
|
|
268
|
+
].includes(reason)) {
|
|
269
|
+
const path = reason === "workspace_approval_required"
|
|
270
|
+
? "/settings/connections?connect=linear"
|
|
271
|
+
: reason === "github_installation_approval_required"
|
|
272
|
+
? "/settings/connections?install=github"
|
|
273
|
+
: `/settings/connected-accounts?connect=${step.id === "github.personal" ? "github" : "linear"}`;
|
|
274
|
+
const where = context.baseUrl
|
|
275
|
+
? `${normalizeBaseUrl(context.baseUrl)}${path}`
|
|
276
|
+
: "Catalyst settings";
|
|
277
|
+
return `Open ${where} and approve this connection, then run catalyst onboard.`;
|
|
278
|
+
}
|
|
121
279
|
if (reason === "workflow_mapping_unverified")
|
|
122
280
|
return `This team's workflow mapping is not verified yet. Run catalyst team adopt ${onboardTeamKey(context.journal) ?? "<TEAM KEY>"} to preview the adoption plan. Run it again with the --yes --plan-hash value it prints. Then run catalyst onboard.`;
|
|
281
|
+
if (reason === "team_created_not_adopted" ||
|
|
282
|
+
reason === "team_create_unverified") {
|
|
283
|
+
const key = step.evidence?.teamKey;
|
|
284
|
+
const team = typeof key === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(key)
|
|
285
|
+
? key
|
|
286
|
+
: undefined;
|
|
287
|
+
const adopt = (key) => `Run catalyst team adopt ${key} to preview the adoption plan. Run it again with the --yes --plan-hash value it prints. Then run catalyst onboard.`;
|
|
288
|
+
if (reason === "team_created_not_adopted")
|
|
289
|
+
return `Linear team ${team ?? "<TEAM KEY>"} was created, but Catalyst's workflow is not set up on it yet. ${adopt(team ?? "<TEAM KEY>")}`;
|
|
290
|
+
if (team)
|
|
291
|
+
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.`;
|
|
292
|
+
}
|
|
123
293
|
if (reason === "account_enrollment_required") {
|
|
124
294
|
const where = context.baseUrl
|
|
125
295
|
? `at ${normalizeBaseUrl(context.baseUrl)}/settings/coding-accounts, the Settings → AI accounts page`
|
|
126
296
|
: "in the web app on the Settings → AI accounts page";
|
|
127
297
|
return `No coding account is enrolled. A workspace owner or administrator adds one ${where}. A Claude account needs its email and the token printed by \`claude setup-token\`. Then run catalyst onboard.`;
|
|
128
298
|
}
|
|
299
|
+
if (reason === "automation_management_unavailable") {
|
|
300
|
+
const conflicts = typeof step.evidence?.automations === "string" &&
|
|
301
|
+
/^(?:open|review|ready|merge)(?:,(?:open|review|ready|merge))*$/.test(step.evidence.automations);
|
|
302
|
+
if (!conflicts)
|
|
303
|
+
return "could not read them; checked again before work starts";
|
|
304
|
+
const key = onboardTeamKey(context.journal);
|
|
305
|
+
const workspace = context.journal?.steps.find((row) => row.id === "linear.workspace" && row.state === "done");
|
|
306
|
+
const slug = workspace?.evidence?.workspaceSlug;
|
|
307
|
+
if (key &&
|
|
308
|
+
/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(key) &&
|
|
309
|
+
typeof slug === "string" &&
|
|
310
|
+
/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(slug))
|
|
311
|
+
return `Open https://linear.app/${slug}/settings/teams/${key}/workflow and set each pull request automation to No action.`;
|
|
312
|
+
return `Open your ${key ? `${key} team's` : "team's"} workflow settings in Linear and set each pull request automation to No action.`;
|
|
313
|
+
}
|
|
129
314
|
if (reason === "cloud_capability_unavailable") {
|
|
130
315
|
// The capability guard records the web page in evidence.path only when it printed it.
|
|
131
316
|
const page = webUrl(step.evidence?.path);
|
|
@@ -133,7 +318,37 @@ export function onboardReasonText(step, context = {}) {
|
|
|
133
318
|
? `This setup step is not available on this server yet. Continue in the web app at ${page}, then run catalyst onboard.`
|
|
134
319
|
: "This setup step is not available on this server yet. Run catalyst onboard after the server update.";
|
|
135
320
|
}
|
|
321
|
+
if (reason === "runner_native_egress_setup_required") {
|
|
322
|
+
const commands = nativeEgressInstallCommands();
|
|
323
|
+
return "Native Linux needs a root-managed session egress producer before the runner can take work. Its elevated installation creates the isolated session network and private-range and host-isolation nftables rules. Review and authorize this separately; setup has not run it. It requires systemd, python3, Docker and nftables. " + (commands ? "Install the verified bundled artifacts, then run the same setup command:\n" + commands : "Setup could not verify its bundled artifacts. Reinstall the CLI before authorizing installation.");
|
|
324
|
+
}
|
|
325
|
+
if (reason === "runner_images_unavailable") {
|
|
326
|
+
const image = step.evidence?.image;
|
|
327
|
+
const named = typeof image === "string" &&
|
|
328
|
+
/^[a-z0-9][a-z0-9._:/-]{0,255}@sha256:[0-9a-f]{64}$/.test(image)
|
|
329
|
+
? ` ${image}`
|
|
330
|
+
: "";
|
|
331
|
+
return `This machine does not have the Catalyst image${named}, and its pull did not complete. Check network access and run the same setup command to retry the anonymous pull.`;
|
|
332
|
+
}
|
|
333
|
+
if (reason === "runner_org_key_missing") {
|
|
334
|
+
const where = context.baseUrl
|
|
335
|
+
? `at ${normalizeBaseUrl(context.baseUrl)}/settings/account-keys`
|
|
336
|
+
: "on the web app's Account API keys page";
|
|
337
|
+
return `The runner is enrolled, but it takes no work without an organization key. A workspace owner or administrator creates one with the mirror:read, mirror:write and mirror:feed scopes ${where} and saves it in a file only they can read. Then run catalyst onboard --runner with CATALYST_RUNNER_ORG_KEY_FILE set to that file.`;
|
|
338
|
+
}
|
|
339
|
+
if (reason === "runner_host_not_ready") {
|
|
340
|
+
const failing = step.evidence?.failing;
|
|
341
|
+
const checks = typeof failing === "string" && /^[a-z0-9.,_-]{1,500}$/.test(failing)
|
|
342
|
+
? `: ${failing.split(",").join(", ")}`
|
|
343
|
+
: "";
|
|
344
|
+
return `The runner is enrolled but its readiness checks fail${checks}. Run docker compose -p catalyst-host logs supervisor for the remedy, then run catalyst onboard again.`;
|
|
345
|
+
}
|
|
346
|
+
if (reason === "runner_admission_operator")
|
|
347
|
+
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.`;
|
|
136
348
|
if (reason === "prerequisite_not_ready" && context.waitsFor)
|
|
137
349
|
return `Runs after "${context.waitsFor}".`;
|
|
350
|
+
const permission = permissionText(step, context.baseUrl);
|
|
351
|
+
if (permission)
|
|
352
|
+
return permission;
|
|
138
353
|
return REASONS[reason] ?? reason.replaceAll("_", " ");
|
|
139
354
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** CTC-4629: the cloud's typed permissions verdict on an existing connection, verified before use.
|
|
2
|
+
* A verdict that fails any check is a shape error, so no unverified URL is ever printed. */
|
|
3
|
+
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
4
|
+
/** The saved login's origin, which a re-authorize URL must share; an unreadable one matches none. */
|
|
5
|
+
export function savedOrigin(ctx) {
|
|
6
|
+
try {
|
|
7
|
+
const cfg = loadConfig(ctx.home);
|
|
8
|
+
return cfg ? new URL(normalizeBaseUrl(cfg.baseUrl)).origin : "";
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
|
|
15
|
+
? v
|
|
16
|
+
: null;
|
|
17
|
+
export const LINEAR_SCOPE = /^[A-Za-z][A-Za-z0-9:_-]{0,63}$/;
|
|
18
|
+
export const GITHUB_PERMISSION = /^[A-Za-z0-9_-]{1,80} \((?:read|write|admin)\)$/;
|
|
19
|
+
const UNKNOWN = new Set([
|
|
20
|
+
"not-run",
|
|
21
|
+
"unreachable",
|
|
22
|
+
"expired-or-revoked",
|
|
23
|
+
"not-connected",
|
|
24
|
+
"grant-unreadable",
|
|
25
|
+
]);
|
|
26
|
+
const labels = (value, label, min = 0) => Array.isArray(value) &&
|
|
27
|
+
value.length >= min &&
|
|
28
|
+
value.length <= 128 &&
|
|
29
|
+
value.every((item) => typeof item === "string" && label.test(item))
|
|
30
|
+
? value
|
|
31
|
+
: null;
|
|
32
|
+
/** `undefined` when the cloud sent no verdict (an older cloud); `null` when it sent a bad one. */
|
|
33
|
+
export function parsePermissions(value, expected) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const row = object(value);
|
|
37
|
+
if (!row || row.grant !== expected.grant)
|
|
38
|
+
return null;
|
|
39
|
+
if (row.state === "unknown")
|
|
40
|
+
return typeof row.reason === "string" && UNKNOWN.has(row.reason)
|
|
41
|
+
? { state: "unknown", reason: row.reason }
|
|
42
|
+
: null;
|
|
43
|
+
const granted = labels(row.granted, expected.label);
|
|
44
|
+
if (!granted)
|
|
45
|
+
return null;
|
|
46
|
+
if (row.state === "current")
|
|
47
|
+
return { state: "current", granted };
|
|
48
|
+
const missing = labels(row.missing, expected.label, 1);
|
|
49
|
+
const action = object(row.action);
|
|
50
|
+
if (row.state !== "outdated" ||
|
|
51
|
+
!missing ||
|
|
52
|
+
!action ||
|
|
53
|
+
action.kind !== expected.action ||
|
|
54
|
+
typeof action.url !== "string" ||
|
|
55
|
+
action.url.length > 2048 ||
|
|
56
|
+
typeof action.actor !== "string")
|
|
57
|
+
return null;
|
|
58
|
+
let url;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(action.url);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (url.protocol !== "https:" ||
|
|
66
|
+
url.username ||
|
|
67
|
+
url.password ||
|
|
68
|
+
(url.search &&
|
|
69
|
+
!(expected.grant === "linear-workspace" &&
|
|
70
|
+
url.pathname === "/settings/connections" &&
|
|
71
|
+
url.search === "?reauthorize=linear")) ||
|
|
72
|
+
url.hash ||
|
|
73
|
+
expected.actorFor(url) !== action.actor)
|
|
74
|
+
return null;
|
|
75
|
+
return {
|
|
76
|
+
state: "outdated",
|
|
77
|
+
granted,
|
|
78
|
+
missing,
|
|
79
|
+
url: url.toString(),
|
|
80
|
+
actor: action.actor,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const GITHUB_INSTALLATION_PAGE = /^\/(?:organizations\/([A-Za-z0-9][A-Za-z0-9-]{0,38})\/)?settings\/installations\/([0-9]{1,20})(\/permissions\/update)?$/;
|
|
84
|
+
/** github.com's page for one installation: its settings, or its permission-request review. The
|
|
85
|
+
* actor is GitHub's: an organization owner, or the owner of a personal account. */
|
|
86
|
+
export function githubInstallationPage(url, installationId, org, review) {
|
|
87
|
+
const match = GITHUB_INSTALLATION_PAGE.exec(url.pathname);
|
|
88
|
+
if (url.origin !== "https://github.com" ||
|
|
89
|
+
!match ||
|
|
90
|
+
match[2] !== installationId ||
|
|
91
|
+
Boolean(match[3]) !== review)
|
|
92
|
+
return null;
|
|
93
|
+
const login = match[1] ?? null;
|
|
94
|
+
if (login && org && login.toLowerCase() !== org.toLowerCase())
|
|
95
|
+
return null;
|
|
96
|
+
return {
|
|
97
|
+
actor: login ? "github-org-admin" : "github-account-owner",
|
|
98
|
+
login,
|
|
99
|
+
};
|
|
100
|
+
}
|
package/dist/onboard-personal.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { finishOnTheWeb } from "./consent-browser.js";
|
|
2
2
|
import { loadConfig, normalizeBaseUrl, } from "./config.js";
|
|
3
3
|
import { pollConsent } from "./onboard-consent.js";
|
|
4
|
+
import { LINEAR_SCOPE, parsePermissions, savedOrigin, } from "./onboard-permissions.js";
|
|
4
5
|
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
5
6
|
? value
|
|
6
7
|
: null;
|
|
@@ -160,15 +161,47 @@ export function personalConsentAdapter(options) {
|
|
|
160
161
|
(body.reason !== undefined &&
|
|
161
162
|
(body.connected || body.reason !== "lapsed")))
|
|
162
163
|
return { state: "failed", reason: "personal_status_shape" };
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
if (!body.connected)
|
|
165
|
+
return { state: "pending" };
|
|
166
|
+
// CTC-4629: only a Linear grant carries scopes; GitHub's are the App's, checked at installation.
|
|
167
|
+
const permissions = options.provider === "linear"
|
|
168
|
+
? parsePermissions(body.permissions, {
|
|
169
|
+
grant: "linear-personal",
|
|
170
|
+
action: "reauthorize",
|
|
171
|
+
label: LINEAR_SCOPE,
|
|
172
|
+
actorFor: (url) => url.origin === savedOrigin(ctx) &&
|
|
173
|
+
url.pathname === "/connect/linear/personal/start"
|
|
174
|
+
? "member"
|
|
175
|
+
: null,
|
|
176
|
+
})
|
|
177
|
+
: undefined;
|
|
178
|
+
if (permissions === null)
|
|
179
|
+
return { state: "failed", reason: "personal_status_shape" };
|
|
180
|
+
if (permissions?.state === "unknown")
|
|
181
|
+
return waiting("personal_permissions_unverified");
|
|
182
|
+
if (permissions?.state === "outdated")
|
|
183
|
+
return {
|
|
184
|
+
state: "waiting",
|
|
185
|
+
reason: "linear_personal_scope_outdated",
|
|
166
186
|
evidence: {
|
|
167
|
-
provider:
|
|
168
|
-
|
|
187
|
+
provider: "linear",
|
|
188
|
+
grant: "linear-personal",
|
|
189
|
+
granted: permissions.granted.join(", "),
|
|
190
|
+
missing: permissions.missing.join(", "),
|
|
191
|
+
url: permissions.url,
|
|
192
|
+
actor: permissions.actor,
|
|
169
193
|
},
|
|
170
|
-
}
|
|
171
|
-
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
state: "done",
|
|
197
|
+
evidence: {
|
|
198
|
+
provider: options.provider,
|
|
199
|
+
checkedAt: ctx.now().getTime(),
|
|
200
|
+
...(permissions?.state === "current"
|
|
201
|
+
? { granted: permissions.granted.join(", ") }
|
|
202
|
+
: {}),
|
|
203
|
+
},
|
|
204
|
+
};
|
|
172
205
|
};
|
|
173
206
|
return {
|
|
174
207
|
check: inspect,
|
|
@@ -181,6 +214,8 @@ export function personalConsentAdapter(options) {
|
|
|
181
214
|
const before = await inspect(ctx, journal, signal);
|
|
182
215
|
if (before.state !== "pending")
|
|
183
216
|
return before;
|
|
217
|
+
if (!options.wait)
|
|
218
|
+
return waiting("personal_approval_required");
|
|
184
219
|
const start = await read(ctx, journal, `/connect/${options.provider}/personal/start`, timeout, signal);
|
|
185
220
|
if ("status" in start &&
|
|
186
221
|
start.status === 409 &&
|
|
@@ -197,7 +232,6 @@ export function personalConsentAdapter(options) {
|
|
|
197
232
|
if (!same(cfg, loadConfig(ctx.home)))
|
|
198
233
|
return { state: "refused", reason: "personal_identity_refused" };
|
|
199
234
|
const name = options.provider === "linear" ? "Linear" : "GitHub";
|
|
200
|
-
ctx.stderr(`Approve your personal ${name} connection in the browser.`);
|
|
201
235
|
// Signed continuation credentials belong only to the browser, never transcript or journal.
|
|
202
236
|
let browserUnavailable = false;
|
|
203
237
|
try {
|
|
@@ -218,7 +252,9 @@ export function personalConsentAdapter(options) {
|
|
|
218
252
|
latest = await inspect(ctx, journal, pollSignal);
|
|
219
253
|
if (latest.reason === "cloud_capability_unavailable")
|
|
220
254
|
return { outcome: "waiting", reason: latest.reason };
|
|
221
|
-
|
|
255
|
+
// An outdated grant is re-authorized at its own URL; polling longer cannot finish it.
|
|
256
|
+
if (latest.reason === "personal_login_refresh_required" ||
|
|
257
|
+
(latest.reason === "linear_personal_scope_outdated" || latest.reason === "personal_permissions_unverified"))
|
|
222
258
|
return { outcome: "waiting", reason: latest.reason };
|
|
223
259
|
if (latest.state === "done")
|
|
224
260
|
return { outcome: "connected" };
|
|
@@ -232,7 +268,7 @@ export function personalConsentAdapter(options) {
|
|
|
232
268
|
const result = options.wait
|
|
233
269
|
? await options.wait(`Waiting for your ${name} approval`, run)
|
|
234
270
|
: await run();
|
|
235
|
-
if (result.state === "done")
|
|
271
|
+
if (result.state === "done" || (latest.reason === "linear_personal_scope_outdated" || latest.reason === "personal_permissions_unverified"))
|
|
236
272
|
return latest;
|
|
237
273
|
return browserUnavailable && result.reason === "consent_timeout"
|
|
238
274
|
? waiting("personal_browser_unavailable")
|