@catalyst-cloud/cli 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +2 -2
- package/dist/args.js +291 -69
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +109 -17
- package/dist/config.js +3 -1
- package/dist/env/draft.js +134 -0
- package/dist/env/legacy.js +54 -0
- package/dist/env/parse-env.js +50 -0
- package/dist/env/settings-toml.js +66 -0
- package/dist/env.js +126 -26
- package/dist/onboard-consent.js +65 -0
- package/dist/onboard.js +836 -0
- package/dist/project.js +75 -13
- package/dist/ready.js +11 -1
- package/dist/secret.js +24 -3
- package/dist/var.js +118 -0
- 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 +1 -1
- package/skills/catalyst-onboard/references/declaring-a-repository.md +19 -12
- package/skills/catalyst-onboard/references/reading-ready.md +1 -1
- package/skills/catalyst-onboard/references/the-one-path.md +3 -3
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +2 -2
- package/skills/catalyst-onboard/scripts/lib/project-coverage.mjs +53 -0
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +884 -195
- 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 +4 -4
- package/skills/what-this-repo-needs/scripts/check.mjs +12 -13
- package/skills/whats-happening/SKILL.md +19 -35
- package/skills/whats-happening/references/coding-accounts.md +6 -19
- package/skills/whats-happening/references/routing-work.md +4 -15
- package/skills/whats-happening/references/settings-and-where-they-live.md +15 -18
- package/skills/whats-happening/references/stages-and-mapping.md +12 -18
- package/skills/whats-happening/references/status-reply.md +11 -29
- package/skills/whats-happening/references/the-ladder.md +15 -25
- package/skills/whats-happening/references/what-runs-next.md +7 -26
- package/skills/whats-happening/references/when-a-phase-fails.md +16 -22
- package/skills/whats-happening/references/why-is-it-stuck.md +38 -48
- package/dist/env/declaration-rules.js +0 -163
|
@@ -7,14 +7,26 @@
|
|
|
7
7
|
// owner's name on it, never as something the person at this keyboard can fix by running anything.
|
|
8
8
|
//
|
|
9
9
|
// Every count and every name below is read off what a verb printed. Nothing here is written down.
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cliTarget,
|
|
12
|
+
CONNECT_LINE,
|
|
13
|
+
parseFlags,
|
|
14
|
+
printHelp,
|
|
15
|
+
runCli,
|
|
16
|
+
tryJson,
|
|
17
|
+
tryLoadConfig,
|
|
18
|
+
} from "./lib/cli.mjs";
|
|
19
|
+
import { projectCoverageLines } from "./lib/project-coverage.mjs";
|
|
11
20
|
import { spawnSync } from "node:child_process";
|
|
12
21
|
import { runLocalSync } from "./local-sync.mjs";
|
|
13
22
|
|
|
14
23
|
const SPEC = {
|
|
15
24
|
next: { help: "print only the single next step" },
|
|
16
25
|
json: { help: "one JSON document instead of the report" },
|
|
17
|
-
repo: {
|
|
26
|
+
repo: {
|
|
27
|
+
value: true,
|
|
28
|
+
help: "a checkout of a mapped repository on this machine: its agent setup (AGENTS.md block, portable layout) is read and reported, never changed",
|
|
29
|
+
},
|
|
18
30
|
};
|
|
19
31
|
const NOTES = [
|
|
20
32
|
"Reads, in this order: `status` (machine), `ready --json` (machine checks and project checks, kept apart),",
|
|
@@ -32,7 +44,11 @@ const NOTES = [
|
|
|
32
44
|
|
|
33
45
|
const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
|
|
34
46
|
if (help) {
|
|
35
|
-
printHelp(
|
|
47
|
+
printHelp(
|
|
48
|
+
"node scripts/where-am-i.mjs [--next] [--json] [--repo <path>]",
|
|
49
|
+
SPEC,
|
|
50
|
+
NOTES,
|
|
51
|
+
);
|
|
36
52
|
process.exit(0);
|
|
37
53
|
}
|
|
38
54
|
if (positionals.length > 0) {
|
|
@@ -55,13 +71,32 @@ let repositoryRegistered = false;
|
|
|
55
71
|
let capabilities = null;
|
|
56
72
|
let personRole = null;
|
|
57
73
|
const canManage = () => personRole === "admin" || personRole === "owner";
|
|
58
|
-
const capability = (verb) =>
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
const capability = (verb) =>
|
|
75
|
+
Array.isArray(capabilities)
|
|
76
|
+
? capabilities.find((c) => c && c.verb === verb)
|
|
77
|
+
: undefined;
|
|
78
|
+
const verbAvailable = (verb) => {
|
|
79
|
+
const c = capability(verb);
|
|
80
|
+
return (
|
|
81
|
+
c?.availability === "available" && (c.needs === "member" || canManage())
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
const verbAdminOnly = (verb) => {
|
|
85
|
+
const c = capability(verb);
|
|
86
|
+
return c?.availability === "available" && c.needs === "admin" && !canManage();
|
|
87
|
+
};
|
|
61
88
|
// `blocking` is false for a finding that is real and reportable but does not stop the next step —
|
|
62
89
|
// an unmatched Linear identity is the one that matters: it must be said, and it must not become the
|
|
63
90
|
// thing the person is told to go and do before they can map a project.
|
|
64
|
-
const add = (
|
|
91
|
+
const add = (
|
|
92
|
+
part,
|
|
93
|
+
instrument,
|
|
94
|
+
verdict,
|
|
95
|
+
lines,
|
|
96
|
+
owner = null,
|
|
97
|
+
where = null,
|
|
98
|
+
blocking = true,
|
|
99
|
+
) => {
|
|
65
100
|
const p = { part, instrument, verdict, lines, owner, where, blocking };
|
|
66
101
|
parts.push(p);
|
|
67
102
|
return p;
|
|
@@ -73,8 +108,12 @@ const add = (part, instrument, verdict, lines, owner = null, where = null, block
|
|
|
73
108
|
let contractMismatch = null;
|
|
74
109
|
const noteMismatch = (text) => {
|
|
75
110
|
const m =
|
|
76
|
-
(text ?? "").match(
|
|
77
|
-
|
|
111
|
+
(text ?? "").match(
|
|
112
|
+
/serves contract version (\S+) but this bundle accepts (\S+)/,
|
|
113
|
+
) ??
|
|
114
|
+
(text ?? "").match(
|
|
115
|
+
/contract: version (\S+) is outside this bundle's range (\S+)/,
|
|
116
|
+
);
|
|
78
117
|
if (m !== null) contractMismatch ??= { served: m[1], accepts: m[2] };
|
|
79
118
|
return m !== null;
|
|
80
119
|
};
|
|
@@ -88,10 +127,15 @@ const readContract = (args) => {
|
|
|
88
127
|
const status = runCli(["status"]);
|
|
89
128
|
if (!status.ran) {
|
|
90
129
|
process.stderr.write(`${status.stderr}\n`);
|
|
91
|
-
process.stderr.write(
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`the catalyst CLI could not be started. Install it, then run this again.\n`,
|
|
132
|
+
);
|
|
92
133
|
process.exit(2);
|
|
93
134
|
}
|
|
94
|
-
const statusLines = status.stdout
|
|
135
|
+
const statusLines = status.stdout
|
|
136
|
+
.split("\n")
|
|
137
|
+
.map((l) => l.trim())
|
|
138
|
+
.filter(Boolean);
|
|
95
139
|
const field = (name) => {
|
|
96
140
|
const line = statusLines.find((l) => l.startsWith(`${name}:`));
|
|
97
141
|
return line ? line.slice(name.length + 1).trim() : null;
|
|
@@ -102,9 +146,12 @@ const connected = tenant !== null && tryLoadConfig() !== null;
|
|
|
102
146
|
// The app and the API share an origin; the base URL is whatever this machine was connected to, so
|
|
103
147
|
// no page link in this report is a host anyone typed from memory.
|
|
104
148
|
const cloud = api === null ? null : api.split(" ")[0].replace(/\/+$/, "");
|
|
105
|
-
const link = (path) =>
|
|
149
|
+
const link = (path) =>
|
|
150
|
+
cloud === null ? `<your cloud>${path}` : `${cloud}${path}`;
|
|
106
151
|
|
|
107
|
-
const machineLines = connected
|
|
152
|
+
const machineLines = connected
|
|
153
|
+
? statusLines
|
|
154
|
+
: [statusLines[0] ?? "status printed nothing"];
|
|
108
155
|
let machineVerdict = connected ? "ok" : "unfinished";
|
|
109
156
|
|
|
110
157
|
// `ready` is ONE verdict over two parts; split it by check id before anything is reported. A check
|
|
@@ -117,19 +164,30 @@ if (connected) {
|
|
|
117
164
|
const report = tryJson(ready.stdout);
|
|
118
165
|
const checks = Array.isArray(report?.checks) ? report.checks : null;
|
|
119
166
|
if (checks === null) {
|
|
120
|
-
machineLines.push(
|
|
167
|
+
machineLines.push(
|
|
168
|
+
`ready: could not be read (${(ready.stderr || ready.stdout).trim().split("\n")[0] ?? "no output"})`,
|
|
169
|
+
);
|
|
121
170
|
machineVerdict = "unfinished";
|
|
122
171
|
} else {
|
|
123
|
-
projectChecks = checks.filter(
|
|
124
|
-
|
|
172
|
+
projectChecks = checks.filter(
|
|
173
|
+
(c) => typeof c.id === "string" && c.id.startsWith("team:"),
|
|
174
|
+
);
|
|
175
|
+
const machineChecks = checks.filter(
|
|
176
|
+
(c) => !(typeof c.id === "string" && c.id.startsWith("team:")),
|
|
177
|
+
);
|
|
125
178
|
const failed = machineChecks.filter((c) => !c.ok && !c.note);
|
|
126
179
|
// A refused contract version is printed without the CLI's "update" fix: whether an update exists
|
|
127
180
|
// is checked below, once, against what npm has published.
|
|
128
181
|
const refused = (c) => !c.ok && !c.note && noteMismatch(c.line);
|
|
129
|
-
for (const c of machineChecks)
|
|
182
|
+
for (const c of machineChecks)
|
|
183
|
+
machineLines.push(
|
|
184
|
+
`${c.note ? "note" : c.ok ? "ok " : "FAIL"} ${c.line}${!c.ok && !c.note && c.fix && !refused(c) ? ` — fix: ${c.fix}` : ""}`,
|
|
185
|
+
);
|
|
130
186
|
if (failed.length > 0) {
|
|
131
187
|
machineVerdict = "unfinished";
|
|
132
|
-
machineFix =
|
|
188
|
+
machineFix =
|
|
189
|
+
failed.find((c) => typeof c.fix === "string" && !refused(c))?.fix ??
|
|
190
|
+
null;
|
|
133
191
|
}
|
|
134
192
|
}
|
|
135
193
|
// Supplemental only: local caches are optional and do not change setup completion or --next.
|
|
@@ -139,7 +197,11 @@ if (connected) {
|
|
|
139
197
|
);
|
|
140
198
|
} else {
|
|
141
199
|
localSync = {
|
|
142
|
-
assessment: {
|
|
200
|
+
assessment: {
|
|
201
|
+
verdict: "unknown",
|
|
202
|
+
current: false,
|
|
203
|
+
reason: "connect this machine before local freshness can be checked",
|
|
204
|
+
},
|
|
143
205
|
started: false,
|
|
144
206
|
recovery: "catalyst login",
|
|
145
207
|
};
|
|
@@ -152,11 +214,17 @@ add(
|
|
|
152
214
|
"you, on this machine",
|
|
153
215
|
// The keyless login alone: a person who holds a personal key already knows the key form. A machine
|
|
154
216
|
// that holds the CLI is told its own command; only one with no CLI at all gets the npx line.
|
|
155
|
-
connected
|
|
217
|
+
connected
|
|
218
|
+
? null
|
|
219
|
+
: cliTarget().recorded
|
|
220
|
+
? "catalyst login"
|
|
221
|
+
: CONNECT_LINE.split(" (or")[0],
|
|
156
222
|
);
|
|
157
223
|
if (connected) {
|
|
158
224
|
const capDoc = tryJson(runCli(["capabilities", "--json"]).stdout);
|
|
159
|
-
capabilities = Array.isArray(capDoc?.capabilities)
|
|
225
|
+
capabilities = Array.isArray(capDoc?.capabilities)
|
|
226
|
+
? capDoc.capabilities
|
|
227
|
+
: null;
|
|
160
228
|
}
|
|
161
229
|
// Leftovers of the old local Catalyst runtime (its plugin, jobs, commands, state), from the CLI's own
|
|
162
230
|
// fixed list. Reported on the machine part and as a note; removal is the person's yes, never this script's.
|
|
@@ -167,38 +235,67 @@ if (verbAvailable("legacy")) {
|
|
|
167
235
|
if (found.length > 0) {
|
|
168
236
|
const machine = parts.find((p) => p.part === "machine");
|
|
169
237
|
const names = found.map((f) => `${f.kind} ${f.name}`);
|
|
170
|
-
machine?.lines.push(
|
|
238
|
+
machine?.lines.push(
|
|
239
|
+
`note leftovers of the old local Catalyst runtime (${found.length}): ${names.slice(0, 6).join(", ")}${found.length > 6 ? ", …" : ""}; removing them is strongly recommended: catalyst legacy --remove (the data folders are a separate yes, --data)`,
|
|
240
|
+
);
|
|
171
241
|
legacyNote = `this machine still carries ${found.length} piece${found.length === 1 ? "" : "s"} of the old local Catalyst runtime (${names.slice(0, 4).join(", ")}${found.length > 4 ? ", …" : ""}); offer catalyst legacy --remove once, strongly recommended, then --data as a separate question`;
|
|
172
242
|
}
|
|
173
243
|
}
|
|
174
244
|
|
|
175
245
|
// ── person ────────────────────────────────────────────────────────────────────────────────────────
|
|
176
246
|
if (!connected) {
|
|
177
|
-
add(
|
|
247
|
+
add(
|
|
248
|
+
"person",
|
|
249
|
+
"catalyst me",
|
|
250
|
+
"unreadable",
|
|
251
|
+
["not readable until this machine is connected"],
|
|
252
|
+
null,
|
|
253
|
+
null,
|
|
254
|
+
);
|
|
178
255
|
} else {
|
|
179
256
|
const me = runCli(["me", "--json"]);
|
|
180
257
|
const doc = tryJson(me.stdout);
|
|
181
|
-
const user =
|
|
258
|
+
const user =
|
|
259
|
+
doc && typeof doc.user === "object" && doc.user !== null ? doc.user : null;
|
|
182
260
|
if (doc === null) {
|
|
183
|
-
add(
|
|
261
|
+
add(
|
|
262
|
+
"person",
|
|
263
|
+
"catalyst me",
|
|
264
|
+
"unreadable",
|
|
265
|
+
[
|
|
266
|
+
`me could not be read (${(me.stderr || me.stdout).trim().split("\n")[0] ?? "no output"})`,
|
|
267
|
+
],
|
|
268
|
+
null,
|
|
269
|
+
null,
|
|
270
|
+
).next =
|
|
184
271
|
"run this again; catalyst me could not be read, so no step after it can be named yet";
|
|
185
272
|
} else if (user === null) {
|
|
186
273
|
add(
|
|
187
274
|
"person",
|
|
188
275
|
"catalyst me",
|
|
189
276
|
"unfinished",
|
|
190
|
-
[
|
|
277
|
+
[
|
|
278
|
+
"this credential names no person — it is a host credential, so nothing an agent writes will carry a name",
|
|
279
|
+
],
|
|
191
280
|
"you: connect again with your own login",
|
|
192
281
|
CONNECT_LINE,
|
|
193
282
|
);
|
|
194
283
|
} else {
|
|
195
|
-
const matched =
|
|
284
|
+
const matched =
|
|
285
|
+
typeof user.linearUserId === "string" && user.linearUserId !== "";
|
|
196
286
|
personRole = typeof user.role === "string" ? user.role : null;
|
|
197
287
|
const grantLines = [];
|
|
198
288
|
for (const provider of ["linear", "github"]) {
|
|
199
|
-
const read = runCli([
|
|
289
|
+
const read = runCli([
|
|
290
|
+
"connections",
|
|
291
|
+
"personal",
|
|
292
|
+
provider,
|
|
293
|
+
"status",
|
|
294
|
+
"--json",
|
|
295
|
+
]);
|
|
200
296
|
const result = tryJson(read.stdout);
|
|
201
|
-
const outcome =
|
|
297
|
+
const outcome =
|
|
298
|
+
typeof result?.outcome === "string" ? result.outcome : "unreadable";
|
|
202
299
|
personalConnections[provider] = outcome;
|
|
203
300
|
if (outcome === "connected") {
|
|
204
301
|
grantLines.push(`personal ${provider}: connected`);
|
|
@@ -206,23 +303,43 @@ if (!connected) {
|
|
|
206
303
|
personalGrantIncomplete = true;
|
|
207
304
|
if (provider === "linear") personalLinearIncomplete = true;
|
|
208
305
|
else personalGithubIncomplete = true;
|
|
209
|
-
grantLines.push(
|
|
210
|
-
|
|
306
|
+
grantLines.push(
|
|
307
|
+
`personal ${provider}: ${outcome === "absent" ? "not connected" : "expired"}`,
|
|
308
|
+
);
|
|
309
|
+
if (provider === "linear")
|
|
310
|
+
personalNext ??= "connect your personal linear account";
|
|
211
311
|
} else {
|
|
212
312
|
personalGrantIncomplete = true;
|
|
213
313
|
if (provider === "linear") personalLinearIncomplete = true;
|
|
214
314
|
else personalGithubIncomplete = true;
|
|
215
|
-
grantLines.push(
|
|
216
|
-
|
|
315
|
+
grantLines.push(
|
|
316
|
+
`personal ${provider}: ${outcome === "unavailable" ? "temporarily unavailable; grant state unknown" : "could not be checked; update the catalyst CLI or inspect its status output"}`,
|
|
317
|
+
);
|
|
318
|
+
if (provider === "linear")
|
|
319
|
+
personalNext ??= "re-check your personal linear connection";
|
|
217
320
|
}
|
|
218
321
|
}
|
|
219
322
|
add(
|
|
220
323
|
"person",
|
|
221
324
|
"catalyst me, and catalyst connections personal <provider> status --json",
|
|
222
325
|
matched && !personalGrantIncomplete ? "ok" : "unfinished",
|
|
223
|
-
[
|
|
224
|
-
|
|
225
|
-
|
|
326
|
+
[
|
|
327
|
+
`${user.label ?? "(unnamed)"} (${user.role ?? "role unknown"})`,
|
|
328
|
+
matched
|
|
329
|
+
? "Linear identity matched"
|
|
330
|
+
: "Linear identity NOT matched — asks assigned to you cannot be told apart from everyone else's. It blocks nothing below. Run catalyst identity linear options for self-service recovery; personal Linear consent normally binds its viewer automatically.",
|
|
331
|
+
...grantLines,
|
|
332
|
+
],
|
|
333
|
+
personalGrantIncomplete
|
|
334
|
+
? "you"
|
|
335
|
+
: matched
|
|
336
|
+
? null
|
|
337
|
+
: "a workspace owner or admin",
|
|
338
|
+
personalGrantIncomplete
|
|
339
|
+
? "catalyst connections personal <provider> start or status"
|
|
340
|
+
: matched
|
|
341
|
+
? null
|
|
342
|
+
: link("/settings/account"),
|
|
226
343
|
false,
|
|
227
344
|
);
|
|
228
345
|
}
|
|
@@ -230,15 +347,38 @@ if (!connected) {
|
|
|
230
347
|
|
|
231
348
|
// ── account ───────────────────────────────────────────────────────────────────────────────────────
|
|
232
349
|
if (!connected) {
|
|
233
|
-
add(
|
|
350
|
+
add(
|
|
351
|
+
"account",
|
|
352
|
+
"catalyst contract --path account",
|
|
353
|
+
"unreadable",
|
|
354
|
+
["not readable until this machine is connected"],
|
|
355
|
+
null,
|
|
356
|
+
null,
|
|
357
|
+
);
|
|
234
358
|
} else {
|
|
235
359
|
const acct = readContract(["contract", "--path", "account", "--json"]);
|
|
236
360
|
const doc = tryJson(acct.stdout);
|
|
237
361
|
if (doc === null) {
|
|
238
|
-
add(
|
|
362
|
+
add(
|
|
363
|
+
"account",
|
|
364
|
+
"catalyst contract --path account",
|
|
365
|
+
"unreadable",
|
|
366
|
+
[
|
|
367
|
+
"the account block could not be read — try: catalyst contract --refresh",
|
|
368
|
+
],
|
|
369
|
+
null,
|
|
370
|
+
null,
|
|
371
|
+
).next =
|
|
239
372
|
"refresh the contract (catalyst contract --refresh), then run this again; the account could not be read, so no step after it can be named yet";
|
|
240
373
|
} else {
|
|
241
|
-
const workspace =
|
|
374
|
+
const workspace =
|
|
375
|
+
typeof doc.linearWorkspaceSlug === "string" &&
|
|
376
|
+
doc.linearWorkspaceSlug !== ""
|
|
377
|
+
? doc.linearWorkspaceSlug
|
|
378
|
+
: typeof doc.linearWorkspaceId === "string" &&
|
|
379
|
+
doc.linearWorkspaceId !== ""
|
|
380
|
+
? doc.linearWorkspaceId
|
|
381
|
+
: null;
|
|
242
382
|
workspaceResolved = workspace !== null;
|
|
243
383
|
// The declaration is the one part of the account a key can also READ — and it is the one part a
|
|
244
384
|
// key can WRITE, so it is reported here rather than left to the settings page like the rest.
|
|
@@ -246,14 +386,29 @@ if (!connected) {
|
|
|
246
386
|
const env = runCli(["environment", "read", "--json"]);
|
|
247
387
|
const envDoc = tryJson(env.stdout);
|
|
248
388
|
if (envDoc === null) {
|
|
249
|
-
envLines.push(
|
|
389
|
+
envLines.push(
|
|
390
|
+
`environment: not readable (${(env.stderr || env.stdout).trim().split("\n")[0] ?? "no output"})`,
|
|
391
|
+
);
|
|
250
392
|
} else if (envDoc.current === null || envDoc.current === undefined) {
|
|
251
|
-
envLines.push(
|
|
393
|
+
envLines.push(
|
|
394
|
+
"environment: nothing declared yet — declare it with `catalyst environment propose --file <path> --approve`",
|
|
395
|
+
);
|
|
252
396
|
} else {
|
|
253
|
-
envLines.push(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
397
|
+
envLines.push(
|
|
398
|
+
`environment: revision ${envDoc.current.revision} (${envDoc.current.canonicalHash}), ${envDoc.isApproved ? "approved" : "NOT approved — a proposal nobody approved changes nothing"}`,
|
|
399
|
+
);
|
|
400
|
+
envLines.push(
|
|
401
|
+
envDoc.delivered
|
|
402
|
+
? `environment delivered to phases: revision ${envDoc.delivered.revision}`
|
|
403
|
+
: "environment delivered to phases: nothing yet",
|
|
404
|
+
);
|
|
405
|
+
const unresolved = Array.isArray(envDoc.unresolvedReferences)
|
|
406
|
+
? envDoc.unresolvedReferences
|
|
407
|
+
: [];
|
|
408
|
+
if (unresolved.length > 0)
|
|
409
|
+
envLines.push(
|
|
410
|
+
`environment names values this tenant does not carry yet: ${unresolved.join(", ")}`,
|
|
411
|
+
);
|
|
257
412
|
}
|
|
258
413
|
add(
|
|
259
414
|
"account",
|
|
@@ -261,7 +416,9 @@ if (!connected) {
|
|
|
261
416
|
workspace === null ? "unfinished" : "ok",
|
|
262
417
|
[
|
|
263
418
|
`${doc.name ?? "(unnamed tenant)"} (${doc.slug ?? "?"})`,
|
|
264
|
-
workspace === null
|
|
419
|
+
workspace === null
|
|
420
|
+
? "no Linear workspace resolved on this contract"
|
|
421
|
+
: `Linear workspace resolved: ${workspace}`,
|
|
265
422
|
"the GitHub App install is NOT carried here — the connections page is the only place that shows it",
|
|
266
423
|
...envLines,
|
|
267
424
|
],
|
|
@@ -278,12 +435,14 @@ let teamRows = null;
|
|
|
278
435
|
let readinessMetaCache;
|
|
279
436
|
const readinessMeta = () => {
|
|
280
437
|
if (readinessMetaCache === undefined) {
|
|
281
|
-
const m = tryJson(
|
|
438
|
+
const m = tryJson(
|
|
439
|
+
readContract(["contract", "--path", "readinessChecks", "--json"]).stdout,
|
|
440
|
+
);
|
|
282
441
|
readinessMetaCache = Array.isArray(m) ? m : [];
|
|
283
442
|
}
|
|
284
443
|
return readinessMetaCache;
|
|
285
444
|
};
|
|
286
|
-
// Linear's own Git automation rules, by check id, as the Settings → Workflow →
|
|
445
|
+
// Linear's own Git automation rules, by check id, as the Settings → Teams → <team> → Workflow → Workflows & automations screen
|
|
287
446
|
// names them. A rule that moves a card is a conflict with the stage Catalyst manages, and no Catalyst
|
|
288
447
|
// key can change a Linear automation rule: the fix is in Linear, by a Linear admin.
|
|
289
448
|
const LINEAR_AUTOMATION_RULES = {
|
|
@@ -296,27 +455,49 @@ let blockedProjects = [];
|
|
|
296
455
|
|
|
297
456
|
// ── projects (a project is one Linear team) ───────────────────────────────────────────────────────
|
|
298
457
|
if (!connected) {
|
|
299
|
-
add(
|
|
458
|
+
add(
|
|
459
|
+
"projects",
|
|
460
|
+
"catalyst contract --path teams",
|
|
461
|
+
"unreadable",
|
|
462
|
+
["not readable until this machine is connected"],
|
|
463
|
+
null,
|
|
464
|
+
null,
|
|
465
|
+
);
|
|
300
466
|
} else {
|
|
301
467
|
// The mapping write updates the cloud before the cached contract's dispatchGate projection.
|
|
302
468
|
// Revalidate this one read so a successful save does not appear unmapped on the next step.
|
|
303
|
-
const teams = readContract([
|
|
469
|
+
const teams = readContract([
|
|
470
|
+
"contract",
|
|
471
|
+
"--refresh",
|
|
472
|
+
"--path",
|
|
473
|
+
"teams",
|
|
474
|
+
"--json",
|
|
475
|
+
]);
|
|
304
476
|
const doc = tryJson(teams.stdout);
|
|
305
477
|
const rows = Array.isArray(doc) ? doc : null;
|
|
306
478
|
teamRows = rows;
|
|
307
479
|
if (rows === null) {
|
|
308
|
-
add(
|
|
480
|
+
add(
|
|
481
|
+
"projects",
|
|
482
|
+
"catalyst contract --path teams",
|
|
483
|
+
"unreadable",
|
|
484
|
+
["the project list could not be read — try: catalyst contract --refresh"],
|
|
485
|
+
null,
|
|
486
|
+
null,
|
|
487
|
+
).next =
|
|
309
488
|
"refresh the contract (catalyst contract --refresh), then run this again; the project list could not be read, so no step after it can be named yet";
|
|
310
489
|
} else {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
for (const c of projectChecks)
|
|
319
|
-
|
|
490
|
+
const inventory = runCli(["project", "list", "--json"]);
|
|
491
|
+
const projects = tryJson(inventory.stdout);
|
|
492
|
+
const lines = Array.isArray(projects)
|
|
493
|
+
? projectCoverageLines(projects, rows).lines
|
|
494
|
+
: [
|
|
495
|
+
`the full project inventory could not be read (${(inventory.stderr || inventory.stdout).trim().split("\n")[0] ?? "no output"})`,
|
|
496
|
+
];
|
|
497
|
+
for (const c of projectChecks)
|
|
498
|
+
lines.push(
|
|
499
|
+
`${c.ok ? "note" : "FAIL"} ${c.line}${c.who ? ` — who: ${c.who}` : ""}`,
|
|
500
|
+
);
|
|
320
501
|
// A project is set up when its dispatch gate is open (its stages are mapped), or when its
|
|
321
502
|
// readiness reads ready. Readiness alone kept `--next` on "map its stages" for a tenant whose
|
|
322
503
|
// gates were open: readiness stays "unchecked" until someone presses Re-check, and a new team
|
|
@@ -324,34 +505,77 @@ if (!connected) {
|
|
|
324
505
|
// ⛔ A team whose readiness reads `blocked` is NOT set up, whatever its gate says: a blocking
|
|
325
506
|
// check (a Linear automation that moves cards off Catalyst's stages, a missing OAuth scope) breaks
|
|
326
507
|
// every run in it. The step is that check's own fix, by its own owner, then a re-check.
|
|
327
|
-
const isBlocking = (c) =>
|
|
508
|
+
const isBlocking = (c) =>
|
|
509
|
+
readinessMeta().length === 0 ||
|
|
510
|
+
readinessMeta().find((r) => r.id === c.id)?.severity === "blocking";
|
|
328
511
|
for (const t of rows.filter((r) => r.readiness?.status === "blocked")) {
|
|
329
512
|
const key = t.key ?? t.id ?? "(unkeyed)";
|
|
330
|
-
const fails = (
|
|
513
|
+
const fails = (
|
|
514
|
+
Array.isArray(t.readiness?.checks) ? t.readiness.checks : []
|
|
515
|
+
).filter((c) => c.state === "fail" && isBlocking(c));
|
|
331
516
|
const ids = fails.map((c) => c.id);
|
|
332
|
-
const rules = ids
|
|
517
|
+
const rules = ids
|
|
518
|
+
.filter((id) => id in LINEAR_AUTOMATION_RULES)
|
|
519
|
+
.map((id) => LINEAR_AUTOMATION_RULES[id]);
|
|
333
520
|
const others = ids.filter((id) => !(id in LINEAR_AUTOMATION_RULES));
|
|
334
|
-
const ownerRow = fails
|
|
335
|
-
|
|
521
|
+
const ownerRow = fails
|
|
522
|
+
.map((c) => readinessMeta().find((r) => r.id === c.id))
|
|
523
|
+
.find((r) => r && typeof r.fixedByLine === "string");
|
|
524
|
+
const owner = ownerRow
|
|
525
|
+
? ownerRow.fixedByLine
|
|
526
|
+
: "a workspace owner or admin";
|
|
336
527
|
const how = [
|
|
337
|
-
rules.length > 0
|
|
528
|
+
rules.length > 0
|
|
529
|
+
? `in Linear, open Settings → Teams → ${key} → Workflow → Workflows & automations → Pull request and commit automations and set ${rules.join(", ")} to No action (Catalyst does not yet offer to change these rules)`
|
|
530
|
+
: null,
|
|
338
531
|
others.length > 0 ? `${others.join(", ")}: ${owner}` : null,
|
|
339
|
-
]
|
|
340
|
-
|
|
341
|
-
|
|
532
|
+
]
|
|
533
|
+
.filter(Boolean)
|
|
534
|
+
.join("; ");
|
|
535
|
+
lines.push(
|
|
536
|
+
`${key}: BLOCKED — ${ids.length > 0 ? ids.join(", ") : "a blocking check failed"}; ${how || `owner: ${owner}`}`,
|
|
537
|
+
);
|
|
538
|
+
blockedProjects.push({
|
|
539
|
+
key,
|
|
540
|
+
action: `fix ${key}'s blocking check${ids.length === 1 ? "" : "s"} (${ids.join(", ") || "see the projects lines"}): ${how || owner}; then run catalyst team check ${key} (or press Re-check) and run this again`,
|
|
541
|
+
owner,
|
|
542
|
+
});
|
|
342
543
|
}
|
|
343
|
-
const setUp = (t) =>
|
|
544
|
+
const setUp = (t) =>
|
|
545
|
+
t.readiness?.status !== "blocked" &&
|
|
546
|
+
(t.dispatchGate?.status === "open" ||
|
|
547
|
+
(t.readiness?.status ?? "unchecked") === "ready");
|
|
344
548
|
for (const t of rows) {
|
|
345
|
-
if (
|
|
346
|
-
|
|
549
|
+
if (
|
|
550
|
+
t.dispatchGate?.status === "open" &&
|
|
551
|
+
(t.readiness?.status ?? "unchecked") === "unchecked"
|
|
552
|
+
) {
|
|
553
|
+
lines.push(
|
|
554
|
+
`${t.key ?? t.id ?? "(unkeyed)"}: stages mapped; readiness not checked yet: run catalyst team check ${t.key ?? t.id} to see the rest`,
|
|
555
|
+
);
|
|
347
556
|
}
|
|
348
557
|
}
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
558
|
+
const mappedRows = rows.filter(
|
|
559
|
+
(team) => team.stages && Object.keys(team.stages).length > 0,
|
|
560
|
+
);
|
|
561
|
+
const ready = mappedRows.length > 0 && mappedRows.every(setUp);
|
|
562
|
+
const mapOwner = verbAvailable("team map")
|
|
563
|
+
? "you, with your own login"
|
|
564
|
+
: "a workspace owner or admin";
|
|
565
|
+
const mapWhere = verbAvailable("team map")
|
|
566
|
+
? "catalyst team list"
|
|
567
|
+
: link("/settings/linear-teams");
|
|
568
|
+
const projectOwner =
|
|
569
|
+
blockedProjects.length > 0 ? blockedProjects[0].owner : mapOwner;
|
|
353
570
|
const projectWhere = blockedProjects.length > 0 ? null : mapWhere;
|
|
354
|
-
add(
|
|
571
|
+
add(
|
|
572
|
+
"projects",
|
|
573
|
+
"catalyst contract --path teams, and the team: checks of ready",
|
|
574
|
+
ready ? "ok" : "unfinished",
|
|
575
|
+
lines,
|
|
576
|
+
projectOwner,
|
|
577
|
+
projectWhere,
|
|
578
|
+
);
|
|
355
579
|
}
|
|
356
580
|
}
|
|
357
581
|
|
|
@@ -361,27 +585,75 @@ if (!connected) {
|
|
|
361
585
|
// reach it, so this note never claims that, and it never changes a verdict.
|
|
362
586
|
function thoughtsNote(owner) {
|
|
363
587
|
const repo = `${owner}/thoughts`;
|
|
364
|
-
const res = spawnSync("gh", ["repo", "view", repo, "--json", "name"], {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
588
|
+
const res = spawnSync("gh", ["repo", "view", repo, "--json", "name"], {
|
|
589
|
+
encoding: "utf8",
|
|
590
|
+
timeout: 15_000,
|
|
591
|
+
env: { ...process.env, GH_PROMPT_DISABLED: "1" },
|
|
592
|
+
});
|
|
593
|
+
if (res.error)
|
|
594
|
+
return `note ${repo}: not checked (the gh command could not run here). Confirm it exists on GitHub (step 5a).`;
|
|
595
|
+
if (res.status === 0)
|
|
596
|
+
return `note ${repo}: exists; App access not verifiable from here. Confirm the GitHub App installation includes it (step 5a).`;
|
|
597
|
+
const why =
|
|
598
|
+
(res.stderr || res.stdout).trim().split("\n")[0] ||
|
|
599
|
+
`gh exited ${res.status}`;
|
|
368
600
|
return `note ${repo}: gh could not see it (${why}). It does not exist, or this GitHub login cannot see it. Create or confirm it (step 5a).`;
|
|
369
601
|
}
|
|
370
602
|
if (!connected) {
|
|
371
|
-
add(
|
|
603
|
+
add(
|
|
604
|
+
"repositories",
|
|
605
|
+
"catalyst contract --path merge.repositories",
|
|
606
|
+
"unreadable",
|
|
607
|
+
["not readable until this machine is connected"],
|
|
608
|
+
null,
|
|
609
|
+
null,
|
|
610
|
+
);
|
|
372
611
|
} else {
|
|
373
|
-
const repos = readContract([
|
|
612
|
+
const repos = readContract([
|
|
613
|
+
"contract",
|
|
614
|
+
"--path",
|
|
615
|
+
"merge.repositories",
|
|
616
|
+
"--json",
|
|
617
|
+
]);
|
|
374
618
|
const doc = tryJson(repos.stdout);
|
|
375
619
|
const rows = Array.isArray(doc) ? doc : null;
|
|
376
620
|
if (rows === null) {
|
|
377
|
-
add(
|
|
621
|
+
add(
|
|
622
|
+
"repositories",
|
|
623
|
+
"catalyst contract --path merge.repositories",
|
|
624
|
+
"unreadable",
|
|
625
|
+
[
|
|
626
|
+
"the repository list could not be read — try: catalyst contract --refresh",
|
|
627
|
+
],
|
|
628
|
+
null,
|
|
629
|
+
null,
|
|
630
|
+
).next =
|
|
378
631
|
"refresh the contract (catalyst contract --refresh), then run this again; the repository list could not be read, so no step after it can be named yet";
|
|
379
632
|
} else {
|
|
380
|
-
const lines = [
|
|
633
|
+
const lines = [
|
|
634
|
+
`${rows.length} registered`,
|
|
635
|
+
...rows.map((r) => `${r.owner ?? "?"}/${r.name ?? "?"}`),
|
|
636
|
+
];
|
|
381
637
|
repositoryRegistered = rows.length > 0;
|
|
382
|
-
lines.push(
|
|
383
|
-
|
|
384
|
-
|
|
638
|
+
lines.push(
|
|
639
|
+
"⛔ REGISTRATION only. This carries no status and no project attachment, so it never proves a repository can be dispatched into.",
|
|
640
|
+
);
|
|
641
|
+
for (const owner of [
|
|
642
|
+
...new Set(
|
|
643
|
+
rows
|
|
644
|
+
.map((r) => r.owner)
|
|
645
|
+
.filter((o) => typeof o === "string" && o !== ""),
|
|
646
|
+
),
|
|
647
|
+
])
|
|
648
|
+
lines.push(thoughtsNote(owner));
|
|
649
|
+
add(
|
|
650
|
+
"repositories",
|
|
651
|
+
"catalyst contract --path merge.repositories",
|
|
652
|
+
rows.length > 0 ? "ok" : "unfinished",
|
|
653
|
+
lines,
|
|
654
|
+
"a workspace owner or admin",
|
|
655
|
+
link("/settings/projects"),
|
|
656
|
+
);
|
|
385
657
|
}
|
|
386
658
|
}
|
|
387
659
|
|
|
@@ -404,19 +676,30 @@ const CREDENTIAL_ERROR_CODES = new Set(["no_access_token", "no_credential"]);
|
|
|
404
676
|
const CREDENTIAL_FAILURE_STREAK = 3;
|
|
405
677
|
// A cancelled subscription or an ended account is not a credential problem: no token brings it
|
|
406
678
|
// back. It is kept for reporting, never re-credentialed, and it does not make setup unfinished.
|
|
407
|
-
const isRetirable = (a) =>
|
|
679
|
+
const isRetirable = (a) =>
|
|
680
|
+
a?.renewalStatus === "canceled" ||
|
|
681
|
+
a?.status === "ended" ||
|
|
682
|
+
a?.declaredState === "ended";
|
|
408
683
|
const credentialProblem = (a) => {
|
|
409
684
|
if (isRetirable(a)) return null;
|
|
410
685
|
if (typeof a?.needsCredential === "boolean") {
|
|
411
686
|
if (!a.needsCredential) return null;
|
|
412
|
-
if (a.quarantined === true)
|
|
687
|
+
if (a.quarantined === true)
|
|
688
|
+
return `quarantined${typeof a.quarantineReason === "string" && a.quarantineReason !== "" ? `: ${a.quarantineReason}` : ""}`;
|
|
413
689
|
if (a.status === "expired-or-revoked") return "expired or revoked";
|
|
414
|
-
return typeof a.lastPollErrorCode === "string"
|
|
690
|
+
return typeof a.lastPollErrorCode === "string"
|
|
691
|
+
? `its last ${a.pollFailureCount} polls failed with ${a.lastPollErrorCode}`
|
|
692
|
+
: "the cloud says it needs a new credential";
|
|
415
693
|
}
|
|
416
694
|
if (a?.status === "ended" || (a?.revokedAtMs ?? null) !== null) return null;
|
|
417
|
-
if (a?.quarantined === true)
|
|
695
|
+
if (a?.quarantined === true)
|
|
696
|
+
return `quarantined${typeof a.quarantineReason === "string" && a.quarantineReason !== "" ? `: ${a.quarantineReason}` : ""}`;
|
|
418
697
|
if (a?.status === "expired-or-revoked") return "expired or revoked";
|
|
419
|
-
if (
|
|
698
|
+
if (
|
|
699
|
+
CREDENTIAL_ERROR_CODES.has(a?.lastPollErrorCode) &&
|
|
700
|
+
typeof a?.pollFailureCount === "number" &&
|
|
701
|
+
a.pollFailureCount >= CREDENTIAL_FAILURE_STREAK
|
|
702
|
+
) {
|
|
420
703
|
return `its last ${a.pollFailureCount} polls failed with ${a.lastPollErrorCode}`;
|
|
421
704
|
}
|
|
422
705
|
return null;
|
|
@@ -424,11 +707,28 @@ const credentialProblem = (a) => {
|
|
|
424
707
|
const readAccountRows = () => {
|
|
425
708
|
const acc = runCli(["accounts", "--json"]);
|
|
426
709
|
const doc = tryJson(acc.stdout);
|
|
427
|
-
const rows = Array.isArray(doc)
|
|
428
|
-
|
|
710
|
+
const rows = Array.isArray(doc)
|
|
711
|
+
? doc
|
|
712
|
+
: Array.isArray(doc?.accounts)
|
|
713
|
+
? doc.accounts
|
|
714
|
+
: null;
|
|
715
|
+
return {
|
|
716
|
+
rows,
|
|
717
|
+
error:
|
|
718
|
+
rows === null
|
|
719
|
+
? (acc.stderr || acc.stdout).trim().split("\n")[0] || "no output"
|
|
720
|
+
: null,
|
|
721
|
+
};
|
|
429
722
|
};
|
|
430
723
|
if (!connected) {
|
|
431
|
-
add(
|
|
724
|
+
add(
|
|
725
|
+
"coding accounts",
|
|
726
|
+
"codingAccounts in catalyst contract",
|
|
727
|
+
"unreadable",
|
|
728
|
+
["not readable until this machine is connected"],
|
|
729
|
+
null,
|
|
730
|
+
null,
|
|
731
|
+
);
|
|
432
732
|
} else {
|
|
433
733
|
const res = readContract(["contract", "--path", "codingAccounts", "--json"]);
|
|
434
734
|
const ca = tryJson(res.stdout);
|
|
@@ -436,76 +736,203 @@ if (!connected) {
|
|
|
436
736
|
const INSTRUMENT = "codingAccounts in catalyst contract";
|
|
437
737
|
if (ca !== null && typeof ca === "object" && typeof ca.state === "string") {
|
|
438
738
|
const line = typeof ca.line === "string" ? ca.line : `state ${ca.state}`;
|
|
439
|
-
const owner =
|
|
739
|
+
const owner =
|
|
740
|
+
typeof ca.enrolledByLine === "string" ? ca.enrolledByLine : null;
|
|
440
741
|
const where = typeof ca.page === "string" ? link(ca.page) : null;
|
|
441
742
|
if (ca.state === "enrolled" || ca.state === "needs_credential") {
|
|
442
743
|
const DETAIL = `${INSTRUMENT}, and catalyst accounts`;
|
|
443
744
|
const head = [line, `${ca.activeCount ?? "?"} active`];
|
|
444
745
|
const { rows, error } = readAccountRows();
|
|
445
|
-
const dead =
|
|
746
|
+
const dead =
|
|
747
|
+
rows === null ? [] : rows.filter((a) => credentialProblem(a) !== null);
|
|
446
748
|
const retirable = rows === null ? [] : rows.filter(isRetirable);
|
|
447
|
-
const who = (a) =>
|
|
448
|
-
|
|
749
|
+
const who = (a) =>
|
|
750
|
+
[a.label, a.email].find((v) => typeof v === "string" && v !== "");
|
|
751
|
+
const retireLine =
|
|
752
|
+
retirable.length === 0
|
|
753
|
+
? []
|
|
754
|
+
: [
|
|
755
|
+
`${retirable.length} account(s) cancelled or ended (${retirable.map((a) => `${who(a) ?? a.displayName ?? a.accountSlot ?? "?"} (${a.provider ?? "unknown provider"})`).join(", ")}): kept for reporting, not used, and not counted here. Never replace their credential; reactivate one on the AI accounts page only if its subscription is live again.`,
|
|
756
|
+
];
|
|
449
757
|
retireNote = retireLine[0] ?? null;
|
|
450
758
|
if (rows === null) {
|
|
451
|
-
accountsNext =
|
|
452
|
-
|
|
759
|
+
accountsNext =
|
|
760
|
+
"read the coding accounts again; each account's credential could not be checked";
|
|
761
|
+
add(
|
|
762
|
+
"coding accounts",
|
|
763
|
+
DETAIL,
|
|
764
|
+
"unreadable",
|
|
765
|
+
[
|
|
766
|
+
...head,
|
|
767
|
+
`coding accounts could not be checked in detail (${error}). Whether each one still has a working credential is unknown. Do not enroll one on this reading.`,
|
|
768
|
+
],
|
|
769
|
+
null,
|
|
770
|
+
null,
|
|
771
|
+
);
|
|
453
772
|
} else if (dead.length > 0) {
|
|
454
|
-
const name = (a) =>
|
|
773
|
+
const name = (a) =>
|
|
774
|
+
`${a.provider ?? "unknown provider"} account ${a.accountSlot ?? "?"}${who(a) ? ` (${who(a)})` : ""}`;
|
|
455
775
|
const more = dead.length > 1 ? ` (and ${dead.length - 1} more)` : "";
|
|
456
776
|
accountsNext = `${name(dead[0])} needs a new credential${more}. Replace it on the AI accounts page (Settings → AI accounts → the account → Replace credential). Do not enroll another account.`;
|
|
457
777
|
add(
|
|
458
778
|
"coding accounts",
|
|
459
779
|
DETAIL,
|
|
460
780
|
"unfinished",
|
|
461
|
-
[
|
|
781
|
+
[
|
|
782
|
+
...head,
|
|
783
|
+
...dead.map(
|
|
784
|
+
(a) =>
|
|
785
|
+
`${name(a)} needs a new credential: ${credentialProblem(a)}`,
|
|
786
|
+
),
|
|
787
|
+
"Replace its credential on its own page, with a credential minted from THAT account: check the login you mint from matches the account named here first. Do not enroll another account. The steps are in references/replacing-a-credential.md.",
|
|
788
|
+
...retireLine,
|
|
789
|
+
],
|
|
462
790
|
owner,
|
|
463
791
|
where,
|
|
464
792
|
);
|
|
465
793
|
} else if (ca.state === "needs_credential") {
|
|
466
|
-
accountsNext =
|
|
467
|
-
|
|
794
|
+
accountsNext =
|
|
795
|
+
"replace the credential of the coding account the page marks; do not enroll another account";
|
|
796
|
+
add(
|
|
797
|
+
"coding accounts",
|
|
798
|
+
DETAIL,
|
|
799
|
+
"unfinished",
|
|
800
|
+
[
|
|
801
|
+
...head,
|
|
802
|
+
"The contract says an account needs a new credential, and the account list does not say which. Open the page and look for it.",
|
|
803
|
+
],
|
|
804
|
+
owner,
|
|
805
|
+
where,
|
|
806
|
+
);
|
|
468
807
|
} else {
|
|
469
|
-
add("coding accounts", DETAIL, "ok", [
|
|
808
|
+
add("coding accounts", DETAIL, "ok", [
|
|
809
|
+
...head,
|
|
810
|
+
`${rows.length} checked, none needs a new credential`,
|
|
811
|
+
...retireLine,
|
|
812
|
+
]);
|
|
470
813
|
}
|
|
471
814
|
} else if (ca.state === "inactive") {
|
|
472
|
-
accountsNext =
|
|
473
|
-
|
|
815
|
+
accountsNext =
|
|
816
|
+
"reactivate a coding account that is out of rotation; do not enroll another one";
|
|
817
|
+
add(
|
|
818
|
+
"coding accounts",
|
|
819
|
+
INSTRUMENT,
|
|
820
|
+
"unfinished",
|
|
821
|
+
[
|
|
822
|
+
line,
|
|
823
|
+
"Every account is out of rotation. Reactivate one. Do not enroll another account.",
|
|
824
|
+
],
|
|
825
|
+
owner,
|
|
826
|
+
where,
|
|
827
|
+
);
|
|
474
828
|
} else if (ca.state === "none_enrolled") {
|
|
475
|
-
add(
|
|
829
|
+
add(
|
|
830
|
+
"coding accounts",
|
|
831
|
+
INSTRUMENT,
|
|
832
|
+
"unfinished",
|
|
833
|
+
[line, "no phase can start until one is enrolled"],
|
|
834
|
+
owner,
|
|
835
|
+
where,
|
|
836
|
+
);
|
|
476
837
|
} else {
|
|
477
838
|
// `unread`, or a state this bundle does not know: could not look is not the same as none there.
|
|
478
|
-
accountsNext =
|
|
479
|
-
|
|
839
|
+
accountsNext =
|
|
840
|
+
"read the coding accounts again; the cloud could not read them this time";
|
|
841
|
+
add(
|
|
842
|
+
"coding accounts",
|
|
843
|
+
INSTRUMENT,
|
|
844
|
+
"unreadable",
|
|
845
|
+
[
|
|
846
|
+
line,
|
|
847
|
+
"This is not a missing account. Do not enroll one on this reading. Run this again later.",
|
|
848
|
+
],
|
|
849
|
+
null,
|
|
850
|
+
null,
|
|
851
|
+
);
|
|
480
852
|
}
|
|
481
853
|
} else if (older) {
|
|
482
854
|
const { rows, error } = readAccountRows();
|
|
483
|
-
const olderLine =
|
|
855
|
+
const olderLine =
|
|
856
|
+
"this cloud is older than the bundle: its contract does not say whether a coding account is enrolled, so the account list is read instead";
|
|
484
857
|
if (rows === null) {
|
|
485
|
-
accountsNext =
|
|
486
|
-
|
|
858
|
+
accountsNext =
|
|
859
|
+
"read the coding accounts again; the account list could not be read this time";
|
|
860
|
+
add(
|
|
861
|
+
"coding accounts",
|
|
862
|
+
"catalyst accounts",
|
|
863
|
+
"unreadable",
|
|
864
|
+
[olderLine, `coding accounts could not be read (${error})`],
|
|
865
|
+
null,
|
|
866
|
+
null,
|
|
867
|
+
);
|
|
487
868
|
} else {
|
|
488
869
|
// An expired or revoked slot, or a quarantined one, cannot take work until an admin acts on it.
|
|
489
|
-
const usable = rows.filter(
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
870
|
+
const usable = rows.filter(
|
|
871
|
+
(a) => a?.status !== "expired-or-revoked" && a?.quarantined !== true,
|
|
872
|
+
);
|
|
873
|
+
const lines = [
|
|
874
|
+
olderLine,
|
|
875
|
+
`${rows.length} enrolled, ${usable.length} able to take work`,
|
|
876
|
+
];
|
|
877
|
+
for (const a of rows)
|
|
878
|
+
lines.push(
|
|
879
|
+
`${a.accountSlot ?? "?"}: ${a.provider ?? "?"}, ${a.status ?? "status unknown"}${a.quarantined ? ", quarantined" : ""}`,
|
|
880
|
+
);
|
|
881
|
+
if (usable.length === 0)
|
|
882
|
+
lines.push(
|
|
883
|
+
"no phase can start until one is enrolled and able to take work",
|
|
884
|
+
);
|
|
885
|
+
add(
|
|
886
|
+
"coding accounts",
|
|
887
|
+
"catalyst accounts",
|
|
888
|
+
usable.length > 0 ? "ok" : "unfinished",
|
|
889
|
+
lines,
|
|
890
|
+
"a workspace owner or admin",
|
|
891
|
+
link("/settings/coding-accounts"),
|
|
892
|
+
);
|
|
494
893
|
}
|
|
495
894
|
} else {
|
|
496
|
-
accountsNext =
|
|
497
|
-
|
|
895
|
+
accountsNext =
|
|
896
|
+
"read the coding accounts again; the contract could not be read";
|
|
897
|
+
add(
|
|
898
|
+
"coding accounts",
|
|
899
|
+
INSTRUMENT,
|
|
900
|
+
"unreadable",
|
|
901
|
+
[
|
|
902
|
+
`the contract could not be read (${(res.stderr || res.stdout).trim().split("\n").pop() || "no output"}); try: catalyst contract --refresh`,
|
|
903
|
+
],
|
|
904
|
+
null,
|
|
905
|
+
null,
|
|
906
|
+
);
|
|
498
907
|
}
|
|
499
908
|
}
|
|
500
909
|
|
|
501
910
|
// the Re-check step, as a command when this person can run it, else as the page with the
|
|
502
911
|
// person who can. `team check` is the CLI twin of the page's Re-check button (admin or owner).
|
|
503
|
-
const mappedKeys = () =>
|
|
912
|
+
const mappedKeys = () =>
|
|
913
|
+
(teamRows ?? [])
|
|
914
|
+
.map((t) => t.key ?? t.id)
|
|
915
|
+
.filter((k) => typeof k === "string" && k !== "");
|
|
504
916
|
const recheckStep = () => {
|
|
505
917
|
const cmds = mappedKeys().map((k) => `catalyst team check ${k}`);
|
|
506
|
-
if (cmds.length > 0 && verbAvailable("team check"))
|
|
507
|
-
|
|
508
|
-
|
|
918
|
+
if (cmds.length > 0 && verbAvailable("team check"))
|
|
919
|
+
return {
|
|
920
|
+
action: `run ${cmds.join(" and ")}, then run this again`,
|
|
921
|
+
owner:
|
|
922
|
+
"you, the assistant: run it now with the person's login, without asking",
|
|
923
|
+
where: cmds[0],
|
|
924
|
+
};
|
|
925
|
+
if (cmds.length > 0 && verbAdminOnly("team check"))
|
|
926
|
+
return {
|
|
927
|
+
action: `a workspace owner or admin runs ${cmds.join(" and ")} (or presses Re-check on the projects page); then run this again`,
|
|
928
|
+
owner: "a workspace owner or admin",
|
|
929
|
+
where: link("/settings/linear-teams"),
|
|
930
|
+
};
|
|
931
|
+
return {
|
|
932
|
+
action: "press Re-check on the projects page, then run this again",
|
|
933
|
+
owner: "a workspace owner or admin",
|
|
934
|
+
where: link("/settings/linear-teams"),
|
|
935
|
+
};
|
|
509
936
|
};
|
|
510
937
|
|
|
511
938
|
// ── host ──────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -513,34 +940,83 @@ const recheckStep = () => {
|
|
|
513
940
|
// contract says who owns it. The check is account-wide, so any one project's reading is the answer.
|
|
514
941
|
// A tenant that runs no host of its own reads `pass` here, and then nothing is asked of anyone.
|
|
515
942
|
if (!connected) {
|
|
516
|
-
add(
|
|
943
|
+
add(
|
|
944
|
+
"host",
|
|
945
|
+
"hosts_current in catalyst contract --path teams",
|
|
946
|
+
"unreadable",
|
|
947
|
+
["not readable until this machine is connected"],
|
|
948
|
+
null,
|
|
949
|
+
null,
|
|
950
|
+
);
|
|
517
951
|
} else {
|
|
518
|
-
const checks = (teamRows ?? []).flatMap((t) =>
|
|
952
|
+
const checks = (teamRows ?? []).flatMap((t) =>
|
|
953
|
+
(Array.isArray(t.readiness?.checks) ? t.readiness.checks : [])
|
|
954
|
+
.filter((c) => c.id === "hosts_current")
|
|
955
|
+
.map((c) => ({ ...c, team: t.key ?? t.id ?? "(unkeyed)" })),
|
|
956
|
+
);
|
|
519
957
|
const row = readinessMeta().find((r) => r.id === "hosts_current") ?? null;
|
|
520
|
-
const hc =
|
|
958
|
+
const hc =
|
|
959
|
+
checks.find((c) => c.state === "fail") ??
|
|
960
|
+
checks.find((c) => c.state === "unknown") ??
|
|
961
|
+
checks[0] ??
|
|
962
|
+
null;
|
|
521
963
|
// The contract's printed line for this owner is written for a host that is behind. When no host is
|
|
522
964
|
// connected there is nothing behind, so the owner is named by its id instead of by that sentence.
|
|
523
|
-
const owner =
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
965
|
+
const owner =
|
|
966
|
+
row?.fixedBy === undefined
|
|
967
|
+
? "the contract names no owner for hosts_current"
|
|
968
|
+
: hc?.state === "fail" && typeof row.fixedByLine === "string"
|
|
969
|
+
? row.fixedByLine
|
|
970
|
+
: `the host operator (the contract's owner for hosts_current: ${row.fixedBy})`;
|
|
528
971
|
// Where the owner acts comes from the contract's `fixedWhere`. Null (every fixer today, and absent on
|
|
529
972
|
// an older cloud) means no page exists, so the owner sentence is printed alone and no page is made up.
|
|
530
|
-
const fw =
|
|
973
|
+
const fw =
|
|
974
|
+
row?.fixedWhere && typeof row.fixedWhere.page === "string"
|
|
975
|
+
? row.fixedWhere
|
|
976
|
+
: null;
|
|
531
977
|
const how = fw === null ? null : link(fw.page);
|
|
532
978
|
const howLines = fw?.command ? [`the owner runs: ${fw.command}`] : [];
|
|
533
979
|
if (hc === null) {
|
|
534
980
|
const step = recheckStep();
|
|
535
|
-
add(
|
|
981
|
+
add(
|
|
982
|
+
"host",
|
|
983
|
+
"hosts_current in catalyst contract --path teams",
|
|
984
|
+
"unreadable",
|
|
985
|
+
[
|
|
986
|
+
teamRows === null
|
|
987
|
+
? "the project list could not be read, so the host check cannot be either"
|
|
988
|
+
: "no project has a readiness check yet, so whether a host is connected cannot be read.",
|
|
989
|
+
],
|
|
990
|
+
teamRows === null ? "a workspace owner or admin" : step.owner,
|
|
991
|
+
teamRows === null ? link("/settings/linear-teams") : step.where,
|
|
992
|
+
).next =
|
|
536
993
|
teamRows === null
|
|
537
994
|
? "refresh the contract (catalyst contract --refresh), then run this again; the project list could not be read, so the host check cannot be either"
|
|
538
995
|
: step.action;
|
|
539
996
|
} else if (hc.state === "pass") {
|
|
540
|
-
add("host", "hosts_current in catalyst contract --path teams", "ok", [
|
|
997
|
+
add("host", "hosts_current in catalyst contract --path teams", "ok", [
|
|
998
|
+
`hosts_current pass (read on ${hc.team})`,
|
|
999
|
+
]);
|
|
541
1000
|
} else {
|
|
542
|
-
const detail =
|
|
543
|
-
|
|
1001
|
+
const detail =
|
|
1002
|
+
hc.reason === "no_host_connected"
|
|
1003
|
+
? "no Catalyst host is connected"
|
|
1004
|
+
: hc.reason === "hosts_behind"
|
|
1005
|
+
? "a connected host runs an older mapping"
|
|
1006
|
+
: hc.reason === "hosts_unreported"
|
|
1007
|
+
? "a host is connected but has not reported what it loaded"
|
|
1008
|
+
: `state ${hc.state}`;
|
|
1009
|
+
add(
|
|
1010
|
+
"host",
|
|
1011
|
+
"hosts_current in catalyst contract --path teams",
|
|
1012
|
+
"unfinished",
|
|
1013
|
+
[
|
|
1014
|
+
`hosts_current ${hc.state}${hc.reason ? ` (${hc.reason})` : ""} on ${hc.team}: ${detail}`,
|
|
1015
|
+
...howLines,
|
|
1016
|
+
],
|
|
1017
|
+
owner,
|
|
1018
|
+
how,
|
|
1019
|
+
);
|
|
544
1020
|
}
|
|
545
1021
|
}
|
|
546
1022
|
|
|
@@ -549,37 +1025,107 @@ if (!connected) {
|
|
|
549
1025
|
// default repository has one in effect is the contract's `environment_declared` check; the project's
|
|
550
1026
|
// other repositories appear under that check's `repos`. Names only: nothing here reads a value, and
|
|
551
1027
|
// nothing here reads a file on this machine. A cloud that sends no such check has nothing to read.
|
|
552
|
-
const DECL_INSTRUMENT =
|
|
1028
|
+
const DECL_INSTRUMENT =
|
|
1029
|
+
"environment_declared in catalyst contract --path teams";
|
|
553
1030
|
const DECL_REASONS = {
|
|
554
|
-
no_team_repo_default: {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1031
|
+
no_team_repo_default: {
|
|
1032
|
+
text: "no repository is the project's default yet: register one and make it the default",
|
|
1033
|
+
who: "a workspace owner or admin",
|
|
1034
|
+
page: "/settings/projects",
|
|
1035
|
+
},
|
|
1036
|
+
no_environment_declaration: {
|
|
1037
|
+
text: "no .catalyst/catalyst.toml on its default branch yet: write it with the person (names only, never a value), open a pull request, merge it",
|
|
1038
|
+
who: "the person, in the repository, with your help",
|
|
1039
|
+
page: null,
|
|
1040
|
+
},
|
|
1041
|
+
declaration_invalid: {
|
|
1042
|
+
text: "the committed .catalyst/catalyst.toml did not validate: fix the file (the ingest names the error) and merge the fix",
|
|
1043
|
+
who: "the person, in the repository, with your help",
|
|
1044
|
+
page: null,
|
|
1045
|
+
},
|
|
1046
|
+
declaration_read_failed: {
|
|
1047
|
+
text: "the committed .catalyst/catalyst.toml could not be read: fix the file (the ingest names the error) and merge the fix",
|
|
1048
|
+
who: "the person, in the repository, with your help",
|
|
1049
|
+
page: null,
|
|
1050
|
+
},
|
|
1051
|
+
declaration_awaiting_approval: {
|
|
1052
|
+
text: "the declaration is proposed and waits for approval: Settings → Your projects → the project → Repositories → the repository → Environment → Setup declaration → Approve this revision",
|
|
1053
|
+
who: "a workspace owner or admin",
|
|
1054
|
+
page: "/settings/projects",
|
|
1055
|
+
},
|
|
559
1056
|
};
|
|
560
|
-
const DECL_DO =
|
|
561
|
-
|
|
1057
|
+
const DECL_DO =
|
|
1058
|
+
"write .catalyst/catalyst.toml with references/declaring-a-repository.md, then open a pull request";
|
|
1059
|
+
let declNext =
|
|
1060
|
+
"commit .catalyst/catalyst.toml to the repository and have an owner or admin approve it";
|
|
562
1061
|
if (!connected) {
|
|
563
|
-
add(
|
|
1062
|
+
add(
|
|
1063
|
+
"repository declarations",
|
|
1064
|
+
DECL_INSTRUMENT,
|
|
1065
|
+
"unreadable",
|
|
1066
|
+
["not readable until this machine is connected"],
|
|
1067
|
+
null,
|
|
1068
|
+
null,
|
|
1069
|
+
);
|
|
564
1070
|
} else if (teamRows === null) {
|
|
565
|
-
add(
|
|
1071
|
+
add(
|
|
1072
|
+
"repository declarations",
|
|
1073
|
+
DECL_INSTRUMENT,
|
|
1074
|
+
"unreadable",
|
|
1075
|
+
[
|
|
1076
|
+
"the project list could not be read, so no repository declaration can be either",
|
|
1077
|
+
],
|
|
1078
|
+
null,
|
|
1079
|
+
null,
|
|
1080
|
+
).next =
|
|
566
1081
|
"refresh the contract (catalyst contract --refresh), then run this again; the project list could not be read, so no repository declaration can be either";
|
|
567
1082
|
} else if (teamRows.length === 0) {
|
|
568
|
-
add(
|
|
569
|
-
"
|
|
570
|
-
|
|
1083
|
+
add(
|
|
1084
|
+
"repository declarations",
|
|
1085
|
+
DECL_INSTRUMENT,
|
|
1086
|
+
"unreadable",
|
|
1087
|
+
[
|
|
1088
|
+
"no project is mapped yet, so there is no repository whose declaration could be read",
|
|
1089
|
+
],
|
|
1090
|
+
null,
|
|
1091
|
+
null,
|
|
1092
|
+
).next = "map a project first; a repository declaration is read per project";
|
|
1093
|
+
} else if (
|
|
1094
|
+
teamRows.every(
|
|
1095
|
+
(t) =>
|
|
1096
|
+
!Array.isArray(t.readiness?.checks) || t.readiness.checks.length === 0,
|
|
1097
|
+
)
|
|
1098
|
+
) {
|
|
571
1099
|
// Mapped, never checked: the contract carries no checks at all until someone presses Re-check, and
|
|
572
1100
|
// that silence is not "nothing to declare".
|
|
573
1101
|
const step = recheckStep();
|
|
574
|
-
add(
|
|
1102
|
+
add(
|
|
1103
|
+
"repository declarations",
|
|
1104
|
+
DECL_INSTRUMENT,
|
|
1105
|
+
"unreadable",
|
|
1106
|
+
[
|
|
1107
|
+
"no project has a readiness check yet, so whether its repository is declared cannot be read.",
|
|
1108
|
+
],
|
|
1109
|
+
step.owner,
|
|
1110
|
+
step.where,
|
|
1111
|
+
).next = step.action;
|
|
575
1112
|
} else {
|
|
576
|
-
const found = teamRows.flatMap((t) =>
|
|
1113
|
+
const found = teamRows.flatMap((t) =>
|
|
1114
|
+
(Array.isArray(t.readiness?.checks) ? t.readiness.checks : [])
|
|
1115
|
+
.filter((c) => c.id === "environment_declared")
|
|
1116
|
+
.map((c) => ({ ...c, team: t.key ?? t.id ?? "(unkeyed)" })),
|
|
1117
|
+
);
|
|
577
1118
|
const lines = [];
|
|
578
1119
|
let verdict = "ok";
|
|
579
1120
|
let owner = null;
|
|
580
1121
|
let where = null;
|
|
581
1122
|
let unknown = false;
|
|
582
|
-
const describe = (reason) =>
|
|
1123
|
+
const describe = (reason) =>
|
|
1124
|
+
DECL_REASONS[reason] ?? {
|
|
1125
|
+
text: `environment_declared ${reason ?? "failed"} (a reason this bundle does not know; read it on the page)`,
|
|
1126
|
+
who: "a workspace owner or admin",
|
|
1127
|
+
page: "/settings/projects",
|
|
1128
|
+
};
|
|
583
1129
|
const flag = (team, repo, reason) => {
|
|
584
1130
|
const r = describe(reason);
|
|
585
1131
|
lines.push(`${team}${repo ? `, ${repo}` : ""}: ${r.text}`);
|
|
@@ -589,23 +1135,49 @@ if (!connected) {
|
|
|
589
1135
|
where ??= r.page === null ? DECL_DO : link(r.page);
|
|
590
1136
|
};
|
|
591
1137
|
if (found.length === 0) {
|
|
592
|
-
lines.push(
|
|
1138
|
+
lines.push(
|
|
1139
|
+
"this cloud reports no repository declaration check for the mapped projects, so there is nothing to read here; the repository's Environment page is where one would show",
|
|
1140
|
+
);
|
|
593
1141
|
}
|
|
594
1142
|
for (const c of found) {
|
|
595
|
-
if (c.state === "pass")
|
|
1143
|
+
if (c.state === "pass")
|
|
1144
|
+
lines.push(
|
|
1145
|
+
`${c.team}: a declaration is in effect for the project's default repository`,
|
|
1146
|
+
);
|
|
596
1147
|
else if (c.state === "fail") flag(c.team, null, c.reason);
|
|
597
1148
|
else {
|
|
598
1149
|
unknown = true;
|
|
599
|
-
lines.push(
|
|
1150
|
+
lines.push(
|
|
1151
|
+
`${c.team}: environment_declared could not be read${c.reason ? ` (${c.reason})` : ""}; a re-check is needed`,
|
|
1152
|
+
);
|
|
600
1153
|
}
|
|
601
|
-
for (const note of Array.isArray(c.repos) ? c.repos : [])
|
|
1154
|
+
for (const note of Array.isArray(c.repos) ? c.repos : [])
|
|
1155
|
+
if (typeof note?.repo === "string" && note.reason)
|
|
1156
|
+
flag(c.team, note.repo, note.reason);
|
|
602
1157
|
}
|
|
603
1158
|
if (unknown && verdict === "ok") {
|
|
604
1159
|
const step = recheckStep();
|
|
605
|
-
add(
|
|
1160
|
+
add(
|
|
1161
|
+
"repository declarations",
|
|
1162
|
+
DECL_INSTRUMENT,
|
|
1163
|
+
"unreadable",
|
|
1164
|
+
lines,
|
|
1165
|
+
step.owner,
|
|
1166
|
+
step.where,
|
|
1167
|
+
).next = step.action;
|
|
606
1168
|
} else {
|
|
607
|
-
if (verdict !== "ok")
|
|
608
|
-
|
|
1169
|
+
if (verdict !== "ok")
|
|
1170
|
+
lines.push(
|
|
1171
|
+
"Names only: a value never passes through this script or the file. The person enters values on the repository's Environment page.",
|
|
1172
|
+
);
|
|
1173
|
+
add(
|
|
1174
|
+
"repository declarations",
|
|
1175
|
+
DECL_INSTRUMENT,
|
|
1176
|
+
verdict,
|
|
1177
|
+
lines,
|
|
1178
|
+
verdict === "ok" ? null : owner,
|
|
1179
|
+
verdict === "ok" ? null : where,
|
|
1180
|
+
);
|
|
609
1181
|
}
|
|
610
1182
|
}
|
|
611
1183
|
|
|
@@ -618,15 +1190,43 @@ let repoNext = null;
|
|
|
618
1190
|
if (typeof flags.repo === "string" && flags.repo !== "") {
|
|
619
1191
|
const REPO_INSTRUMENT = `repo agent-setup ${flags.repo} --json`;
|
|
620
1192
|
if (!connected) {
|
|
621
|
-
add(
|
|
1193
|
+
add(
|
|
1194
|
+
"repository agent setup",
|
|
1195
|
+
REPO_INSTRUMENT,
|
|
1196
|
+
"unreadable",
|
|
1197
|
+
["not readable until this machine is connected"],
|
|
1198
|
+
null,
|
|
1199
|
+
null,
|
|
1200
|
+
false,
|
|
1201
|
+
);
|
|
622
1202
|
} else if (!verbAvailable("repo agent-setup")) {
|
|
623
|
-
add(
|
|
1203
|
+
add(
|
|
1204
|
+
"repository agent setup",
|
|
1205
|
+
REPO_INSTRUMENT,
|
|
1206
|
+
"unreadable",
|
|
1207
|
+
[
|
|
1208
|
+
"this CLI has no repo agent-setup verb; update the CLI to read a checkout's agent setup",
|
|
1209
|
+
],
|
|
1210
|
+
null,
|
|
1211
|
+
null,
|
|
1212
|
+
false,
|
|
1213
|
+
).next =
|
|
624
1214
|
"update the CLI (it lacks repo agent-setup); the checkout's agent setup cannot be read until then (does not block anything)";
|
|
625
1215
|
} else {
|
|
626
1216
|
const res = runCli(["repo", "agent-setup", flags.repo, "--json"]);
|
|
627
1217
|
const doc = tryJson(res.stdout);
|
|
628
1218
|
if (doc === null) {
|
|
629
|
-
add(
|
|
1219
|
+
add(
|
|
1220
|
+
"repository agent setup",
|
|
1221
|
+
REPO_INSTRUMENT,
|
|
1222
|
+
"unreadable",
|
|
1223
|
+
[
|
|
1224
|
+
`the checkout could not be read (${(res.stderr || res.stdout).trim().split("\n")[0] || "no output"})`,
|
|
1225
|
+
],
|
|
1226
|
+
null,
|
|
1227
|
+
null,
|
|
1228
|
+
false,
|
|
1229
|
+
).next =
|
|
630
1230
|
`run this again with the checkout's real path; ${flags.repo} could not be read`;
|
|
631
1231
|
} else {
|
|
632
1232
|
const block = doc.agentsMd?.block ?? "unknown";
|
|
@@ -638,12 +1238,32 @@ if (typeof flags.repo === "string" && flags.repo !== "") {
|
|
|
638
1238
|
`layout: ${verdict}${Array.isArray(doc.plan) && doc.plan.length ? ` — ${doc.plan.join("; ")}` : ""}${Array.isArray(doc.blockers) && doc.blockers.length ? ` — by hand: ${doc.blockers.join("; ")}` : ""}`,
|
|
639
1239
|
];
|
|
640
1240
|
const offers = [];
|
|
641
|
-
if (block !== "current")
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1241
|
+
if (block !== "current")
|
|
1242
|
+
offers.push(
|
|
1243
|
+
`the Catalyst block for AGENTS.md (catalyst repo agents-block ${flags.repo} --write)`,
|
|
1244
|
+
);
|
|
1245
|
+
if (verdict === "convertible")
|
|
1246
|
+
offers.push(
|
|
1247
|
+
`the portable layout (catalyst repo agent-setup ${flags.repo} --apply)`,
|
|
1248
|
+
);
|
|
1249
|
+
if (verdict === "needs_hand_merge")
|
|
1250
|
+
offers.push("a hand merge first (the layout line names what)");
|
|
1251
|
+
repoNext = ok
|
|
1252
|
+
? null
|
|
1253
|
+
: `say in one clause what the checkout holds, then offer ${offers.join(" and ")}, each after a yes, on a branch for the pull request the person opens`;
|
|
1254
|
+
add(
|
|
1255
|
+
"repository agent setup",
|
|
1256
|
+
REPO_INSTRUMENT,
|
|
1257
|
+
ok ? "ok" : "unfinished",
|
|
1258
|
+
lines,
|
|
1259
|
+
ok
|
|
1260
|
+
? null
|
|
1261
|
+
: "you, after the person's yes, in their checkout; the pull request is theirs",
|
|
1262
|
+
ok ? null : `catalyst repo agent-setup ${flags.repo}`,
|
|
1263
|
+
false,
|
|
1264
|
+
);
|
|
1265
|
+
if (!ok)
|
|
1266
|
+
repoNote = `repository ${flags.repo}: Catalyst block ${block}, layout ${verdict} — offer ${offers.join(" and ")} after a yes (references/repository-agent-setup.md)`;
|
|
647
1267
|
}
|
|
648
1268
|
}
|
|
649
1269
|
}
|
|
@@ -664,15 +1284,18 @@ const NEXT = {
|
|
|
664
1284
|
: personalGithubIncomplete
|
|
665
1285
|
? "install the tenant GitHub App and register its repository before connecting your personal GitHub account"
|
|
666
1286
|
: "get this person's seat and Linear identity sorted",
|
|
667
|
-
account:
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1287
|
+
account:
|
|
1288
|
+
"connect the Linear integration on the Integrations page (the GitHub App comes later, with the repository)",
|
|
1289
|
+
projects:
|
|
1290
|
+
blockedProjects.length > 0
|
|
1291
|
+
? blockedProjects.map((b) => b.action).join("; and ")
|
|
1292
|
+
: verbAvailable("team map")
|
|
1293
|
+
? "pick ONE project: run catalyst team list, then catalyst team map <KEY> (or team adopt <KEY>) and approve its preview"
|
|
1294
|
+
: verbAdminOnly("team map")
|
|
1295
|
+
? "a workspace owner or admin maps ONE project: catalyst team map <KEY>, or Map my stages on the projects page"
|
|
1296
|
+
: "pick ONE project and map its stages (or adopt the Catalyst workflow)",
|
|
1297
|
+
repositories:
|
|
1298
|
+
"install the GitHub App on the Integrations page, granting it the repository you want worked; register it on Your projects and attach it to the project before continuing",
|
|
676
1299
|
"coding accounts": accountsNext,
|
|
677
1300
|
"repository declarations": declNext,
|
|
678
1301
|
"repository agent setup": repoNext ?? "read the checkout's agent setup again",
|
|
@@ -683,7 +1306,17 @@ const NEXT = {
|
|
|
683
1306
|
// workspace's Linear integration, the project, the GitHub App with its repository, the person's own
|
|
684
1307
|
// connected accounts (personal GitHub after the repository proves the App), the repository's
|
|
685
1308
|
// declaration, and last the host. `--next` is the first unfinished part in this order.
|
|
686
|
-
const ORDER = [
|
|
1309
|
+
const ORDER = [
|
|
1310
|
+
"machine",
|
|
1311
|
+
"coding accounts",
|
|
1312
|
+
"account",
|
|
1313
|
+
"projects",
|
|
1314
|
+
"repositories",
|
|
1315
|
+
"person",
|
|
1316
|
+
"repository declarations",
|
|
1317
|
+
"repository agent setup",
|
|
1318
|
+
"host",
|
|
1319
|
+
];
|
|
687
1320
|
parts.sort((a, b) => ORDER.indexOf(a.part) - ORDER.indexOf(b.part));
|
|
688
1321
|
// A personal Linear grant cannot start until the tenant's Linear workspace exists. Once that account
|
|
689
1322
|
// connection is present, a missing personal grant becomes the next member step before project setup.
|
|
@@ -692,7 +1325,9 @@ const personPart = parts.find((p) => p.part === "person");
|
|
|
692
1325
|
// GitHub grant is sequenced after the tenant GitHub App: successful repository registration is the
|
|
693
1326
|
// onboarding path's existing proof that the App installation is usable.
|
|
694
1327
|
if (personPart && personalGrantIncomplete && workspaceResolved) {
|
|
695
|
-
personPart.blocking =
|
|
1328
|
+
personPart.blocking =
|
|
1329
|
+
personalLinearIncomplete ||
|
|
1330
|
+
(personalGithubIncomplete && repositoryRegistered);
|
|
696
1331
|
}
|
|
697
1332
|
const blocked = parts.filter((p) => p.verdict !== "ok" && p.blocking);
|
|
698
1333
|
const stuck = blocked[0] ?? parts.find((p) => p.verdict !== "ok") ?? null;
|
|
@@ -702,45 +1337,92 @@ const stuck = blocked[0] ?? parts.find((p) => p.verdict !== "ok") ?? null;
|
|
|
702
1337
|
// their own unreadable step in `accountsNext`.
|
|
703
1338
|
const actionFor = (p) =>
|
|
704
1339
|
p.verdict === "unreadable" && p.part !== "coding accounts"
|
|
705
|
-
? (p.next ??
|
|
1340
|
+
? (p.next ??
|
|
1341
|
+
`run this again; the ${p.part} part could not be read, so no step after it can be named yet`)
|
|
706
1342
|
: NEXT[p.part];
|
|
707
1343
|
let next =
|
|
708
1344
|
stuck === null
|
|
709
1345
|
? null
|
|
710
|
-
: {
|
|
1346
|
+
: {
|
|
1347
|
+
part: stuck.part,
|
|
1348
|
+
action: actionFor(stuck),
|
|
1349
|
+
owner: stuck.owner,
|
|
1350
|
+
where: stuck.where,
|
|
1351
|
+
blocking: stuck.blocking,
|
|
1352
|
+
};
|
|
711
1353
|
|
|
712
1354
|
// A refused contract version outranks every step above: each of them was read from, or waits on, a
|
|
713
1355
|
// contract this CLI would not accept. Whether an update exists is asked of npm, once, and only here.
|
|
714
1356
|
function versionAdvice() {
|
|
715
1357
|
const semver = (t) => (t ?? "").match(/\b(\d+)\.(\d+)\.(\d+)\b/);
|
|
716
1358
|
const installed = semver(field("Bundle"));
|
|
717
|
-
const npm = spawnSync("npm", ["view", "@catalyst-cloud/cli", "version"], {
|
|
1359
|
+
const npm = spawnSync("npm", ["view", "@catalyst-cloud/cli", "version"], {
|
|
1360
|
+
encoding: "utf8",
|
|
1361
|
+
timeout: 10_000,
|
|
1362
|
+
shell: process.platform === "win32",
|
|
1363
|
+
});
|
|
718
1364
|
const latest = npm.error || npm.status !== 0 ? null : semver(npm.stdout);
|
|
719
1365
|
// The installer is the update path, not `catalyst install`. Its command is on the app's
|
|
720
1366
|
// setup page; this names the script it fetches rather than composing the request here.
|
|
721
1367
|
const reinstall = `re-run the install command from the app's setup page (it installs from ${link("/install.sh")})`;
|
|
722
1368
|
if (installed === null || latest === null) {
|
|
723
|
-
return {
|
|
1369
|
+
return {
|
|
1370
|
+
text: `Whether a newer Catalyst CLI is published could not be checked. You can ${reinstall}; if this still appears afterwards, tell the Catalyst team.`,
|
|
1371
|
+
owner: null,
|
|
1372
|
+
};
|
|
724
1373
|
}
|
|
725
|
-
const order =
|
|
1374
|
+
const order =
|
|
1375
|
+
[1, 2, 3]
|
|
1376
|
+
.map((i) => Math.sign(Number(installed[i]) - Number(latest[i])))
|
|
1377
|
+
.find((d) => d !== 0) ?? 0;
|
|
726
1378
|
return order >= 0
|
|
727
|
-
? {
|
|
1379
|
+
? {
|
|
1380
|
+
text: "A newer Catalyst CLI isn't published yet. Tell the Catalyst team; nothing on this machine needs to change.",
|
|
1381
|
+
owner: "the Catalyst team",
|
|
1382
|
+
}
|
|
728
1383
|
: { text: `Update the CLI: ${reinstall}.`, owner: "you, on this machine" };
|
|
729
1384
|
}
|
|
730
1385
|
if (contractMismatch !== null && stuck !== null) {
|
|
731
1386
|
const advice = versionAdvice();
|
|
732
1387
|
const line = `the tenant serves contract ${contractMismatch.served} and this CLI accepts ${contractMismatch.accepts}. ${advice.text}`;
|
|
733
1388
|
stuck.lines.push(line);
|
|
734
|
-
next = {
|
|
1389
|
+
next = {
|
|
1390
|
+
part: stuck.part,
|
|
1391
|
+
action: line,
|
|
1392
|
+
owner: advice.owner,
|
|
1393
|
+
where: null,
|
|
1394
|
+
blocking: true,
|
|
1395
|
+
};
|
|
735
1396
|
}
|
|
736
1397
|
const finished = parts.every((p) => p.verdict === "ok");
|
|
737
1398
|
|
|
738
1399
|
if (flags.json) {
|
|
739
|
-
console.log(
|
|
1400
|
+
console.log(
|
|
1401
|
+
JSON.stringify({
|
|
1402
|
+
cli: via,
|
|
1403
|
+
connected,
|
|
1404
|
+
cloud,
|
|
1405
|
+
personalConnections,
|
|
1406
|
+
parts,
|
|
1407
|
+
localSync,
|
|
1408
|
+
next,
|
|
1409
|
+
finished,
|
|
1410
|
+
notes: [retireNote, repoNote, legacyNote].filter((n) => n !== null),
|
|
1411
|
+
}),
|
|
1412
|
+
);
|
|
740
1413
|
} else if (flags.next) {
|
|
741
|
-
process.on("exit", () => {
|
|
742
|
-
|
|
743
|
-
|
|
1414
|
+
process.on("exit", () => {
|
|
1415
|
+
for (const n of [retireNote, repoNote, legacyNote])
|
|
1416
|
+
if (n !== null) console.log(`note: ${n}`);
|
|
1417
|
+
});
|
|
1418
|
+
if (next === null)
|
|
1419
|
+
console.log(
|
|
1420
|
+
"nothing left: every part is finished, a coding account is enrolled and the host check passes. Move one card into the project's start stage (usually Todo).",
|
|
1421
|
+
);
|
|
1422
|
+
else
|
|
1423
|
+
console.log(
|
|
1424
|
+
`${next.part}: ${next.action}${next.blocking ? "" : " (does not block the steps below)"}${next.owner ? ` — who: ${next.owner}` : ""}${next.where ? ` — ${next.where.startsWith("http") ? "where" : "do"}: ${next.where}` : ""}`,
|
|
1425
|
+
);
|
|
744
1426
|
} else {
|
|
745
1427
|
for (const p of parts) {
|
|
746
1428
|
console.log(`${p.part} [${p.verdict}]`);
|
|
@@ -749,9 +1431,16 @@ if (flags.json) {
|
|
|
749
1431
|
if (p.verdict !== "ok" && p.owner) console.log(` who: ${p.owner}`);
|
|
750
1432
|
// A page gets "where"; a command gets "do". Labelling a command "where" is how a person ends up
|
|
751
1433
|
// looking for a settings page that is actually a line to run.
|
|
752
|
-
if (p.verdict !== "ok" && p.where)
|
|
1434
|
+
if (p.verdict !== "ok" && p.where)
|
|
1435
|
+
console.log(
|
|
1436
|
+
` ${p.where.startsWith("http") ? "where" : "do"}: ${p.where}`,
|
|
1437
|
+
);
|
|
753
1438
|
console.log("");
|
|
754
1439
|
}
|
|
755
|
-
console.log(
|
|
1440
|
+
console.log(
|
|
1441
|
+
next === null
|
|
1442
|
+
? "next: nothing left — move one card into the project's start stage (usually Todo) and watch."
|
|
1443
|
+
: `next: ${next.part} — ${next.action}${next.owner ? ` (${next.owner})` : ""}${next.where ? ` — ${next.where}` : ""}`,
|
|
1444
|
+
);
|
|
756
1445
|
}
|
|
757
1446
|
process.exit(finished ? 0 : 1);
|