@catalyst-cloud/cli 0.15.3 → 0.15.5
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 +2 -0
- package/dist/args.js +5 -1
- package/dist/cli.js +39 -3
- package/dist/onboard-checklist.js +3 -2
- package/dist/onboard-headless.js +3 -0
- package/dist/onboard-repositories.js +197 -6
- package/dist/onboard-standalone-copy.js +21 -5
- package/dist/onboard-ui.js +166 -29
- package/dist/onboard.js +24 -7
- package/dist/setup-brand.js +202 -0
- package/dist/setup-onboard-copy.js +92 -16
- package/dist/setup-render.js +62 -37
- package/dist/terminal-background.js +220 -0
- package/package.json +1 -1
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.5
|
|
4
|
+
|
|
5
|
+
Setup explains what running work on this computer means and lists every repository registered to your Linear team.
|
|
6
|
+
|
|
7
|
+
## 0.15.4
|
|
8
|
+
|
|
9
|
+
- Setup shows a colour mark and clearer progress, with a plain display when your terminal needs it.
|
|
10
|
+
- Setup separates your next steps from tasks waiting on an owner or admin, and reports when it stops waiting for an account connection.
|
|
11
|
+
- Setup wraps long text by words and keeps numbered steps in order.
|
|
12
|
+
|
|
3
13
|
## 0.15.3
|
|
4
14
|
|
|
5
15
|
- Setup can finish once the steps needed to run work are ready. It lists optional next steps and keeps your first-ticket choice when you resume.
|
package/README.md
CHANGED
|
@@ -133,6 +133,8 @@ catalyst onboard
|
|
|
133
133
|
catalyst ready --onboarding --json
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
`catalyst setup` opens with the Catalyst Cloud mark in your terminal's colours. It asks the terminal whether its background is light or dark; set `CATALYST_THEME=light` or `CATALYST_THEME=dark` when it guesses wrong. `--no-color` or `NO_COLOR` prints plain text without the mark.
|
|
137
|
+
|
|
136
138
|
The dry run writes no files. `--only <step>` runs one step and reports its scope separately from full onboarding. Exit 10 means a failed step, 11 means waiting, and 12 means a guard refused the action. The private receipt is `install/last-run.json` under your machine's Catalyst state directory; credentials stay in the login config. A fake HOME reports old services and keeps them intact.
|
|
137
139
|
|
|
138
140
|
### Headless setup (CI, image builds, agent VMs)
|
package/dist/args.js
CHANGED
|
@@ -64,6 +64,10 @@ export const FLAG_TABLES = {
|
|
|
64
64
|
value: false,
|
|
65
65
|
help: "do not run Catalyst's work on this machine, and do not ask",
|
|
66
66
|
},
|
|
67
|
+
"no-color": {
|
|
68
|
+
value: false,
|
|
69
|
+
help: "print setup in plain text, without colour or the Catalyst Cloud mark",
|
|
70
|
+
},
|
|
67
71
|
"resume-from": {
|
|
68
72
|
value: true,
|
|
69
73
|
help: "resume at a verified onboarding step",
|
|
@@ -459,7 +463,7 @@ export const FLAG_TABLES = {
|
|
|
459
463
|
export const VERB_USAGE = {
|
|
460
464
|
setup: "setup [onboard options] (sets up Catalyst step by step; same as catalyst onboard) | setup --engine <file> --engine-sha256 <hex> [-- <install options>] (run by install.sh; install.sh --help lists the options)",
|
|
461
465
|
login: "login [--base-url <url>] [--start-replica] (keyless; or --key <personal-key> / CATALYST_CLOUD_TOKEN)",
|
|
462
|
-
onboard: "onboard [--team <ID|key>] [--repo <owner/name>]... [--coding-account <slot>] [--resume-from <step>] [--only <step>] [--local-sync] [--runner|--no-runner] [--yes] [--dry-run] [--json] | onboard --headless [--key-file <path>] [--team ...] [--repo ...]... [--coding-account ...] [--runner yes|no] [--json]",
|
|
466
|
+
onboard: "onboard [--team <ID|key>] [--repo <owner/name>]... [--coding-account <slot>] [--resume-from <step>] [--only <step>] [--local-sync] [--runner|--no-runner] [--no-color] [--yes] [--dry-run] [--json] | onboard --headless [--key-file <path>] [--team ...] [--repo ...]... [--coding-account ...] [--runner yes|no] [--json]",
|
|
463
467
|
install: "install [--skills-dir <dir>] [--force]",
|
|
464
468
|
status: "status",
|
|
465
469
|
notice: "notice",
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cli.ts — the `catalyst` dispatcher (npm: @catalyst-cloud/cli). Verbs live in
|
|
2
2
|
// their own modules; this file keeps every export the 0.1 tests and the bin import.
|
|
3
|
-
import { createSetupRenderer } from "./setup-render.js";
|
|
3
|
+
import { createSetupRenderer, setupBanner, } from "./setup-render.js";
|
|
4
4
|
import { existsSync, openSync, closeSync } from "node:fs";
|
|
5
5
|
import { ReadStream as TerminalInput } from "node:tty";
|
|
6
6
|
import { Writable } from "node:stream";
|
|
@@ -210,6 +210,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
210
210
|
// CTC-4625: setup passes the install options through to its engine untouched, so they never meet
|
|
211
211
|
// this CLI's own flag tables. CTC-4680: without --engine, `setup` is the name a person types for
|
|
212
212
|
// the onboard flow, so it runs exactly that.
|
|
213
|
+
const viaSetup = drawsMark(argv);
|
|
213
214
|
if (argv[0] === "setup") {
|
|
214
215
|
const rest = argv.slice(1);
|
|
215
216
|
const own = rest.indexOf("--") === -1 ? rest : rest.slice(0, rest.indexOf("--"));
|
|
@@ -278,6 +279,9 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
278
279
|
// Headless runs drop the terminal UI, browser sign-in and every browser action (CTC-4633).
|
|
279
280
|
const runOnboard = async (args, ctx, headless) => {
|
|
280
281
|
let ui = headless ? undefined : deps.setupUi;
|
|
282
|
+
// --no-color reaches the renderer as NO_COLOR, so the banner and every step row go plain.
|
|
283
|
+
const noColor = args.flags["no-color"] === true;
|
|
284
|
+
const rendererEnv = noColor ? { ...ctx.env, NO_COLOR: "1" } : ctx.env;
|
|
281
285
|
let terminal;
|
|
282
286
|
let input = process.stdin;
|
|
283
287
|
const originalRaw = Boolean(input.isRaw);
|
|
@@ -300,10 +304,18 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
300
304
|
}
|
|
301
305
|
if (input.isTTY) {
|
|
302
306
|
try {
|
|
307
|
+
const banner = await setupBanner({
|
|
308
|
+
setup: viaSetup,
|
|
309
|
+
env: rendererEnv,
|
|
310
|
+
input,
|
|
311
|
+
stdout: process.stdout,
|
|
312
|
+
stream: bannerStream(ctx),
|
|
313
|
+
noColor,
|
|
314
|
+
});
|
|
303
315
|
ui = createClackOnboardUi(await import("@clack/prompts"), { input, output: process.stdout }, {
|
|
304
316
|
baseUrl: () => savedOnboardBaseUrl(ctx.home),
|
|
305
317
|
verbose: args.flags.verbose === true,
|
|
306
|
-
renderer: createSetupRenderer(process.stdout,
|
|
318
|
+
renderer: createSetupRenderer(process.stdout, rendererEnv, banner),
|
|
307
319
|
disposeRenderer: true,
|
|
308
320
|
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
309
321
|
version: manifest.version,
|
|
@@ -326,7 +338,14 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
|
|
|
326
338
|
});
|
|
327
339
|
ui = createClackOnboardUi(await import("@clack/prompts"), { input: process.stdin, output }, {
|
|
328
340
|
baseUrl: () => savedOnboardBaseUrl(ctx.home),
|
|
329
|
-
renderer: createSetupRenderer(output,
|
|
341
|
+
renderer: createSetupRenderer(output, rendererEnv, await setupBanner({
|
|
342
|
+
setup: viaSetup,
|
|
343
|
+
env: rendererEnv,
|
|
344
|
+
input: process.stdin,
|
|
345
|
+
stdout: process.stdout,
|
|
346
|
+
stream: bannerStream(ctx),
|
|
347
|
+
noColor,
|
|
348
|
+
})),
|
|
330
349
|
disposeRenderer: true,
|
|
331
350
|
signinTimeoutMs: deps.onboardSigninTimeoutMs,
|
|
332
351
|
version: manifest.version,
|
|
@@ -560,6 +579,23 @@ const VERB_HELP_KNOWN = Object.fromEntries([
|
|
|
560
579
|
"legacy",
|
|
561
580
|
"onboard",
|
|
562
581
|
].map((v) => [v, true]));
|
|
582
|
+
/** CTC-4680: the Pixel Nucleus mark belongs to `catalyst setup` alone. `setup` runs the onboard
|
|
583
|
+
* flow, so this is read from the verb typed, before setup becomes onboard. */
|
|
584
|
+
export function drawsMark(argv) {
|
|
585
|
+
return argv[0] === "setup";
|
|
586
|
+
}
|
|
587
|
+
/** CTC-4680: the banner goes to stderr, through ctx when stderr is not a terminal. */
|
|
588
|
+
function bannerStream(ctx) {
|
|
589
|
+
if (process.stderr.isTTY === true)
|
|
590
|
+
return process.stderr;
|
|
591
|
+
return {
|
|
592
|
+
write(chunk) {
|
|
593
|
+
for (const line of chunk.replace(/\n$/, "").split("\n"))
|
|
594
|
+
ctx.stderr(line);
|
|
595
|
+
return true;
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
}
|
|
563
599
|
/** CTC-4625: `catalyst setup`, wired to this terminal, @clack/prompts and the keyless login. */
|
|
564
600
|
async function runSetup(argv, ctx, deps) {
|
|
565
601
|
if (argv[0] === "--help" || argv[0] === "-h") {
|
|
@@ -235,8 +235,9 @@ export function connectChecklistAdapter(options) {
|
|
|
235
235
|
// The contract advertised the route for this read; the wait's polls read the checklist alone.
|
|
236
236
|
const origin = new URL(url).origin;
|
|
237
237
|
let instruction;
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
// One tab per run: a second wait (Keep waiting, or another step) points at the page already
|
|
239
|
+
// open, and the wait still shows its address.
|
|
240
|
+
if (options.page.openedBy === null) {
|
|
240
241
|
try {
|
|
241
242
|
await options.openBrowser(url, signal);
|
|
242
243
|
options.page.openedBy = options.step;
|
package/dist/onboard-headless.js
CHANGED
|
@@ -242,6 +242,9 @@ export function planOnboardHeadless(args, ctx) {
|
|
|
242
242
|
delete flags["key-file"];
|
|
243
243
|
delete flags.runner;
|
|
244
244
|
delete flags["no-runner"];
|
|
245
|
+
// CATALYST_ONBOARD_HEADLESS=1 alone leaves the flag unset; the plan names the headless input
|
|
246
|
+
// rather than a --no-runner nobody typed (CTC-4739).
|
|
247
|
+
flags.headless = true;
|
|
245
248
|
if (runner === "yes")
|
|
246
249
|
flags.runner = true;
|
|
247
250
|
if (runner === "no")
|
|
@@ -97,6 +97,17 @@ export async function readOnboardRepositoryInventory(ctx, journal, signal) {
|
|
|
97
97
|
const teamId = selectedOnboardTeam(journal);
|
|
98
98
|
if (!teamId)
|
|
99
99
|
return { reason: "team_selection_unverified" };
|
|
100
|
+
// CTC-4742: the per-person repository list names every project a repository belongs to, so a
|
|
101
|
+
// team that uses several repositories shows all of them. A member's list is narrowed to their
|
|
102
|
+
// own access by the server. An older server without the route falls back to the one-per-team
|
|
103
|
+
// registry below.
|
|
104
|
+
const agent = await readExistingOnboardJson(ctx, "/api/v1/agent/repos", signal);
|
|
105
|
+
if (!sameIdentity())
|
|
106
|
+
return { reason: "repository_identity_unverified" };
|
|
107
|
+
if (!("reason" in agent))
|
|
108
|
+
return teamRepositories(ctx, journal, teamId, agent.body, sameIdentity, signal);
|
|
109
|
+
if (agent.status !== 404)
|
|
110
|
+
return { reason: "repository_read_unavailable" };
|
|
100
111
|
const read = await readExistingOnboardJson(ctx, "/api/v1/repos", signal);
|
|
101
112
|
if (!sameIdentity())
|
|
102
113
|
return { reason: "repository_identity_unverified" };
|
|
@@ -171,6 +182,162 @@ export async function readOnboardRepositoryInventory(ctx, journal, signal) {
|
|
|
171
182
|
}
|
|
172
183
|
return { repositories };
|
|
173
184
|
}
|
|
185
|
+
/** The saved login is still the person, account and origin this run started with (and, when
|
|
186
|
+
* `manager`, an owner or admin). Returns that config, or null. */
|
|
187
|
+
function journalIdentity(ctx, journal, manager = false) {
|
|
188
|
+
const cfg = loadConfig(ctx.home);
|
|
189
|
+
return cfg?.user &&
|
|
190
|
+
cfg.account === journal.account &&
|
|
191
|
+
cfg.user.id === journal.membershipId &&
|
|
192
|
+
!!journal.baseUrl &&
|
|
193
|
+
normalizeBaseUrl(cfg.baseUrl) === normalizeBaseUrl(journal.baseUrl) &&
|
|
194
|
+
(!manager || ["owner", "admin"].includes(cfg.user.role))
|
|
195
|
+
? cfg
|
|
196
|
+
: null;
|
|
197
|
+
}
|
|
198
|
+
const REGISTER_PATH = "/api/v1/agent/project-repositories";
|
|
199
|
+
/** The fresh contract's repository IDs and the repositories registered to the chosen team (by its
|
|
200
|
+
* id), or why they can't be trusted. */
|
|
201
|
+
async function contractIds(ctx, journal, teamId, sameIdentity, signal) {
|
|
202
|
+
const read = await readExistingOnboardJson(ctx, "/api/v1/agent/contract", signal);
|
|
203
|
+
if (!sameIdentity())
|
|
204
|
+
return { reason: "repository_identity_unverified" };
|
|
205
|
+
if ("reason" in read)
|
|
206
|
+
return { reason: "repository_contract_unavailable" };
|
|
207
|
+
const doc = object(read.body);
|
|
208
|
+
const account = object(doc?.account);
|
|
209
|
+
const merge = object(doc?.merge);
|
|
210
|
+
if (!doc ||
|
|
211
|
+
account?.id !== journal.account ||
|
|
212
|
+
typeof doc.contractVersion !== "string" ||
|
|
213
|
+
contractVersionInRange(doc.contractVersion, readManifest().tenantContractRange) !== true ||
|
|
214
|
+
!merge ||
|
|
215
|
+
!Array.isArray(merge.repositories) ||
|
|
216
|
+
merge.repositories.length > 10_000 ||
|
|
217
|
+
!Array.isArray(doc.teams) ||
|
|
218
|
+
doc.teams.length > 1_000)
|
|
219
|
+
return { reason: "repository_contract_unverified" };
|
|
220
|
+
// The team's repositories, keyed by the team's id (a team key can be renamed or missing).
|
|
221
|
+
const teams = doc.teams.filter((value) => object(value)?.id === teamId);
|
|
222
|
+
const names = object(object(teams[0])?.repositories)?.registered;
|
|
223
|
+
if (teams.length !== 1 ||
|
|
224
|
+
!Array.isArray(names) ||
|
|
225
|
+
names.length > 1_000 ||
|
|
226
|
+
!names.every((name) => typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}\/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/.test(name)))
|
|
227
|
+
return { reason: "team_selection_unverified" };
|
|
228
|
+
const registered = new Set(names.map((name) => name.toLowerCase()));
|
|
229
|
+
const ids = new Map();
|
|
230
|
+
const idNames = new Map();
|
|
231
|
+
for (const value of merge.repositories) {
|
|
232
|
+
const row = object(value);
|
|
233
|
+
if (!row ||
|
|
234
|
+
typeof row.owner !== "string" ||
|
|
235
|
+
!part.test(row.owner) ||
|
|
236
|
+
typeof row.name !== "string" ||
|
|
237
|
+
!part.test(row.name) ||
|
|
238
|
+
!isRepositoryId(row.repoId))
|
|
239
|
+
return { reason: "repository_contract_unverified" };
|
|
240
|
+
const name = key({ owner: row.owner, name: row.name });
|
|
241
|
+
if (idNames.has(row.repoId) && idNames.get(row.repoId) !== name)
|
|
242
|
+
return { reason: "repository_contract_unverified" };
|
|
243
|
+
idNames.set(row.repoId, name);
|
|
244
|
+
ids.set(name, [...(ids.get(name) ?? []), row.repoId]);
|
|
245
|
+
}
|
|
246
|
+
// Registering is offered only when this account's contract lists the route (CTC-4742).
|
|
247
|
+
const canRegister = Array.isArray(doc.routes) &&
|
|
248
|
+
doc.routes.some((value) => {
|
|
249
|
+
const route = object(value);
|
|
250
|
+
return route?.method === "POST" && route.path === REGISTER_PATH;
|
|
251
|
+
});
|
|
252
|
+
return { registered, ids, canRegister };
|
|
253
|
+
}
|
|
254
|
+
/** CTC-4742: every repository an active project of the chosen team uses, with its contract ID. */
|
|
255
|
+
async function teamRepositories(ctx, journal, teamId, body, sameIdentity, signal) {
|
|
256
|
+
const list = object(body);
|
|
257
|
+
if (!list || !Array.isArray(list.repositories) || list.repositories.length > 10_000)
|
|
258
|
+
return { reason: "repository_inventory_shape" };
|
|
259
|
+
const contract = await contractIds(ctx, journal, teamId, sameIdentity, signal);
|
|
260
|
+
if ("reason" in contract)
|
|
261
|
+
return contract;
|
|
262
|
+
const repositories = [];
|
|
263
|
+
const seen = new Set();
|
|
264
|
+
for (const value of list.repositories) {
|
|
265
|
+
const row = object(value);
|
|
266
|
+
if (!row ||
|
|
267
|
+
typeof row.id !== "string" ||
|
|
268
|
+
typeof row.owner !== "string" ||
|
|
269
|
+
!part.test(row.owner) ||
|
|
270
|
+
typeof row.name !== "string" ||
|
|
271
|
+
!part.test(row.name))
|
|
272
|
+
return { reason: "repository_inventory_shape" };
|
|
273
|
+
// The contract says which repositories the team uses; this person's list says which of them
|
|
274
|
+
// they can see (the server narrows it for a member).
|
|
275
|
+
const name = key({ owner: row.owner, name: row.name });
|
|
276
|
+
if (!contract.registered.has(name))
|
|
277
|
+
continue;
|
|
278
|
+
if (seen.has(name))
|
|
279
|
+
return { reason: "repository_binding_ambiguous" };
|
|
280
|
+
seen.add(name);
|
|
281
|
+
const ids = contract.ids.get(name);
|
|
282
|
+
if (!ids || ids.length !== 1 || ids[0] !== row.id)
|
|
283
|
+
return { reason: "repository_id_unverified" };
|
|
284
|
+
repositories.push({ teamId, owner: row.owner, name: row.name, repoId: ids[0], registered: true });
|
|
285
|
+
}
|
|
286
|
+
return { repositories, canRegister: contract.canRegister };
|
|
287
|
+
}
|
|
288
|
+
/** CTC-4742: repositories the GitHub App can reach that the team does not use yet, for an owner or
|
|
289
|
+
* admin only (only they can register one). Any read problem means no offers, never a stopped step. */
|
|
290
|
+
async function offeredRepositories(ctx, journal, teamId, registered, signal) {
|
|
291
|
+
if (!journalIdentity(ctx, journal, true))
|
|
292
|
+
return [];
|
|
293
|
+
const read = await readExistingOnboardJson(ctx, "/api/v1/me/repositories/options", signal);
|
|
294
|
+
if (!journalIdentity(ctx, journal, true) || "reason" in read)
|
|
295
|
+
return [];
|
|
296
|
+
const github = object(object(read.body)?.github);
|
|
297
|
+
if (!github || github.connected !== true || !Array.isArray(github.repositories) || github.repositories.length > 10_000)
|
|
298
|
+
return [];
|
|
299
|
+
const used = new Set(registered.map(key));
|
|
300
|
+
const offers = [];
|
|
301
|
+
for (const value of github.repositories) {
|
|
302
|
+
const row = object(value);
|
|
303
|
+
if (!row || typeof row.owner !== "string" || !part.test(row.owner) || typeof row.name !== "string" || !part.test(row.name))
|
|
304
|
+
return [];
|
|
305
|
+
const name = key({ owner: row.owner, name: row.name });
|
|
306
|
+
if (used.has(name))
|
|
307
|
+
continue;
|
|
308
|
+
used.add(name);
|
|
309
|
+
offers.push({ teamId, owner: row.owner, name: row.name, repoId: null, registered: false });
|
|
310
|
+
}
|
|
311
|
+
return offers;
|
|
312
|
+
}
|
|
313
|
+
/** CTC-4742: register one repository to the team's project. Only adds; nothing here removes one. */
|
|
314
|
+
async function registerToTeam(ctx, journal, teamId, repository, signal) {
|
|
315
|
+
// The person who answered the question is the one who registers: checked right before the write.
|
|
316
|
+
const cfg = journalIdentity(ctx, journal, true);
|
|
317
|
+
if (!cfg)
|
|
318
|
+
return "repository_identity_unverified";
|
|
319
|
+
const expiry = cfg.auth ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime() : 0;
|
|
320
|
+
const bearer = cfg.key || (cfg.auth && Number.isFinite(expiry) && expiry > 30_000 ? cfg.auth.accessToken : undefined);
|
|
321
|
+
if (!bearer)
|
|
322
|
+
return "project_login_refresh_required";
|
|
323
|
+
// Bounded like every read here, so a server that never answers can't hold setup.
|
|
324
|
+
const deadline = AbortSignal.timeout(30_000);
|
|
325
|
+
const bounded = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
326
|
+
try {
|
|
327
|
+
const response = await ctx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}${REGISTER_PATH}`, {
|
|
328
|
+
method: "POST",
|
|
329
|
+
redirect: "error",
|
|
330
|
+
signal: bounded,
|
|
331
|
+
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json", accept: "application/json" },
|
|
332
|
+
body: JSON.stringify({ teamId, repository }),
|
|
333
|
+
});
|
|
334
|
+
// A response is not evidence: the next read must show the repository on the team.
|
|
335
|
+
return response.status === 200 ? null : "repository_register_unverified";
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return signal?.aborted ? "interrupted" : "repository_register_unverified";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
174
341
|
export function existingRepositoryAdapter(args, choose) {
|
|
175
342
|
let chosen;
|
|
176
343
|
const requested = (repositories, journal) => {
|
|
@@ -218,7 +385,7 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
218
385
|
return {
|
|
219
386
|
state: "done",
|
|
220
387
|
evidence: {
|
|
221
|
-
repository: JSON.stringify(rows),
|
|
388
|
+
repository: JSON.stringify(rows.map(({ teamId, owner, name, repoId }) => ({ teamId, owner, name, repoId }))),
|
|
222
389
|
count: rows.length,
|
|
223
390
|
checkedAt: ctx.now().getTime(),
|
|
224
391
|
},
|
|
@@ -234,6 +401,14 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
234
401
|
return waiting("repository_inventory_empty");
|
|
235
402
|
if (!choose)
|
|
236
403
|
return waiting("repository_choice_required");
|
|
404
|
+
const teamId = list.repositories[0].teamId;
|
|
405
|
+
const offers = list.canRegister
|
|
406
|
+
? await offeredRepositories(ctx, journal, teamId, list.repositories, signal)
|
|
407
|
+
: [];
|
|
408
|
+
const choices = [
|
|
409
|
+
...list.repositories.map((row) => ({ ...row })),
|
|
410
|
+
...offers.map((row) => ({ ...row })),
|
|
411
|
+
];
|
|
237
412
|
let remove = () => { };
|
|
238
413
|
const answer = await new Promise((resolve, reject) => {
|
|
239
414
|
const stopped = () => resolve(null);
|
|
@@ -244,9 +419,8 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
244
419
|
signal?.addEventListener("abort", stopped, { once: true });
|
|
245
420
|
remove = () => signal?.removeEventListener("abort", stopped);
|
|
246
421
|
Promise.resolve()
|
|
247
|
-
|
|
248
|
-
? null
|
|
249
|
-
: choose(list.repositories.map((row) => ({ ...row }))))
|
|
422
|
+
// The hook gets copies: changing one can never change what is matched below.
|
|
423
|
+
.then(() => signal?.aborted ? null : choose(choices.map((row) => ({ ...row }))))
|
|
250
424
|
.then(resolve, reject);
|
|
251
425
|
}).finally(() => remove());
|
|
252
426
|
if (!answer || signal?.aborted)
|
|
@@ -259,9 +433,26 @@ export function existingRepositoryAdapter(args, choose) {
|
|
|
259
433
|
const names = answer.map((name) => name.toLowerCase());
|
|
260
434
|
if (new Set(names).size !== names.length)
|
|
261
435
|
return waiting("repository_selection_unverified");
|
|
262
|
-
const
|
|
263
|
-
if (!
|
|
436
|
+
const picked = names.map((name) => choices.find((row) => key(row) === name));
|
|
437
|
+
if (!picked.every((row) => !!row))
|
|
264
438
|
return waiting("repository_selection_unverified");
|
|
439
|
+
// CTC-4742: a selected offer is registered to the team. Leaving a registered repository
|
|
440
|
+
// unselected never unregisters it; nothing here removes a repository.
|
|
441
|
+
for (const row of picked) {
|
|
442
|
+
if (row.registered !== false)
|
|
443
|
+
continue;
|
|
444
|
+
const failed = await registerToTeam(ctx, journal, teamId, `${row.owner}/${row.name}`, signal);
|
|
445
|
+
if (failed)
|
|
446
|
+
return waiting(failed);
|
|
447
|
+
}
|
|
448
|
+
const fresh = picked.some((row) => row.registered === false)
|
|
449
|
+
? await readOnboardRepositoryInventory(ctx, journal, signal)
|
|
450
|
+
: list;
|
|
451
|
+
if ("reason" in fresh)
|
|
452
|
+
return waiting(fresh.reason);
|
|
453
|
+
const rows = names.map((name) => fresh.repositories.find((row) => key(row) === name));
|
|
454
|
+
if (!rows.every((row) => !!row))
|
|
455
|
+
return waiting("repository_registration_visibility_pending");
|
|
265
456
|
chosen = rows;
|
|
266
457
|
return check(ctx, journal, signal);
|
|
267
458
|
},
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { setupPartHeading, setupStepView, SETUP_NUMBERS, SETUP_PART_NUMBERS, SETUP_PART_OF, } from "./setup-onboard-copy.js";
|
|
2
2
|
export const COMPUTER_CHECKS = ["machine", "cli", "skills", "legacy"];
|
|
3
|
+
/** The runner row when no runner was chosen up front, naming where that choice came from, so the
|
|
4
|
+
* plan says why no question comes (CTC-4739). */
|
|
5
|
+
const runnerDeclined = (by) => `this computer will not take work, because ${by}`;
|
|
3
6
|
const PLAN = [
|
|
4
7
|
["On this computer", "signin", "you approve once in your browser"],
|
|
5
8
|
["On this computer", "housekeeping", "schedule the daily update"],
|
|
@@ -22,7 +25,14 @@ const PLAN = [
|
|
|
22
25
|
* part. `json` is the plain-text plan an agent reads beside `--json` (dry run, --json on stderr,
|
|
23
26
|
* headless): the older groups and the numbers JSON `actions[].number` reports (CTC-4680).
|
|
24
27
|
*/
|
|
25
|
-
export function standalonePlan(journal, scope,
|
|
28
|
+
export function standalonePlan(journal, scope,
|
|
29
|
+
/** `true` for --runner, `false` for --no-runner, absent when setup will ask (CTC-4739). */
|
|
30
|
+
runner, numbering = "parts",
|
|
31
|
+
/** Why `runner` is false: the flag on an interactive run, the headless input otherwise. */
|
|
32
|
+
declinedBy = "--no-runner was passed") {
|
|
33
|
+
const runnerSelected = runner === true;
|
|
34
|
+
const RUNNER_DECLINED = runnerDeclined(declinedBy);
|
|
35
|
+
const declined = runner === false;
|
|
26
36
|
const json = numbering === "json";
|
|
27
37
|
const includes = (id) => !scope || scope.includes(id);
|
|
28
38
|
const steps = new Map(journal.steps.map((step) => [step.id, step]));
|
|
@@ -53,7 +63,9 @@ export function standalonePlan(journal, scope, runnerSelected = false, numbering
|
|
|
53
63
|
if (includes("runner"))
|
|
54
64
|
row("runner", runnerSelected
|
|
55
65
|
? "start a Catalyst runner here with Docker"
|
|
56
|
-
:
|
|
66
|
+
: declined
|
|
67
|
+
? RUNNER_DECLINED
|
|
68
|
+
: "optional; this computer takes work only if you choose it");
|
|
57
69
|
continue;
|
|
58
70
|
}
|
|
59
71
|
// The terminal gives repository values their own row; the plain-text plan keeps them folded
|
|
@@ -66,9 +78,13 @@ export function standalonePlan(journal, scope, runnerSelected = false, numbering
|
|
|
66
78
|
continue;
|
|
67
79
|
const reviewedDetail = id === "capacity" && includes("runner") && runnerSelected
|
|
68
80
|
? "start a Catalyst runner here with Docker"
|
|
69
|
-
: id === "capacity" &&
|
|
70
|
-
? "
|
|
71
|
-
|
|
81
|
+
: id === "capacity" && includes("runner") && declined
|
|
82
|
+
? includes("capacity")
|
|
83
|
+
? `${detail}; ${RUNNER_DECLINED}`
|
|
84
|
+
: RUNNER_DECLINED
|
|
85
|
+
: id === "capacity" && !includes("capacity")
|
|
86
|
+
? "review the optional runner on this machine; start only if selected"
|
|
87
|
+
: detail;
|
|
72
88
|
rows.push({
|
|
73
89
|
group: json ? group : setupPartHeading(SETUP_PART_OF[id], false),
|
|
74
90
|
part: SETUP_PART_OF[id],
|