@catalyst-cloud/cli 0.14.1 → 0.14.2
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 +10 -0
- package/README.md +14 -0
- package/dist/args.js +7 -2
- package/dist/cli.js +94 -37
- package/dist/contract.js +5 -0
- package/dist/env/parse-env.js +40 -4
- package/dist/env/selected-import.js +249 -0
- package/dist/oauth.js +57 -6
- package/dist/onboard-existing.js +169 -0
- package/dist/onboard-progress.js +70 -0
- package/dist/onboard-ready.js +67 -0
- package/dist/onboard-repositories.js +160 -0
- package/dist/onboard-runtime.js +153 -0
- package/dist/onboard-signin.js +43 -0
- package/dist/onboard-ui.js +155 -0
- package/dist/onboard.js +151 -240
- package/dist/ready.js +28 -1
- package/package.json +2 -1
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +18 -5
- 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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { userInfo } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { openBrowser } from "./browser.js";
|
|
5
|
+
import { defaultSkillsDirFor, loadConfig, normalizeBaseUrl, readManifest } from "./config.js";
|
|
6
|
+
import { CliError } from "./errors.js";
|
|
7
|
+
import { cmdLegacy, findLegacy } from "./legacy.js";
|
|
8
|
+
import { bearerFor } from "./oauth.js";
|
|
9
|
+
import { pollConsent } from "./onboard-consent.js";
|
|
10
|
+
import { boundedOnboardSignin } from "./onboard-signin.js";
|
|
11
|
+
import { existingLinearAdapters } from "./onboard-existing.js";
|
|
12
|
+
import { existingRepositoryAdapter } from "./onboard-repositories.js";
|
|
13
|
+
import { fetchMe } from "./transport.js";
|
|
14
|
+
const waiting = (reason) => ({ state: "waiting", reason });
|
|
15
|
+
function sameHome(a, b) {
|
|
16
|
+
try {
|
|
17
|
+
return realpathSync(a) === realpathSync(b);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Only existing bearer routes are registered here. Missing server capabilities stay unfinished. */
|
|
24
|
+
export function createOnboardRuntime(args, ctx, hooks) {
|
|
25
|
+
const identity = async (stepCtx = ctx, journal) => {
|
|
26
|
+
const cfg = loadConfig(stepCtx.home);
|
|
27
|
+
if (!cfg)
|
|
28
|
+
return null;
|
|
29
|
+
const requested = args.baseUrl ?? stepCtx.env.CATALYST_CLOUD_BASE_URL;
|
|
30
|
+
if (requested && normalizeBaseUrl(requested) !== normalizeBaseUrl(cfg.baseUrl))
|
|
31
|
+
throw new CliError("the saved login uses another cloud; resume against that cloud", "onboard-base-url-mismatch", 12);
|
|
32
|
+
if (!cfg.user)
|
|
33
|
+
throw new CliError("sign in as yourself to set up Catalyst", "onboard-person-required", 12);
|
|
34
|
+
if (journal && (((journal.account ?? journal.tenant) && (journal.account ?? journal.tenant) !== cfg.account) ||
|
|
35
|
+
(journal.membershipId && journal.membershipId !== cfg.user.id) ||
|
|
36
|
+
(journal.baseUrl && normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))))
|
|
37
|
+
throw new CliError("the saved setup belongs to another workspace or member", "onboard-identity-mismatch", 12);
|
|
38
|
+
const me = await fetchMe(cfg.baseUrl, await bearerFor(stepCtx, cfg), stepCtx.fetch);
|
|
39
|
+
if (!me.user || me.account !== cfg.account || me.user.id !== cfg.user.id)
|
|
40
|
+
throw new CliError("the saved login and live member do not match", "onboard-identity-mismatch", 12);
|
|
41
|
+
// /me names a person within the account, not an independently exposed membership-row ID.
|
|
42
|
+
return { account: me.account, membershipId: me.user.id, baseUrl: normalizeBaseUrl(cfg.baseUrl), role: me.user.role };
|
|
43
|
+
};
|
|
44
|
+
const legacyCheck = async (stepCtx) => {
|
|
45
|
+
const found = findLegacy(stepCtx.home, hooks.legacy?.platform ?? process.platform).filter(item => !item.data).length;
|
|
46
|
+
if (!sameHome(stepCtx.home, (hooks.realHome ?? (() => userInfo().homedir))()))
|
|
47
|
+
return { state: "skipped", reason: "fake_home_report_only", evidence: { found, remaining: found } };
|
|
48
|
+
return { state: found ? "pending" : "done", evidence: { found, remaining: found } };
|
|
49
|
+
};
|
|
50
|
+
const unsupported = { check: async () => waiting("cloud_capability_unavailable"), act: async () => waiting("cloud_capability_unavailable") };
|
|
51
|
+
const personalStatus = async (provider, stepCtx, signal) => {
|
|
52
|
+
const cfg = loadConfig(stepCtx.home);
|
|
53
|
+
if (!cfg || !cfg.user)
|
|
54
|
+
return { outcome: "refused", reason: "personal_login_required" };
|
|
55
|
+
const response = await stepCtx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}/api/v1/me/connections/${provider}/personal`, {
|
|
56
|
+
headers: { authorization: `Bearer ${await bearerFor(stepCtx, cfg)}`, accept: "application/json" },
|
|
57
|
+
signal: signal ?? AbortSignal.timeout(30_000),
|
|
58
|
+
});
|
|
59
|
+
if (response.status === 401 || response.status === 403)
|
|
60
|
+
return { outcome: "refused", reason: "personal_consent_refused" };
|
|
61
|
+
if (response.status === 503)
|
|
62
|
+
return { outcome: "unavailable" };
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
return { outcome: "failed", reason: "personal_status_failed" };
|
|
65
|
+
const body = await response.json();
|
|
66
|
+
if (!body || typeof body !== "object" || !("connected" in body))
|
|
67
|
+
return { outcome: "failed", reason: "personal_status_shape" };
|
|
68
|
+
if (body.connected === true)
|
|
69
|
+
return { outcome: "connected" };
|
|
70
|
+
if (body.connected !== false)
|
|
71
|
+
return { outcome: "failed", reason: "personal_status_shape" };
|
|
72
|
+
return { outcome: "reason" in body && body.reason === "lapsed" ? "lapsed" : "absent" };
|
|
73
|
+
};
|
|
74
|
+
const personalAdapter = (provider) => ({
|
|
75
|
+
check: async (stepCtx) => {
|
|
76
|
+
const result = await personalStatus(provider, stepCtx);
|
|
77
|
+
if (result.outcome === "connected")
|
|
78
|
+
return { state: "done" };
|
|
79
|
+
if (result.outcome === "refused" || result.outcome === "failed")
|
|
80
|
+
return { state: result.outcome, reason: result.reason };
|
|
81
|
+
return result.outcome === "unavailable" ? waiting("personal_status_unavailable") : { state: "pending" };
|
|
82
|
+
}, act: async (stepCtx, _journal, signal) => {
|
|
83
|
+
const cfg = loadConfig(stepCtx.home);
|
|
84
|
+
if (!cfg || !cfg.user)
|
|
85
|
+
return { state: "refused", reason: "personal_login_required" };
|
|
86
|
+
const response = await stepCtx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}/connect/${provider}/personal/start`, {
|
|
87
|
+
headers: { authorization: `Bearer ${await bearerFor(stepCtx, cfg)}`, accept: "application/json" }, signal: AbortSignal.timeout(30_000),
|
|
88
|
+
});
|
|
89
|
+
if (response.status === 409)
|
|
90
|
+
return waiting(provider === "linear" ? "linear_workspace_required" : "github_workspace_required");
|
|
91
|
+
if (response.status === 401 || response.status === 403)
|
|
92
|
+
return { state: "refused", reason: "personal_consent_refused" };
|
|
93
|
+
if (!response.ok)
|
|
94
|
+
return { state: "failed", reason: "personal_consent_start_failed" };
|
|
95
|
+
const body = await response.json();
|
|
96
|
+
if (typeof body.authorizationUrl !== "string" || typeof body.expiresAt !== "number")
|
|
97
|
+
return { state: "failed", reason: "personal_consent_shape" };
|
|
98
|
+
let url;
|
|
99
|
+
try {
|
|
100
|
+
url = new URL(body.authorizationUrl);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return { state: "failed", reason: "personal_consent_shape" };
|
|
104
|
+
}
|
|
105
|
+
if (url.protocol !== "https:" || url.origin !== new URL(cfg.baseUrl).origin || url.username || url.password)
|
|
106
|
+
return { state: "refused", reason: "personal_consent_origin" };
|
|
107
|
+
stepCtx.stderr(`Approve your personal ${provider === "linear" ? "Linear" : "GitHub"} connection in the browser.`);
|
|
108
|
+
stepCtx.stderr(url.toString());
|
|
109
|
+
(hooks.openBrowser ?? openBrowser)(url.toString());
|
|
110
|
+
const poll = () => pollConsent({ readStatus: pollSignal => personalStatus(provider, stepCtx, pollSignal), sleep: hooks.sleep, signal });
|
|
111
|
+
return hooks.ui ? hooks.ui.wait(`Waiting for your ${provider === "linear" ? "Linear" : "GitHub"} approval`, poll) : poll();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
identity: journal => identity(ctx, journal), bindSignals: true, ui: hooks.ui,
|
|
116
|
+
adapters: {
|
|
117
|
+
machine: { check: async () => process.platform === "darwin" || process.platform === "linux"
|
|
118
|
+
? { state: "done", evidence: { provider: process.platform } } : waiting("machine_platform_unsupported") },
|
|
119
|
+
cli: { check: async () => ({ state: "done", evidence: { version: readManifest().version } }) },
|
|
120
|
+
skills: { check: async (stepCtx) => {
|
|
121
|
+
const cfg = loadConfig(stepCtx.home);
|
|
122
|
+
const dir = stepCtx.env.CATALYST_SKILLS_DIR ?? cfg?.skillsDir ?? defaultSkillsDirFor(stepCtx.home);
|
|
123
|
+
const names = hooks.skillNames ?? [];
|
|
124
|
+
const count = names.filter(name => existsSync(join(dir, name, "SKILL.md"))).length;
|
|
125
|
+
return names.length > 0 && count === names.length ? { state: "done", evidence: { count, path: dir } } : waiting("skills_install_unverified");
|
|
126
|
+
} },
|
|
127
|
+
legacy: { check: legacyCheck, act: async (stepCtx) => {
|
|
128
|
+
const before = await legacyCheck(stepCtx);
|
|
129
|
+
if (before.state !== "pending")
|
|
130
|
+
return before;
|
|
131
|
+
// Provider/process output is not trusted to be secret-free; keep it out of the receipt/log.
|
|
132
|
+
const quiet = { ...stepCtx, stdout: () => { }, stderr: () => { } };
|
|
133
|
+
const code = await cmdLegacy({ ...args, command: "legacy", subcommand: null, rest: [], flags: { remove: true, yes: true }, json: false }, quiet, hooks.legacy);
|
|
134
|
+
return code === 0 ? legacyCheck(stepCtx) : { state: "failed", reason: "legacy_cleanup_failed" };
|
|
135
|
+
} },
|
|
136
|
+
signin: { check: async (stepCtx, journal) => {
|
|
137
|
+
const who = await identity(stepCtx, journal);
|
|
138
|
+
return who ? { state: "done", evidence: { account: who.account, membershipId: who.membershipId, role: who.role } } : { state: "pending" };
|
|
139
|
+
}, act: async (stepCtx, _journal, signal) => {
|
|
140
|
+
return boundedOnboardSignin({ ...stepCtx, stdout: stepCtx.stderr }, hooks.login, signal, hooks.signinTimeoutMs);
|
|
141
|
+
} },
|
|
142
|
+
...existingLinearAdapters(args, hooks.ui?.chooseTeam ? teams => hooks.ui.chooseTeam(teams.map(team => ({ ...team }))) : undefined),
|
|
143
|
+
"github.install": unsupported,
|
|
144
|
+
"github.repos": existingRepositoryAdapter(args, hooks.ui?.chooseRepositories ? repositories => hooks.ui.chooseRepositories(repositories.map(row => ({ ...row }))) : undefined),
|
|
145
|
+
"linear.personal": personalAdapter("linear"),
|
|
146
|
+
"github.personal": personalAdapter("github"),
|
|
147
|
+
daemon: { check: async (_stepCtx, journal) => (journal.localSync ?? args.flags["local-sync"] === true) ? waiting("local_sync_capability_unavailable")
|
|
148
|
+
: { state: "skipped", reason: "local_sync_not_selected", evidence: { provider: "cloud" } } },
|
|
149
|
+
housekeeping: { check: async () => waiting("housekeeping_service_unverified") },
|
|
150
|
+
ready: { check: hooks.ready },
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CliError, UsageError } from "./errors.js";
|
|
2
|
+
const TIMED_OUT = Symbol("signin_timeout");
|
|
3
|
+
const cancelled = () => new CliError("Sign-in paused. Run the same command to resume.", "login-cancelled", 11);
|
|
4
|
+
/** One onboarding budget includes every device-code round and final member verification. */
|
|
5
|
+
export async function boundedOnboardSignin(ctx, login, external, timeoutMs = 600_000) {
|
|
6
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 600_000)
|
|
7
|
+
throw new UsageError("onboarding sign-in timeout must be 1 to 600000 milliseconds");
|
|
8
|
+
const deadline = new AbortController();
|
|
9
|
+
const timer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
10
|
+
const signal = external ? AbortSignal.any([external, deadline.signal]) : deadline.signal;
|
|
11
|
+
let removeAbort = () => { };
|
|
12
|
+
try {
|
|
13
|
+
const exit = await new Promise((resolve, reject) => {
|
|
14
|
+
const stopped = () => external?.aborted ? reject(cancelled()) : resolve(TIMED_OUT);
|
|
15
|
+
if (signal.aborted) {
|
|
16
|
+
stopped();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
signal.addEventListener("abort", stopped, { once: true });
|
|
20
|
+
removeAbort = () => signal.removeEventListener("abort", stopped);
|
|
21
|
+
const stepCtx = { ...ctx, fetch: (async (input, init) => {
|
|
22
|
+
if (signal.aborted)
|
|
23
|
+
throw cancelled();
|
|
24
|
+
const response = await ctx.fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal });
|
|
25
|
+
if (signal.aborted)
|
|
26
|
+
throw cancelled();
|
|
27
|
+
return response;
|
|
28
|
+
}) };
|
|
29
|
+
// Attach both handlers even when a custom callback ignores abort and settles after the budget.
|
|
30
|
+
Promise.resolve().then(() => {
|
|
31
|
+
if (signal.aborted)
|
|
32
|
+
throw cancelled();
|
|
33
|
+
return login(stepCtx, signal);
|
|
34
|
+
}).then(code => signal.aborted ? stopped() : resolve(code), error => signal.aborted ? stopped() : reject(error));
|
|
35
|
+
});
|
|
36
|
+
return exit === TIMED_OUT ? { state: "waiting", reason: "signin_timeout" }
|
|
37
|
+
: exit === 0 ? { state: "done" } : { state: "failed", reason: "signin_failed" };
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
removeAbort();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createOnboardProgress } from "./onboard-progress.js";
|
|
2
|
+
import { ONBOARD_STEPS, ONBOARD_TITLES } from "./onboard.js";
|
|
3
|
+
export function shouldUseOnboardUi(args, stdoutTty) {
|
|
4
|
+
return stdoutTty && !args.json && args.flags.yes !== true && args.flags["dry-run"] !== true;
|
|
5
|
+
}
|
|
6
|
+
export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
7
|
+
const abort = new AbortController();
|
|
8
|
+
const options = { ...streams, signal: abort.signal };
|
|
9
|
+
// Clack 1.8.1's spinner calls process.exit(0) on raw Ctrl-C, bypassing receipt/lock cleanup.
|
|
10
|
+
const spin = deps.progress ?? createOnboardProgress(streams.output, abort.signal, () => abort.abort());
|
|
11
|
+
const signals = deps.signals ?? process;
|
|
12
|
+
const interrupt = () => abort.abort();
|
|
13
|
+
for (const event of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
14
|
+
signals.on(event, interrupt);
|
|
15
|
+
let active = false;
|
|
16
|
+
let introduced = false;
|
|
17
|
+
let group;
|
|
18
|
+
const stop = () => { if (active) {
|
|
19
|
+
spin.stop();
|
|
20
|
+
active = false;
|
|
21
|
+
} };
|
|
22
|
+
const message = (text) => { stop(); prompts.log.message(text, { output: streams.output }); };
|
|
23
|
+
const start = (text) => { stop(); if (!abort.signal.aborted) {
|
|
24
|
+
spin.start(text);
|
|
25
|
+
active = true;
|
|
26
|
+
} };
|
|
27
|
+
return {
|
|
28
|
+
signal: abort.signal,
|
|
29
|
+
plan(journal) {
|
|
30
|
+
stop();
|
|
31
|
+
if (!introduced) {
|
|
32
|
+
prompts.intro("Catalyst setup", { output: streams.output });
|
|
33
|
+
introduced = true;
|
|
34
|
+
}
|
|
35
|
+
const steps = new Map(journal.steps.map(step => [step.id, step]));
|
|
36
|
+
message(ONBOARD_STEPS.map(id => `· ${ONBOARD_TITLES[id]}${steps.get(id)?.state === "done" ? " (recheck)" : ""}`).join("\n"));
|
|
37
|
+
message("Use cloud reads by default. Local sync is optional for SQL, offline work or sustained reads.");
|
|
38
|
+
},
|
|
39
|
+
async confirmPlan(localSync) {
|
|
40
|
+
stop();
|
|
41
|
+
const answer = await prompts.select({ ...options, message: "Continue with this plan?", initialValue: localSync ? "local" : "cloud", options: [
|
|
42
|
+
{ value: "cloud", label: "Continue using cloud reads", hint: "default" },
|
|
43
|
+
{ value: "local", label: "Continue and set up local sync" },
|
|
44
|
+
{ value: "stop", label: "Stop without changes" },
|
|
45
|
+
] });
|
|
46
|
+
if (prompts.isCancel(answer)) {
|
|
47
|
+
abort.abort();
|
|
48
|
+
return { proceed: false, localSync };
|
|
49
|
+
}
|
|
50
|
+
return { proceed: answer === "cloud" || answer === "local", localSync: answer === "local" };
|
|
51
|
+
},
|
|
52
|
+
async chooseTeam(teams) {
|
|
53
|
+
stop();
|
|
54
|
+
if (!teams.length || abort.signal.aborted)
|
|
55
|
+
return null;
|
|
56
|
+
const answer = await prompts.select({ ...options, message: "Which Linear team should this project use?", initialValue: teams[0].id,
|
|
57
|
+
options: teams.map(team => ({ value: team.id, label: `${team.name || team.key || team.id}${team.key && team.name ? ` (${team.key})` : ""}` })) });
|
|
58
|
+
if (prompts.isCancel(answer)) {
|
|
59
|
+
abort.abort();
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return typeof answer === "string" ? answer : null;
|
|
63
|
+
},
|
|
64
|
+
...(prompts.multiselect ? { async chooseRepositories(repositories) {
|
|
65
|
+
stop();
|
|
66
|
+
if (!repositories.length || abort.signal.aborted)
|
|
67
|
+
return null;
|
|
68
|
+
const answer = await prompts.multiselect({ ...options, message: "Which repositories should use this Linear team?", required: true,
|
|
69
|
+
options: repositories.map(row => ({ value: `${row.owner}/${row.name}`, label: `${row.owner}/${row.name}` })) });
|
|
70
|
+
if (prompts.isCancel(answer)) {
|
|
71
|
+
abort.abort();
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return Array.isArray(answer) ? answer : null;
|
|
75
|
+
} } : {}),
|
|
76
|
+
stepStart(id) {
|
|
77
|
+
const next = ["machine", "cli", "skills", "legacy"].includes(id) ? "This computer"
|
|
78
|
+
: id === "signin" ? "Catalyst sign-in" : id.startsWith("linear.") || id.startsWith("github.") ? "Connections"
|
|
79
|
+
: ["projects", "accounts", "settings", "values"].includes(id) ? "Project setup"
|
|
80
|
+
: ["capacity", "daemon", "housekeeping"].includes(id) ? "Runner and services" : "Work and readiness";
|
|
81
|
+
if (next !== group) {
|
|
82
|
+
message(next);
|
|
83
|
+
group = next;
|
|
84
|
+
}
|
|
85
|
+
start(ONBOARD_TITLES[id]);
|
|
86
|
+
},
|
|
87
|
+
stepEnd(step) {
|
|
88
|
+
stop();
|
|
89
|
+
const reasons = {
|
|
90
|
+
cloud_capability_unavailable: "Your cloud does not support this setup step yet.",
|
|
91
|
+
step_not_available_in_this_release: "This setup step is not available yet.",
|
|
92
|
+
prerequisite_not_ready: "Waiting for an earlier setup step.",
|
|
93
|
+
local_sync_not_selected: "Using cloud reads. Local sync was not selected.",
|
|
94
|
+
local_sync_capability_unavailable: "Local sync was selected but could not be verified.",
|
|
95
|
+
member_scope: "Your workspace administrator handles this step.",
|
|
96
|
+
onboarding_checks_pending: "Some required checks are still unverified.",
|
|
97
|
+
interrupted: "Setup paused. Run the same command to resume.",
|
|
98
|
+
signin_timeout: "Sign-in timed out. Run catalyst onboard to try again.",
|
|
99
|
+
linear_workspace_unverified: "Your Linear workspace connection could not be verified.",
|
|
100
|
+
team_inventory_empty: "No existing Linear teams could be verified.",
|
|
101
|
+
team_inventory_shape: "The cloud returned a team list that could not be verified.",
|
|
102
|
+
team_read_unavailable: "Your existing Linear teams could not be read. Run catalyst onboard to try again.",
|
|
103
|
+
team_read_login_refresh_required: "Your login needs a refresh. Run catalyst onboard to resume.",
|
|
104
|
+
team_choice_required: "Choose an existing team with catalyst onboard --team <team ID or key>.",
|
|
105
|
+
team_selection_unverified: "Your selected Linear team could not be verified. Run catalyst onboard --team <team ID or key> to choose again.",
|
|
106
|
+
repository_choice_required: "Choose repositories with catalyst onboard --repo <owner/name>. Repeat --repo to choose more than one.",
|
|
107
|
+
repository_inventory_empty: "No accessible repositories are registered to this Linear team yet.",
|
|
108
|
+
repository_read_unavailable: "Your repository list could not be read. Run catalyst onboard to try again.",
|
|
109
|
+
repository_inventory_shape: "The cloud returned a repository list that could not be verified.",
|
|
110
|
+
repository_contract_unavailable: "Current repository IDs could not be checked. Run catalyst onboard to try again.",
|
|
111
|
+
repository_contract_unverified: "The cloud returned repository IDs that could not be verified for this workspace.",
|
|
112
|
+
repository_id_unverified: "A repository's current ID could not be verified. Keep your selection and try again.",
|
|
113
|
+
repository_binding_ambiguous: "A repository's Linear team binding is ambiguous. Your administrator needs to check it.",
|
|
114
|
+
repository_identity_unverified: "Sign in to the original workspace to choose repositories.",
|
|
115
|
+
repository_selection_unverified: "Your selected repositories could not be verified. Run catalyst onboard --repo <owner/name> to choose again.",
|
|
116
|
+
};
|
|
117
|
+
const text = `${ONBOARD_TITLES[step.id]}${step.reason ? `: ${reasons[step.reason] ?? step.reason.replaceAll("_", " ")}` : ""}`;
|
|
118
|
+
const kind = step.state === "done" ? "info" : step.state === "failed" ? "error" : step.state === "waiting" || step.state === "pending" ? "warn" : "message";
|
|
119
|
+
prompts.log[kind](`${step.state === "done" ? "✓ " : ""}${text}`, { output: streams.output });
|
|
120
|
+
},
|
|
121
|
+
message,
|
|
122
|
+
finish(journal, only) {
|
|
123
|
+
stop();
|
|
124
|
+
let text = "Setup still needs checks. Run catalyst onboard to resume.";
|
|
125
|
+
if (abort.signal.aborted)
|
|
126
|
+
text = "Setup paused. Your progress is saved.\nresume: catalyst onboard";
|
|
127
|
+
else if (journal.complete && journal.exit === 0 && !only)
|
|
128
|
+
text = "Onboarding complete.";
|
|
129
|
+
else if (only && journal.exit === 0)
|
|
130
|
+
text = `${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.\nresume: catalyst onboard`;
|
|
131
|
+
prompts.outro(text, { output: streams.output });
|
|
132
|
+
},
|
|
133
|
+
async wait(text, run) { start(text); try {
|
|
134
|
+
return await run();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
stop();
|
|
138
|
+
} },
|
|
139
|
+
dispose() {
|
|
140
|
+
try {
|
|
141
|
+
stop();
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
abort.abort();
|
|
145
|
+
try {
|
|
146
|
+
spin.dispose();
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
for (const event of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
150
|
+
signals.off(event, interrupt);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|