@a11ign/screenreader-fleet 0.2.0 → 0.4.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/dist/capture-client.d.mts +0 -1
- package/dist/capture-client.mjs +144 -306
- package/dist/check-worker-code.d.mts +0 -1
- package/dist/check-worker-code.mjs +42 -110
- package/dist/cli-flags.d.mts +0 -1
- package/dist/cli-flags.mjs +33 -179
- package/dist/code-drift.d.mts +4 -4
- package/dist/command-line-census.d.mts +0 -1
- package/dist/compare-workers.d.mts +0 -1
- package/dist/compare-workers.mjs +383 -255
- package/dist/control-plane-isolation.d.mts +0 -1
- package/dist/doctor.d.mts +0 -1
- package/dist/doctor.mjs +361 -809
- package/dist/fleet-consistency.d.mts +0 -1
- package/dist/fleet-consistency.mjs +155 -379
- package/dist/fleet-env.d.mts +1 -2
- package/dist/fleet-env.mjs +148 -438
- package/dist/fleet-scripts.d.mts +0 -1
- package/dist/git-safe-env.d.mts +0 -1
- package/dist/guest-run.d.mts +0 -1
- package/dist/host-address.d.mts +0 -1
- package/dist/host-address.mjs +19 -90
- package/dist/host-capacity.d.mts +0 -1
- package/dist/host-capacity.mjs +22 -136
- package/dist/host-metrics.d.mts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +233 -0
- package/dist/local-vm.d.ts +0 -1
- package/dist/measure-guard.d.mts +0 -1
- package/dist/normalise-fleet.d.mts +0 -1
- package/dist/npm-cli-executable.d.mts +0 -1
- package/dist/probe-outcome.d.mts +0 -1
- package/dist/probe-outcome.mjs +50 -96
- package/dist/protocol-guard.d.mts +0 -1
- package/dist/source-walk.d.mts +0 -1
- package/dist/src_fleet-scripts_mjs.mjs +16 -0
- package/dist/src_git-safe-env_mjs.mjs +9 -0
- package/dist/transient-fault.d.mts +0 -1
- package/dist/transient-fault.mjs +21 -81
- package/dist/utm-deprecated.d.mts +0 -1
- package/dist/worker-code-check.d.mts +0 -1
- package/dist/worker-code-check.mjs +127 -83
- package/dist/worker-health.d.mts +0 -1
- package/dist/worker-health.mjs +16 -61
- package/dist/worker-http.d.mts +0 -1
- package/dist/worker-http.mjs +42 -234
- package/dist/worker-stats.d.mts +0 -1
- package/package.json +13 -7
- package/src/local-worker/worker-ctl.sh +1 -1
- package/src/provisioning/bootstrap-windows-worker.ps1 +1 -1
- package/dist/capture-client.d.mts.map +0 -1
- package/dist/capture-client.mjs.map +0 -1
- package/dist/check-worker-code.d.mts.map +0 -1
- package/dist/check-worker-code.mjs.map +0 -1
- package/dist/cli-flags.d.mts.map +0 -1
- package/dist/cli-flags.mjs.map +0 -1
- package/dist/code-drift.d.mts.map +0 -1
- package/dist/code-drift.mjs +0 -284
- package/dist/code-drift.mjs.map +0 -1
- package/dist/command-line-census.d.mts.map +0 -1
- package/dist/command-line-census.mjs +0 -96
- package/dist/command-line-census.mjs.map +0 -1
- package/dist/compare-workers.d.mts.map +0 -1
- package/dist/compare-workers.mjs.map +0 -1
- package/dist/control-plane-isolation.d.mts.map +0 -1
- package/dist/control-plane-isolation.mjs +0 -67
- package/dist/control-plane-isolation.mjs.map +0 -1
- package/dist/deploy-worker.d.mts +0 -3
- package/dist/deploy-worker.d.mts.map +0 -1
- package/dist/deploy-worker.mjs +0 -333
- package/dist/deploy-worker.mjs.map +0 -1
- package/dist/doctor.d.mts.map +0 -1
- package/dist/doctor.mjs.map +0 -1
- package/dist/fleet-consistency.d.mts.map +0 -1
- package/dist/fleet-consistency.mjs.map +0 -1
- package/dist/fleet-env.d.mts.map +0 -1
- package/dist/fleet-env.mjs.map +0 -1
- package/dist/fleet-scripts.d.mts.map +0 -1
- package/dist/fleet-scripts.mjs +0 -41
- package/dist/fleet-scripts.mjs.map +0 -1
- package/dist/git-safe-env.d.mts.map +0 -1
- package/dist/git-safe-env.mjs +0 -44
- package/dist/git-safe-env.mjs.map +0 -1
- package/dist/guest-run.d.mts.map +0 -1
- package/dist/guest-run.mjs +0 -164
- package/dist/guest-run.mjs.map +0 -1
- package/dist/host-address.d.mts.map +0 -1
- package/dist/host-address.mjs.map +0 -1
- package/dist/host-capacity.d.mts.map +0 -1
- package/dist/host-capacity.mjs.map +0 -1
- package/dist/host-metrics.d.mts.map +0 -1
- package/dist/host-metrics.mjs +0 -201
- package/dist/host-metrics.mjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -25
- package/dist/index.js.map +0 -1
- package/dist/local-vm.d.ts.map +0 -1
- package/dist/local-vm.js +0 -360
- package/dist/local-vm.js.map +0 -1
- package/dist/measure-guard.d.mts.map +0 -1
- package/dist/measure-guard.mjs +0 -73
- package/dist/measure-guard.mjs.map +0 -1
- package/dist/normalise-fleet.d.mts.map +0 -1
- package/dist/normalise-fleet.mjs +0 -76
- package/dist/normalise-fleet.mjs.map +0 -1
- package/dist/npm-cli-executable.d.mts.map +0 -1
- package/dist/npm-cli-executable.mjs +0 -159
- package/dist/npm-cli-executable.mjs.map +0 -1
- package/dist/probe-outcome.d.mts.map +0 -1
- package/dist/probe-outcome.mjs.map +0 -1
- package/dist/protocol-guard.d.mts.map +0 -1
- package/dist/protocol-guard.mjs +0 -121
- package/dist/protocol-guard.mjs.map +0 -1
- package/dist/source-walk.d.mts.map +0 -1
- package/dist/source-walk.mjs +0 -56
- package/dist/source-walk.mjs.map +0 -1
- package/dist/transient-fault.d.mts.map +0 -1
- package/dist/transient-fault.mjs.map +0 -1
- package/dist/utm-deprecated.d.mts.map +0 -1
- package/dist/utm-deprecated.mjs +0 -23
- package/dist/utm-deprecated.mjs.map +0 -1
- package/dist/worker-code-check.d.mts.map +0 -1
- package/dist/worker-code-check.mjs.map +0 -1
- package/dist/worker-health.d.mts.map +0 -1
- package/dist/worker-health.mjs.map +0 -1
- package/dist/worker-http.d.mts.map +0 -1
- package/dist/worker-http.mjs.map +0 -1
- package/dist/worker-stats.d.mts.map +0 -1
- package/dist/worker-stats.mjs +0 -143
- package/dist/worker-stats.mjs.map +0 -1
|
@@ -1,100 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// @ts-check
|
|
3
|
-
// Is every worker running the code in this checkout?
|
|
4
|
-
//
|
|
5
|
-
// npm run worker:code
|
|
6
|
-
//
|
|
7
|
-
// Deploying is push-then-restart and both halves fail silently: `utmctl exec` reports success
|
|
8
|
-
// whether or not it ran, so a worker can serve the previous process indefinitely. Reading the
|
|
9
|
-
// guest's file hash does not help, because that read goes through exec too -- when exec is
|
|
10
|
-
// dead the check returns empty rather than mismatched, which reads as a flaky tool instead of a
|
|
11
|
-
// failed deploy. That cost an hour, and the stale workers looked exactly like a logic bug.
|
|
12
|
-
//
|
|
13
|
-
// This asks each worker over HTTP, which is reachable exactly when the worker is usable and
|
|
14
|
-
// involves no guest agent. Exit 0 when every worker matches, 1 otherwise.
|
|
15
2
|
import { realpathSync } from "node:fs";
|
|
16
3
|
import { pathToFileURL } from "node:url";
|
|
17
4
|
import { execFileSync } from "node:child_process";
|
|
18
|
-
import { fleetScriptPaths } from "./fleet-scripts.mjs";
|
|
19
|
-
import { configuredWorkers, inventoryWorkerUrls, resolveWorkerPool } from "./fleet-env.mjs";
|
|
20
|
-
// The comparison, the remedy and the expected hash live in ONE place, because the capture entry points ask
|
|
21
|
-
// the same question before every run and a second copy of "is this worker stale" is a second answer.
|
|
22
|
-
import { expectedWorkerCode, codeDrift, remedyLines } from "./worker-code-check.mjs";
|
|
23
|
-
import { refuseUnknownFlags } from "./cli-flags.mjs";
|
|
24
5
|
import { errorText } from "@a11ign/screenreader-worker/error-text";
|
|
6
|
+
import { fleetScriptPaths } from "./src_fleet-scripts_mjs.mjs";
|
|
7
|
+
import { inventoryWorkerUrls, configuredWorkers, resolveWorkerPool } from "./fleet-env.mjs";
|
|
8
|
+
import { expectedWorkerCode, remedyLines, codeDrift } from "./worker-code-check.mjs";
|
|
9
|
+
import { refuseUnknownFlags } from "./cli-flags.mjs";
|
|
25
10
|
import { requestJson } from "./worker-http.mjs";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* An unrecognised flag is otherwise IGNORED, so it runs the default and reports success.
|
|
31
|
-
*/
|
|
32
|
-
refuseUnknownFlags([], { entry: import.meta.url, command: "npm run worker:code" });
|
|
33
|
-
// Resolved from THIS module: the fleet scripts ship with this package, so a cwd-relative path was only ever
|
|
34
|
-
// right when run from the repo root.
|
|
11
|
+
refuseUnknownFlags([], {
|
|
12
|
+
entry: import.meta.url,
|
|
13
|
+
command: "npm run worker:code"
|
|
14
|
+
});
|
|
35
15
|
const CTL = fleetScriptPaths().workerCtl;
|
|
36
|
-
// /health now reports installed runtime versions as well as the code hash. The first
|
|
37
|
-
// request after a Windows boot may need PowerShell file-version discovery, so four seconds
|
|
38
|
-
// was too tight and made a healthy worker look unreachable.
|
|
39
16
|
const HEALTH_TIMEOUT_MS = 15000;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* `A11Y_WORKERS` set reported `5 stale worker(s)`. One env var apart, and the quiet answer was the wrong one.
|
|
47
|
-
*
|
|
48
|
-
* That is `lab:inventory`'s lesson at a different layer — *"'none here' and 'none anywhere' are different
|
|
49
|
-
* answers, and it now refuses to turn the first into the second"* — and it lands harder here, because this
|
|
50
|
-
* command exists to stop a corpus being captured on the wrong code. A false clean from it is the failure it
|
|
51
|
-
* was written to prevent, delivered by the tool itself.
|
|
52
|
-
*
|
|
53
|
-
* The inventory was already imported and already read, twelve lines below, to print the REMEDY. So the
|
|
54
|
-
* command could name the five workers it should have checked while insisting it had none to check.
|
|
55
|
-
*
|
|
56
|
-
* Reading it here is safe in a way it would not be for a capture run: this probes `/health` and starts
|
|
57
|
-
* nothing, so the rule that naming workers means you are managing them does not apply.
|
|
58
|
-
*/
|
|
59
|
-
export function workerUrls({ named = configuredWorkers, local = localPoolUrls, inventory = inventoryWorkerUrls, } = {}) {
|
|
60
|
-
// ONE PRECEDENCE, in fleet-env.mjs. This function held its own -- named, then the LOCAL UTM POOL, then
|
|
61
|
-
// the inventory -- while `doctor` went named then inventory, so on any Mac with a registered guest the
|
|
62
|
-
// two commands described different fleets. That is the divergence the comment here used to claim it had
|
|
63
|
-
// closed; it closed the NAMED half only.
|
|
64
|
-
return resolveWorkerPool({ named, inventory, local });
|
|
17
|
+
function workerUrls({ named = configuredWorkers, local = localPoolUrls, inventory = inventoryWorkerUrls } = {}) {
|
|
18
|
+
return resolveWorkerPool({
|
|
19
|
+
named,
|
|
20
|
+
inventory,
|
|
21
|
+
local
|
|
22
|
+
});
|
|
65
23
|
}
|
|
66
|
-
/** The local UTM pool, or none — `utmctl` is absent on a machine that never had one. */
|
|
67
24
|
function localPoolUrls() {
|
|
68
25
|
try {
|
|
69
|
-
const pool = JSON.parse(execFileSync(CTL, [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (process.env.A11Y_DEBUG)
|
|
77
|
-
console.log(` (local pool unavailable: ${errorText(e)})`);
|
|
26
|
+
const pool = JSON.parse(execFileSync(CTL, [
|
|
27
|
+
"pool"
|
|
28
|
+
], {
|
|
29
|
+
encoding: "utf8"
|
|
30
|
+
}));
|
|
31
|
+
return pool.filter((vm)=>vm.ip).map((vm)=>`http://${vm.ip}:${vm.port}`);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
if (process.env.A11Y_DEBUG) console.log(` (local pool unavailable: ${errorText(e)})`);
|
|
78
34
|
return [];
|
|
79
35
|
}
|
|
80
36
|
}
|
|
81
|
-
/** @param {string} url */
|
|
82
37
|
async function versionOf(url) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// miss is a normal outcome for its usual callers) -- `fetch`'s `.json()` threw, and the caller here relies
|
|
88
|
-
// on that to report "unreachable" for a worker that answered garbage. Restored explicitly.
|
|
89
|
-
if (response.json === undefined)
|
|
90
|
-
throw new Error(`invalid JSON from ${url}`);
|
|
38
|
+
const response = await requestJson(`${url.replace(/\/$/, "")}/health`, {
|
|
39
|
+
timeoutMs: HEALTH_TIMEOUT_MS
|
|
40
|
+
});
|
|
41
|
+
if (void 0 === response.json) throw new Error(`invalid JSON from ${url}`);
|
|
91
42
|
return response.json.code ?? "absent";
|
|
92
43
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Nothing here runs on import, for the same reason as `deploy-worker.mjs`: a module that probes every worker
|
|
95
|
-
* over HTTP should be invoked, not merely mentioned. It also lets `code-version.test.ts` import this rather
|
|
96
|
-
* than parse its source as text.
|
|
97
|
-
*/
|
|
98
44
|
async function main() {
|
|
99
45
|
const expected = expectedWorkerCode();
|
|
100
46
|
const { urls, source } = workerUrls();
|
|
@@ -103,40 +49,26 @@ async function main() {
|
|
|
103
49
|
console.log("no worker configured, running locally, or listed in inventory.yml — nothing to compare");
|
|
104
50
|
process.exit(0);
|
|
105
51
|
}
|
|
106
|
-
// Say which list this is. A reading of "all current" means nothing until you know whether it examined
|
|
107
|
-
// the fleet you are about to capture on or the empty pool on your laptop.
|
|
108
52
|
console.log(`checking ${urls.length} worker(s) from ${source}`);
|
|
109
|
-
// Read first, CLASSIFY second, and the classifier is the one the capture preflight uses. This loop used
|
|
110
|
-
// to decide staleness itself with `actual === expected` — the same judgement in a second place, which is
|
|
111
|
-
// exactly what `worker-code-check.mjs` exists to stop. `versionOf` stays only because the CLI reports the
|
|
112
|
-
// network error text per worker, which a preflight has no use for.
|
|
113
53
|
const readings = [];
|
|
114
|
-
for (const url of urls) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
54
|
+
for (const url of urls)try {
|
|
55
|
+
readings.push({
|
|
56
|
+
worker: url,
|
|
57
|
+
code: await versionOf(url)
|
|
58
|
+
});
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.log(` ${url} unreachable (${errorText(e)})`);
|
|
61
|
+
readings.push({
|
|
62
|
+
worker: url,
|
|
63
|
+
code: null
|
|
64
|
+
});
|
|
123
65
|
}
|
|
124
66
|
const { stale } = codeDrift(expected, readings);
|
|
125
|
-
const staleUrls = stale.map((s)
|
|
67
|
+
const staleUrls = stale.map((s)=>s.worker);
|
|
126
68
|
const isStale = new Set(staleUrls);
|
|
127
|
-
for (const { worker, code } of readings.filter((r)
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
if (staleUrls.length) {
|
|
131
|
-
for (const line of remedyLines(staleUrls, inventoryWorkerUrls()))
|
|
132
|
-
console.log(line);
|
|
133
|
-
}
|
|
69
|
+
for (const { worker, code } of readings.filter((r)=>null !== r.code))console.log(` ${worker} ${code} ${isStale.has(worker) ? "STALE — redeploy and REBOOT the guest" : "matches"}`);
|
|
70
|
+
if (staleUrls.length) for (const line of remedyLines(staleUrls, inventoryWorkerUrls()))console.log(line);
|
|
134
71
|
process.exit(staleUrls.length ? 1 : 0);
|
|
135
72
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// and this guard silently read false — the tool loaded, did nothing, and exited 0. `/var` and `/tmp` are
|
|
139
|
-
// themselves symlinks on macOS, so this fired every time. Same defect, same fix, as `cli.ts`'s `isProgram`.
|
|
140
|
-
if (import.meta.url === pathToFileURL(process.argv[1] ? realpathSync(process.argv[1]) : "").href)
|
|
141
|
-
await main();
|
|
142
|
-
//# sourceMappingURL=check-worker-code.mjs.map
|
|
73
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ? realpathSync(process.argv[1]) : "").href) await main();
|
|
74
|
+
export { workerUrls };
|
package/dist/cli-flags.d.mts
CHANGED
package/dist/cli-flags.mjs
CHANGED
|
@@ -1,207 +1,61 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
/**
|
|
3
|
-
* Refuse a flag this command does not read.
|
|
4
|
-
*
|
|
5
|
-
* Every CLI here parses argv the same way — `process.argv.find((a) => a.startsWith("--only="))` or
|
|
6
|
-
* `process.argv.includes("--resume")` — and every one of them therefore IGNORES anything it does not
|
|
7
|
-
* recognise. A mistyped, renamed or hallucinated flag runs the default and reports success, and the
|
|
8
|
-
* operator believes their value was applied. That is the same defect as an Ansible extra var a job does
|
|
9
|
-
* not read, one layer out, and this repo has paid for it twice:
|
|
10
|
-
*
|
|
11
|
-
* - a blocker's own message told the reader to run `--write-baseline`; the flag is `--update-baseline`
|
|
12
|
-
* - `--only=route-title-stale` covered 1 of the 7 cases in that family, because the match was exact-id
|
|
13
|
-
*
|
|
14
|
-
* Neither produced an error. Both produced a plausible wrong answer, which this file's governing rule
|
|
15
|
-
* says to replace with a refusal that names the cause.
|
|
16
|
-
*
|
|
17
|
-
* ## Why the known list is passed in rather than read from the caller's source
|
|
18
|
-
*
|
|
19
|
-
* Deriving it at runtime — reading the calling module and regexing out its `--flags` — needs no
|
|
20
|
-
* maintenance, and is wrong for one reason that matters: a CLI whose flags are consumed by a HELPER in
|
|
21
|
-
* another module would refuse them, so the guard would break exactly the commands with the most moving
|
|
22
|
-
* parts. The list is explicit here and `cli-flags.test.ts` derives the same set from source and pins the
|
|
23
|
-
* two equal, which is this repo's remedy when a duplication is forced.
|
|
24
|
-
*/
|
|
25
1
|
import { basename } from "node:path";
|
|
26
2
|
import { realpathSync } from "node:fs";
|
|
27
3
|
import { pathToFileURL } from "node:url";
|
|
28
|
-
/** How far apart two flags may be and still be worth suggesting. One typo, or one word. */
|
|
29
4
|
const NEAR = 4;
|
|
30
|
-
/**
|
|
31
|
-
* Levenshtein, small and iterative — the inputs are flag names, never long strings.
|
|
32
|
-
* @param {string} a @param {string} b @returns {number}
|
|
33
|
-
*/
|
|
34
5
|
function distance(a, b) {
|
|
35
|
-
let previous = Array.from({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
6
|
+
let previous = Array.from({
|
|
7
|
+
length: b.length + 1
|
|
8
|
+
}, (_unused, index)=>index);
|
|
9
|
+
for(let i = 1; i <= a.length; i += 1){
|
|
10
|
+
const row = [
|
|
11
|
+
i
|
|
12
|
+
];
|
|
13
|
+
for(let j = 1; j <= b.length; j += 1)row[j] = Math.min(row[j - 1] + 1, previous[j] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
41
14
|
previous = row;
|
|
42
15
|
}
|
|
43
16
|
return previous[b.length];
|
|
44
17
|
}
|
|
45
|
-
|
|
46
|
-
* The flag name alone: `--shard=0/4` and `--shard` both name `--shard`.
|
|
47
|
-
* @param {string} argument @returns {string}
|
|
48
|
-
*/
|
|
49
|
-
export function nameOf(argument) {
|
|
18
|
+
function nameOf(argument) {
|
|
50
19
|
const equals = argument.indexOf("=");
|
|
51
|
-
return
|
|
20
|
+
return -1 === equals ? argument : argument.slice(0, equals);
|
|
52
21
|
}
|
|
53
|
-
|
|
54
|
-
* `--name=value`'s value, or `undefined` if `--name=` was never passed. audit §9's "argv parsing" row:
|
|
55
|
-
* VALIDATION is owned here (`refuseUnknownFlags`), but 15+ files each hand-rolled this exact three-line
|
|
56
|
-
* idiom for EXTRACTION, and one of them had already drifted from the rest.
|
|
57
|
-
*
|
|
58
|
-
* MEASURED before writing this, across all fifteen, against five vectors (a normal value, a missing flag,
|
|
59
|
-
* an empty value, a repeated flag, and a value containing its own `=`): fourteen agreed on all five —
|
|
60
|
-
* `argv.find((a) => a.startsWith("--name=")).slice("--name=".length)`, verbatim or with `name` templated
|
|
61
|
-
* in. `fleet-discover.mjs`'s `arg()` used `.split("=")[1]` instead, which is identical on four vectors and
|
|
62
|
-
* silently WRONG on the fifth: `--url=http://host?a=b` came back as `"http://host?a"`, truncated at the
|
|
63
|
-
* value's own `=`. Dormant today — that helper is only ever asked for `--cidr=` and `--port=`, neither of
|
|
64
|
-
* which can contain one — but a live discrepancy the day it is asked for a URL or a `key=value` pair.
|
|
65
|
-
*
|
|
66
|
-
* Kept as `.slice`, matching the fourteen rather than the one, and `fleet-discover.mjs` converted to it.
|
|
67
|
-
*
|
|
68
|
-
* @param {readonly string[]} argv @param {string} name
|
|
69
|
-
* @returns {string | undefined}
|
|
70
|
-
*/
|
|
71
|
-
export function flagValue(argv, name) {
|
|
22
|
+
function flagValue(argv, name) {
|
|
72
23
|
const prefix = `--${name}=`;
|
|
73
|
-
const hit = argv.find((a)
|
|
74
|
-
return
|
|
24
|
+
const hit = argv.find((a)=>a.startsWith(prefix));
|
|
25
|
+
return void 0 === hit ? void 0 : hit.slice(prefix.length);
|
|
75
26
|
}
|
|
76
|
-
|
|
77
|
-
* Which of `argv` are flags this command does not know. PURE, so it is testable without a process.
|
|
78
|
-
*
|
|
79
|
-
* A bare `--` is npm's separator and never a flag. Anything not starting with `-` is positional — a URL,
|
|
80
|
-
* a worker address, a page path — and is not this guard's business.
|
|
81
|
-
*
|
|
82
|
-
* SINGLE-DASH FLAGS ARE INSPECTED TOO, AND THE OMISSION COST A 14-MINUTE FLEET OPERATION.
|
|
83
|
-
*
|
|
84
|
-
* This read `startsWith("--")`, on the reasoning that only long flags are ever this repo's own. But an
|
|
85
|
-
* ANSIBLE-shaped argument is single-dash, and several of these commands wrap `ansible-playbook` — so
|
|
86
|
-
* `npm run fleet:provision -- -e worker_edge_allow_downgrade=true` passed straight through the guard,
|
|
87
|
-
* was never forwarded by the wrapper, and the whole fleet was provisioned WITHOUT the authorisation the
|
|
88
|
-
* operator believed they had given. Measured 2026-09-05. The role then refused, correctly, with a message
|
|
89
|
-
* telling the operator to pass the very flag they had just passed.
|
|
90
|
-
*
|
|
91
|
-
* That is precisely the defect this file exists to prevent — "an ignored flag runs the default and reports
|
|
92
|
-
* success" — surviving inside its own remedy, because the remedy was written to match one flag SHAPE
|
|
93
|
-
* rather than the idea of a flag. `-e` is not a URL and not a page path; nothing positional here begins
|
|
94
|
-
* with a dash followed by a letter, which is what makes this safe to refuse rather than merely warn on.
|
|
95
|
-
*/
|
|
96
|
-
/**
|
|
97
|
-
* @param {string[]} argv @param {string[]} known @returns {string[]}
|
|
98
|
-
*/
|
|
99
|
-
export function unknownFlags(argv, known) {
|
|
27
|
+
function unknownFlags(argv, known) {
|
|
100
28
|
const accepted = new Set(known.map(nameOf));
|
|
101
|
-
return argv
|
|
102
|
-
.filter((argument) => argument !== "--" && (argument.startsWith("--") || /^-[A-Za-z]/.test(argument)))
|
|
103
|
-
.map(nameOf)
|
|
104
|
-
.filter((flag) => !accepted.has(flag));
|
|
29
|
+
return argv.filter((argument)=>"--" !== argument && (argument.startsWith("--") || /^-[A-Za-z]/.test(argument))).map(nameOf).filter((flag)=>!accepted.has(flag));
|
|
105
30
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.map((candidate) => ({ candidate, gap: distance(flag, candidate) }))
|
|
113
|
-
.sort((left, right) => left.gap - right.gap);
|
|
114
|
-
return ranked[0] && ranked[0].gap <= NEAR ? ranked[0].candidate : undefined;
|
|
31
|
+
function didYouMean(flag, known) {
|
|
32
|
+
const ranked = known.map(nameOf).map((candidate)=>({
|
|
33
|
+
candidate,
|
|
34
|
+
gap: distance(flag, candidate)
|
|
35
|
+
})).sort((left, right)=>left.gap - right.gap);
|
|
36
|
+
return ranked[0] && ranked[0].gap <= NEAR ? ranked[0].candidate : void 0;
|
|
115
37
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
*
|
|
119
|
-
* `command` names the thing a human typed — the npm script, not the file — because that is what they will
|
|
120
|
-
* retype. Defaults to the script's basename, which is right for the ones invoked directly.
|
|
121
|
-
*/
|
|
122
|
-
/**
|
|
123
|
-
* @param {string[]} known Every flag this command reads, `--name` or `--name=`.
|
|
124
|
-
* @param {{entry: string, argv?: string[], command?: string}} options
|
|
125
|
-
* `entry` is the caller's `import.meta.url`, and it is REQUIRED. `command` names the thing a HUMAN
|
|
126
|
-
* typed — the npm script, not the file — because that is what they will retype.
|
|
127
|
-
*/
|
|
128
|
-
// NO `= {}` DEFAULT, because `entry` is required and the docstring above has always said so. A default
|
|
129
|
-
// that lets the whole options object be omitted contradicts that: it produces `entry === undefined`, and
|
|
130
|
-
// the guard below decides whether THIS module is the command by comparing `entry` against argv[1] -- so a
|
|
131
|
-
// caller who forgot it would get a guard that silently never fires. Types found the contradiction the
|
|
132
|
-
// moment this file entered the program. Every one of the 60 real call sites passes it.
|
|
133
|
-
export function refuseUnknownFlags(known, { entry, argv = process.argv.slice(2), command }) {
|
|
134
|
-
// ONLY WHEN THIS MODULE IS THE COMMAND, never when it is imported.
|
|
135
|
-
//
|
|
136
|
-
// These calls sit at module top level, so they run on IMPORT — and then inspect the IMPORTING process's
|
|
137
|
-
// argv. Measured 2026-08-27, an hour after the guards went in: `capture-real-pages --role=calibration`
|
|
138
|
-
// imports `fleet-env.mjs`, whose guard woke up, saw `--role`, decided it did not know it, and killed a
|
|
139
|
-
// 50-page capture with "unknown flag --role — did you mean --list?". The guard was right about its own
|
|
140
|
-
// flags and asking the wrong process.
|
|
141
|
-
//
|
|
142
|
-
// `entry` is REQUIRED rather than defaulted, for the reason `createHostThrottle`'s `minGapMs` is: a
|
|
143
|
-
// default here would silently restore exactly this behaviour for any caller who forgot it, and the
|
|
144
|
-
// failure mode is a guard that fires on somebody else's command line.
|
|
145
|
-
if (!entry) {
|
|
146
|
-
throw new TypeError("refuseUnknownFlags needs { entry: import.meta.url } — without it the guard runs "
|
|
147
|
-
+ "on import and inspects the importing process's flags");
|
|
148
|
-
}
|
|
149
|
-
// REALPATH'D, and without it this guard silently does not fire through a symlink — #237.
|
|
150
|
-
//
|
|
151
|
-
// An entry guard in the realpath'd form reads
|
|
152
|
-
// `import.meta.url === pathToFileURL(process.argv[1] ? realpathSync(process.argv[1]) : "").href`, and
|
|
153
|
-
// this comparison had no `realpathSync`. So when `argv[1]` reaches such a script through a symlink — npm's
|
|
154
|
-
// own `.bin` links are symlinks, which is why those call sites resolve — the OUTER condition is true and
|
|
155
|
-
// this one is FALSE. `main()` runs; the flag guard returns early and inspects nothing.
|
|
156
|
-
//
|
|
157
|
-
// NOT EVERY CALL SITE HAS THAT FORM (#1248). Some still compare the plain, symlink-blind
|
|
158
|
-
// `pathToFileURL(process.argv[1] ?? "")`, which fails the other way through a symlink: the OUTER condition is
|
|
159
|
-
// false, `main()` never runs, and the tool exits 0. Which files still do is `KNOWN_PLAIN_ENTRY_GUARDS` in
|
|
160
|
-
// `entry-points.test.ts` (#1086), a ratchet that may shrink and may not grow. It is the list of record, so no
|
|
161
|
-
// count is repeated here.
|
|
162
|
-
//
|
|
163
|
-
// Measured on `piped-exit-status-guard.mjs`, same file, same flag:
|
|
164
|
-
//
|
|
165
|
-
// node scripts/tmp-symlink-probe.mjs --bogus 'echo hi' -> ran, exit 0, flag IGNORED
|
|
166
|
-
// node packages/guards/src/piped-exit-status-guard.mjs --bogus '...' -> refused, exit 2
|
|
167
|
-
//
|
|
168
|
-
// Through the symlink the mistyped flag is ignored and the command reports success — which is the
|
|
169
|
-
// sentence this refusal itself prints as the reason it exists.
|
|
170
|
-
//
|
|
171
|
-
// It is a remedy whose TRIGGER is narrower than the thing it guards, the `refreshBrowseBuffer` shape:
|
|
172
|
-
// reachable from the right path, with a condition that could not be true there. And the census
|
|
173
|
-
// (`cli-flags.test.ts`) cannot see it, because it asserts a file CONTAINS `refuseUnknownFlags(` — it
|
|
174
|
-
// cannot ask whether that call can FIRE, so every guarded CLI reads GUARDED either way.
|
|
175
|
-
//
|
|
176
|
-
// `realpathSync` THROWS on a path that does not exist, and `process.argv[1]` is absent for `node -e`
|
|
177
|
-
// and `node --eval`. Absent stays absent rather than becoming a throw: the guard must be inert when
|
|
178
|
-
// there is no script, never fatal.
|
|
179
|
-
/** The invoking script's REAL path as a URL, so a symlinked `argv[1]` still matches `entry`. */
|
|
38
|
+
function refuseUnknownFlags(known, { entry, argv = process.argv.slice(2), command }) {
|
|
39
|
+
if (!entry) throw new TypeError("refuseUnknownFlags needs { entry: import.meta.url } — without it the guard runs on import and inspects the importing process's flags");
|
|
180
40
|
let invoked;
|
|
181
41
|
try {
|
|
182
42
|
invoked = process.argv[1] ? pathToFileURL(realpathSync(process.argv[1])).href : "";
|
|
43
|
+
} catch {
|
|
44
|
+
invoked = pathToFileURL(process.argv[1] ?? "").href;
|
|
183
45
|
}
|
|
184
|
-
|
|
185
|
-
invoked = pathToFileURL(process.argv[1] ?? "").href; // unresolvable: compare what we were given
|
|
186
|
-
}
|
|
187
|
-
if (entry !== invoked)
|
|
188
|
-
return;
|
|
46
|
+
if (entry !== invoked) return;
|
|
189
47
|
const unknown = unknownFlags(argv, known);
|
|
190
|
-
if (unknown.length
|
|
191
|
-
return;
|
|
48
|
+
if (0 === unknown.length) return;
|
|
192
49
|
const name = command ?? basename(process.argv[1] ?? "this command");
|
|
193
|
-
for (const flag of unknown)
|
|
50
|
+
for (const flag of unknown){
|
|
194
51
|
const near = didYouMean(flag, known);
|
|
195
52
|
console.error(` ${name}: unknown flag ${flag}${near ? ` — did you mean ${near}?` : ""}`);
|
|
196
53
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
console.error(accepted.length === 0
|
|
202
|
-
? " It takes no flags at all."
|
|
203
|
-
: ` It takes: ${accepted.join(" ")}`);
|
|
54
|
+
const accepted = [
|
|
55
|
+
...known
|
|
56
|
+
].map(nameOf).sort();
|
|
57
|
+
console.error(0 === accepted.length ? " It takes no flags at all." : ` It takes: ${accepted.join(" ")}`);
|
|
204
58
|
console.error(" Refusing rather than ignoring it: an ignored flag runs the default and reports success.");
|
|
205
59
|
process.exit(2);
|
|
206
60
|
}
|
|
207
|
-
|
|
61
|
+
export { didYouMean, flagValue, nameOf, refuseUnknownFlags, unknownFlags };
|
package/dist/code-drift.d.mts
CHANGED
|
@@ -31,9 +31,10 @@ export function codeDrift(expected: string, readings: Array<{
|
|
|
31
31
|
/**
|
|
32
32
|
* Name the deploy route that can actually reach these workers.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
* `utmctl file push` plus a `utmctl` reboot
|
|
36
|
-
*
|
|
34
|
+
* Only bare-metal workers have one: they are git-cloned and deploy by PULLING, through Ansible. The UTM
|
|
35
|
+
* route (`a11ign-worker-deploy`, `utmctl file push` plus a `utmctl` reboot, keyed on a VM UUID) is gone: it
|
|
36
|
+
* pushed from the worker package's `src`, which a built install does not have (#3765), and no fleet box
|
|
37
|
+
* was ever reachable by it.
|
|
37
38
|
*
|
|
38
39
|
* This printed the utmctl advice unconditionally, including to a fleet of four mini PCs where none of it
|
|
39
40
|
* applies — a tool confidently prescribing a remedy for a different kind of machine. Which kind a worker is
|
|
@@ -137,4 +138,3 @@ export function assertWorkersServe(expected: string, workers: string[], options:
|
|
|
137
138
|
bareMetalUrls?: string[];
|
|
138
139
|
sourceDir: string;
|
|
139
140
|
}): Promise<void>;
|
|
140
|
-
//# sourceMappingURL=code-drift.d.mts.map
|