@catalyst-cloud/cli 0.13.0 → 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 +10 -0
- package/README.md +27 -30
- package/dist/args.js +299 -61
- package/dist/ask.js +30 -18
- package/dist/capabilities.js +7 -0
- package/dist/cli.js +125 -20
- 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/environment.js +1 -1
- package/dist/legacy.js +224 -0
- package/dist/onboard-consent.js +65 -0
- package/dist/onboard.js +836 -0
- package/dist/project.js +191 -0
- package/dist/ready.js +11 -1
- package/dist/release.js +1 -1
- package/dist/repo.js +297 -0
- package/dist/runtime-verb.js +1 -1
- package/dist/secret.js +25 -4
- package/dist/skill-shape.js +21 -13
- package/dist/skills.js +44 -4
- package/dist/transport.js +26 -1
- package/dist/var.js +118 -0
- package/package.json +6 -4
- package/skills/catalyst-github/SKILL.md +7 -7
- package/skills/catalyst-github/references/is-it-mergeable.md +8 -2
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +3 -3
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +3 -3
- package/skills/catalyst-github/scripts/lib/cli.mjs +9 -7
- package/skills/catalyst-github/scripts/lib/credential.mjs +2 -2
- package/skills/catalyst-github/scripts/read-pr.mjs +2 -2
- package/skills/catalyst-linear/SKILL.md +17 -19
- package/skills/catalyst-linear/agents/openai.yaml +1 -1
- package/skills/catalyst-linear/agents/portability.yaml +1 -1
- package/skills/catalyst-linear/references/reading-a-ticket.md +9 -26
- package/skills/catalyst-linear/references/what-a-ticket-accumulates.md +2 -2
- package/skills/catalyst-linear/references/writing-to-linear.md +7 -9
- package/skills/catalyst-linear/scripts/comment.mjs +1 -1
- package/skills/catalyst-linear/scripts/create-ticket.mjs +1 -1
- package/skills/catalyst-linear/scripts/label.mjs +1 -1
- package/skills/catalyst-linear/scripts/lib/cli.mjs +9 -7
- package/skills/catalyst-linear/scripts/lib/credential.mjs +2 -2
- package/skills/catalyst-linear/scripts/move.mjs +1 -1
- package/skills/catalyst-linear/scripts/read-ticket.mjs +1 -1
- package/skills/catalyst-linear/scripts/search.mjs +1 -1
- package/skills/catalyst-onboard/SKILL.md +37 -57
- package/skills/catalyst-onboard/agents/openai.yaml +2 -2
- package/skills/catalyst-onboard/agents/portability.yaml +1 -1
- package/skills/catalyst-onboard/references/connecting-this-machine.md +35 -0
- package/skills/catalyst-onboard/references/declaring-a-repository.md +18 -21
- package/skills/catalyst-onboard/references/local-sync.md +19 -7
- package/skills/catalyst-onboard/references/reading-ready.md +79 -0
- package/skills/catalyst-onboard/references/replacing-a-credential.md +8 -33
- package/skills/catalyst-onboard/references/repository-agent-setup.md +23 -0
- package/skills/catalyst-onboard/references/skill-sources.md +13 -19
- package/skills/catalyst-onboard/references/the-one-path.md +18 -74
- package/skills/catalyst-onboard/references/what-a-phase-needs.md +21 -28
- package/skills/catalyst-onboard/references/what-the-browser-owns.md +20 -37
- package/skills/{catalyst-setup → catalyst-onboard}/scripts/check.mjs +10 -9
- package/skills/catalyst-onboard/scripts/lib/cli.mjs +6 -4
- package/skills/catalyst-onboard/scripts/lib/credential.mjs +2 -2
- package/skills/catalyst-onboard/scripts/lib/project-coverage.mjs +53 -0
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +929 -184
- package/skills/run-this-project/SKILL.md +23 -25
- package/skills/run-this-project/agents/openai.yaml +1 -1
- package/skills/run-this-project/agents/portability.yaml +1 -1
- package/skills/run-this-project/references/making-work-ready.md +20 -37
- package/skills/run-this-project/references/reacting-to-events.md +25 -48
- package/skills/run-this-project/references/stalls-and-escalation.md +20 -42
- package/skills/run-this-project/scripts/lib/cli.mjs +7 -5
- package/skills/run-this-project/scripts/lib/credential.mjs +2 -2
- package/skills/run-this-project/scripts/scope-status.mjs +0 -0
- package/skills/run-this-project/scripts/watch-scope.mjs +1 -1
- package/skills/unstick/SKILL.md +12 -12
- package/skills/unstick/agents/openai.yaml +1 -1
- package/skills/unstick/agents/portability.yaml +1 -1
- package/skills/unstick/references/playbook.md +13 -13
- package/skills/unstick/scripts/lib/cli.mjs +6 -4
- package/skills/unstick/scripts/lib/credential.mjs +2 -2
- package/skills/unstick/scripts/unstick.mjs +1 -1
- package/skills/what-needs-me/SKILL.md +12 -14
- package/skills/what-needs-me/agents/openai.yaml +2 -2
- package/skills/what-needs-me/agents/portability.yaml +1 -1
- package/skills/what-needs-me/references/raising-a-decision.md +8 -16
- package/skills/what-needs-me/references/reading-the-inbox.md +6 -13
- package/skills/what-needs-me/references/settling-an-answer.md +6 -12
- package/skills/what-needs-me/scripts/inbox.mjs +14 -13
- package/skills/what-needs-me/scripts/lib/cli.mjs +6 -4
- package/skills/what-needs-me/scripts/lib/credential.mjs +2 -2
- package/skills/what-needs-me/scripts/raise.mjs +1 -1
- package/skills/what-needs-me/scripts/settle.mjs +1 -1
- package/skills/what-this-repo-needs/SKILL.md +10 -19
- package/skills/what-this-repo-needs/references/what-each-group-means.md +14 -44
- package/skills/what-this-repo-needs/scripts/check.mjs +13 -14
- package/skills/what-this-repo-needs/scripts/inventory.mjs +2 -2
- package/skills/what-this-repo-needs/scripts/lib/cli.mjs +11 -9
- package/skills/what-this-repo-needs/scripts/lib/credential.mjs +2 -2
- package/skills/whats-happening/SKILL.md +20 -30
- package/skills/whats-happening/agents/openai.yaml +1 -1
- package/skills/whats-happening/references/coding-accounts.md +11 -0
- package/skills/whats-happening/references/routing-work.md +4 -33
- package/skills/whats-happening/references/settings-and-where-they-live.md +17 -0
- package/skills/whats-happening/references/stages-and-mapping.md +19 -0
- package/skills/whats-happening/references/status-reply.md +11 -31
- package/skills/whats-happening/references/the-ladder.md +20 -0
- package/skills/whats-happening/references/what-runs-next.md +13 -0
- package/skills/whats-happening/references/when-a-phase-fails.md +21 -0
- package/skills/whats-happening/references/why-is-it-stuck.md +38 -62
- package/skills/whats-happening/scripts/explain.mjs +1 -1
- package/skills/whats-happening/scripts/lib/cli.mjs +7 -5
- package/skills/whats-happening/scripts/lib/credential.mjs +2 -2
- package/skills/{how-catalyst-works → whats-happening}/scripts/show-my-map.mjs +18 -32
- package/skills/whats-happening/scripts/snapshot.mjs +10 -6
- package/dist/env/declaration-rules.js +0 -163
- package/skills/catalyst-onboard/references/choosing-a-coding-account.md +0 -52
- package/skills/catalyst-onboard/references/who-fixes-what.md +0 -64
- package/skills/catalyst-setup/SKILL.md +0 -36
- package/skills/catalyst-setup/agents/openai.yaml +0 -6
- package/skills/catalyst-setup/agents/portability.yaml +0 -4
- package/skills/catalyst-setup/references/what-each-check-means.md +0 -88
- package/skills/catalyst-setup/scripts/lib/cli.mjs +0 -103
- package/skills/catalyst-setup/scripts/lib/credential.mjs +0 -29
- package/skills/catalyst-setup/scripts/replica-status.mjs +0 -46
- package/skills/connect-me/SKILL.md +0 -63
- package/skills/connect-me/agents/openai.yaml +0 -6
- package/skills/connect-me/agents/portability.yaml +0 -5
- package/skills/connect-me/references/keeping-the-replica-running.md +0 -88
- package/skills/connect-me/scripts/lib/cli.mjs +0 -185
- package/skills/connect-me/scripts/lib/credential.mjs +0 -29
- package/skills/connect-me/scripts/verify-connection.mjs +0 -68
- package/skills/how-catalyst-works/SKILL.md +0 -45
- package/skills/how-catalyst-works/agents/openai.yaml +0 -6
- package/skills/how-catalyst-works/agents/portability.yaml +0 -4
- package/skills/how-catalyst-works/references/coding-accounts.md +0 -51
- package/skills/how-catalyst-works/references/settings-and-where-they-live.md +0 -20
- package/skills/how-catalyst-works/references/stages-and-mapping.md +0 -56
- package/skills/how-catalyst-works/references/the-ladder.md +0 -41
- package/skills/how-catalyst-works/references/what-catalyst-is.md +0 -30
- package/skills/how-catalyst-works/references/what-runs-next.md +0 -95
- package/skills/how-catalyst-works/references/when-a-phase-fails.md +0 -57
- package/skills/how-catalyst-works/scripts/explain-ticket.mjs +0 -41
- package/skills/how-catalyst-works/scripts/lib/cli.mjs +0 -164
- package/skills/how-catalyst-works/scripts/lib/credential.mjs +0 -29
- package/skills/how-catalyst-works/scripts/whats-running.mjs +0 -65
- package/skills/whats-happening/references/reading-the-board.md +0 -43
- package/skills/whats-happening/references/reprioritising.md +0 -37
|
@@ -7,13 +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" },
|
|
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
|
+
},
|
|
17
30
|
};
|
|
18
31
|
const NOTES = [
|
|
19
32
|
"Reads, in this order: `status` (machine), `ready --json` (machine checks and project checks, kept apart),",
|
|
@@ -22,6 +35,8 @@ const NOTES = [
|
|
|
22
35
|
"`contract --path codingAccounts` and `accounts --json` (coding accounts, and which one needs a new credential), and each",
|
|
23
36
|
"project's hosts_current check with its fixedWhere (host), and each project's environment_declared check with its per-repository notes (repository declarations). The teams read is refreshed, so a mapping just saved reads back mapped.",
|
|
24
37
|
"Reads `capabilities --json` once: when the installed CLI can run a step for this person's role (`team check`, `team map`), the next step is that command; otherwise the page, with who can.",
|
|
38
|
+
"Reads `legacy --json` when the CLI has the verb: leftovers of the old local Catalyst runtime become a machine line and a note; removing them is the person's yes.",
|
|
39
|
+
"With --repo <path>, reads `repo agent-setup <path> --json` (a read; nothing changes) and adds a `repository agent setup` part that never blocks: the Catalyst block in AGENTS.md and the portable layout, with the writes named for the person to approve.",
|
|
25
40
|
"Also runs `gh repo view <owner>/thoughts` for each registered repository's owner, as a note: it shows the repository exists, never that the GitHub App can reach it.",
|
|
26
41
|
"When the tenant serves a contract version this CLI refuses, runs `npm view @catalyst-cloud/cli version` once to say whether a newer CLI is published.",
|
|
27
42
|
"Writes nothing and changes nothing. Runs before this machine is connected — that is one of the states it reports.",
|
|
@@ -29,7 +44,11 @@ const NOTES = [
|
|
|
29
44
|
|
|
30
45
|
const { help, flags, positionals } = parseFlags(process.argv.slice(2), SPEC);
|
|
31
46
|
if (help) {
|
|
32
|
-
printHelp(
|
|
47
|
+
printHelp(
|
|
48
|
+
"node scripts/where-am-i.mjs [--next] [--json] [--repo <path>]",
|
|
49
|
+
SPEC,
|
|
50
|
+
NOTES,
|
|
51
|
+
);
|
|
33
52
|
process.exit(0);
|
|
34
53
|
}
|
|
35
54
|
if (positionals.length > 0) {
|
|
@@ -52,13 +71,32 @@ let repositoryRegistered = false;
|
|
|
52
71
|
let capabilities = null;
|
|
53
72
|
let personRole = null;
|
|
54
73
|
const canManage = () => personRole === "admin" || personRole === "owner";
|
|
55
|
-
const capability = (verb) =>
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
};
|
|
58
88
|
// `blocking` is false for a finding that is real and reportable but does not stop the next step —
|
|
59
89
|
// an unmatched Linear identity is the one that matters: it must be said, and it must not become the
|
|
60
90
|
// thing the person is told to go and do before they can map a project.
|
|
61
|
-
const add = (
|
|
91
|
+
const add = (
|
|
92
|
+
part,
|
|
93
|
+
instrument,
|
|
94
|
+
verdict,
|
|
95
|
+
lines,
|
|
96
|
+
owner = null,
|
|
97
|
+
where = null,
|
|
98
|
+
blocking = true,
|
|
99
|
+
) => {
|
|
62
100
|
const p = { part, instrument, verdict, lines, owner, where, blocking };
|
|
63
101
|
parts.push(p);
|
|
64
102
|
return p;
|
|
@@ -70,8 +108,12 @@ const add = (part, instrument, verdict, lines, owner = null, where = null, block
|
|
|
70
108
|
let contractMismatch = null;
|
|
71
109
|
const noteMismatch = (text) => {
|
|
72
110
|
const m =
|
|
73
|
-
(text ?? "").match(
|
|
74
|
-
|
|
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
|
+
);
|
|
75
117
|
if (m !== null) contractMismatch ??= { served: m[1], accepts: m[2] };
|
|
76
118
|
return m !== null;
|
|
77
119
|
};
|
|
@@ -85,10 +127,15 @@ const readContract = (args) => {
|
|
|
85
127
|
const status = runCli(["status"]);
|
|
86
128
|
if (!status.ran) {
|
|
87
129
|
process.stderr.write(`${status.stderr}\n`);
|
|
88
|
-
process.stderr.write(
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`the catalyst CLI could not be started. Install it, then run this again.\n`,
|
|
132
|
+
);
|
|
89
133
|
process.exit(2);
|
|
90
134
|
}
|
|
91
|
-
const statusLines = status.stdout
|
|
135
|
+
const statusLines = status.stdout
|
|
136
|
+
.split("\n")
|
|
137
|
+
.map((l) => l.trim())
|
|
138
|
+
.filter(Boolean);
|
|
92
139
|
const field = (name) => {
|
|
93
140
|
const line = statusLines.find((l) => l.startsWith(`${name}:`));
|
|
94
141
|
return line ? line.slice(name.length + 1).trim() : null;
|
|
@@ -99,9 +146,12 @@ const connected = tenant !== null && tryLoadConfig() !== null;
|
|
|
99
146
|
// The app and the API share an origin; the base URL is whatever this machine was connected to, so
|
|
100
147
|
// no page link in this report is a host anyone typed from memory.
|
|
101
148
|
const cloud = api === null ? null : api.split(" ")[0].replace(/\/+$/, "");
|
|
102
|
-
const link = (path) =>
|
|
149
|
+
const link = (path) =>
|
|
150
|
+
cloud === null ? `<your cloud>${path}` : `${cloud}${path}`;
|
|
103
151
|
|
|
104
|
-
const machineLines = connected
|
|
152
|
+
const machineLines = connected
|
|
153
|
+
? statusLines
|
|
154
|
+
: [statusLines[0] ?? "status printed nothing"];
|
|
105
155
|
let machineVerdict = connected ? "ok" : "unfinished";
|
|
106
156
|
|
|
107
157
|
// `ready` is ONE verdict over two parts; split it by check id before anything is reported. A check
|
|
@@ -114,19 +164,30 @@ if (connected) {
|
|
|
114
164
|
const report = tryJson(ready.stdout);
|
|
115
165
|
const checks = Array.isArray(report?.checks) ? report.checks : null;
|
|
116
166
|
if (checks === null) {
|
|
117
|
-
machineLines.push(
|
|
167
|
+
machineLines.push(
|
|
168
|
+
`ready: could not be read (${(ready.stderr || ready.stdout).trim().split("\n")[0] ?? "no output"})`,
|
|
169
|
+
);
|
|
118
170
|
machineVerdict = "unfinished";
|
|
119
171
|
} else {
|
|
120
|
-
projectChecks = checks.filter(
|
|
121
|
-
|
|
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
|
+
);
|
|
122
178
|
const failed = machineChecks.filter((c) => !c.ok && !c.note);
|
|
123
179
|
// A refused contract version is printed without the CLI's "update" fix: whether an update exists
|
|
124
180
|
// is checked below, once, against what npm has published.
|
|
125
181
|
const refused = (c) => !c.ok && !c.note && noteMismatch(c.line);
|
|
126
|
-
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
|
+
);
|
|
127
186
|
if (failed.length > 0) {
|
|
128
187
|
machineVerdict = "unfinished";
|
|
129
|
-
machineFix =
|
|
188
|
+
machineFix =
|
|
189
|
+
failed.find((c) => typeof c.fix === "string" && !refused(c))?.fix ??
|
|
190
|
+
null;
|
|
130
191
|
}
|
|
131
192
|
}
|
|
132
193
|
// Supplemental only: local caches are optional and do not change setup completion or --next.
|
|
@@ -136,7 +197,11 @@ if (connected) {
|
|
|
136
197
|
);
|
|
137
198
|
} else {
|
|
138
199
|
localSync = {
|
|
139
|
-
assessment: {
|
|
200
|
+
assessment: {
|
|
201
|
+
verdict: "unknown",
|
|
202
|
+
current: false,
|
|
203
|
+
reason: "connect this machine before local freshness can be checked",
|
|
204
|
+
},
|
|
140
205
|
started: false,
|
|
141
206
|
recovery: "catalyst login",
|
|
142
207
|
};
|
|
@@ -149,40 +214,88 @@ add(
|
|
|
149
214
|
"you, on this machine",
|
|
150
215
|
// The keyless login alone: a person who holds a personal key already knows the key form. A machine
|
|
151
216
|
// that holds the CLI is told its own command; only one with no CLI at all gets the npx line.
|
|
152
|
-
connected
|
|
217
|
+
connected
|
|
218
|
+
? null
|
|
219
|
+
: cliTarget().recorded
|
|
220
|
+
? "catalyst login"
|
|
221
|
+
: CONNECT_LINE.split(" (or")[0],
|
|
153
222
|
);
|
|
154
223
|
if (connected) {
|
|
155
224
|
const capDoc = tryJson(runCli(["capabilities", "--json"]).stdout);
|
|
156
|
-
capabilities = Array.isArray(capDoc?.capabilities)
|
|
225
|
+
capabilities = Array.isArray(capDoc?.capabilities)
|
|
226
|
+
? capDoc.capabilities
|
|
227
|
+
: null;
|
|
228
|
+
}
|
|
229
|
+
// Leftovers of the old local Catalyst runtime (its plugin, jobs, commands, state), from the CLI's own
|
|
230
|
+
// fixed list. Reported on the machine part and as a note; removal is the person's yes, never this script's.
|
|
231
|
+
let legacyNote = null;
|
|
232
|
+
if (verbAvailable("legacy")) {
|
|
233
|
+
const legacyDoc = tryJson(runCli(["legacy", "--json"]).stdout);
|
|
234
|
+
const found = Array.isArray(legacyDoc?.found) ? legacyDoc.found : [];
|
|
235
|
+
if (found.length > 0) {
|
|
236
|
+
const machine = parts.find((p) => p.part === "machine");
|
|
237
|
+
const names = found.map((f) => `${f.kind} ${f.name}`);
|
|
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
|
+
);
|
|
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`;
|
|
242
|
+
}
|
|
157
243
|
}
|
|
158
244
|
|
|
159
245
|
// ── person ────────────────────────────────────────────────────────────────────────────────────────
|
|
160
246
|
if (!connected) {
|
|
161
|
-
add(
|
|
247
|
+
add(
|
|
248
|
+
"person",
|
|
249
|
+
"catalyst me",
|
|
250
|
+
"unreadable",
|
|
251
|
+
["not readable until this machine is connected"],
|
|
252
|
+
null,
|
|
253
|
+
null,
|
|
254
|
+
);
|
|
162
255
|
} else {
|
|
163
256
|
const me = runCli(["me", "--json"]);
|
|
164
257
|
const doc = tryJson(me.stdout);
|
|
165
|
-
const user =
|
|
258
|
+
const user =
|
|
259
|
+
doc && typeof doc.user === "object" && doc.user !== null ? doc.user : null;
|
|
166
260
|
if (doc === null) {
|
|
167
|
-
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 =
|
|
168
271
|
"run this again; catalyst me could not be read, so no step after it can be named yet";
|
|
169
272
|
} else if (user === null) {
|
|
170
273
|
add(
|
|
171
274
|
"person",
|
|
172
275
|
"catalyst me",
|
|
173
276
|
"unfinished",
|
|
174
|
-
[
|
|
277
|
+
[
|
|
278
|
+
"this credential names no person — it is a host credential, so nothing an agent writes will carry a name",
|
|
279
|
+
],
|
|
175
280
|
"you: connect again with your own login",
|
|
176
281
|
CONNECT_LINE,
|
|
177
282
|
);
|
|
178
283
|
} else {
|
|
179
|
-
const matched =
|
|
284
|
+
const matched =
|
|
285
|
+
typeof user.linearUserId === "string" && user.linearUserId !== "";
|
|
180
286
|
personRole = typeof user.role === "string" ? user.role : null;
|
|
181
287
|
const grantLines = [];
|
|
182
288
|
for (const provider of ["linear", "github"]) {
|
|
183
|
-
const read = runCli([
|
|
289
|
+
const read = runCli([
|
|
290
|
+
"connections",
|
|
291
|
+
"personal",
|
|
292
|
+
provider,
|
|
293
|
+
"status",
|
|
294
|
+
"--json",
|
|
295
|
+
]);
|
|
184
296
|
const result = tryJson(read.stdout);
|
|
185
|
-
const outcome =
|
|
297
|
+
const outcome =
|
|
298
|
+
typeof result?.outcome === "string" ? result.outcome : "unreadable";
|
|
186
299
|
personalConnections[provider] = outcome;
|
|
187
300
|
if (outcome === "connected") {
|
|
188
301
|
grantLines.push(`personal ${provider}: connected`);
|
|
@@ -190,23 +303,43 @@ if (!connected) {
|
|
|
190
303
|
personalGrantIncomplete = true;
|
|
191
304
|
if (provider === "linear") personalLinearIncomplete = true;
|
|
192
305
|
else personalGithubIncomplete = true;
|
|
193
|
-
grantLines.push(
|
|
194
|
-
|
|
306
|
+
grantLines.push(
|
|
307
|
+
`personal ${provider}: ${outcome === "absent" ? "not connected" : "expired"}`,
|
|
308
|
+
);
|
|
309
|
+
if (provider === "linear")
|
|
310
|
+
personalNext ??= "connect your personal linear account";
|
|
195
311
|
} else {
|
|
196
312
|
personalGrantIncomplete = true;
|
|
197
313
|
if (provider === "linear") personalLinearIncomplete = true;
|
|
198
314
|
else personalGithubIncomplete = true;
|
|
199
|
-
grantLines.push(
|
|
200
|
-
|
|
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";
|
|
201
320
|
}
|
|
202
321
|
}
|
|
203
322
|
add(
|
|
204
323
|
"person",
|
|
205
324
|
"catalyst me, and catalyst connections personal <provider> status --json",
|
|
206
325
|
matched && !personalGrantIncomplete ? "ok" : "unfinished",
|
|
207
|
-
[
|
|
208
|
-
|
|
209
|
-
|
|
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"),
|
|
210
343
|
false,
|
|
211
344
|
);
|
|
212
345
|
}
|
|
@@ -214,15 +347,38 @@ if (!connected) {
|
|
|
214
347
|
|
|
215
348
|
// ── account ───────────────────────────────────────────────────────────────────────────────────────
|
|
216
349
|
if (!connected) {
|
|
217
|
-
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
|
+
);
|
|
218
358
|
} else {
|
|
219
359
|
const acct = readContract(["contract", "--path", "account", "--json"]);
|
|
220
360
|
const doc = tryJson(acct.stdout);
|
|
221
361
|
if (doc === null) {
|
|
222
|
-
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 =
|
|
223
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";
|
|
224
373
|
} else {
|
|
225
|
-
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;
|
|
226
382
|
workspaceResolved = workspace !== null;
|
|
227
383
|
// The declaration is the one part of the account a key can also READ — and it is the one part a
|
|
228
384
|
// key can WRITE, so it is reported here rather than left to the settings page like the rest.
|
|
@@ -230,14 +386,29 @@ if (!connected) {
|
|
|
230
386
|
const env = runCli(["environment", "read", "--json"]);
|
|
231
387
|
const envDoc = tryJson(env.stdout);
|
|
232
388
|
if (envDoc === null) {
|
|
233
|
-
envLines.push(
|
|
389
|
+
envLines.push(
|
|
390
|
+
`environment: not readable (${(env.stderr || env.stdout).trim().split("\n")[0] ?? "no output"})`,
|
|
391
|
+
);
|
|
234
392
|
} else if (envDoc.current === null || envDoc.current === undefined) {
|
|
235
|
-
envLines.push(
|
|
393
|
+
envLines.push(
|
|
394
|
+
"environment: nothing declared yet — declare it with `catalyst environment propose --file <path> --approve`",
|
|
395
|
+
);
|
|
236
396
|
} else {
|
|
237
|
-
envLines.push(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
);
|
|
241
412
|
}
|
|
242
413
|
add(
|
|
243
414
|
"account",
|
|
@@ -245,7 +416,9 @@ if (!connected) {
|
|
|
245
416
|
workspace === null ? "unfinished" : "ok",
|
|
246
417
|
[
|
|
247
418
|
`${doc.name ?? "(unnamed tenant)"} (${doc.slug ?? "?"})`,
|
|
248
|
-
workspace === null
|
|
419
|
+
workspace === null
|
|
420
|
+
? "no Linear workspace resolved on this contract"
|
|
421
|
+
: `Linear workspace resolved: ${workspace}`,
|
|
249
422
|
"the GitHub App install is NOT carried here — the connections page is the only place that shows it",
|
|
250
423
|
...envLines,
|
|
251
424
|
],
|
|
@@ -262,12 +435,14 @@ let teamRows = null;
|
|
|
262
435
|
let readinessMetaCache;
|
|
263
436
|
const readinessMeta = () => {
|
|
264
437
|
if (readinessMetaCache === undefined) {
|
|
265
|
-
const m = tryJson(
|
|
438
|
+
const m = tryJson(
|
|
439
|
+
readContract(["contract", "--path", "readinessChecks", "--json"]).stdout,
|
|
440
|
+
);
|
|
266
441
|
readinessMetaCache = Array.isArray(m) ? m : [];
|
|
267
442
|
}
|
|
268
443
|
return readinessMetaCache;
|
|
269
444
|
};
|
|
270
|
-
// 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
|
|
271
446
|
// names them. A rule that moves a card is a conflict with the stage Catalyst manages, and no Catalyst
|
|
272
447
|
// key can change a Linear automation rule: the fix is in Linear, by a Linear admin.
|
|
273
448
|
const LINEAR_AUTOMATION_RULES = {
|
|
@@ -280,27 +455,49 @@ let blockedProjects = [];
|
|
|
280
455
|
|
|
281
456
|
// ── projects (a project is one Linear team) ───────────────────────────────────────────────────────
|
|
282
457
|
if (!connected) {
|
|
283
|
-
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
|
+
);
|
|
284
466
|
} else {
|
|
285
467
|
// The mapping write updates the cloud before the cached contract's dispatchGate projection.
|
|
286
468
|
// Revalidate this one read so a successful save does not appear unmapped on the next step.
|
|
287
|
-
const teams = readContract([
|
|
469
|
+
const teams = readContract([
|
|
470
|
+
"contract",
|
|
471
|
+
"--refresh",
|
|
472
|
+
"--path",
|
|
473
|
+
"teams",
|
|
474
|
+
"--json",
|
|
475
|
+
]);
|
|
288
476
|
const doc = tryJson(teams.stdout);
|
|
289
477
|
const rows = Array.isArray(doc) ? doc : null;
|
|
290
478
|
teamRows = rows;
|
|
291
479
|
if (rows === null) {
|
|
292
|
-
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 =
|
|
293
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";
|
|
294
489
|
} else {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
for (const c of projectChecks)
|
|
303
|
-
|
|
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
|
+
);
|
|
304
501
|
// A project is set up when its dispatch gate is open (its stages are mapped), or when its
|
|
305
502
|
// readiness reads ready. Readiness alone kept `--next` on "map its stages" for a tenant whose
|
|
306
503
|
// gates were open: readiness stays "unchecked" until someone presses Re-check, and a new team
|
|
@@ -308,34 +505,77 @@ if (!connected) {
|
|
|
308
505
|
// ⛔ A team whose readiness reads `blocked` is NOT set up, whatever its gate says: a blocking
|
|
309
506
|
// check (a Linear automation that moves cards off Catalyst's stages, a missing OAuth scope) breaks
|
|
310
507
|
// every run in it. The step is that check's own fix, by its own owner, then a re-check.
|
|
311
|
-
const isBlocking = (c) =>
|
|
508
|
+
const isBlocking = (c) =>
|
|
509
|
+
readinessMeta().length === 0 ||
|
|
510
|
+
readinessMeta().find((r) => r.id === c.id)?.severity === "blocking";
|
|
312
511
|
for (const t of rows.filter((r) => r.readiness?.status === "blocked")) {
|
|
313
512
|
const key = t.key ?? t.id ?? "(unkeyed)";
|
|
314
|
-
const fails = (
|
|
513
|
+
const fails = (
|
|
514
|
+
Array.isArray(t.readiness?.checks) ? t.readiness.checks : []
|
|
515
|
+
).filter((c) => c.state === "fail" && isBlocking(c));
|
|
315
516
|
const ids = fails.map((c) => c.id);
|
|
316
|
-
const rules = ids
|
|
517
|
+
const rules = ids
|
|
518
|
+
.filter((id) => id in LINEAR_AUTOMATION_RULES)
|
|
519
|
+
.map((id) => LINEAR_AUTOMATION_RULES[id]);
|
|
317
520
|
const others = ids.filter((id) => !(id in LINEAR_AUTOMATION_RULES));
|
|
318
|
-
const ownerRow = fails
|
|
319
|
-
|
|
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";
|
|
320
527
|
const how = [
|
|
321
|
-
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,
|
|
322
531
|
others.length > 0 ? `${others.join(", ")}: ${owner}` : null,
|
|
323
|
-
]
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
});
|
|
326
543
|
}
|
|
327
|
-
const setUp = (t) =>
|
|
544
|
+
const setUp = (t) =>
|
|
545
|
+
t.readiness?.status !== "blocked" &&
|
|
546
|
+
(t.dispatchGate?.status === "open" ||
|
|
547
|
+
(t.readiness?.status ?? "unchecked") === "ready");
|
|
328
548
|
for (const t of rows) {
|
|
329
|
-
if (
|
|
330
|
-
|
|
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
|
+
);
|
|
331
556
|
}
|
|
332
557
|
}
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
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;
|
|
337
570
|
const projectWhere = blockedProjects.length > 0 ? null : mapWhere;
|
|
338
|
-
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
|
+
);
|
|
339
579
|
}
|
|
340
580
|
}
|
|
341
581
|
|
|
@@ -345,27 +585,75 @@ if (!connected) {
|
|
|
345
585
|
// reach it, so this note never claims that, and it never changes a verdict.
|
|
346
586
|
function thoughtsNote(owner) {
|
|
347
587
|
const repo = `${owner}/thoughts`;
|
|
348
|
-
const res = spawnSync("gh", ["repo", "view", repo, "--json", "name"], {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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}`;
|
|
352
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).`;
|
|
353
601
|
}
|
|
354
602
|
if (!connected) {
|
|
355
|
-
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
|
+
);
|
|
356
611
|
} else {
|
|
357
|
-
const repos = readContract([
|
|
612
|
+
const repos = readContract([
|
|
613
|
+
"contract",
|
|
614
|
+
"--path",
|
|
615
|
+
"merge.repositories",
|
|
616
|
+
"--json",
|
|
617
|
+
]);
|
|
358
618
|
const doc = tryJson(repos.stdout);
|
|
359
619
|
const rows = Array.isArray(doc) ? doc : null;
|
|
360
620
|
if (rows === null) {
|
|
361
|
-
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 =
|
|
362
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";
|
|
363
632
|
} else {
|
|
364
|
-
const lines = [
|
|
633
|
+
const lines = [
|
|
634
|
+
`${rows.length} registered`,
|
|
635
|
+
...rows.map((r) => `${r.owner ?? "?"}/${r.name ?? "?"}`),
|
|
636
|
+
];
|
|
365
637
|
repositoryRegistered = rows.length > 0;
|
|
366
|
-
lines.push(
|
|
367
|
-
|
|
368
|
-
|
|
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
|
+
);
|
|
369
657
|
}
|
|
370
658
|
}
|
|
371
659
|
|
|
@@ -388,19 +676,30 @@ const CREDENTIAL_ERROR_CODES = new Set(["no_access_token", "no_credential"]);
|
|
|
388
676
|
const CREDENTIAL_FAILURE_STREAK = 3;
|
|
389
677
|
// A cancelled subscription or an ended account is not a credential problem: no token brings it
|
|
390
678
|
// back. It is kept for reporting, never re-credentialed, and it does not make setup unfinished.
|
|
391
|
-
const isRetirable = (a) =>
|
|
679
|
+
const isRetirable = (a) =>
|
|
680
|
+
a?.renewalStatus === "canceled" ||
|
|
681
|
+
a?.status === "ended" ||
|
|
682
|
+
a?.declaredState === "ended";
|
|
392
683
|
const credentialProblem = (a) => {
|
|
393
684
|
if (isRetirable(a)) return null;
|
|
394
685
|
if (typeof a?.needsCredential === "boolean") {
|
|
395
686
|
if (!a.needsCredential) return null;
|
|
396
|
-
if (a.quarantined === true)
|
|
687
|
+
if (a.quarantined === true)
|
|
688
|
+
return `quarantined${typeof a.quarantineReason === "string" && a.quarantineReason !== "" ? `: ${a.quarantineReason}` : ""}`;
|
|
397
689
|
if (a.status === "expired-or-revoked") return "expired or revoked";
|
|
398
|
-
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";
|
|
399
693
|
}
|
|
400
694
|
if (a?.status === "ended" || (a?.revokedAtMs ?? null) !== null) return null;
|
|
401
|
-
if (a?.quarantined === true)
|
|
695
|
+
if (a?.quarantined === true)
|
|
696
|
+
return `quarantined${typeof a.quarantineReason === "string" && a.quarantineReason !== "" ? `: ${a.quarantineReason}` : ""}`;
|
|
402
697
|
if (a?.status === "expired-or-revoked") return "expired or revoked";
|
|
403
|
-
if (
|
|
698
|
+
if (
|
|
699
|
+
CREDENTIAL_ERROR_CODES.has(a?.lastPollErrorCode) &&
|
|
700
|
+
typeof a?.pollFailureCount === "number" &&
|
|
701
|
+
a.pollFailureCount >= CREDENTIAL_FAILURE_STREAK
|
|
702
|
+
) {
|
|
404
703
|
return `its last ${a.pollFailureCount} polls failed with ${a.lastPollErrorCode}`;
|
|
405
704
|
}
|
|
406
705
|
return null;
|
|
@@ -408,11 +707,28 @@ const credentialProblem = (a) => {
|
|
|
408
707
|
const readAccountRows = () => {
|
|
409
708
|
const acc = runCli(["accounts", "--json"]);
|
|
410
709
|
const doc = tryJson(acc.stdout);
|
|
411
|
-
const rows = Array.isArray(doc)
|
|
412
|
-
|
|
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
|
+
};
|
|
413
722
|
};
|
|
414
723
|
if (!connected) {
|
|
415
|
-
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
|
+
);
|
|
416
732
|
} else {
|
|
417
733
|
const res = readContract(["contract", "--path", "codingAccounts", "--json"]);
|
|
418
734
|
const ca = tryJson(res.stdout);
|
|
@@ -420,76 +736,203 @@ if (!connected) {
|
|
|
420
736
|
const INSTRUMENT = "codingAccounts in catalyst contract";
|
|
421
737
|
if (ca !== null && typeof ca === "object" && typeof ca.state === "string") {
|
|
422
738
|
const line = typeof ca.line === "string" ? ca.line : `state ${ca.state}`;
|
|
423
|
-
const owner =
|
|
739
|
+
const owner =
|
|
740
|
+
typeof ca.enrolledByLine === "string" ? ca.enrolledByLine : null;
|
|
424
741
|
const where = typeof ca.page === "string" ? link(ca.page) : null;
|
|
425
742
|
if (ca.state === "enrolled" || ca.state === "needs_credential") {
|
|
426
743
|
const DETAIL = `${INSTRUMENT}, and catalyst accounts`;
|
|
427
744
|
const head = [line, `${ca.activeCount ?? "?"} active`];
|
|
428
745
|
const { rows, error } = readAccountRows();
|
|
429
|
-
const dead =
|
|
746
|
+
const dead =
|
|
747
|
+
rows === null ? [] : rows.filter((a) => credentialProblem(a) !== null);
|
|
430
748
|
const retirable = rows === null ? [] : rows.filter(isRetirable);
|
|
431
|
-
const who = (a) =>
|
|
432
|
-
|
|
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
|
+
];
|
|
433
757
|
retireNote = retireLine[0] ?? null;
|
|
434
758
|
if (rows === null) {
|
|
435
|
-
accountsNext =
|
|
436
|
-
|
|
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
|
+
);
|
|
437
772
|
} else if (dead.length > 0) {
|
|
438
|
-
const name = (a) =>
|
|
773
|
+
const name = (a) =>
|
|
774
|
+
`${a.provider ?? "unknown provider"} account ${a.accountSlot ?? "?"}${who(a) ? ` (${who(a)})` : ""}`;
|
|
439
775
|
const more = dead.length > 1 ? ` (and ${dead.length - 1} more)` : "";
|
|
440
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.`;
|
|
441
777
|
add(
|
|
442
778
|
"coding accounts",
|
|
443
779
|
DETAIL,
|
|
444
780
|
"unfinished",
|
|
445
|
-
[
|
|
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
|
+
],
|
|
446
790
|
owner,
|
|
447
791
|
where,
|
|
448
792
|
);
|
|
449
793
|
} else if (ca.state === "needs_credential") {
|
|
450
|
-
accountsNext =
|
|
451
|
-
|
|
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
|
+
);
|
|
452
807
|
} else {
|
|
453
|
-
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
|
+
]);
|
|
454
813
|
}
|
|
455
814
|
} else if (ca.state === "inactive") {
|
|
456
|
-
accountsNext =
|
|
457
|
-
|
|
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
|
+
);
|
|
458
828
|
} else if (ca.state === "none_enrolled") {
|
|
459
|
-
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
|
+
);
|
|
460
837
|
} else {
|
|
461
838
|
// `unread`, or a state this bundle does not know: could not look is not the same as none there.
|
|
462
|
-
accountsNext =
|
|
463
|
-
|
|
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
|
+
);
|
|
464
852
|
}
|
|
465
853
|
} else if (older) {
|
|
466
854
|
const { rows, error } = readAccountRows();
|
|
467
|
-
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";
|
|
468
857
|
if (rows === null) {
|
|
469
|
-
accountsNext =
|
|
470
|
-
|
|
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
|
+
);
|
|
471
868
|
} else {
|
|
472
869
|
// An expired or revoked slot, or a quarantined one, cannot take work until an admin acts on it.
|
|
473
|
-
const usable = rows.filter(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
);
|
|
478
893
|
}
|
|
479
894
|
} else {
|
|
480
|
-
accountsNext =
|
|
481
|
-
|
|
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
|
+
);
|
|
482
907
|
}
|
|
483
908
|
}
|
|
484
909
|
|
|
485
910
|
// the Re-check step, as a command when this person can run it, else as the page with the
|
|
486
911
|
// person who can. `team check` is the CLI twin of the page's Re-check button (admin or owner).
|
|
487
|
-
const mappedKeys = () =>
|
|
912
|
+
const mappedKeys = () =>
|
|
913
|
+
(teamRows ?? [])
|
|
914
|
+
.map((t) => t.key ?? t.id)
|
|
915
|
+
.filter((k) => typeof k === "string" && k !== "");
|
|
488
916
|
const recheckStep = () => {
|
|
489
917
|
const cmds = mappedKeys().map((k) => `catalyst team check ${k}`);
|
|
490
|
-
if (cmds.length > 0 && verbAvailable("team check"))
|
|
491
|
-
|
|
492
|
-
|
|
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
|
+
};
|
|
493
936
|
};
|
|
494
937
|
|
|
495
938
|
// ── host ──────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -497,34 +940,83 @@ const recheckStep = () => {
|
|
|
497
940
|
// contract says who owns it. The check is account-wide, so any one project's reading is the answer.
|
|
498
941
|
// A tenant that runs no host of its own reads `pass` here, and then nothing is asked of anyone.
|
|
499
942
|
if (!connected) {
|
|
500
|
-
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
|
+
);
|
|
501
951
|
} else {
|
|
502
|
-
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
|
+
);
|
|
503
957
|
const row = readinessMeta().find((r) => r.id === "hosts_current") ?? null;
|
|
504
|
-
const hc =
|
|
958
|
+
const hc =
|
|
959
|
+
checks.find((c) => c.state === "fail") ??
|
|
960
|
+
checks.find((c) => c.state === "unknown") ??
|
|
961
|
+
checks[0] ??
|
|
962
|
+
null;
|
|
505
963
|
// The contract's printed line for this owner is written for a host that is behind. When no host is
|
|
506
964
|
// connected there is nothing behind, so the owner is named by its id instead of by that sentence.
|
|
507
|
-
const owner =
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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})`;
|
|
512
971
|
// Where the owner acts comes from the contract's `fixedWhere`. Null (every fixer today, and absent on
|
|
513
972
|
// an older cloud) means no page exists, so the owner sentence is printed alone and no page is made up.
|
|
514
|
-
const fw =
|
|
973
|
+
const fw =
|
|
974
|
+
row?.fixedWhere && typeof row.fixedWhere.page === "string"
|
|
975
|
+
? row.fixedWhere
|
|
976
|
+
: null;
|
|
515
977
|
const how = fw === null ? null : link(fw.page);
|
|
516
978
|
const howLines = fw?.command ? [`the owner runs: ${fw.command}`] : [];
|
|
517
979
|
if (hc === null) {
|
|
518
980
|
const step = recheckStep();
|
|
519
|
-
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 =
|
|
520
993
|
teamRows === null
|
|
521
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"
|
|
522
995
|
: step.action;
|
|
523
996
|
} else if (hc.state === "pass") {
|
|
524
|
-
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
|
+
]);
|
|
525
1000
|
} else {
|
|
526
|
-
const detail =
|
|
527
|
-
|
|
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
|
+
);
|
|
528
1020
|
}
|
|
529
1021
|
}
|
|
530
1022
|
|
|
@@ -533,37 +1025,107 @@ if (!connected) {
|
|
|
533
1025
|
// default repository has one in effect is the contract's `environment_declared` check; the project's
|
|
534
1026
|
// other repositories appear under that check's `repos`. Names only: nothing here reads a value, and
|
|
535
1027
|
// nothing here reads a file on this machine. A cloud that sends no such check has nothing to read.
|
|
536
|
-
const DECL_INSTRUMENT =
|
|
1028
|
+
const DECL_INSTRUMENT =
|
|
1029
|
+
"environment_declared in catalyst contract --path teams";
|
|
537
1030
|
const DECL_REASONS = {
|
|
538
|
-
no_team_repo_default: {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
+
},
|
|
543
1056
|
};
|
|
544
|
-
const DECL_DO =
|
|
545
|
-
|
|
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";
|
|
546
1061
|
if (!connected) {
|
|
547
|
-
add(
|
|
1062
|
+
add(
|
|
1063
|
+
"repository declarations",
|
|
1064
|
+
DECL_INSTRUMENT,
|
|
1065
|
+
"unreadable",
|
|
1066
|
+
["not readable until this machine is connected"],
|
|
1067
|
+
null,
|
|
1068
|
+
null,
|
|
1069
|
+
);
|
|
548
1070
|
} else if (teamRows === null) {
|
|
549
|
-
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 =
|
|
550
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";
|
|
551
1082
|
} else if (teamRows.length === 0) {
|
|
552
|
-
add(
|
|
553
|
-
"
|
|
554
|
-
|
|
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
|
+
) {
|
|
555
1099
|
// Mapped, never checked: the contract carries no checks at all until someone presses Re-check, and
|
|
556
1100
|
// that silence is not "nothing to declare".
|
|
557
1101
|
const step = recheckStep();
|
|
558
|
-
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;
|
|
559
1112
|
} else {
|
|
560
|
-
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
|
+
);
|
|
561
1118
|
const lines = [];
|
|
562
1119
|
let verdict = "ok";
|
|
563
1120
|
let owner = null;
|
|
564
1121
|
let where = null;
|
|
565
1122
|
let unknown = false;
|
|
566
|
-
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
|
+
};
|
|
567
1129
|
const flag = (team, repo, reason) => {
|
|
568
1130
|
const r = describe(reason);
|
|
569
1131
|
lines.push(`${team}${repo ? `, ${repo}` : ""}: ${r.text}`);
|
|
@@ -573,23 +1135,136 @@ if (!connected) {
|
|
|
573
1135
|
where ??= r.page === null ? DECL_DO : link(r.page);
|
|
574
1136
|
};
|
|
575
1137
|
if (found.length === 0) {
|
|
576
|
-
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
|
+
);
|
|
577
1141
|
}
|
|
578
1142
|
for (const c of found) {
|
|
579
|
-
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
|
+
);
|
|
580
1147
|
else if (c.state === "fail") flag(c.team, null, c.reason);
|
|
581
1148
|
else {
|
|
582
1149
|
unknown = true;
|
|
583
|
-
lines.push(
|
|
1150
|
+
lines.push(
|
|
1151
|
+
`${c.team}: environment_declared could not be read${c.reason ? ` (${c.reason})` : ""}; a re-check is needed`,
|
|
1152
|
+
);
|
|
584
1153
|
}
|
|
585
|
-
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);
|
|
586
1157
|
}
|
|
587
1158
|
if (unknown && verdict === "ok") {
|
|
588
1159
|
const step = recheckStep();
|
|
589
|
-
add(
|
|
1160
|
+
add(
|
|
1161
|
+
"repository declarations",
|
|
1162
|
+
DECL_INSTRUMENT,
|
|
1163
|
+
"unreadable",
|
|
1164
|
+
lines,
|
|
1165
|
+
step.owner,
|
|
1166
|
+
step.where,
|
|
1167
|
+
).next = step.action;
|
|
590
1168
|
} else {
|
|
591
|
-
if (verdict !== "ok")
|
|
592
|
-
|
|
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
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// ── repository agent setup (only with --repo) ─────────────────────────────────────────────────────
|
|
1185
|
+
// A checkout the person named. Read through the CLI's own verb, never by this script opening files:
|
|
1186
|
+
// the verb is the one place that knows the block's words and the portable layout. Never blocking:
|
|
1187
|
+
// a repository that is not portable still runs; this is an offer, and the writes wait for a yes.
|
|
1188
|
+
let repoNote = null;
|
|
1189
|
+
let repoNext = null;
|
|
1190
|
+
if (typeof flags.repo === "string" && flags.repo !== "") {
|
|
1191
|
+
const REPO_INSTRUMENT = `repo agent-setup ${flags.repo} --json`;
|
|
1192
|
+
if (!connected) {
|
|
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
|
+
);
|
|
1202
|
+
} else if (!verbAvailable("repo agent-setup")) {
|
|
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 =
|
|
1214
|
+
"update the CLI (it lacks repo agent-setup); the checkout's agent setup cannot be read until then (does not block anything)";
|
|
1215
|
+
} else {
|
|
1216
|
+
const res = runCli(["repo", "agent-setup", flags.repo, "--json"]);
|
|
1217
|
+
const doc = tryJson(res.stdout);
|
|
1218
|
+
if (doc === null) {
|
|
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 =
|
|
1230
|
+
`run this again with the checkout's real path; ${flags.repo} could not be read`;
|
|
1231
|
+
} else {
|
|
1232
|
+
const block = doc.agentsMd?.block ?? "unknown";
|
|
1233
|
+
const verdict = doc.verdict ?? "unknown";
|
|
1234
|
+
const ok = verdict === "portable" && block === "current";
|
|
1235
|
+
const lines = [
|
|
1236
|
+
`AGENTS.md: ${doc.agentsMd?.present ? `present; Catalyst block ${block}` : "absent"}`,
|
|
1237
|
+
`CLAUDE.md: ${doc.claudeMd?.present ? (doc.claudeMd.importsAgentsMd ? "imports AGENTS.md" : `${doc.claudeMd.otherLines ?? "?"} lines of its own guidance, no @AGENTS.md import`) : "absent"}`,
|
|
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("; ")}` : ""}`,
|
|
1239
|
+
];
|
|
1240
|
+
const offers = [];
|
|
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)`;
|
|
1267
|
+
}
|
|
593
1268
|
}
|
|
594
1269
|
}
|
|
595
1270
|
|
|
@@ -609,17 +1284,21 @@ const NEXT = {
|
|
|
609
1284
|
: personalGithubIncomplete
|
|
610
1285
|
? "install the tenant GitHub App and register its repository before connecting your personal GitHub account"
|
|
611
1286
|
: "get this person's seat and Linear identity sorted",
|
|
612
|
-
account:
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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",
|
|
621
1299
|
"coding accounts": accountsNext,
|
|
622
1300
|
"repository declarations": declNext,
|
|
1301
|
+
"repository agent setup": repoNext ?? "read the checkout's agent setup again",
|
|
623
1302
|
host: "connect a Catalyst host",
|
|
624
1303
|
};
|
|
625
1304
|
// The parts are read in the order their data allows and reported in the order a person can act on
|
|
@@ -627,7 +1306,17 @@ const NEXT = {
|
|
|
627
1306
|
// workspace's Linear integration, the project, the GitHub App with its repository, the person's own
|
|
628
1307
|
// connected accounts (personal GitHub after the repository proves the App), the repository's
|
|
629
1308
|
// declaration, and last the host. `--next` is the first unfinished part in this order.
|
|
630
|
-
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
|
+
];
|
|
631
1320
|
parts.sort((a, b) => ORDER.indexOf(a.part) - ORDER.indexOf(b.part));
|
|
632
1321
|
// A personal Linear grant cannot start until the tenant's Linear workspace exists. Once that account
|
|
633
1322
|
// connection is present, a missing personal grant becomes the next member step before project setup.
|
|
@@ -636,7 +1325,9 @@ const personPart = parts.find((p) => p.part === "person");
|
|
|
636
1325
|
// GitHub grant is sequenced after the tenant GitHub App: successful repository registration is the
|
|
637
1326
|
// onboarding path's existing proof that the App installation is usable.
|
|
638
1327
|
if (personPart && personalGrantIncomplete && workspaceResolved) {
|
|
639
|
-
personPart.blocking =
|
|
1328
|
+
personPart.blocking =
|
|
1329
|
+
personalLinearIncomplete ||
|
|
1330
|
+
(personalGithubIncomplete && repositoryRegistered);
|
|
640
1331
|
}
|
|
641
1332
|
const blocked = parts.filter((p) => p.verdict !== "ok" && p.blocking);
|
|
642
1333
|
const stuck = blocked[0] ?? parts.find((p) => p.verdict !== "ok") ?? null;
|
|
@@ -646,45 +1337,92 @@ const stuck = blocked[0] ?? parts.find((p) => p.verdict !== "ok") ?? null;
|
|
|
646
1337
|
// their own unreadable step in `accountsNext`.
|
|
647
1338
|
const actionFor = (p) =>
|
|
648
1339
|
p.verdict === "unreadable" && p.part !== "coding accounts"
|
|
649
|
-
? (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`)
|
|
650
1342
|
: NEXT[p.part];
|
|
651
1343
|
let next =
|
|
652
1344
|
stuck === null
|
|
653
1345
|
? null
|
|
654
|
-
: {
|
|
1346
|
+
: {
|
|
1347
|
+
part: stuck.part,
|
|
1348
|
+
action: actionFor(stuck),
|
|
1349
|
+
owner: stuck.owner,
|
|
1350
|
+
where: stuck.where,
|
|
1351
|
+
blocking: stuck.blocking,
|
|
1352
|
+
};
|
|
655
1353
|
|
|
656
1354
|
// A refused contract version outranks every step above: each of them was read from, or waits on, a
|
|
657
1355
|
// contract this CLI would not accept. Whether an update exists is asked of npm, once, and only here.
|
|
658
1356
|
function versionAdvice() {
|
|
659
1357
|
const semver = (t) => (t ?? "").match(/\b(\d+)\.(\d+)\.(\d+)\b/);
|
|
660
1358
|
const installed = semver(field("Bundle"));
|
|
661
|
-
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
|
+
});
|
|
662
1364
|
const latest = npm.error || npm.status !== 0 ? null : semver(npm.stdout);
|
|
663
1365
|
// The installer is the update path, not `catalyst install`. Its command is on the app's
|
|
664
1366
|
// setup page; this names the script it fetches rather than composing the request here.
|
|
665
1367
|
const reinstall = `re-run the install command from the app's setup page (it installs from ${link("/install.sh")})`;
|
|
666
1368
|
if (installed === null || latest === null) {
|
|
667
|
-
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
|
+
};
|
|
668
1373
|
}
|
|
669
|
-
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;
|
|
670
1378
|
return order >= 0
|
|
671
|
-
? {
|
|
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
|
+
}
|
|
672
1383
|
: { text: `Update the CLI: ${reinstall}.`, owner: "you, on this machine" };
|
|
673
1384
|
}
|
|
674
1385
|
if (contractMismatch !== null && stuck !== null) {
|
|
675
1386
|
const advice = versionAdvice();
|
|
676
1387
|
const line = `the tenant serves contract ${contractMismatch.served} and this CLI accepts ${contractMismatch.accepts}. ${advice.text}`;
|
|
677
1388
|
stuck.lines.push(line);
|
|
678
|
-
next = {
|
|
1389
|
+
next = {
|
|
1390
|
+
part: stuck.part,
|
|
1391
|
+
action: line,
|
|
1392
|
+
owner: advice.owner,
|
|
1393
|
+
where: null,
|
|
1394
|
+
blocking: true,
|
|
1395
|
+
};
|
|
679
1396
|
}
|
|
680
1397
|
const finished = parts.every((p) => p.verdict === "ok");
|
|
681
1398
|
|
|
682
1399
|
if (flags.json) {
|
|
683
|
-
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
|
+
);
|
|
684
1413
|
} else if (flags.next) {
|
|
685
|
-
process.on("exit", () => {
|
|
686
|
-
|
|
687
|
-
|
|
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
|
+
);
|
|
688
1426
|
} else {
|
|
689
1427
|
for (const p of parts) {
|
|
690
1428
|
console.log(`${p.part} [${p.verdict}]`);
|
|
@@ -693,9 +1431,16 @@ if (flags.json) {
|
|
|
693
1431
|
if (p.verdict !== "ok" && p.owner) console.log(` who: ${p.owner}`);
|
|
694
1432
|
// A page gets "where"; a command gets "do". Labelling a command "where" is how a person ends up
|
|
695
1433
|
// looking for a settings page that is actually a line to run.
|
|
696
|
-
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
|
+
);
|
|
697
1438
|
console.log("");
|
|
698
1439
|
}
|
|
699
|
-
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
|
+
);
|
|
700
1445
|
}
|
|
701
1446
|
process.exit(finished ? 0 : 1);
|