@celilo/cli 1.1.0 → 1.2.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/CELILO_SUBSYSTEMS.md +12 -0
- package/package.json +2 -2
- package/src/cli/commands/hook-run.ts +5 -8
- package/src/cli/commands/system-audit.ts +2 -0
- package/src/cli/commands/system-doctor.ts +30 -1
- package/src/cli/commands/system-update.ts +2 -0
- package/src/cli/tui/audit-state.ts +2 -0
- package/src/db/schema.ts +41 -1
- package/src/hooks/artifact-retention.test.ts +136 -0
- package/src/hooks/artifact-retention.ts +159 -0
- package/src/hooks/executor.test.ts +80 -0
- package/src/hooks/executor.ts +68 -23
- package/src/hooks/test-fixtures/artifact-writing-hook.ts +25 -0
- package/src/hooks/types.ts +20 -2
- package/src/policy/module-business-baseline.ts +404 -0
- package/src/policy/no-module-business-in-core.test.ts +504 -0
- package/src/services/alerting/keys.ts +21 -1
- package/src/services/alerting/run-monitor.ts +6 -1
- package/src/services/audit/browser-pin.test.ts +167 -0
- package/src/services/audit/browser-pin.ts +185 -0
- package/src/services/audit/index.test.ts +1 -0
- package/src/services/audit/index.ts +3 -0
- package/src/services/audit/types.ts +1 -0
- package/src/services/health-runner.ts +15 -1
- package/src/services/module-deploy.ts +4 -4
- package/src/services/update/orchestrator.test.ts +1 -0
- package/src/system/browser-provisioning.test.ts +67 -0
- package/src/system/prereqs.test.ts +73 -0
- package/src/system/prereqs.ts +89 -12
package/src/system/prereqs.ts
CHANGED
|
@@ -11,13 +11,19 @@
|
|
|
11
11
|
* runtime invokers (ansible/terraform shell-outs) all consume it from
|
|
12
12
|
* here.
|
|
13
13
|
*
|
|
14
|
-
* All entries are universally
|
|
14
|
+
* All entries are universally DECLARED — no required-vs-recommended
|
|
15
15
|
* distinction. The platform is the platform; an operator who currently
|
|
16
|
-
* doesn't use Terraform still
|
|
17
|
-
* predictability beats marginal install-friction savings.
|
|
16
|
+
* doesn't use Terraform still sees a terraform row, on the theory that
|
|
17
|
+
* predictability beats marginal install-friction savings. Declaring is not
|
|
18
|
+
* the same as installing: terraform and the browser are both installed by
|
|
19
|
+
* `celilo-mgmt` only on request, and both report here either way, which is
|
|
20
|
+
* how an operator finds out a host is missing one.
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { constants, accessSync, statSync } from 'node:fs';
|
|
25
|
+
import { isAbsolute } from 'node:path';
|
|
26
|
+
import { BROWSER_EXECUTABLE_PATH } from '@celilo/capabilities';
|
|
21
27
|
|
|
22
28
|
// ── Types ─────────────────────────────────────────────────────────────
|
|
23
29
|
|
|
@@ -33,15 +39,22 @@ export type PackageManager = 'apt' | 'dnf' | 'yum' | 'pacman' | 'apk' | 'brew' |
|
|
|
33
39
|
* table below; consumers don't construct these themselves.
|
|
34
40
|
*/
|
|
35
41
|
export interface PrerequisiteSpec {
|
|
36
|
-
/**
|
|
42
|
+
/** Row label in doctor output, and the binary name when `command` is
|
|
43
|
+
* absent (e.g. 'ansible'). */
|
|
37
44
|
name: string;
|
|
38
45
|
/** One-line "what this is for" text used in doctor output. */
|
|
39
46
|
description: string;
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
47
|
+
/** What to execute, when that differs from `name`. An ABSOLUTE path
|
|
48
|
+
* switches the presence check from `command -v` to "this exact file
|
|
49
|
+
* exists and is executable" — the celilo-provisioned browser lives at
|
|
50
|
+
* a known path and is never on PATH. */
|
|
51
|
+
command?: string;
|
|
52
|
+
/** Argument that makes the tool report what the row displays
|
|
53
|
+
* (`--version` for most; `version` for terraform; `-V` for ssh;
|
|
54
|
+
* `-v` for unzip; a font pattern for fc-match). */
|
|
42
55
|
versionFlag: string;
|
|
43
|
-
/** Captures the
|
|
44
|
-
*
|
|
56
|
+
/** Captures the displayed string from that output. First capture group
|
|
57
|
+
* must be the bare value (e.g. `2.16.3`). */
|
|
45
58
|
versionRegex: RegExp;
|
|
46
59
|
/** Minimum semver. `null` means presence-only — any version OK. */
|
|
47
60
|
minVersion: string | null;
|
|
@@ -54,9 +67,9 @@ export interface PrerequisiteSpec {
|
|
|
54
67
|
export interface PrereqCheck {
|
|
55
68
|
name: string;
|
|
56
69
|
description: string;
|
|
57
|
-
/**
|
|
70
|
+
/** The tool was found — on PATH, or at its declared absolute path. */
|
|
58
71
|
present: boolean;
|
|
59
|
-
/** Resolved
|
|
72
|
+
/** Resolved executable (or null when absent). */
|
|
60
73
|
binaryPath: string | null;
|
|
61
74
|
/** Version captured from `--version` output, or null on
|
|
62
75
|
* parse-fail / not-present. */
|
|
@@ -139,6 +152,42 @@ export const PREREQUISITES: PrerequisiteSpec[] = [
|
|
|
139
152
|
versionRegex: /UnZip\s+(\d+\.\d+)/,
|
|
140
153
|
minVersion: null,
|
|
141
154
|
},
|
|
155
|
+
// Declared unconditionally even though installation is opt-in — same as
|
|
156
|
+
// terraform above. `system doctor` is the ONLY place an operator learns
|
|
157
|
+
// this host has no browser, because a not-provisioned browser is
|
|
158
|
+
// deliberately not a check failure.
|
|
159
|
+
//
|
|
160
|
+
// The check probes the PROVISIONED PATH and must never ask Playwright
|
|
161
|
+
// which executable it would use: `chromium.executablePath()` reports the
|
|
162
|
+
// FULL browser while a headless launch opens the SHELL — measured
|
|
163
|
+
// disagreeing on one machine in one run. On a shell-only host that would
|
|
164
|
+
// validate a path which does not exist while the binary that actually
|
|
165
|
+
// runs is fine. Running it is also what separates a real install from a
|
|
166
|
+
// build directory with no binary in it, which satisfies a path test and
|
|
167
|
+
// then fails at launch.
|
|
168
|
+
{
|
|
169
|
+
name: 'browser',
|
|
170
|
+
description: 'Runs browser-driven module health checks',
|
|
171
|
+
command: BROWSER_EXECUTABLE_PATH,
|
|
172
|
+
versionFlag: '--version',
|
|
173
|
+
// e.g. "Chromium 148.0.7778.0"
|
|
174
|
+
versionRegex: /(\d+\.\d+\.\d+(?:\.\d+)?)/,
|
|
175
|
+
minVersion: null,
|
|
176
|
+
},
|
|
177
|
+
// Fonts belong here rather than in the record consumers read: the party
|
|
178
|
+
// who needs to know a retained screenshot has legible glyphs is the
|
|
179
|
+
// operator looking at it. `fc-match sans-serif` resolves an actual face,
|
|
180
|
+
// so it answers "is there a text font" rather than "is fontconfig
|
|
181
|
+
// installed", and the family name is what the row displays.
|
|
182
|
+
{
|
|
183
|
+
name: 'fonts',
|
|
184
|
+
description: 'Legible text in browser screenshots',
|
|
185
|
+
command: 'fc-match',
|
|
186
|
+
versionFlag: 'sans-serif',
|
|
187
|
+
// e.g. 'DejaVuSans.ttf: "DejaVu Sans" "Book"'
|
|
188
|
+
versionRegex: /"([^"]+)"/,
|
|
189
|
+
minVersion: null,
|
|
190
|
+
},
|
|
142
191
|
];
|
|
143
192
|
|
|
144
193
|
// ── Per-OS install hints ──────────────────────────────────────────────
|
|
@@ -210,6 +259,14 @@ const INSTALL_HINTS: Record<string, Partial<Record<PackageManager, string>>> = {
|
|
|
210
259
|
apk: 'sudo apk add curl',
|
|
211
260
|
// macOS ships /usr/bin/curl built-in.
|
|
212
261
|
},
|
|
262
|
+
fonts: {
|
|
263
|
+
apt: 'sudo apt-get install fontconfig fonts-dejavu-core',
|
|
264
|
+
dnf: 'sudo dnf install fontconfig dejavu-sans-fonts',
|
|
265
|
+
yum: 'sudo yum install fontconfig dejavu-sans-fonts',
|
|
266
|
+
pacman: 'sudo pacman -S fontconfig ttf-dejavu',
|
|
267
|
+
apk: 'sudo apk add fontconfig font-dejavu',
|
|
268
|
+
brew: 'brew install fontconfig',
|
|
269
|
+
},
|
|
213
270
|
unzip: {
|
|
214
271
|
apt: 'sudo apt-get install unzip',
|
|
215
272
|
dnf: 'sudo dnf install unzip',
|
|
@@ -227,6 +284,9 @@ const INSTALL_HINTS: Record<string, Partial<Record<PackageManager, string>>> = {
|
|
|
227
284
|
const FALLBACK_HINTS: Record<string, string> = {
|
|
228
285
|
bun: 'See https://bun.sh/install',
|
|
229
286
|
terraform: 'See https://developer.hashicorp.com/terraform/install',
|
|
287
|
+
// Not a package: celilo installs it itself, on request.
|
|
288
|
+
browser:
|
|
289
|
+
'celilo module config set celilo-mgmt install_browser true && celilo module deploy celilo-mgmt',
|
|
230
290
|
};
|
|
231
291
|
|
|
232
292
|
// ── Detection ─────────────────────────────────────────────────────────
|
|
@@ -301,13 +361,30 @@ export function compareVersions(a: string, b: string): number {
|
|
|
301
361
|
* is a misbehaving binary or a stuck PATH lookup; either way we
|
|
302
362
|
* shouldn't wedge the doctor command waiting on it.
|
|
303
363
|
*/
|
|
364
|
+
/**
|
|
365
|
+
* Resolve an absolute-path prerequisite. Returns the path only when it is
|
|
366
|
+
* a real file that is executable — a symlink pointing at nothing, or a
|
|
367
|
+
* build directory with no binary in it, resolves to null.
|
|
368
|
+
*/
|
|
369
|
+
function executableAt(path: string): string | null {
|
|
370
|
+
try {
|
|
371
|
+
// statSync follows symlinks, so a dangling link throws here.
|
|
372
|
+
if (!statSync(path).isFile()) return null;
|
|
373
|
+
accessSync(path, constants.X_OK);
|
|
374
|
+
return path;
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
304
380
|
export function checkPrerequisite(
|
|
305
381
|
spec: PrerequisiteSpec,
|
|
306
382
|
pm: PackageManager = detectPackageManager(),
|
|
307
383
|
): PrereqCheck {
|
|
308
384
|
const installHint = getInstallHint(spec.name, pm);
|
|
309
385
|
|
|
310
|
-
const
|
|
386
|
+
const command = spec.command ?? spec.name;
|
|
387
|
+
const binaryPath = isAbsolute(command) ? executableAt(command) : Bun.which(command);
|
|
311
388
|
if (!binaryPath) {
|
|
312
389
|
return {
|
|
313
390
|
name: spec.name,
|
|
@@ -322,7 +399,7 @@ export function checkPrerequisite(
|
|
|
322
399
|
|
|
323
400
|
let version: string | null = null;
|
|
324
401
|
try {
|
|
325
|
-
const result = spawnSync(
|
|
402
|
+
const result = spawnSync(binaryPath, [spec.versionFlag], {
|
|
326
403
|
encoding: 'utf-8',
|
|
327
404
|
timeout: 5000,
|
|
328
405
|
// Some tools (notably git on macOS) refuse to run with a
|