@catalyst-cloud/cli 0.14.6 → 0.14.7
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 +5 -0
- package/README.md +5 -3
- package/dist/args.js +51 -13
- package/dist/cli.js +58 -10
- package/dist/config.js +16 -3
- package/dist/daemon-authority-lifetime.js +171 -0
- package/dist/daemon-authority.js +216 -0
- package/dist/daemon-cache.js +348 -0
- package/dist/daemon-store.js +370 -0
- package/dist/daemon-supervisor.js +460 -0
- package/dist/env/onboard-draft-store.js +243 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +203 -0
- package/dist/events-cloud-tail.js +134 -0
- package/dist/events-cloud.js +500 -0
- package/dist/events.js +13 -4
- package/dist/oauth.js +125 -33
- package/dist/onboard-accounts.js +259 -0
- package/dist/onboard-automation-management.js +15 -0
- package/dist/onboard-bootstrap.js +162 -0
- package/dist/onboard-capabilities.js +15 -7
- package/dist/onboard-capacity.js +226 -0
- package/dist/onboard-existing.js +106 -32
- package/dist/onboard-file-snapshot.js +45 -0
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +147 -37
- package/dist/onboard-runtime.js +104 -9
- package/dist/onboard-settings-context.js +141 -0
- package/dist/onboard-settings-import.js +127 -0
- package/dist/onboard-settings.js +234 -0
- package/dist/onboard-ui.js +99 -11
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard.js +359 -27
- package/dist/owned-fetch.js +182 -0
- package/dist/query.js +77 -27
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +2 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
- package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
- package/skills/catalyst-linear/SKILL.md +3 -3
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +5 -4
- package/skills/catalyst-onboard/references/local-sync.md +3 -1
- 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 +2 -2
- package/skills/whats-happening/references/status-reply.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +104 -26
- package/vendor/settings/index.d.ts +4 -0
- package/vendor/settings/index.js +2937 -0
- package/vendor/settings/provenance.json +148 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { verifyOnboardRoutes } from "./onboard-capabilities.js";
|
|
2
|
+
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
3
|
+
import { createSelectedImportWriter, } from "./env/selected-import-writer.js";
|
|
4
|
+
const uncertain = () => new Error("selected import outcome uncertain");
|
|
5
|
+
const bind = (context) => JSON.stringify([
|
|
6
|
+
context.accountId,
|
|
7
|
+
context.personId,
|
|
8
|
+
context.baseUrl,
|
|
9
|
+
context.role,
|
|
10
|
+
context.teamId,
|
|
11
|
+
context.teamKey,
|
|
12
|
+
context.repoId,
|
|
13
|
+
context.repoName,
|
|
14
|
+
context.repoRoot,
|
|
15
|
+
]);
|
|
16
|
+
/** The review boundary owns declaration authority. Each actual write additionally rechecks
|
|
17
|
+
* current access and identity; neither the disk contract nor the receipt supplies a scope. */
|
|
18
|
+
export function createOnboardSettingsWriter(input) {
|
|
19
|
+
const expected = { ...input.context };
|
|
20
|
+
const binding = bind(expected);
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(expected.baseUrl);
|
|
23
|
+
if (url.protocol !== "https:" ||
|
|
24
|
+
url.origin !== expected.baseUrl ||
|
|
25
|
+
!["owner", "admin"].includes(expected.role))
|
|
26
|
+
throw uncertain();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw uncertain();
|
|
30
|
+
}
|
|
31
|
+
const post = async (path, body) => {
|
|
32
|
+
const owned = new AbortController();
|
|
33
|
+
const signal = input.signal
|
|
34
|
+
? AbortSignal.any([input.signal, owned.signal])
|
|
35
|
+
: owned.signal;
|
|
36
|
+
let timer;
|
|
37
|
+
let remove = () => { };
|
|
38
|
+
try {
|
|
39
|
+
return await new Promise((resolve, reject) => {
|
|
40
|
+
const stopped = () => reject(uncertain());
|
|
41
|
+
if (signal.aborted) {
|
|
42
|
+
stopped();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
signal.addEventListener("abort", stopped, { once: true });
|
|
46
|
+
remove = () => signal.removeEventListener("abort", stopped);
|
|
47
|
+
timer = setTimeout(() => owned.abort(), 30_000);
|
|
48
|
+
Promise.resolve()
|
|
49
|
+
.then(async () => {
|
|
50
|
+
const current = await input.ports.readContext(signal);
|
|
51
|
+
if (!current || bind(current) !== binding || signal.aborted)
|
|
52
|
+
throw uncertain();
|
|
53
|
+
const cfg = loadConfig(input.ctx.home);
|
|
54
|
+
if (!cfg?.user ||
|
|
55
|
+
cfg.account !== expected.accountId ||
|
|
56
|
+
cfg.user.id !== expected.personId ||
|
|
57
|
+
normalizeBaseUrl(cfg.baseUrl) !== expected.baseUrl ||
|
|
58
|
+
!["owner", "admin"].includes(cfg.user.role))
|
|
59
|
+
throw uncertain();
|
|
60
|
+
const expiry = cfg.auth
|
|
61
|
+
? Date.parse(cfg.auth.expiresAt) - input.ctx.now().getTime()
|
|
62
|
+
: 0;
|
|
63
|
+
const bearer = cfg.key ||
|
|
64
|
+
(cfg.auth && Number.isFinite(expiry) && expiry > 30_000
|
|
65
|
+
? cfg.auth.accessToken
|
|
66
|
+
: undefined);
|
|
67
|
+
if (!bearer || signal.aborted)
|
|
68
|
+
throw uncertain();
|
|
69
|
+
const supported = await verifyOnboardRoutes(input.ctx, {
|
|
70
|
+
account: expected.accountId,
|
|
71
|
+
tenant: expected.accountId,
|
|
72
|
+
membershipId: expected.personId,
|
|
73
|
+
baseUrl: expected.baseUrl,
|
|
74
|
+
}, [{ method: "POST", path }], signal);
|
|
75
|
+
if ("reason" in supported || signal.aborted)
|
|
76
|
+
throw uncertain();
|
|
77
|
+
const response = await input.ctx.fetch(`${expected.baseUrl}${path}`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
redirect: "error",
|
|
80
|
+
signal,
|
|
81
|
+
headers: {
|
|
82
|
+
authorization: `Bearer ${bearer}`,
|
|
83
|
+
accept: "application/json",
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
});
|
|
88
|
+
// Refusal bodies may contain unsafe text. The adapter needs their status only.
|
|
89
|
+
const result = {
|
|
90
|
+
status: response.status,
|
|
91
|
+
body: response.ok
|
|
92
|
+
? (await response.json())
|
|
93
|
+
: undefined,
|
|
94
|
+
};
|
|
95
|
+
if (signal.aborted)
|
|
96
|
+
throw uncertain();
|
|
97
|
+
return result;
|
|
98
|
+
})
|
|
99
|
+
.then((value) => (signal.aborted ? stopped() : resolve(value)), () => reject(uncertain()));
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (timer)
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
remove();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const writer = createSelectedImportWriter({
|
|
109
|
+
kind: input.kind,
|
|
110
|
+
writeVariable: (name, value) => post("/me/env-vars", {
|
|
111
|
+
scope: "repo",
|
|
112
|
+
repoId: expected.repoId,
|
|
113
|
+
name,
|
|
114
|
+
value,
|
|
115
|
+
}),
|
|
116
|
+
importSecrets: (text) => post("/me/secrets/import", {
|
|
117
|
+
repo: expected.repoName,
|
|
118
|
+
text,
|
|
119
|
+
rotateExisting: [],
|
|
120
|
+
source: "onboarding selected import",
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
123
|
+
return async (text, names) => {
|
|
124
|
+
const result = await writer(text, names);
|
|
125
|
+
return { writtenNames: result.written, failedNames: result.failed };
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { storeOnboardDraft } from "./env/onboard-draft-store.js";
|
|
3
|
+
import { isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { validateFullSettings } from "../vendor/settings/index.js";
|
|
5
|
+
import { draftOnboardSettings, } from "./env/onboard-draft.js";
|
|
6
|
+
import { onboardSettingsAuthorityPorts } from "./onboard-settings-context.js";
|
|
7
|
+
import { selectedOnboardRepositories, } from "./onboard-repositories.js";
|
|
8
|
+
import { normalizeBaseUrl } from "./config.js";
|
|
9
|
+
import { selectedOnboardTeam } from "./onboard-existing.js";
|
|
10
|
+
const waiting = (reason, evidence) => ({
|
|
11
|
+
state: "waiting",
|
|
12
|
+
reason,
|
|
13
|
+
...(evidence ? { evidence } : {}),
|
|
14
|
+
});
|
|
15
|
+
const contextKey = (value) => JSON.stringify([
|
|
16
|
+
value.accountId,
|
|
17
|
+
value.personId,
|
|
18
|
+
value.baseUrl,
|
|
19
|
+
value.role,
|
|
20
|
+
value.teamId,
|
|
21
|
+
value.teamKey,
|
|
22
|
+
value.repoId,
|
|
23
|
+
value.repoName,
|
|
24
|
+
value.repoRoot,
|
|
25
|
+
]);
|
|
26
|
+
const safeLabel = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").slice(0, 240);
|
|
27
|
+
async function boundedRead(work, external, timeoutMs = 30_000) {
|
|
28
|
+
const owned = new AbortController();
|
|
29
|
+
const signal = external
|
|
30
|
+
? AbortSignal.any([external, owned.signal])
|
|
31
|
+
: owned.signal;
|
|
32
|
+
let timer;
|
|
33
|
+
let remove = () => { };
|
|
34
|
+
try {
|
|
35
|
+
return await new Promise((resolve) => {
|
|
36
|
+
const stopped = () => resolve(null);
|
|
37
|
+
if (signal.aborted) {
|
|
38
|
+
stopped();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
signal.addEventListener("abort", stopped, { once: true });
|
|
42
|
+
remove = () => signal.removeEventListener("abort", stopped);
|
|
43
|
+
timer = setTimeout(() => owned.abort(), timeoutMs);
|
|
44
|
+
Promise.resolve()
|
|
45
|
+
.then(() => work(signal))
|
|
46
|
+
.then((result) => resolve(signal.aborted ? null : result), () => resolve(null));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (timer)
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
remove();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Q4 drafts are local review artifacts. A full local validator is not declaration approval;
|
|
56
|
+
* neither a kept draft nor a valid existing file marks this required step complete. */
|
|
57
|
+
export function onboardSettingsAdapter(hooks = {}) {
|
|
58
|
+
const storeTimeoutMs = hooks.storeTimeoutMs ?? 30_000;
|
|
59
|
+
if (!Number.isSafeInteger(storeTimeoutMs) ||
|
|
60
|
+
storeTimeoutMs < 1 ||
|
|
61
|
+
storeTimeoutMs > 30_000)
|
|
62
|
+
throw new Error("settings_storage_timeout_invalid");
|
|
63
|
+
const observe = async (ctx, journal, signal) => {
|
|
64
|
+
const repositories = selectedOnboardRepositories(journal);
|
|
65
|
+
const summaries = [];
|
|
66
|
+
const candidates = [];
|
|
67
|
+
for (const repository of repositories) {
|
|
68
|
+
if (signal?.aborted)
|
|
69
|
+
return null;
|
|
70
|
+
const summary = {
|
|
71
|
+
repository: `${repository.owner}/${repository.name}`,
|
|
72
|
+
state: "unavailable",
|
|
73
|
+
variableNames: [],
|
|
74
|
+
secretNames: [],
|
|
75
|
+
sources: [],
|
|
76
|
+
};
|
|
77
|
+
summaries.push(summary);
|
|
78
|
+
try {
|
|
79
|
+
const requested = hooks.repositoryRoot
|
|
80
|
+
? hooks.repositoryRoot({ ...repository })
|
|
81
|
+
: process.cwd();
|
|
82
|
+
if (!requested ||
|
|
83
|
+
!isAbsolute(requested) ||
|
|
84
|
+
(await realpath(requested)) !== resolve(requested) ||
|
|
85
|
+
signal?.aborted)
|
|
86
|
+
continue;
|
|
87
|
+
const ports = (hooks.authorityPorts ??
|
|
88
|
+
((c, j, r, root) => onboardSettingsAuthorityPorts({
|
|
89
|
+
ctx: c,
|
|
90
|
+
journal: j,
|
|
91
|
+
repository: r,
|
|
92
|
+
repoRoot: root,
|
|
93
|
+
})))(ctx, journal, repository, requested);
|
|
94
|
+
const before = await ports.readContext(signal);
|
|
95
|
+
if (!before ||
|
|
96
|
+
before.accountId !== (journal.account ?? journal.tenant) ||
|
|
97
|
+
before.personId !== journal.membershipId ||
|
|
98
|
+
!journal.baseUrl ||
|
|
99
|
+
before.baseUrl !== normalizeBaseUrl(journal.baseUrl) ||
|
|
100
|
+
!["owner", "admin"].includes(before.role) ||
|
|
101
|
+
before.teamId !== selectedOnboardTeam(journal) ||
|
|
102
|
+
before.teamId !== repository.teamId ||
|
|
103
|
+
!/^[A-Z][A-Z0-9]{0,15}$/.test(before.teamKey) ||
|
|
104
|
+
before.repoId !== repository.repoId ||
|
|
105
|
+
before.repoName !== summary.repository.toLowerCase() ||
|
|
106
|
+
before.repoRoot !== resolve(requested) ||
|
|
107
|
+
signal?.aborted)
|
|
108
|
+
continue;
|
|
109
|
+
const binding = contextKey(before);
|
|
110
|
+
const existing = await ports.readSettings(requested, signal);
|
|
111
|
+
if (existing !== null) {
|
|
112
|
+
const checked = validateFullSettings(existing);
|
|
113
|
+
const after = await ports.readContext(signal);
|
|
114
|
+
if (!checked.ok ||
|
|
115
|
+
checked.linearTeam !== before.teamKey ||
|
|
116
|
+
!after ||
|
|
117
|
+
contextKey(after) !== binding ||
|
|
118
|
+
signal?.aborted)
|
|
119
|
+
continue;
|
|
120
|
+
Object.assign(summary, {
|
|
121
|
+
state: "existing",
|
|
122
|
+
variableNames: checked.variableNames,
|
|
123
|
+
secretNames: checked.secretNames,
|
|
124
|
+
sources: [".catalyst/catalyst.toml"],
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const draft = (hooks.draft ?? draftOnboardSettings)(requested, before.teamKey);
|
|
129
|
+
if (draft.state !== "draft")
|
|
130
|
+
continue;
|
|
131
|
+
const checked = validateFullSettings(draft.toml);
|
|
132
|
+
const after = await ports.readContext(signal);
|
|
133
|
+
if (!checked.ok ||
|
|
134
|
+
checked.linearTeam !== before.teamKey ||
|
|
135
|
+
!after ||
|
|
136
|
+
contextKey(after) !== binding ||
|
|
137
|
+
signal?.aborted)
|
|
138
|
+
continue;
|
|
139
|
+
Object.assign(summary, {
|
|
140
|
+
state: "draft",
|
|
141
|
+
variableNames: checked.variableNames,
|
|
142
|
+
secretNames: checked.secretNames,
|
|
143
|
+
sources: draft.sources.map(safeLabel),
|
|
144
|
+
});
|
|
145
|
+
candidates.push({
|
|
146
|
+
repository: { ...repository },
|
|
147
|
+
context: { ...before },
|
|
148
|
+
ports,
|
|
149
|
+
draft,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* One unavailable checkout cannot approve the other repositories. */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { summaries, candidates };
|
|
157
|
+
};
|
|
158
|
+
const bounded = (ctx, journal, external) => boundedRead((signal) => observe(ctx, journal, signal), external);
|
|
159
|
+
return {
|
|
160
|
+
check: async (ctx, journal, signal) => {
|
|
161
|
+
const read = await bounded(ctx, journal, signal);
|
|
162
|
+
if (!read?.summaries.length)
|
|
163
|
+
return waiting("settings_checkout_unverified");
|
|
164
|
+
if (read.summaries.some((summary) => summary.state === "unavailable"))
|
|
165
|
+
return waiting("settings_checkout_unverified");
|
|
166
|
+
return read.candidates.length
|
|
167
|
+
? { state: "pending" }
|
|
168
|
+
: waiting("settings_approval_unverified");
|
|
169
|
+
},
|
|
170
|
+
act: async (ctx, journal, signal) => {
|
|
171
|
+
const read = await bounded(ctx, journal, signal);
|
|
172
|
+
if (!read?.summaries.length || signal?.aborted)
|
|
173
|
+
return waiting("settings_checkout_unverified");
|
|
174
|
+
for (const summary of read.summaries) {
|
|
175
|
+
hooks.message?.(`Settings for ${summary.repository}\n ${summary.state === "unavailable" ? "Local checkout or settings could not be verified." : `Full settings schema validation passed locally (${summary.state === "draft" ? "new draft; private copy pending" : "existing repository file; kept unchanged"}).\n Variables: ${summary.variableNames.join(", ") || "none"}\n Secrets: ${summary.secretNames.join(", ") || "none"}\n Sources: ${summary.sources.join(", ") || "none"}`}`);
|
|
176
|
+
}
|
|
177
|
+
const choice = hooks.review
|
|
178
|
+
? await boundedRead(async () => hooks.review(read.summaries.map((row) => ({
|
|
179
|
+
...row,
|
|
180
|
+
variableNames: [...row.variableNames],
|
|
181
|
+
secretNames: [...row.secretNames],
|
|
182
|
+
sources: [...row.sources],
|
|
183
|
+
}))), signal, 600_000)
|
|
184
|
+
: "keep";
|
|
185
|
+
if (choice === null && !signal?.aborted)
|
|
186
|
+
return waiting("settings_review_timeout");
|
|
187
|
+
if (choice !== "keep" || signal?.aborted)
|
|
188
|
+
return waiting("interrupted");
|
|
189
|
+
if (read.summaries.some((summary) => summary.state === "unavailable"))
|
|
190
|
+
return waiting("settings_checkout_unverified");
|
|
191
|
+
let stored = 0;
|
|
192
|
+
for (const candidate of read.candidates) {
|
|
193
|
+
const current = await boundedRead((owned) => candidate.ports.readContext(owned), signal);
|
|
194
|
+
if (!current ||
|
|
195
|
+
contextKey(current) !== contextKey(candidate.context) ||
|
|
196
|
+
signal?.aborted)
|
|
197
|
+
return waiting("settings_checkout_unverified", { count: stored });
|
|
198
|
+
const ownedStore = new AbortController();
|
|
199
|
+
const storeSignal = signal
|
|
200
|
+
? AbortSignal.any([signal, ownedStore.signal])
|
|
201
|
+
: ownedStore.signal;
|
|
202
|
+
const timer = setTimeout(() => ownedStore.abort(), storeTimeoutMs);
|
|
203
|
+
let result;
|
|
204
|
+
try {
|
|
205
|
+
// Do not race this promise. The writer settles all IO/owned cleanup before the engine
|
|
206
|
+
// may release its receipt lock, including after cancellation or the cooperative budget.
|
|
207
|
+
result = await (hooks.storeDraft ?? storeOnboardDraft)({
|
|
208
|
+
home: ctx.home,
|
|
209
|
+
env: { ...ctx.env },
|
|
210
|
+
runId: journal.runId,
|
|
211
|
+
repoId: candidate.repository.repoId,
|
|
212
|
+
toml: candidate.draft.toml,
|
|
213
|
+
signal: storeSignal,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
result = { state: "rejected" };
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
clearTimeout(timer);
|
|
221
|
+
}
|
|
222
|
+
if (storeSignal.aborted)
|
|
223
|
+
return waiting(signal?.aborted ? "interrupted" : "settings_draft_storage_timeout", { count: stored });
|
|
224
|
+
if (result.state !== "stored" || signal?.aborted)
|
|
225
|
+
return waiting("settings_draft_storage_unavailable", {
|
|
226
|
+
count: stored,
|
|
227
|
+
});
|
|
228
|
+
stored++;
|
|
229
|
+
hooks.message?.(`Kept a private settings draft copy for ${candidate.repository.owner}/${candidate.repository.name}: ${safeLabel(result.path)}`);
|
|
230
|
+
}
|
|
231
|
+
return waiting("settings_approval_unverified", { count: stored });
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
package/dist/onboard-ui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createOnboardProgress, } from "./onboard-progress.js";
|
|
2
|
-
import { ONBOARD_STEPS, ONBOARD_TITLES, } from "./onboard.js";
|
|
2
|
+
import { ONBOARD_STEPS, ONBOARD_TITLES, onboardIdentityLines, } from "./onboard.js";
|
|
3
3
|
export function shouldUseOnboardUi(args, stdoutTty) {
|
|
4
4
|
return (stdoutTty &&
|
|
5
5
|
!args.json &&
|
|
@@ -38,29 +38,47 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
38
38
|
};
|
|
39
39
|
return {
|
|
40
40
|
signal: abort.signal,
|
|
41
|
-
plan(journal) {
|
|
41
|
+
plan(journal, identity) {
|
|
42
42
|
stop();
|
|
43
43
|
if (!introduced) {
|
|
44
44
|
prompts.intro("Catalyst setup", { output: streams.output });
|
|
45
45
|
introduced = true;
|
|
46
46
|
}
|
|
47
|
+
if (identity !== undefined)
|
|
48
|
+
for (const line of onboardIdentityLines(identity))
|
|
49
|
+
message(line);
|
|
47
50
|
const steps = new Map(journal.steps.map((step) => [step.id, step]));
|
|
48
51
|
message(ONBOARD_STEPS.map((id) => `· ${ONBOARD_TITLES[id]}${steps.get(id)?.state === "done" ? " (recheck)" : ""}`).join("\n"));
|
|
49
|
-
message("Use cloud reads by default. Local sync is optional for SQL
|
|
52
|
+
message("Use cloud reads by default. Local sync is optional for SQL or sustained local reads.");
|
|
53
|
+
message("Setup may check one stored Claude account using a one-token provider request. This may use Claude quota. It does not refresh Codex credentials.");
|
|
50
54
|
},
|
|
51
|
-
async confirmPlan(localSync) {
|
|
55
|
+
async confirmPlan(localSync, signin = "unavailable") {
|
|
52
56
|
stop();
|
|
53
57
|
const answer = await prompts.select({
|
|
54
58
|
...options,
|
|
55
59
|
message: "Continue with this plan?",
|
|
56
|
-
initialValue: localSync ? "local" : "cloud",
|
|
60
|
+
initialValue: signin === "required" ? "signin" : localSync ? "local" : "cloud",
|
|
57
61
|
options: [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
...(signin === "required"
|
|
63
|
+
? []
|
|
64
|
+
: [
|
|
65
|
+
{
|
|
66
|
+
value: "cloud",
|
|
67
|
+
label: "Continue using cloud reads",
|
|
68
|
+
hint: "default",
|
|
69
|
+
},
|
|
70
|
+
{ value: "local", label: "Continue and set up local sync" },
|
|
71
|
+
]),
|
|
72
|
+
...(signin === "unavailable"
|
|
73
|
+
? []
|
|
74
|
+
: [
|
|
75
|
+
{
|
|
76
|
+
value: "signin",
|
|
77
|
+
label: signin === "saved"
|
|
78
|
+
? "Sign in again before continuing"
|
|
79
|
+
: "Sign in and review this plan",
|
|
80
|
+
},
|
|
81
|
+
]),
|
|
64
82
|
{ value: "stop", label: "Stop without changes" },
|
|
65
83
|
],
|
|
66
84
|
});
|
|
@@ -71,6 +89,7 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
71
89
|
return {
|
|
72
90
|
proceed: answer === "cloud" || answer === "local",
|
|
73
91
|
localSync: answer === "local",
|
|
92
|
+
...(answer === "signin" ? { signin: true } : {}),
|
|
74
93
|
};
|
|
75
94
|
},
|
|
76
95
|
async chooseTeam(teams) {
|
|
@@ -115,6 +134,48 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
115
134
|
},
|
|
116
135
|
}
|
|
117
136
|
: {}),
|
|
137
|
+
async reviewSettings(summaries) {
|
|
138
|
+
stop();
|
|
139
|
+
if (!summaries.length || abort.signal.aborted)
|
|
140
|
+
return "cancel";
|
|
141
|
+
const answer = await prompts.select({
|
|
142
|
+
...options,
|
|
143
|
+
message: "Review settings for all selected repositories. Keep private copies of new drafts for review?",
|
|
144
|
+
initialValue: "keep",
|
|
145
|
+
options: [
|
|
146
|
+
{
|
|
147
|
+
value: "keep",
|
|
148
|
+
label: "Keep private draft copies",
|
|
149
|
+
hint: "default; repository files stay unchanged; approval and value import stay pending",
|
|
150
|
+
},
|
|
151
|
+
{ value: "stop", label: "Stop without keeping new copies" },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
if (prompts.isCancel(answer) || answer === "stop") {
|
|
155
|
+
abort.abort();
|
|
156
|
+
return "cancel";
|
|
157
|
+
}
|
|
158
|
+
return answer === "keep" ? "keep" : "cancel";
|
|
159
|
+
},
|
|
160
|
+
async chooseFirstRepository(repositories) {
|
|
161
|
+
stop();
|
|
162
|
+
if (!repositories.length || abort.signal.aborted)
|
|
163
|
+
return null;
|
|
164
|
+
const answer = await prompts.select({
|
|
165
|
+
...options,
|
|
166
|
+
message: "Which repository should start this project?",
|
|
167
|
+
initialValue: `${repositories[0].owner}/${repositories[0].name}`,
|
|
168
|
+
options: repositories.map((row) => ({
|
|
169
|
+
value: `${row.owner}/${row.name}`,
|
|
170
|
+
label: `${row.owner}/${row.name}`,
|
|
171
|
+
})),
|
|
172
|
+
});
|
|
173
|
+
if (prompts.isCancel(answer)) {
|
|
174
|
+
abort.abort();
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return typeof answer === "string" ? answer : null;
|
|
178
|
+
},
|
|
118
179
|
stepStart(id) {
|
|
119
180
|
const next = ["machine", "cli", "skills", "legacy"].includes(id)
|
|
120
181
|
? "This computer"
|
|
@@ -136,9 +197,22 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
136
197
|
stepEnd(step) {
|
|
137
198
|
stop();
|
|
138
199
|
const reasons = {
|
|
200
|
+
account_enrollment_required: "No coding account is enrolled. An administrator can add one in the workspace's coding-account settings, then run catalyst onboard again.",
|
|
201
|
+
account_identity_unverified: "Your login changed while coding accounts were checked. Run catalyst onboard again to verify your person and workspace.",
|
|
202
|
+
account_inventory_unavailable: "Coding accounts could not be read. Run catalyst onboard again to retry.",
|
|
203
|
+
account_inventory_unverified: "The server did not return a fresh supported coding-account list. Run catalyst onboard after the server update.",
|
|
204
|
+
account_validation_admin_required: "An administrator must check the workspace's coding-account access.",
|
|
205
|
+
account_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard.",
|
|
206
|
+
account_validation_unavailable: "The provider check could not finish. Run catalyst onboard again to retry.",
|
|
207
|
+
account_validation_unverified: "The server did not return a fresh provider check. Run catalyst onboard after the server update.",
|
|
208
|
+
account_provider_access_unverified: "Coding-account provider access is not freshly verified. Check the account in the workspace's coding-account settings, then resume setup.",
|
|
209
|
+
codex_provider_access_unverified: "Codex credentials are stored, but provider access is not freshly verified. Setup did not refresh or rotate the Codex login.",
|
|
210
|
+
account_provider_walled: "Claude's usage limit is spent for now. Check the account's reset time, then run catalyst onboard again.",
|
|
211
|
+
account_provider_rejected: "Claude refused the stored login. Replace it in the workspace's coding-account settings, then resume setup.",
|
|
139
212
|
personal_consent_handoff: "The browser approval link could not be verified. Run catalyst onboard to try again.",
|
|
140
213
|
personal_identity_refused: "Your current login no longer matches this setup. Resume with the original workspace and person.",
|
|
141
214
|
personal_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
|
|
215
|
+
onboard_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
|
|
142
216
|
personal_status_unavailable: "Your personal connection could not be checked. Run catalyst onboard to try again.",
|
|
143
217
|
personal_browser_unavailable: "The browser could not open. Run catalyst onboard to try again.",
|
|
144
218
|
cloud_capability_unavailable: "This setup step is not available on this server yet. Use the web app link shown above, then resume after the server update.",
|
|
@@ -166,6 +240,15 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
166
240
|
team_inventory_empty: "No existing Linear teams could be verified.",
|
|
167
241
|
team_inventory_shape: "The cloud returned a team list that could not be verified.",
|
|
168
242
|
team_read_unavailable: "Your existing Linear teams could not be read. Run catalyst onboard to try again.",
|
|
243
|
+
team_read_identity_unverified: "Your login changed while setup was reading this workspace. Resume with the original workspace and person.",
|
|
244
|
+
project_identity_unverified: "Your login changed while setup was checking this project. Resume with the original workspace and person.",
|
|
245
|
+
project_options_unavailable: "Available teams and repositories could not be checked. Run catalyst onboard to try again.",
|
|
246
|
+
project_provider_inventory_unavailable: "Linear or GitHub access could not be verified. Check this workspace's connections, then resume.",
|
|
247
|
+
project_options_unverified: "The server returned a team or repository list that could not be verified.",
|
|
248
|
+
project_registration_visibility_pending: "Your project exists, but its repository binding is not visible yet. Run catalyst onboard to check again.",
|
|
249
|
+
project_binding_conflict: "This team or repository is already bound to a project. Ask your workspace administrator to check its binding, then resume.",
|
|
250
|
+
project_login_refresh_required: "Renew your login with catalyst onboard, then review the plan before creating this project.",
|
|
251
|
+
project_create_unverified: "Project creation could not be confirmed. Run catalyst onboard to check for the project before trying again.",
|
|
169
252
|
team_read_login_refresh_required: "Your login needs a refresh. Run catalyst onboard to resume.",
|
|
170
253
|
team_choice_required: "Choose an existing team with catalyst onboard --team <team ID or key>.",
|
|
171
254
|
team_selection_unverified: "Your selected Linear team could not be verified. Run catalyst onboard --team <team ID or key> to choose again.",
|
|
@@ -178,6 +261,11 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
178
261
|
repository_id_unverified: "A repository's current ID could not be verified. Keep your selection and try again.",
|
|
179
262
|
repository_binding_ambiguous: "A repository's Linear team binding is ambiguous. Your administrator needs to check it.",
|
|
180
263
|
repository_identity_unverified: "Sign in to the original workspace to choose repositories.",
|
|
264
|
+
settings_review_timeout: "The settings review timed out. Run catalyst onboard to resume.",
|
|
265
|
+
settings_checkout_unverified: "A selected repository's local checkout or full settings file could not be verified. Resume from its checkout.",
|
|
266
|
+
settings_draft_storage_unavailable: "A private settings draft could not be kept. Run catalyst onboard to retry.",
|
|
267
|
+
settings_draft_storage_timeout: "Keeping your settings draft took too long. Run catalyst onboard to retry.",
|
|
268
|
+
settings_approval_unverified: "Settings still need review and approval. Kept drafts and local validation do not approve them.",
|
|
181
269
|
repository_selection_unverified: "Your selected repositories could not be verified. Run catalyst onboard --repo <owner/name> to choose again.",
|
|
182
270
|
};
|
|
183
271
|
const text = `${ONBOARD_TITLES[step.id]}${step.reason ? `: ${reasons[step.reason] ?? step.reason.replaceAll("_", " ")}` : ""}`;
|