@valescoagency/runway 0.1.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/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/cli.js +88 -0
- package/dist/commands/doctor.js +464 -0
- package/dist/commands/init.js +421 -0
- package/dist/commands/run.js +61 -0
- package/dist/commands/upgrade-repo.js +325 -0
- package/dist/commands/upgrade.js +177 -0
- package/dist/config.js +45 -0
- package/dist/github.js +34 -0
- package/dist/linear.js +81 -0
- package/dist/orchestrator.js +191 -0
- package/dist/prompts.js +40 -0
- package/package.json +63 -0
- package/templates/.env.schema.target-repo +32 -0
- package/templates/Dockerfile.claude-code.base +55 -0
- package/templates/dockerfile-varlock.snippet +43 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Usage
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
export function printDoctorUsage() {
|
|
8
|
+
console.log(`runway doctor — read-only preflight diagnostic
|
|
9
|
+
|
|
10
|
+
Reports on host tooling, environment, repo state, and the agent docker
|
|
11
|
+
image. Never modifies anything. Use this when something stopped working
|
|
12
|
+
and you want a sanity report.
|
|
13
|
+
|
|
14
|
+
USAGE
|
|
15
|
+
cd /path/to/your/repo # (or runway's own clone)
|
|
16
|
+
runway doctor [--tier=1|2] [--detailed] [--json]
|
|
17
|
+
|
|
18
|
+
OPTIONS
|
|
19
|
+
--tier=N Force tier 1 or 2 checks. Default: auto-detect from
|
|
20
|
+
.sandcastle/Dockerfile + .env.schema in cwd.
|
|
21
|
+
--detailed Print version numbers, paths, and full error output.
|
|
22
|
+
Default mode is terse (just ✓/✗/⚠).
|
|
23
|
+
--json Machine-readable output for CI / scripted health checks.
|
|
24
|
+
--help, -h Show this help.
|
|
25
|
+
|
|
26
|
+
SECTIONS REPORTED
|
|
27
|
+
1. Host tooling git, node ≥22, docker (daemon), gh (auth);
|
|
28
|
+
tier 2 also checks varlock + op CLI.
|
|
29
|
+
2. Environment LINEAR_API_KEY (set/unset; never printed);
|
|
30
|
+
tier 2 also checks OP_SERVICE_ACCOUNT_TOKEN.
|
|
31
|
+
3. Repo state git repo? clean tree? branch? scaffold detected?
|
|
32
|
+
4. Docker image sandcastle:<sanitized-cwd> exists; user/group match.
|
|
33
|
+
|
|
34
|
+
EXIT CODES
|
|
35
|
+
0 All required checks pass (warnings allowed).
|
|
36
|
+
1 At least one ✗ in tooling, environment, or image checks.
|
|
37
|
+
Repo-state issues (no init, dirty tree) are warnings only.
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Arg parsing
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
function parseDoctorArgs(argv) {
|
|
44
|
+
let tierOverride;
|
|
45
|
+
let detailed = false;
|
|
46
|
+
let json = false;
|
|
47
|
+
for (const arg of argv) {
|
|
48
|
+
if (arg === "--help" || arg === "-h") {
|
|
49
|
+
printDoctorUsage();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
else if (arg === "--tier=1") {
|
|
53
|
+
tierOverride = 1;
|
|
54
|
+
}
|
|
55
|
+
else if (arg === "--tier=2") {
|
|
56
|
+
tierOverride = 2;
|
|
57
|
+
}
|
|
58
|
+
else if (arg === "--detailed") {
|
|
59
|
+
detailed = true;
|
|
60
|
+
}
|
|
61
|
+
else if (arg === "--json") {
|
|
62
|
+
json = true;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { tierOverride, detailed, json };
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Entry point
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
export async function doctorCommand(argv) {
|
|
74
|
+
const opts = parseDoctorArgs(argv);
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
const repo = detectRepoState(cwd);
|
|
77
|
+
const tier = opts.tierOverride ?? repo.tier;
|
|
78
|
+
// If no scaffold AND no override → still run host-tooling, but skip the rest.
|
|
79
|
+
// We pick tier 2 as the "default check posture" for tooling so varlock/op
|
|
80
|
+
// are reported (Valesco's standard), unless the user explicitly says tier 1.
|
|
81
|
+
const tierForToolingChecks = tier ?? 2;
|
|
82
|
+
const initialised = tier !== null;
|
|
83
|
+
const sections = [];
|
|
84
|
+
sections.push(await checkHostTooling(tierForToolingChecks));
|
|
85
|
+
if (initialised || opts.tierOverride !== undefined) {
|
|
86
|
+
sections.push(checkEnvironment(tierForToolingChecks));
|
|
87
|
+
sections.push(await checkRepoState(cwd, repo));
|
|
88
|
+
sections.push(await checkDockerImage(cwd));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Push placeholder skipped sections so JSON output stays well-shaped.
|
|
92
|
+
sections.push(skippedSection("Environment"));
|
|
93
|
+
sections.push(skippedSection("Repo state"));
|
|
94
|
+
sections.push(skippedSection("Docker image"));
|
|
95
|
+
}
|
|
96
|
+
// Render
|
|
97
|
+
if (opts.json) {
|
|
98
|
+
renderJson(sections, tier, initialised);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
renderText(sections, tier, initialised, opts.detailed);
|
|
102
|
+
}
|
|
103
|
+
// Exit code: required-check failures = 1.
|
|
104
|
+
// Sections 1, 2, 4 are "required"; section 3 (repo state) is informational.
|
|
105
|
+
const requiredSections = [sections[0], sections[1], sections[3]];
|
|
106
|
+
const failed = requiredSections.some((s) => s?.ran && [...s.checks.values()].some((c) => c.status === "fail"));
|
|
107
|
+
process.exit(failed ? 1 : 0);
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Repo / tier detection
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
function detectRepoState(cwd) {
|
|
113
|
+
const hasDockerfile = existsSync(join(cwd, ".sandcastle", "Dockerfile"));
|
|
114
|
+
const hasSchema = existsSync(join(cwd, ".env.schema"));
|
|
115
|
+
let tier = null;
|
|
116
|
+
if (hasSchema) {
|
|
117
|
+
try {
|
|
118
|
+
const schema = readFileSync(join(cwd, ".env.schema"), "utf8");
|
|
119
|
+
if (/ANTHROPIC_API_KEY\s*=\s*exec\(/.test(schema)) {
|
|
120
|
+
tier = 2;
|
|
121
|
+
}
|
|
122
|
+
else if (hasDockerfile) {
|
|
123
|
+
tier = 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// unreadable schema — treat as un-initialised
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (hasDockerfile) {
|
|
131
|
+
tier = 1;
|
|
132
|
+
}
|
|
133
|
+
return { tier, hasDockerfile, hasSchema };
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Section: Host tooling
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
async function checkHostTooling(tier) {
|
|
139
|
+
const checks = new Map();
|
|
140
|
+
checks.set("git", await checkBinaryVersion("git", ["--version"]));
|
|
141
|
+
const nodeVersion = process.versions.node;
|
|
142
|
+
const nodeMajor = Number.parseInt(nodeVersion.split(".")[0] ?? "0", 10);
|
|
143
|
+
checks.set("node", nodeMajor >= 22
|
|
144
|
+
? { status: "ok", label: "node", detail: `v${nodeVersion}` }
|
|
145
|
+
: {
|
|
146
|
+
status: "fail",
|
|
147
|
+
label: "node",
|
|
148
|
+
detail: `v${nodeVersion} — node ≥ 22 required`,
|
|
149
|
+
});
|
|
150
|
+
checks.set("docker", await checkDockerDaemon());
|
|
151
|
+
checks.set("gh", await checkGhAuth());
|
|
152
|
+
if (tier === 2) {
|
|
153
|
+
checks.set("varlock", await checkBinaryVersion("varlock", ["--version"]));
|
|
154
|
+
checks.set("op", await checkBinaryVersion("op", ["--version"]));
|
|
155
|
+
}
|
|
156
|
+
return { title: "Host tooling", checks, ran: true };
|
|
157
|
+
}
|
|
158
|
+
async function checkBinaryVersion(bin, args) {
|
|
159
|
+
try {
|
|
160
|
+
const { stdout, stderr } = await execa(bin, args, { reject: false });
|
|
161
|
+
const out = (stdout || stderr || "").split("\n")[0]?.trim() ?? "";
|
|
162
|
+
return { status: "ok", label: bin, detail: out || "(installed)" };
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return {
|
|
166
|
+
status: "fail",
|
|
167
|
+
label: bin,
|
|
168
|
+
detail: `${bin} not on PATH`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function checkDockerDaemon() {
|
|
173
|
+
try {
|
|
174
|
+
await execa("docker", ["--version"], { reject: true });
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return {
|
|
178
|
+
status: "fail",
|
|
179
|
+
label: "docker",
|
|
180
|
+
detail: "docker not on PATH (Docker Desktop or Podman)",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await execa("docker", ["info"], { reject: true });
|
|
185
|
+
return { status: "ok", label: "docker", detail: "daemon up" };
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
return {
|
|
189
|
+
status: "fail",
|
|
190
|
+
label: "docker",
|
|
191
|
+
detail: `daemon not running — ${errMsg(err)}`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function checkGhAuth() {
|
|
196
|
+
try {
|
|
197
|
+
await execa("gh", ["--version"], { reject: true });
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return {
|
|
201
|
+
status: "fail",
|
|
202
|
+
label: "gh",
|
|
203
|
+
detail: "gh not on PATH",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const { stdout, stderr } = await execa("gh", ["auth", "status"], {
|
|
208
|
+
reject: true,
|
|
209
|
+
});
|
|
210
|
+
// First non-empty line of `gh auth status` is the host header.
|
|
211
|
+
const summary = (stdout || stderr || "")
|
|
212
|
+
.split("\n")
|
|
213
|
+
.map((s) => s.trim())
|
|
214
|
+
.find((s) => s.length > 0);
|
|
215
|
+
return {
|
|
216
|
+
status: "ok",
|
|
217
|
+
label: "gh",
|
|
218
|
+
detail: summary ?? "authenticated",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
return {
|
|
223
|
+
status: "fail",
|
|
224
|
+
label: "gh",
|
|
225
|
+
detail: `not authenticated — ${errMsg(err)}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Section: Environment
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
function checkEnvironment(tier) {
|
|
233
|
+
const checks = new Map();
|
|
234
|
+
checks.set("LINEAR_API_KEY", envSet("LINEAR_API_KEY", "fail"));
|
|
235
|
+
if (tier === 2) {
|
|
236
|
+
// Tier 2: needed by varlock to resolve op:// refs in the container.
|
|
237
|
+
checks.set("OP_SERVICE_ACCOUNT_TOKEN", envSet("OP_SERVICE_ACCOUNT_TOKEN", "fail"));
|
|
238
|
+
}
|
|
239
|
+
return { title: "Environment", checks, ran: true };
|
|
240
|
+
}
|
|
241
|
+
function envSet(name, missingStatus) {
|
|
242
|
+
const v = process.env[name];
|
|
243
|
+
if (v && v.trim().length > 0) {
|
|
244
|
+
return { status: "ok", label: name, detail: "set" };
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
status: missingStatus,
|
|
248
|
+
label: name,
|
|
249
|
+
detail: "unset or empty",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// Section: Repo state
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
async function checkRepoState(cwd, repo) {
|
|
256
|
+
const checks = new Map();
|
|
257
|
+
// Is this a git repo?
|
|
258
|
+
let isGitRepo = false;
|
|
259
|
+
try {
|
|
260
|
+
await execa("git", ["rev-parse", "--git-dir"], { cwd });
|
|
261
|
+
isGitRepo = true;
|
|
262
|
+
checks.set("git_repo", { status: "ok", label: "git repo" });
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
checks.set("git_repo", {
|
|
266
|
+
status: "warn",
|
|
267
|
+
label: "git repo",
|
|
268
|
+
detail: "not inside a git repo",
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (isGitRepo) {
|
|
272
|
+
// Working tree clean? (info-only)
|
|
273
|
+
try {
|
|
274
|
+
const { stdout } = await execa("git", ["status", "--porcelain"], { cwd });
|
|
275
|
+
if (stdout.trim().length === 0) {
|
|
276
|
+
checks.set("clean_tree", { status: "ok", label: "working tree clean" });
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const count = stdout.trim().split("\n").length;
|
|
280
|
+
checks.set("clean_tree", {
|
|
281
|
+
status: "warn",
|
|
282
|
+
label: "working tree clean",
|
|
283
|
+
detail: `${count} change(s) — info only`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
checks.set("clean_tree", {
|
|
289
|
+
status: "warn",
|
|
290
|
+
label: "working tree clean",
|
|
291
|
+
detail: "git status failed",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// Current branch
|
|
295
|
+
try {
|
|
296
|
+
const { stdout } = await execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
|
|
297
|
+
checks.set("branch", {
|
|
298
|
+
status: "ok",
|
|
299
|
+
label: "branch",
|
|
300
|
+
detail: stdout.trim() || "(detached)",
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
checks.set("branch", {
|
|
305
|
+
status: "warn",
|
|
306
|
+
label: "branch",
|
|
307
|
+
detail: "no commits yet",
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Scaffold detection
|
|
312
|
+
if (repo.tier === null) {
|
|
313
|
+
checks.set("scaffold", {
|
|
314
|
+
status: "warn",
|
|
315
|
+
label: "runway scaffold",
|
|
316
|
+
detail: "no runway scaffold detected — run `runway init` first " +
|
|
317
|
+
"(only host-tooling diagnostics apply until then)",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
checks.set("scaffold", {
|
|
322
|
+
status: "ok",
|
|
323
|
+
label: "runway scaffold",
|
|
324
|
+
detail: `tier ${repo.tier} — Dockerfile=${repo.hasDockerfile} schema=${repo.hasSchema}`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return { title: "Repo state", checks, ran: true };
|
|
328
|
+
}
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// Section: Docker image
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
async function checkDockerImage(cwd) {
|
|
333
|
+
const checks = new Map();
|
|
334
|
+
const imageName = expectedImageName(cwd);
|
|
335
|
+
try {
|
|
336
|
+
const { stdout } = await execa("docker", ["image", "inspect", imageName, "--format", "{{.Config.User}}"], { reject: true });
|
|
337
|
+
const imageUser = stdout.trim();
|
|
338
|
+
checks.set("image_present", {
|
|
339
|
+
status: "ok",
|
|
340
|
+
label: `image ${imageName}`,
|
|
341
|
+
detail: "present",
|
|
342
|
+
});
|
|
343
|
+
// User mismatch warning per sandcastle's pre-flight diagnostic:
|
|
344
|
+
// image was built for one UID:GID; if the host UID:GID differs the
|
|
345
|
+
// bind-mount permissions will be off.
|
|
346
|
+
const hostUid = process.getuid?.() ?? 1000;
|
|
347
|
+
const hostGid = process.getgid?.() ?? 1000;
|
|
348
|
+
const expected = `${hostUid}:${hostGid}`;
|
|
349
|
+
if (imageUser && imageUser !== expected) {
|
|
350
|
+
checks.set("image_user", {
|
|
351
|
+
status: "warn",
|
|
352
|
+
label: "image user matches host UID:GID",
|
|
353
|
+
detail: `image User=${imageUser}, host=${expected} — rebuild with --build-arg AGENT_UID/AGENT_GID`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
checks.set("image_user", {
|
|
358
|
+
status: "ok",
|
|
359
|
+
label: "image user matches host UID:GID",
|
|
360
|
+
detail: imageUser ? `User=${imageUser}` : `User unset (root); host=${expected}`,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
checks.set("image_present", {
|
|
366
|
+
status: "fail",
|
|
367
|
+
label: `image ${imageName}`,
|
|
368
|
+
detail: `not built — ${errMsg(err)}`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return { title: "Docker image", checks, ran: true };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Sanitize the cwd's basename the same way sandcastle's `defaultImageName`
|
|
375
|
+
* does: lowercase, replace any char outside `[a-z0-9_.-]` with `-`, fall
|
|
376
|
+
* back to "local" if empty. Prefix `sandcastle:`.
|
|
377
|
+
*/
|
|
378
|
+
function expectedImageName(cwd) {
|
|
379
|
+
const dirName = cwd
|
|
380
|
+
.replace(/[\\/]+$/, "")
|
|
381
|
+
.split(/[\\/]/)
|
|
382
|
+
.pop() ?? "local";
|
|
383
|
+
const sanitized = dirName.toLowerCase().replace(/[^a-z0-9_.-]/g, "-") || "local";
|
|
384
|
+
return `sandcastle:${sanitized}`;
|
|
385
|
+
}
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Helpers
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
function skippedSection(title) {
|
|
390
|
+
return { title, checks: new Map(), ran: false };
|
|
391
|
+
}
|
|
392
|
+
function errMsg(err) {
|
|
393
|
+
if (err instanceof Error)
|
|
394
|
+
return err.message.split("\n")[0] ?? err.message;
|
|
395
|
+
return String(err);
|
|
396
|
+
}
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Renderers
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
function statusGlyph(s) {
|
|
401
|
+
switch (s) {
|
|
402
|
+
case "ok":
|
|
403
|
+
return "✓";
|
|
404
|
+
case "fail":
|
|
405
|
+
return "✗";
|
|
406
|
+
case "warn":
|
|
407
|
+
return "⚠";
|
|
408
|
+
case "skip":
|
|
409
|
+
return "·";
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function renderText(sections, tier, initialised, detailed) {
|
|
413
|
+
const tierLabel = tier === null ? "not initialized" : `tier ${tier}`;
|
|
414
|
+
console.log(`runway doctor — ${tierLabel}`);
|
|
415
|
+
console.log("");
|
|
416
|
+
for (const section of sections) {
|
|
417
|
+
console.log(`${section.title}`);
|
|
418
|
+
if (!section.ran) {
|
|
419
|
+
console.log(" · skipped (no runway scaffold detected)");
|
|
420
|
+
console.log("");
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
for (const check of section.checks.values()) {
|
|
424
|
+
const glyph = statusGlyph(check.status);
|
|
425
|
+
if (detailed && check.detail) {
|
|
426
|
+
console.log(` ${glyph} ${check.label} — ${check.detail}`);
|
|
427
|
+
}
|
|
428
|
+
else if (check.status !== "ok" && check.detail) {
|
|
429
|
+
// Always surface non-OK detail even in terse mode.
|
|
430
|
+
console.log(` ${glyph} ${check.label} — ${check.detail}`);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
console.log(` ${glyph} ${check.label}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
console.log("");
|
|
437
|
+
}
|
|
438
|
+
if (!initialised) {
|
|
439
|
+
console.log("Hint: cwd has no runway scaffold. Run `runway init` from a target repo " +
|
|
440
|
+
"to enable the full diagnostic.");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function renderJson(sections, tier, _initialised) {
|
|
444
|
+
const sectionKey = (title) => title.toLowerCase().replace(/\s+/g, "_");
|
|
445
|
+
const checksByName = (s) => {
|
|
446
|
+
const out = {};
|
|
447
|
+
for (const [k, v] of s.checks.entries())
|
|
448
|
+
out[k] = v.status;
|
|
449
|
+
return out;
|
|
450
|
+
};
|
|
451
|
+
// Determine `ok` from required sections (tooling, env, docker image).
|
|
452
|
+
const requiredSections = [sections[0], sections[1], sections[3]];
|
|
453
|
+
const ok = !requiredSections.some((s) => s?.ran && [...s.checks.values()].some((c) => c.status === "fail"));
|
|
454
|
+
const checks = {};
|
|
455
|
+
for (const s of sections) {
|
|
456
|
+
checks[sectionKey(s.title)] = s.ran ? checksByName(s) : null;
|
|
457
|
+
}
|
|
458
|
+
const payload = {
|
|
459
|
+
ok,
|
|
460
|
+
tier,
|
|
461
|
+
checks,
|
|
462
|
+
};
|
|
463
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
464
|
+
}
|