@catalyst-cloud/cli 0.14.4 → 0.14.6
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/dist/legacy.js +145 -32
- package/dist/onboard-capabilities.js +177 -0
- package/dist/onboard-consent.js +38 -11
- package/dist/onboard-github.js +329 -0
- package/dist/onboard-personal.js +3 -1
- package/dist/onboard-runtime.js +165 -95
- package/dist/onboard-ui.js +13 -1
- package/dist/onboard-workspace.js +150 -46
- package/dist/onboard.js +251 -65
- package/package.json +1 -1
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +1 -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 +1 -1
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { loadConfig, normalizeBaseUrl } from "./config.js";
|
|
2
|
+
import { pollConsent } from "./onboard-consent.js";
|
|
3
|
+
const STATUS = "/api/v1/me/connections/github/workspace";
|
|
4
|
+
const HANDOFF = "/connect/github/workspace/handoff";
|
|
5
|
+
const idPattern = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
6
|
+
const states = new Set([
|
|
7
|
+
"connected",
|
|
8
|
+
"missing-scope",
|
|
9
|
+
"expired-or-revoked",
|
|
10
|
+
"unreachable",
|
|
11
|
+
"not-connected",
|
|
12
|
+
]);
|
|
13
|
+
const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
|
|
14
|
+
? v
|
|
15
|
+
: null;
|
|
16
|
+
const waiting = (reason) => ({
|
|
17
|
+
state: "waiting",
|
|
18
|
+
reason,
|
|
19
|
+
});
|
|
20
|
+
const nullableText = (v) => v === null || typeof v === "string";
|
|
21
|
+
/** A bounded read never rotates credentials or writes a token after the receipt lock is released. */
|
|
22
|
+
async function read(ctx, journal, path, timeoutMs, external) {
|
|
23
|
+
const cfg = loadConfig(ctx.home);
|
|
24
|
+
if (!cfg?.user)
|
|
25
|
+
return { reason: "personal_login_required" };
|
|
26
|
+
const personId = cfg.user.id;
|
|
27
|
+
if ((journal.account ?? journal.tenant) !== cfg.account ||
|
|
28
|
+
journal.membershipId !== cfg.user.id ||
|
|
29
|
+
!journal.baseUrl ||
|
|
30
|
+
normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
|
|
31
|
+
return { reason: "github_installation_identity_refused" };
|
|
32
|
+
const expiry = cfg.auth
|
|
33
|
+
? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime()
|
|
34
|
+
: 0;
|
|
35
|
+
const bearer = cfg.key ||
|
|
36
|
+
(cfg.auth && Number.isFinite(expiry) && expiry > 30_000
|
|
37
|
+
? cfg.auth.accessToken
|
|
38
|
+
: undefined);
|
|
39
|
+
if (!bearer)
|
|
40
|
+
return { reason: "github_installation_login_refresh_required" };
|
|
41
|
+
const deadline = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
43
|
+
const signal = external
|
|
44
|
+
? AbortSignal.any([external, deadline.signal])
|
|
45
|
+
: deadline.signal;
|
|
46
|
+
let remove = () => { };
|
|
47
|
+
try {
|
|
48
|
+
return await new Promise((resolve) => {
|
|
49
|
+
const stopped = () => resolve({
|
|
50
|
+
reason: external?.aborted
|
|
51
|
+
? "interrupted"
|
|
52
|
+
: "github_installation_status_unavailable",
|
|
53
|
+
});
|
|
54
|
+
if (signal.aborted) {
|
|
55
|
+
stopped();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
signal.addEventListener("abort", stopped, { once: true });
|
|
59
|
+
remove = () => signal.removeEventListener("abort", stopped);
|
|
60
|
+
Promise.resolve()
|
|
61
|
+
.then(async () => {
|
|
62
|
+
if (signal.aborted)
|
|
63
|
+
return { reason: "interrupted" };
|
|
64
|
+
const response = await ctx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}${path}`, {
|
|
65
|
+
method: "GET",
|
|
66
|
+
redirect: "error",
|
|
67
|
+
headers: {
|
|
68
|
+
authorization: `Bearer ${bearer}`,
|
|
69
|
+
accept: "application/json",
|
|
70
|
+
},
|
|
71
|
+
signal,
|
|
72
|
+
});
|
|
73
|
+
// Refusal bodies and provider diagnostics are never parsed, returned or printed.
|
|
74
|
+
if (response.status !== 200)
|
|
75
|
+
return { status: response.status };
|
|
76
|
+
const body = await response.json();
|
|
77
|
+
const current = loadConfig(ctx.home);
|
|
78
|
+
if (!current?.user ||
|
|
79
|
+
current.account !== cfg.account ||
|
|
80
|
+
current.user.id !== personId ||
|
|
81
|
+
normalizeBaseUrl(current.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
|
|
82
|
+
return { reason: "github_installation_identity_refused" };
|
|
83
|
+
return { status: 200, body };
|
|
84
|
+
})
|
|
85
|
+
.then((value) => (signal.aborted ? stopped() : resolve(value)), () => signal.aborted
|
|
86
|
+
? stopped()
|
|
87
|
+
: resolve({ reason: "github_installation_status_unavailable" }));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
remove();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function readFailure(value) {
|
|
96
|
+
if ("reason" in value)
|
|
97
|
+
return value.reason === "github_installation_identity_refused" ||
|
|
98
|
+
value.reason === "personal_login_required"
|
|
99
|
+
? { state: "refused", reason: value.reason }
|
|
100
|
+
: waiting(value.reason);
|
|
101
|
+
if (value.status === 401 || value.status === 403)
|
|
102
|
+
return { state: "refused", reason: "github_installation_consent_refused" };
|
|
103
|
+
return value.status === 200
|
|
104
|
+
? null
|
|
105
|
+
: waiting("github_installation_status_unavailable");
|
|
106
|
+
}
|
|
107
|
+
/** Every reported installation must have a fresh live installation/App-permission probe.
|
|
108
|
+
* This proves installation connectivity, not access to repositories selected later in Q3. */
|
|
109
|
+
function observation(body, now) {
|
|
110
|
+
const row = object(body);
|
|
111
|
+
if (!row ||
|
|
112
|
+
typeof row.connected !== "boolean" ||
|
|
113
|
+
!Array.isArray(row.installations) ||
|
|
114
|
+
!Array.isArray(row.pending) ||
|
|
115
|
+
row.installations.length + row.pending.length > 100)
|
|
116
|
+
return waiting("github_installation_status_shape");
|
|
117
|
+
const ids = [];
|
|
118
|
+
let unknown = false;
|
|
119
|
+
let allConnected = row.installations.length > 0;
|
|
120
|
+
let checkedAt = now + 30_000;
|
|
121
|
+
for (const value of row.installations) {
|
|
122
|
+
const item = object(value);
|
|
123
|
+
const verification = object(item?.verification);
|
|
124
|
+
if (!item ||
|
|
125
|
+
typeof item.installationId !== "string" ||
|
|
126
|
+
!idPattern.test(item.installationId) ||
|
|
127
|
+
ids.includes(item.installationId) ||
|
|
128
|
+
!nullableText(item.githubOrg) ||
|
|
129
|
+
(typeof item.githubOrg === "string" &&
|
|
130
|
+
!/^[A-Za-z0-9][A-Za-z0-9-]{0,127}$/.test(item.githubOrg)) ||
|
|
131
|
+
!verification)
|
|
132
|
+
return waiting("github_installation_status_shape");
|
|
133
|
+
ids.push(item.installationId);
|
|
134
|
+
if (verification.source === "none" &&
|
|
135
|
+
verification.state === "not-run" &&
|
|
136
|
+
verification.checkedAt === null) {
|
|
137
|
+
unknown = true;
|
|
138
|
+
allConnected = false;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (verification.source !== "live-probe" ||
|
|
142
|
+
typeof verification.state !== "string" ||
|
|
143
|
+
!states.has(verification.state) ||
|
|
144
|
+
typeof verification.checkedAt !== "number" ||
|
|
145
|
+
!Number.isFinite(verification.checkedAt) ||
|
|
146
|
+
verification.checkedAt < now - 120_000 ||
|
|
147
|
+
verification.checkedAt > now + 30_000)
|
|
148
|
+
return waiting("github_installation_status_shape");
|
|
149
|
+
if (verification.missing !== undefined &&
|
|
150
|
+
(!Array.isArray(verification.missing) ||
|
|
151
|
+
verification.missing.length > 128 ||
|
|
152
|
+
!verification.missing.every((name) => typeof name === "string" &&
|
|
153
|
+
(/^[A-Za-z0-9_-]{1,128}$/.test(name) ||
|
|
154
|
+
/^[A-Za-z0-9_-]{1,80} \((?:read|write|admin)\)$/.test(name)))))
|
|
155
|
+
return waiting("github_installation_status_shape");
|
|
156
|
+
checkedAt = Math.min(checkedAt, verification.checkedAt);
|
|
157
|
+
if (verification.state === "unreachable")
|
|
158
|
+
unknown = true;
|
|
159
|
+
if (verification.state !== "connected")
|
|
160
|
+
allConnected = false;
|
|
161
|
+
}
|
|
162
|
+
for (const value of row.pending) {
|
|
163
|
+
const pending = object(value);
|
|
164
|
+
if (!pending ||
|
|
165
|
+
!nullableText(pending.githubOrg) ||
|
|
166
|
+
(typeof pending.githubOrg === "string" &&
|
|
167
|
+
!/^[A-Za-z0-9][A-Za-z0-9-]{0,127}$/.test(pending.githubOrg)) ||
|
|
168
|
+
typeof pending.requestedAt !== "number" ||
|
|
169
|
+
!Number.isFinite(pending.requestedAt) ||
|
|
170
|
+
pending.requestedAt < 0)
|
|
171
|
+
return waiting("github_installation_status_shape");
|
|
172
|
+
}
|
|
173
|
+
if (row.connected !== allConnected)
|
|
174
|
+
return waiting("github_installation_status_shape");
|
|
175
|
+
if (unknown)
|
|
176
|
+
return waiting("github_installation_status_unavailable");
|
|
177
|
+
if (!allConnected)
|
|
178
|
+
return { state: "pending" };
|
|
179
|
+
return {
|
|
180
|
+
state: "done",
|
|
181
|
+
evidence: {
|
|
182
|
+
provider: "github",
|
|
183
|
+
installation: JSON.stringify(ids),
|
|
184
|
+
checkedAt,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function safeHandoff(body, ctx) {
|
|
189
|
+
const row = object(body);
|
|
190
|
+
const cfg = loadConfig(ctx.home);
|
|
191
|
+
if (!cfg ||
|
|
192
|
+
!row ||
|
|
193
|
+
typeof row.authorizationUrl !== "string" ||
|
|
194
|
+
row.authorizationUrl.length > 8192 ||
|
|
195
|
+
/[\u0000-\u0020\u007f]/.test(row.authorizationUrl) ||
|
|
196
|
+
typeof row.expiresAt !== "number" ||
|
|
197
|
+
!Number.isFinite(row.expiresAt))
|
|
198
|
+
return null;
|
|
199
|
+
const remaining = row.expiresAt - ctx.now().getTime();
|
|
200
|
+
// Thirty seconds accommodates clock skew; an expired link is never launched.
|
|
201
|
+
if (remaining <= 0 || remaining > 630_000)
|
|
202
|
+
return null;
|
|
203
|
+
try {
|
|
204
|
+
const url = new URL(row.authorizationUrl);
|
|
205
|
+
const keys = [...url.searchParams.keys()];
|
|
206
|
+
const handoff = url.searchParams.get("handoff");
|
|
207
|
+
if (url.protocol !== "https:" ||
|
|
208
|
+
url.origin !== new URL(normalizeBaseUrl(cfg.baseUrl)).origin ||
|
|
209
|
+
url.username ||
|
|
210
|
+
url.password ||
|
|
211
|
+
url.pathname !== HANDOFF ||
|
|
212
|
+
url.hash ||
|
|
213
|
+
keys.length !== 1 ||
|
|
214
|
+
keys[0] !== "handoff" ||
|
|
215
|
+
!handoff ||
|
|
216
|
+
handoff.length > 4096 ||
|
|
217
|
+
/[\s\u0000-\u001f\u007f]/.test(handoff))
|
|
218
|
+
return null;
|
|
219
|
+
return url.toString();
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Uses personal bearer App-install routes. Older clouds wait without a browser/session fallback. */
|
|
226
|
+
export function githubInstallationAdapter(options) {
|
|
227
|
+
const timeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
228
|
+
const consentTimeoutMs = options.consentTimeoutMs ?? 600_000;
|
|
229
|
+
if (!Number.isSafeInteger(timeoutMs) ||
|
|
230
|
+
timeoutMs < 1 ||
|
|
231
|
+
timeoutMs > 30_000 ||
|
|
232
|
+
!Number.isSafeInteger(consentTimeoutMs) ||
|
|
233
|
+
consentTimeoutMs < 1 ||
|
|
234
|
+
consentTimeoutMs > 600_000)
|
|
235
|
+
throw new Error("github_installation_timeout_invalid");
|
|
236
|
+
const inspect = async (ctx, journal, signal) => {
|
|
237
|
+
const result = await read(ctx, journal, STATUS, timeoutMs, signal);
|
|
238
|
+
if ("status" in result && [404, 405].includes(result.status))
|
|
239
|
+
return {
|
|
240
|
+
result: waiting("cloud_capability_unavailable"),
|
|
241
|
+
unsupported: true,
|
|
242
|
+
};
|
|
243
|
+
const failure = readFailure(result);
|
|
244
|
+
return {
|
|
245
|
+
result: failure ??
|
|
246
|
+
observation("body" in result ? result.body : undefined, ctx.now().getTime()),
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
check: async (ctx, journal, signal) => (await inspect(ctx, journal, signal)).result,
|
|
251
|
+
act: async (ctx, journal, signal) => {
|
|
252
|
+
const cfg = loadConfig(ctx.home);
|
|
253
|
+
if (!cfg?.user || !["owner", "admin"].includes(cfg.user.role))
|
|
254
|
+
return {
|
|
255
|
+
state: "refused",
|
|
256
|
+
reason: "github_installation_admin_required",
|
|
257
|
+
};
|
|
258
|
+
if (signal?.aborted)
|
|
259
|
+
return waiting("interrupted");
|
|
260
|
+
const before = await inspect(ctx, journal, signal);
|
|
261
|
+
if (before.result.state !== "pending")
|
|
262
|
+
return before.result;
|
|
263
|
+
if (before.unsupported)
|
|
264
|
+
return waiting("cloud_capability_unavailable");
|
|
265
|
+
const start = await read(ctx, journal, `${STATUS}/start`, timeoutMs, signal);
|
|
266
|
+
if ("status" in start && [404, 405].includes(start.status))
|
|
267
|
+
return waiting("cloud_capability_unavailable");
|
|
268
|
+
const failure = readFailure(start);
|
|
269
|
+
if (failure)
|
|
270
|
+
return failure;
|
|
271
|
+
const url = safeHandoff("body" in start ? start.body : undefined, ctx);
|
|
272
|
+
if (!url)
|
|
273
|
+
return {
|
|
274
|
+
state: "refused",
|
|
275
|
+
reason: "github_installation_consent_handoff",
|
|
276
|
+
};
|
|
277
|
+
if (signal?.aborted)
|
|
278
|
+
return waiting("interrupted");
|
|
279
|
+
const current = loadConfig(ctx.home);
|
|
280
|
+
if (!current?.user ||
|
|
281
|
+
current.account !== cfg.account ||
|
|
282
|
+
current.user.id !== cfg.user.id ||
|
|
283
|
+
normalizeBaseUrl(current.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
|
|
284
|
+
return {
|
|
285
|
+
state: "refused",
|
|
286
|
+
reason: "github_installation_identity_refused",
|
|
287
|
+
};
|
|
288
|
+
if (!["owner", "admin"].includes(current.user.role))
|
|
289
|
+
return {
|
|
290
|
+
state: "refused",
|
|
291
|
+
reason: "github_installation_admin_required",
|
|
292
|
+
};
|
|
293
|
+
ctx.stderr("Approve the GitHub App installation in your browser. Your personal GitHub connection is checked separately.");
|
|
294
|
+
// The signed URL is transient credential material: only the browser receives it.
|
|
295
|
+
try {
|
|
296
|
+
options.openBrowser(url);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return waiting("github_installation_browser_unavailable");
|
|
300
|
+
}
|
|
301
|
+
let latest = waiting("github_installation_status_unavailable");
|
|
302
|
+
const run = () => pollConsent({
|
|
303
|
+
timeoutMs: consentTimeoutMs,
|
|
304
|
+
signal,
|
|
305
|
+
sleep: options.sleep,
|
|
306
|
+
readStatus: async (pollSignal) => {
|
|
307
|
+
const value = await inspect(ctx, journal, pollSignal);
|
|
308
|
+
latest = value.result;
|
|
309
|
+
if (value.unsupported)
|
|
310
|
+
return {
|
|
311
|
+
outcome: "waiting",
|
|
312
|
+
reason: "cloud_capability_unavailable",
|
|
313
|
+
};
|
|
314
|
+
if (latest.state === "done")
|
|
315
|
+
return { outcome: "connected" };
|
|
316
|
+
if (latest.state === "refused" || latest.state === "failed")
|
|
317
|
+
return { outcome: latest.state, reason: latest.reason };
|
|
318
|
+
return {
|
|
319
|
+
outcome: latest.state === "pending" ? "absent" : "unavailable",
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
const result = options.wait
|
|
324
|
+
? await options.wait("Waiting for GitHub App approval", run)
|
|
325
|
+
: await run();
|
|
326
|
+
return result.state === "done" ? latest : result;
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
package/dist/onboard-personal.js
CHANGED
|
@@ -90,7 +90,7 @@ function failure(value, start = false) {
|
|
|
90
90
|
return { state: "refused", reason: "personal_consent_refused" };
|
|
91
91
|
if (value.status === 503)
|
|
92
92
|
return waiting("personal_status_unavailable");
|
|
93
|
-
if (value.status
|
|
93
|
+
if ([404, 405].includes(value.status))
|
|
94
94
|
return waiting("cloud_capability_unavailable");
|
|
95
95
|
return value.status === 200
|
|
96
96
|
? null
|
|
@@ -211,6 +211,8 @@ export function personalConsentAdapter(options) {
|
|
|
211
211
|
sleep: options.sleep,
|
|
212
212
|
readStatus: async (pollSignal) => {
|
|
213
213
|
latest = await inspect(ctx, journal, pollSignal);
|
|
214
|
+
if (latest.reason === "cloud_capability_unavailable")
|
|
215
|
+
return { outcome: "waiting", reason: latest.reason };
|
|
214
216
|
if (latest.state === "done")
|
|
215
217
|
return { outcome: "connected" };
|
|
216
218
|
if (latest.state === "refused" || latest.state === "failed")
|
package/dist/onboard-runtime.js
CHANGED
|
@@ -11,6 +11,8 @@ import { boundedOnboardSignin } from "./onboard-signin.js";
|
|
|
11
11
|
import { existingLinearAdapters } from "./onboard-existing.js";
|
|
12
12
|
import { linearWorkspaceAdapter } from "./onboard-workspace.js";
|
|
13
13
|
import { existingRepositoryAdapter } from "./onboard-repositories.js";
|
|
14
|
+
import { githubInstallationAdapter } from "./onboard-github.js";
|
|
15
|
+
import { guardOnboardCapabilities, } from "./onboard-capabilities.js";
|
|
14
16
|
import { fetchMe } from "./transport.js";
|
|
15
17
|
const waiting = (reason) => ({
|
|
16
18
|
state: "waiting",
|
|
@@ -67,10 +69,6 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
67
69
|
evidence: { found, remaining: found },
|
|
68
70
|
};
|
|
69
71
|
};
|
|
70
|
-
const unsupported = {
|
|
71
|
-
check: async () => waiting("cloud_capability_unavailable"),
|
|
72
|
-
act: async () => waiting("cloud_capability_unavailable"),
|
|
73
|
-
};
|
|
74
72
|
const personalAdapter = (provider) => personalConsentAdapter({
|
|
75
73
|
provider,
|
|
76
74
|
openBrowser: hooks.openBrowser ?? openBrowser,
|
|
@@ -82,102 +80,174 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
82
80
|
const linear = existingLinearAdapters(args, hooks.ui?.chooseTeam
|
|
83
81
|
? (teams) => hooks.ui.chooseTeam(teams.map((team) => ({ ...team })))
|
|
84
82
|
: undefined);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
const adapters = {
|
|
84
|
+
machine: {
|
|
85
|
+
check: async () => process.platform === "darwin" || process.platform === "linux"
|
|
86
|
+
? { state: "done", evidence: { provider: process.platform } }
|
|
87
|
+
: waiting("machine_platform_unsupported"),
|
|
88
|
+
},
|
|
89
|
+
cli: {
|
|
90
|
+
check: async () => ({
|
|
91
|
+
state: "done",
|
|
92
|
+
evidence: { version: readManifest().version },
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
skills: {
|
|
96
|
+
check: async (stepCtx) => {
|
|
97
|
+
const cfg = loadConfig(stepCtx.home);
|
|
98
|
+
const dir = stepCtx.env.CATALYST_SKILLS_DIR ??
|
|
99
|
+
cfg?.skillsDir ??
|
|
100
|
+
defaultSkillsDirFor(stepCtx.home);
|
|
101
|
+
const names = hooks.skillNames ?? [];
|
|
102
|
+
const count = names.filter((name) => existsSync(join(dir, name, "SKILL.md"))).length;
|
|
103
|
+
return names.length > 0 && count === names.length
|
|
104
|
+
? { state: "done", evidence: { count, path: dir } }
|
|
105
|
+
: waiting("skills_install_unverified");
|
|
94
106
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
},
|
|
108
|
+
legacy: {
|
|
109
|
+
check: legacyCheck,
|
|
110
|
+
act: async (stepCtx) => {
|
|
111
|
+
const before = await legacyCheck(stepCtx);
|
|
112
|
+
if (before.state !== "pending")
|
|
113
|
+
return before;
|
|
114
|
+
// Provider/process output is not trusted to be secret-free; keep it out of the receipt/log.
|
|
115
|
+
const quiet = { ...stepCtx, stdout: () => { }, stderr: () => { } };
|
|
116
|
+
const code = await cmdLegacy({
|
|
117
|
+
...args,
|
|
118
|
+
command: "legacy",
|
|
119
|
+
subcommand: null,
|
|
120
|
+
rest: [],
|
|
121
|
+
flags: { remove: true, yes: true },
|
|
122
|
+
json: false,
|
|
123
|
+
}, quiet, hooks.legacy);
|
|
124
|
+
return code === 0
|
|
125
|
+
? legacyCheck(stepCtx)
|
|
126
|
+
: { state: "failed", reason: "legacy_cleanup_failed" };
|
|
100
127
|
},
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
128
|
+
},
|
|
129
|
+
signin: {
|
|
130
|
+
check: async (stepCtx, journal) => {
|
|
131
|
+
const who = await identity(stepCtx, journal);
|
|
132
|
+
return who
|
|
133
|
+
? {
|
|
134
|
+
state: "done",
|
|
135
|
+
evidence: {
|
|
136
|
+
account: who.account,
|
|
137
|
+
membershipId: who.membershipId,
|
|
138
|
+
role: who.role,
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
: { state: "pending" };
|
|
113
142
|
},
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
act: async (stepCtx) => {
|
|
117
|
-
const before = await legacyCheck(stepCtx);
|
|
118
|
-
if (before.state !== "pending")
|
|
119
|
-
return before;
|
|
120
|
-
// Provider/process output is not trusted to be secret-free; keep it out of the receipt/log.
|
|
121
|
-
const quiet = { ...stepCtx, stdout: () => { }, stderr: () => { } };
|
|
122
|
-
const code = await cmdLegacy({
|
|
123
|
-
...args,
|
|
124
|
-
command: "legacy",
|
|
125
|
-
subcommand: null,
|
|
126
|
-
rest: [],
|
|
127
|
-
flags: { remove: true, yes: true },
|
|
128
|
-
json: false,
|
|
129
|
-
}, quiet, hooks.legacy);
|
|
130
|
-
return code === 0
|
|
131
|
-
? legacyCheck(stepCtx)
|
|
132
|
-
: { state: "failed", reason: "legacy_cleanup_failed" };
|
|
133
|
-
},
|
|
143
|
+
act: async (stepCtx, _journal, signal) => {
|
|
144
|
+
return boundedOnboardSignin({ ...stepCtx, stdout: stepCtx.stderr }, hooks.login, signal, hooks.signinTimeoutMs);
|
|
134
145
|
},
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
},
|
|
147
|
+
...linear,
|
|
148
|
+
"linear.workspace": linearWorkspaceAdapter({
|
|
149
|
+
fallback: linear["linear.workspace"],
|
|
150
|
+
openBrowser: hooks.openBrowser ?? openBrowser,
|
|
151
|
+
wait: hooks.ui
|
|
152
|
+
? (message, work) => hooks.ui.wait(message, work)
|
|
153
|
+
: undefined,
|
|
154
|
+
sleep: hooks.sleep,
|
|
155
|
+
}),
|
|
156
|
+
"github.install": githubInstallationAdapter({
|
|
157
|
+
openBrowser: hooks.openBrowser ?? openBrowser,
|
|
158
|
+
wait: hooks.ui
|
|
159
|
+
? (message, work) => hooks.ui.wait(message, work)
|
|
160
|
+
: undefined,
|
|
161
|
+
sleep: hooks.sleep,
|
|
162
|
+
}),
|
|
163
|
+
"github.repos": existingRepositoryAdapter(args, hooks.ui?.chooseRepositories
|
|
164
|
+
? (repositories) => hooks.ui.chooseRepositories(repositories.map((row) => ({ ...row })))
|
|
165
|
+
: undefined),
|
|
166
|
+
"linear.personal": personalAdapter("linear"),
|
|
167
|
+
"github.personal": personalAdapter("github"),
|
|
168
|
+
daemon: {
|
|
169
|
+
check: async (_stepCtx, journal) => (journal.localSync ?? args.flags["local-sync"] === true)
|
|
170
|
+
? waiting("local_sync_capability_unavailable")
|
|
171
|
+
: {
|
|
172
|
+
state: "skipped",
|
|
173
|
+
reason: "local_sync_not_selected",
|
|
174
|
+
evidence: { provider: "cloud" },
|
|
151
175
|
},
|
|
152
|
-
},
|
|
153
|
-
...linear,
|
|
154
|
-
"linear.workspace": linearWorkspaceAdapter({
|
|
155
|
-
fallback: linear["linear.workspace"],
|
|
156
|
-
openBrowser: hooks.openBrowser ?? openBrowser,
|
|
157
|
-
wait: hooks.ui
|
|
158
|
-
? (message, work) => hooks.ui.wait(message, work)
|
|
159
|
-
: undefined,
|
|
160
|
-
sleep: hooks.sleep,
|
|
161
|
-
}),
|
|
162
|
-
"github.install": unsupported,
|
|
163
|
-
"github.repos": existingRepositoryAdapter(args, hooks.ui?.chooseRepositories
|
|
164
|
-
? (repositories) => hooks.ui.chooseRepositories(repositories.map((row) => ({ ...row })))
|
|
165
|
-
: undefined),
|
|
166
|
-
"linear.personal": personalAdapter("linear"),
|
|
167
|
-
"github.personal": personalAdapter("github"),
|
|
168
|
-
daemon: {
|
|
169
|
-
check: async (_stepCtx, journal) => (journal.localSync ?? args.flags["local-sync"] === true)
|
|
170
|
-
? waiting("local_sync_capability_unavailable")
|
|
171
|
-
: {
|
|
172
|
-
state: "skipped",
|
|
173
|
-
reason: "local_sync_not_selected",
|
|
174
|
-
evidence: { provider: "cloud" },
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
housekeeping: {
|
|
178
|
-
check: async () => waiting("housekeeping_service_unverified"),
|
|
179
|
-
},
|
|
180
|
-
ready: { check: hooks.ready },
|
|
181
176
|
},
|
|
177
|
+
housekeeping: {
|
|
178
|
+
check: async () => waiting("housekeeping_service_unverified"),
|
|
179
|
+
},
|
|
180
|
+
ready: { check: hooks.ready },
|
|
181
|
+
};
|
|
182
|
+
const get = (path) => ({ method: "GET", path });
|
|
183
|
+
const capabilities = {
|
|
184
|
+
"linear.workspace": {
|
|
185
|
+
check: [get("/api/v1/me/connections/linear/workspace")],
|
|
186
|
+
act: [
|
|
187
|
+
get("/api/v1/me/connections/linear/workspace"),
|
|
188
|
+
get("/api/v1/me/connections/linear/workspace/start"),
|
|
189
|
+
],
|
|
190
|
+
fallback: "connections",
|
|
191
|
+
},
|
|
192
|
+
"linear.personal": {
|
|
193
|
+
check: [get("/api/v1/me/connections/linear/personal")],
|
|
194
|
+
act: [
|
|
195
|
+
get("/api/v1/me/connections/linear/personal"),
|
|
196
|
+
get("/connect/linear/personal/start"),
|
|
197
|
+
],
|
|
198
|
+
fallback: "personalConnections",
|
|
199
|
+
},
|
|
200
|
+
"github.install": {
|
|
201
|
+
check: [get("/api/v1/me/connections/github/workspace")],
|
|
202
|
+
act: [
|
|
203
|
+
get("/api/v1/me/connections/github/workspace"),
|
|
204
|
+
get("/api/v1/me/connections/github/workspace/start"),
|
|
205
|
+
],
|
|
206
|
+
fallback: "connections",
|
|
207
|
+
},
|
|
208
|
+
"github.personal": {
|
|
209
|
+
check: [get("/api/v1/me/connections/github/personal")],
|
|
210
|
+
act: [
|
|
211
|
+
get("/api/v1/me/connections/github/personal"),
|
|
212
|
+
get("/connect/github/personal/start"),
|
|
213
|
+
],
|
|
214
|
+
fallback: "personalConnections",
|
|
215
|
+
},
|
|
216
|
+
"linear.team": {
|
|
217
|
+
check: [
|
|
218
|
+
get("/api/v1/agent/teams"),
|
|
219
|
+
get("/api/v1/agent/tenant/readiness"),
|
|
220
|
+
],
|
|
221
|
+
fallback: "connections",
|
|
222
|
+
},
|
|
223
|
+
"linear.automations": {
|
|
224
|
+
check: [get("/api/v1/agent/tenant/readiness")],
|
|
225
|
+
fallback: "connections",
|
|
226
|
+
},
|
|
227
|
+
"github.repos": {
|
|
228
|
+
check: [get("/api/v1/repos"), get("/api/v1/agent/contract")],
|
|
229
|
+
fallback: "connections",
|
|
230
|
+
},
|
|
231
|
+
ready: {
|
|
232
|
+
check: [get("/api/v1/agent/contract")],
|
|
233
|
+
fallback: "connections",
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
for (const [id, options] of Object.entries(capabilities)) {
|
|
237
|
+
const step = id;
|
|
238
|
+
const adapter = adapters[step];
|
|
239
|
+
if (adapter)
|
|
240
|
+
adapters[step] = guardOnboardCapabilities(adapter, {
|
|
241
|
+
...options,
|
|
242
|
+
message: (text) => hooks.ui ? hooks.ui.message(text) : ctx.stderr(text),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// Public auth discovery and the existing /me sign-in bootstrap establish identity before the
|
|
246
|
+
// tenant-bound step guards can run. Unsupported/local-only steps make no endpoint requests.
|
|
247
|
+
return {
|
|
248
|
+
identity: (journal) => identity(ctx, journal),
|
|
249
|
+
bindSignals: true,
|
|
250
|
+
ui: hooks.ui,
|
|
251
|
+
adapters,
|
|
182
252
|
};
|
|
183
253
|
}
|
package/dist/onboard-ui.js
CHANGED
|
@@ -141,7 +141,19 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
141
141
|
personal_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
|
|
142
142
|
personal_status_unavailable: "Your personal connection could not be checked. Run catalyst onboard to try again.",
|
|
143
143
|
personal_browser_unavailable: "The browser could not open. Run catalyst onboard to try again.",
|
|
144
|
-
cloud_capability_unavailable: "
|
|
144
|
+
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.",
|
|
145
|
+
onboarding_capability_unavailable: "The server's setup capabilities could not be checked. Run catalyst onboard to try again.",
|
|
146
|
+
onboarding_capability_identity_unverified: "The server's setup capabilities belong to another login or workspace. Sign in to the original workspace to resume.",
|
|
147
|
+
onboarding_capability_login_refresh_required: "Your login needs a refresh before setup capabilities can be checked. Run catalyst login, then catalyst onboard.",
|
|
148
|
+
workspace_consent_refused: "The server did not accept this login for Linear workspace setup. Sign in again or ask your workspace administrator.",
|
|
149
|
+
github_installation_status_unavailable: "Your GitHub App installation could not be checked. Run catalyst onboard to try again.",
|
|
150
|
+
github_installation_status_shape: "The server returned a GitHub installation result that could not be verified.",
|
|
151
|
+
github_installation_consent_handoff: "The GitHub installation approval link could not be verified. Run catalyst onboard to try again.",
|
|
152
|
+
github_installation_consent_refused: "The server did not accept this login for GitHub App setup. Sign in again or ask your workspace administrator.",
|
|
153
|
+
github_installation_identity_refused: "GitHub setup belongs to another login or workspace. Sign in to the original workspace to resume.",
|
|
154
|
+
github_installation_admin_required: "A workspace owner or administrator approves the GitHub App installation.",
|
|
155
|
+
github_installation_login_refresh_required: "Your login needs a refresh. Run catalyst onboard to resume.",
|
|
156
|
+
github_installation_browser_unavailable: "The GitHub App approval page could not be opened. Use your workspace's Connections page, then resume.",
|
|
145
157
|
step_not_available_in_this_release: "This setup step is not available yet.",
|
|
146
158
|
prerequisite_not_ready: "Waiting for an earlier setup step.",
|
|
147
159
|
local_sync_not_selected: "Using cloud reads. Local sync was not selected.",
|