clearotron 0.2.2 → 0.2.4
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/.env.example +64 -37
- package/.nvmrc +1 -1
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +36 -14
- package/README.md +18 -22
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +10 -5
- package/bin/onboard.mjs +370 -28
- package/bin/start.mjs +220 -12
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/demo/full-country-search/run/_driver/framework.json +1 -1
- package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
- package/demo/knockout-search/run/_driver/framework.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
- package/docs/E2E.md +5 -5
- package/docs/GLOSSARY.md +85 -0
- package/docs/PORTAL.md +2 -2
- package/docs/README.md +6 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +19 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +90 -0
- package/driver/README.md +25 -6
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/dev-portal.mjs +2 -2
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/cli-version.mjs +103 -0
- package/driver/engine/probe.mjs +45 -5
- package/driver/enqueue-schema.mjs +8 -8
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +52 -2
- package/driver/flag-snapshot.mjs +20 -0
- package/driver/framework.mjs +1 -1
- package/driver/gateway.mjs +20 -3
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +211 -106
- package/driver/plain-register.mjs +134 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +70 -2
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +79 -9
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +151 -2
- package/driver/profile-page.html +10 -2
- package/driver/profile-service.mjs +4 -4
- package/driver/profiles/README.md +3 -3
- package/driver/profiles.mjs +144 -12
- package/driver/publish/index.mjs +2 -2
- package/driver/publish/knockout.mjs +7 -1
- package/driver/publish/render-knockout.mjs +453 -118
- package/driver/publish/render.mjs +111 -16
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +3 -3
- package/driver/skills/blind-frame/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +84 -0
- package/driver/skills/matter-frame/SKILL.md +6 -6
- package/driver/skills/matter-frame/watchlist-reference.md +2 -2
- package/driver/skills/prelim-common-law/SKILL.md +1 -1
- package/driver/skills/prelim-search/SKILL.md +1 -1
- package/driver/skills/prelim-search/report-prose.md +82 -0
- package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
- package/driver/skills/prelim-search/synthesis-rules.md +1 -1
- package/driver/skills/prelim-variants/SKILL.md +3 -3
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +54 -7
- package/driver/suite-census.json +220 -58
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +214 -27
- package/driver/verify.mjs +1 -1
- package/driver/whatif-memo-run.mjs +26 -4
- package/driver/whatif-memo.mjs +19 -1
- package/examples/README.md +2 -2
- package/examples/grants.example.json +6 -7
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/ops/EXAMPLES.md +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +31 -19
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-KFAHMgdT.js} +289 -73
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/term-shape.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/added-reference-check.mjs +45 -89
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/e2e.mjs +2 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/live-surface-check.mjs +38 -1
- package/scripts/mint-names-in-force.mjs +4 -2
- package/scripts/mint-public-residue.mjs +74 -0
- package/scripts/mint-reference-strip-backlog.mjs +12 -1
- package/scripts/mint-suite-census.mjs +5 -2
- package/scripts/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- package/scripts/render-check.mjs +13 -2
- package/scripts/report-frame-check.mjs +8 -1
- package/scripts/report-screenshot.mjs +6 -3
- package/scripts/revisit-render-check.mjs +6 -3
- package/scripts/score.mjs +1 -1
- package/scripts/strip-tracker-citations.mjs +38 -7
- package/scripts/test-full.mjs +226 -0
- package/scripts/test-run.mjs +16 -0
- package/shared/README.md +2 -1
- package/shared/anon-overlay.mjs +3 -3
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +33 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +6 -1
- package/shared/node-floor.mjs +97 -0
- package/shared/os-advice.mjs +91 -0
- package/shared/reap-on-exit.mjs +42 -0
- package/shared/reference-guard-classes.mjs +351 -0
- package/shared/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- package/shared/suite-census.mjs +31 -3
- package/shared/withheld-paths-access.mjs +36 -18
- package/skills/clearotron-ops/SKILL.md +1 -1
- package/driver/recipes/README.md +0 -49
- package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
- package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
- package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
- package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
- package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
- package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
- package/scripts/ai-page-render-check.mjs +0 -532
- package/scripts/clearances-render-check.mjs +0 -480
- package/scripts/composer-render-check.mjs +0 -1027
- package/scripts/home-render-check.mjs +0 -526
- package/scripts/portal-lifecycle-check.mjs +0 -639
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
//
|
|
5
|
+
// RUN EVERY CORPUS, AND REFUSE TO BE SILENT ABOUT ONE THAT WAS NOT RUN.
|
|
6
|
+
//
|
|
7
|
+
// node scripts/test-full.mjs # every corpus
|
|
8
|
+
// node scripts/test-full.mjs --only providers # one of them
|
|
9
|
+
// node scripts/test-full.mjs --list # the plan, run nothing
|
|
10
|
+
//
|
|
11
|
+
// ── WHAT THIS REPLACES, AND WHY A FLAG WAS NOT ENOUGH ────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// `npm run test:full --workspaces --if-present` reported this suite green while running part of it.
|
|
14
|
+
// Two separate holes, and only the smaller one is about the flag:
|
|
15
|
+
//
|
|
16
|
+
// - `providers/` holds twelve directories of register-adapter tests. ELEVEN OF THEM ARE NOT
|
|
17
|
+
// WORKSPACES — they have no package.json at all — so no `--workspaces` command can reach them,
|
|
18
|
+
// with or without `--if-present`. That is the larger half and no flag fixes it.
|
|
19
|
+
// - `--if-present` turns "this workspace declares no such script" into a silent skip. An absence
|
|
20
|
+
// reading as a pass is the shape this repository refuses everywhere else.
|
|
21
|
+
//
|
|
22
|
+
// So the population is declared here rather than inferred from a flag, and every member of it is
|
|
23
|
+
// either run or named. A corpus that was not run is a line on stdout, never an omission.
|
|
24
|
+
//
|
|
25
|
+
// ── THE RULE, AND THE ONE THING THAT MAY SOFTEN IT ───────────────────────────────────────────────
|
|
26
|
+
//
|
|
27
|
+
// A declared workspace that does not define `test:full` FAILS this command. That is the permissive
|
|
28
|
+
// half of the old gate written the other way round.
|
|
29
|
+
//
|
|
30
|
+
// One softening, because refusing here would be a false refusal: a workspace whose tests are already
|
|
31
|
+
// run by another corpus is COVERED, not missing. `providers/oauth-mcp-bridge` is a workspace with no
|
|
32
|
+
// scripts block whose ten tests run every time the providers corpus does. Failing it would refuse a
|
|
33
|
+
// workspace whose tests demonstrably execute, and giving it a script of its own would split one
|
|
34
|
+
// corpus across two mechanisms and run those files twice.
|
|
35
|
+
//
|
|
36
|
+
// THE EXEMPTION IS CHECKED, NEVER TRUSTED. For each entry below, this command asserts that the
|
|
37
|
+
// covering corpus's own file list actually reaches that workspace, and that the workspace has test
|
|
38
|
+
// files at all. Both halves matter: an intersection with an empty set is empty, so a workspace that
|
|
39
|
+
// lost its tests would satisfy a reachability check that only asked "is nothing missing". If someone
|
|
40
|
+
// narrows the providers file list until it no longer reaches this workspace, the exemption stops
|
|
41
|
+
// being true and this command says so instead of going quiet.
|
|
42
|
+
import { spawnSync } from "node:child_process";
|
|
43
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
44
|
+
import { join, dirname } from "node:path";
|
|
45
|
+
import { fileURLToPath } from "node:url";
|
|
46
|
+
|
|
47
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every `*.test.mjs` under `providers/`, at any depth, deduplicated and ordered.
|
|
51
|
+
*
|
|
52
|
+
* RECURSIVE, AND THAT IS THE POINT. This read `providers/<dir>/test/*.test.mjs` and nothing else, one
|
|
53
|
+
* level deep — and the exemption check below counted a covered workspace's own files with the SAME
|
|
54
|
+
* shape. So a test at `providers/x/y.test.mjs`, or at `providers/x/test/sub/y.test.mjs`, was in no
|
|
55
|
+
* corpus at all, both sides missed it together and therefore agreed, and every check reported clean
|
|
56
|
+
* under a command whose whole claim is that it runs every corpus. Two derived sides sharing one
|
|
57
|
+
* assumption cannot disagree about it; that is this file's own argument, and it applied here.
|
|
58
|
+
*
|
|
59
|
+
* `node_modules` is excluded because an installed dependency's tests are not this repository's corpus.
|
|
60
|
+
* Nothing else is: a file is either collected or it is a fault, never quietly outside the shape.
|
|
61
|
+
*/
|
|
62
|
+
export function providerTestFiles(root = ROOT) {
|
|
63
|
+
const base = join(root, "providers");
|
|
64
|
+
if (!existsSync(base)) return [];
|
|
65
|
+
const out = new Set();
|
|
66
|
+
const walk = (dir, rel) => {
|
|
67
|
+
for (const d of readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
+
if (d.name === "node_modules") continue;
|
|
69
|
+
const here = `${rel}/${d.name}`;
|
|
70
|
+
if (d.isDirectory()) walk(join(dir, d.name), here);
|
|
71
|
+
else if (d.name.endsWith(".test.mjs")) out.add(here);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
walk(base, "providers");
|
|
75
|
+
// DEDUPLICATED ON PURPOSE. The shell form this replaced listed `providers/_shared/test/*.test.mjs`
|
|
76
|
+
// and then `providers/*/test/*.test.mjs`, which matches `_shared` as well — 51 paths for 37 files.
|
|
77
|
+
// Measured before it was called a defect: `node --test` runs a repeated path once, so the old form
|
|
78
|
+
// executed nothing twice. Building the list here makes that a property rather than a coincidence.
|
|
79
|
+
return [...out].sort();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A workspace with no `test:full`, whose tests another corpus already runs.
|
|
84
|
+
* `reaches` is the claim this command verifies on every run — not documentation.
|
|
85
|
+
*/
|
|
86
|
+
export const COVERED_ELSEWHERE = [
|
|
87
|
+
{
|
|
88
|
+
workspace: "providers/oauth-mcp-bridge",
|
|
89
|
+
corpus: "providers",
|
|
90
|
+
why: "a workspace for its dependencies, not for a suite of its own: its tests sit in "
|
|
91
|
+
+ "providers/<name>/test like every other adapter's and run with them. A script here would "
|
|
92
|
+
+ "split one corpus across two mechanisms.",
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The corpora, discovered from the manifest rather than listed.
|
|
98
|
+
*
|
|
99
|
+
* `providerFiles` is injectable for one reason: the exemption check below compares the covering
|
|
100
|
+
* corpus's file list against the workspace's own directory, and if both are always read from the
|
|
101
|
+
* same place the comparison can never fail — coverage in appearance with nothing underneath. The
|
|
102
|
+
* state worth refusing is a NARROWED corpus, which is an edit to `providerTestFiles`, so an arm has
|
|
103
|
+
* to be able to hand in a narrowed list and see the refusal.
|
|
104
|
+
*/
|
|
105
|
+
export function plan(root = ROOT, { providerFiles } = {}) {
|
|
106
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
107
|
+
const workspaces = manifest.workspaces ?? [];
|
|
108
|
+
const corpora = [];
|
|
109
|
+
const faults = [];
|
|
110
|
+
|
|
111
|
+
for (const ws of workspaces) {
|
|
112
|
+
let scripts = null;
|
|
113
|
+
try { scripts = JSON.parse(readFileSync(join(root, ws, "package.json"), "utf8")).scripts ?? {}; }
|
|
114
|
+
catch { scripts = null; }
|
|
115
|
+
// A DECLARED WORKSPACE WITH NO READABLE MANIFEST IS A FAULT, not a skip. The old command could
|
|
116
|
+
// not tell this apart from a workspace that simply had nothing to run.
|
|
117
|
+
if (scripts === null) { faults.push(`${ws} is a declared workspace and its package.json could not be read`); continue; }
|
|
118
|
+
|
|
119
|
+
if (scripts["test:full"]) {
|
|
120
|
+
corpora.push({ name: ws, kind: "workspace", argv: ["npm", "run", "test:full", "-w", ws] });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const cover = COVERED_ELSEWHERE.find((c) => c.workspace === ws);
|
|
125
|
+
if (!cover) {
|
|
126
|
+
faults.push(`${ws} is a declared workspace and defines no \`test:full\` script. Either give it `
|
|
127
|
+
+ `one, or add it to COVERED_ELSEWHERE in scripts/test-full.mjs naming the corpus that runs `
|
|
128
|
+
+ `its tests — which is checked, so the claim has to be true.`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
corpora.push({ name: ws, kind: "covered", cover });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const files = providerFiles ?? providerTestFiles(root);
|
|
135
|
+
corpora.push({
|
|
136
|
+
name: "providers",
|
|
137
|
+
kind: "files",
|
|
138
|
+
files,
|
|
139
|
+
argv: ["node", "scripts/test-run.mjs", "node", "--test", ...files],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── THE EXEMPTIONS, CHECKED ────────────────────────────────────────────────────────────────────
|
|
143
|
+
for (const c of corpora.filter((x) => x.kind === "covered")) {
|
|
144
|
+
const covering = corpora.find((x) => x.name === c.cover.corpus);
|
|
145
|
+
if (!covering || !Array.isArray(covering.files)) {
|
|
146
|
+
faults.push(`${c.name} is recorded as covered by the \`${c.cover.corpus}\` corpus, and this `
|
|
147
|
+
+ `command has no such corpus with a file list to check that against.`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const own = covering.files.filter((f) => f.startsWith(c.name + "/"));
|
|
151
|
+
// BOTH HALVES. A workspace that lost its own tests would pass a check that only asked whether
|
|
152
|
+
// the covering corpus was missing any of them, because nothing is missing from nothing.
|
|
153
|
+
const here = existsSync(join(root, c.name, "test"))
|
|
154
|
+
? readdirSync(join(root, c.name, "test")).filter((f) => f.endsWith(".test.mjs")).length : 0;
|
|
155
|
+
if (!here) {
|
|
156
|
+
faults.push(`${c.name} is recorded as covered by the \`${c.cover.corpus}\` corpus, but it has `
|
|
157
|
+
+ `no test files of its own, so that claim covers nothing. Remove the entry or restore the tests.`);
|
|
158
|
+
} else if (own.length !== here) {
|
|
159
|
+
faults.push(`${c.name} is recorded as covered by the \`${c.cover.corpus}\` corpus, which reaches `
|
|
160
|
+
+ `${own.length} of its ${here} test file(s). The exemption is no longer true.`);
|
|
161
|
+
}
|
|
162
|
+
c.reaches = own.length;
|
|
163
|
+
c.hasTests = here;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { corpora, faults };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function main() {
|
|
170
|
+
const only = (() => { const i = process.argv.indexOf("--only"); return i === -1 ? null : process.argv[i + 1]; })();
|
|
171
|
+
const listOnly = process.argv.includes("--list");
|
|
172
|
+
const { corpora, faults } = plan();
|
|
173
|
+
|
|
174
|
+
// FAULTS BEFORE ANYTHING RUNS. A missing corpus discovered after a green suite reads as an
|
|
175
|
+
// afterthought; discovered first, it is the answer.
|
|
176
|
+
if (faults.length) {
|
|
177
|
+
console.error(`\ntest-full: ${faults.length} declared workspace(s) this command cannot account for:\n`);
|
|
178
|
+
for (const f of faults) console.error(` - ${f}`);
|
|
179
|
+
console.error("");
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const chosen = only ? corpora.filter((c) => c.name === only || c.name.endsWith("/" + only)) : corpora;
|
|
184
|
+
if (only && !chosen.length) {
|
|
185
|
+
console.error(`test-full: no corpus named ${only}. Known: ${corpora.map((c) => c.name).join(", ")}`);
|
|
186
|
+
process.exit(2);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (listOnly) {
|
|
190
|
+
for (const c of chosen) {
|
|
191
|
+
console.log(c.kind === "covered"
|
|
192
|
+
? ` ${c.name}: covered by the ${c.cover.corpus} corpus (${c.reaches}/${c.hasTests} file(s))`
|
|
193
|
+
: ` ${c.name}: ${c.kind === "files" ? `${c.files.length} file(s)` : "workspace script"}`);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const ran = [];
|
|
199
|
+
for (const c of chosen) {
|
|
200
|
+
if (c.kind === "covered") { ran.push({ ...c, status: "covered" }); continue; }
|
|
201
|
+
console.log(`\n──── ${c.name} ────`);
|
|
202
|
+
// STDIO INHERITED, NOT CAPTURED. CI reads the child stream for the corpus-guard markers, and a
|
|
203
|
+
// runner that buffered its children would take those markers out of the log the check greps.
|
|
204
|
+
const r = spawnSync(c.argv[0], c.argv.slice(1), { cwd: ROOT, stdio: "inherit" });
|
|
205
|
+
ran.push({ ...c, status: r.status === 0 ? "ok" : "FAILED", code: r.status });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── THE SUMMARY, AT THE END, ON STDOUT ─────────────────────────────────────────────────────────
|
|
209
|
+
// The point of the whole file. A reader who scrolls to the bottom of a long run sees every corpus
|
|
210
|
+
// this command knows about and what became of it, so "the full suite is green" is a claim they can
|
|
211
|
+
// check rather than one they have to trust.
|
|
212
|
+
console.log(`\n──── what test-full ran ────`);
|
|
213
|
+
for (const c of ran) {
|
|
214
|
+
if (c.status === "covered") {
|
|
215
|
+
console.log(` covered ${c.name} — ${c.hasTests} file(s), run by the ${c.cover.corpus} corpus: ${c.cover.why}`);
|
|
216
|
+
} else {
|
|
217
|
+
const n = c.kind === "files" ? `${c.files.length} file(s)` : "workspace suite";
|
|
218
|
+
console.log(` ${c.status === "ok" ? "ran " : "FAILED "} ${c.name} — ${n}${c.code ? ` (exit ${c.code})` : ""}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const bad = ran.filter((c) => c.status === "FAILED");
|
|
222
|
+
console.log(bad.length ? `\n${bad.length} corpus/corpora failed.` : `\nEvery corpus above was run or accounted for.`);
|
|
223
|
+
if (bad.length) process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (import.meta.url === `file://${process.argv[1]}`) main();
|
package/scripts/test-run.mjs
CHANGED
|
@@ -223,6 +223,22 @@ const providerChosen = ["CLEAROTRON_DATABASE"]
|
|
|
223
223
|
.some((n) => String(process.env[n] ?? "").trim() !== "");
|
|
224
224
|
if (!providerChosen) process.env.CLEAROTRON_DATABASE = "corsearch";
|
|
225
225
|
|
|
226
|
+
// ── THE SUITE'S OWN BRAND-OWNER FIXTURES, ASKED FOR IN ONE PLACE ──────────────────────────────────
|
|
227
|
+
//
|
|
228
|
+
// Three profiles in `driver/profiles/` are the suite's, and a resolved roster no longer offers them:
|
|
229
|
+
// an outside user's brand-owner picker listed them on a `git clone` install, where the packaging
|
|
230
|
+
// exclusion that protects the tarball does not apply. They are marked `testFixture` and the loader
|
|
231
|
+
// refuses them on every route.
|
|
232
|
+
//
|
|
233
|
+
// The checks still need them, under the same keys the baselines use, and most do not call the loader —
|
|
234
|
+
// they exercise code that does, so there is no argument to pass. One variable here is the whole ask,
|
|
235
|
+
// and it is visible: a reader of this file can see that the suite runs against a roster no install
|
|
236
|
+
// resolves, which is a thing worth being able to see rather than a default nobody set.
|
|
237
|
+
process.env.CLEAROTRON_TEST_FIXTURE_PROFILES ??= "1";
|
|
238
|
+
// The demo account is refused from a fresh install's roster for the same reason and by the same
|
|
239
|
+
// mechanism; the suite asks for it here, once, so no check has to know it was hidden.
|
|
240
|
+
process.env.CLEAROTRON_DEMO_PROFILES ??= "1";
|
|
241
|
+
|
|
226
242
|
// ── A PARTLY-BLIND SUITE IS NOT A FAILING SUITE ────────────────────────────────────────────
|
|
227
243
|
//
|
|
228
244
|
// `git worktree add` gives you no `node_modules`, nothing in this repo creates one, and the tracker asks
|
package/shared/README.md
CHANGED
|
@@ -25,7 +25,8 @@ resolves the running commit for artifact provenance, but it cannot be the shared
|
|
|
25
25
|
| `listen.mjs` | The one place a bind failure becomes a sentence rather than a stack trace. |
|
|
26
26
|
| `driver-dir.mjs` | Where a run's `_driver/` is: the name, the path, and the one creation call. Product code no longer builds it by hand, so the directory's location — and one day its mode — is a decision in one place rather than a convention in 1123. |
|
|
27
27
|
| `site-nav.mjs`, `anon-overlay.mjs` | The single top nav shared by the internal staff pages, and the display-only overlay that masks client names and marks so those pages can be screen-shared. |
|
|
28
|
-
| `identifier-scan.mjs`, `identifier-
|
|
28
|
+
| `identifier-scan.mjs`, `identifier-sentinels.mjs`, `vetted-identities.mjs` | One matcher, one name table, one list of declared exceptions — shared by the guard that sweeps the tracked tree and the scan that sweeps the whole history. The real table is private; the sentinels are what make a zero from that sweep mean something. |
|
|
29
|
+
| `reference-guard-classes.mjs` | What a public tree must not acquire, as one table: citations, account names, home directories on the build machines, private repository names, this project's own words for how it is organised, and both attribution trailers. Read by the diff guard in `scripts/added-reference-check.mjs` and by the per-file floor beside it. |
|
|
29
30
|
|
|
30
31
|
`brand/assets/` holds the vendored logo SVGs. `tools/` holds the two brand-system generators — see
|
|
31
32
|
[`tools/README.md`](tools/README.md).
|
package/shared/anon-overlay.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Feedback console went with the quality subsystem, so two of those five had not rendered for
|
|
7
7
|
// months. Lets us screen-share the site without
|
|
8
8
|
// revealing real client names or the marks being cleared: client names map to descriptive aliases
|
|
9
|
-
// (e.g.
|
|
9
|
+
// (e.g. Coastline Drinks → "Beverage company") and marks blur out, behind ONE site-wide toggle (no
|
|
10
10
|
// hover-reveal). Display-only — it never mutates stored data, run metadata, or form values.
|
|
11
11
|
//
|
|
12
12
|
// SHAPE (mirrors site-nav.mjs's NAV_CSS/siteNav split):
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// • anonToggle(cfg?) — the toggle button markup for the nav ('' when disabled).
|
|
20
20
|
//
|
|
21
21
|
// Tagging conventions the browser overlay understands:
|
|
22
|
-
// <span data-anon="client" data-anon-key="
|
|
22
|
+
// <span data-anon="client" data-anon-key="coastline">Coastline Drinks</span> → text swapped for the alias
|
|
23
23
|
// <span data-anon="mark" data-anon-key="…" data-anon-run="…">NOVA PULSE</span> → blurred (unless exempt)
|
|
24
24
|
// <a data-anon-href data-anon-key="…" data-anon-run="…" href="…"> → href neutralised to # (unless exempt)
|
|
25
25
|
// A run/client is EXEMPT (shown real) when its key ∈ demoAllow.clientKeys or its run matches
|
|
@@ -81,7 +81,7 @@ const OVERLAY_BODY = `(function(){
|
|
|
81
81
|
var DEAD_TITLE = "hidden in privacy mode — toggle Privacy OFF to open";
|
|
82
82
|
var DA = C.demoAllow || {clientKeys:[],runs:[]};
|
|
83
83
|
// Case-insensitive client lookup, and the reason it is not redundant: surfaces tag rows by profile
|
|
84
|
-
// KEY ("
|
|
84
|
+
// KEY ("coastline") and by display NAME ("Coastline"), so lowercasing both makes a single-token name
|
|
85
85
|
// resolve to its key's alias. The two surfaces that tagged by NAME were Quality and Feedback, both
|
|
86
86
|
// retired (tracker issue 265) — the fold stays because it is what makes the lookup total over either spelling,
|
|
87
87
|
// not because those pages are coming back.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// browser-temp-root.mjs — give a browser launch its own temp root, so an untidy exit leaks nothing shared.
|
|
4
|
+
//
|
|
5
|
+
// ── WHAT LEAKS, AND WHY IT IS NOT THE PROFILE DIRECTORY ───────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// Chrome does not keep its process-singleton lock in `--user-data-dir`. It creates a directory named
|
|
8
|
+
// `com.google.Chrome.XXXXXX` in the system temp directory, holding `SingletonCookie` and a
|
|
9
|
+
// `SingletonSocket` unix socket, and symlinks the profile's entry at it. The reason is length: a
|
|
10
|
+
// profile path may be too long for a unix socket, so the lock is shortened through the temp root.
|
|
11
|
+
//
|
|
12
|
+
// Every launcher here already removes its own profile directory. None of them could remove that one,
|
|
13
|
+
// because none of them knows its name. Measured on a development machine 2026-09-09: 4,855 such
|
|
14
|
+
// directories under one shared temp root, the oldest five days old, still accumulating — the residue
|
|
15
|
+
// of runs that ended without running their cleanup, mostly cancelled continuous-integration jobs.
|
|
16
|
+
//
|
|
17
|
+
// The fix is not to find and delete them. It is to stop them being shared: the directory is created in
|
|
18
|
+
// `GetTempDir()`, which honours `TMPDIR`, so a run that exports its own temp root gets its own copy of
|
|
19
|
+
// the lock inside it and takes it away when the root goes.
|
|
20
|
+
//
|
|
21
|
+
// ── THE BYTE BUDGET, WHICH IS WHY THIS FILE REFUSES RATHER THAN TRUSTS ────────────────────────────
|
|
22
|
+
//
|
|
23
|
+
// A unix socket address is a fixed 108-byte field, so the socket path must fit in 107 bytes plus a
|
|
24
|
+
// terminator. The lock directory and socket name add a fixed 41 characters to the temp root, leaving
|
|
25
|
+
// 66 for the root itself. One character more and Chrome does not degrade — it aborts, with a fatal
|
|
26
|
+
// log line about the socket path and a core dump.
|
|
27
|
+
//
|
|
28
|
+
// That failure is why the check below exists. A crash in a continuous-integration step, reported as a
|
|
29
|
+
// core dump, is not something a reader connects to the length of a checkout path; and the length is
|
|
30
|
+
// not a constant of this project, because a run can be rooted anywhere a caller or a runner puts it.
|
|
31
|
+
// So the budget is asserted at the point the root is chosen, and the refusal states the length it
|
|
32
|
+
// measured, the limit, and the path — which is the whole of what a reader needs to fix it.
|
|
33
|
+
//
|
|
34
|
+
// The boundary is measured, not derived: walking root lengths across it, 66 starts and 67 aborts.
|
|
35
|
+
import { mkdtempSync, mkdirSync } from "node:fs";
|
|
36
|
+
import { tmpdir } from "node:os";
|
|
37
|
+
import { join } from "node:path";
|
|
38
|
+
import { removeOnExit } from "./reap-on-exit.mjs";
|
|
39
|
+
|
|
40
|
+
/** A unix socket address field is 108 bytes; the last is the terminator. */
|
|
41
|
+
export const SUN_PATH_MAX = 107;
|
|
42
|
+
|
|
43
|
+
/** `/com.google.Chrome.XXXXXX/SingletonSocket` — what the lock adds to the temp root. */
|
|
44
|
+
export const LOCK_SUFFIX = "/com.google.Chrome.XXXXXX/SingletonSocket";
|
|
45
|
+
|
|
46
|
+
/** The longest temp root a browser can be launched under. */
|
|
47
|
+
export const MAX_ROOT_LENGTH = SUN_PATH_MAX - LOCK_SUFFIX.length;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The root's own prefix, and it is SHORT ON PURPOSE — this is the one lever on the budget.
|
|
51
|
+
*
|
|
52
|
+
* Every character here is spent on every browser run, and roots nest: under the suite runner the
|
|
53
|
+
* ambient temp directory already carries one `ct-testrun-XXXXXX` level before this is appended.
|
|
54
|
+
* Measured on a development machine 2026-09-09, ambient temp 18 characters:
|
|
55
|
+
*
|
|
56
|
+
* root length worst case with a 22-character name in the root with this prefix
|
|
57
|
+
* ambient 18 47 (19 to spare) 29 (37 to spare)
|
|
58
|
+
* one runner 36 65 (ONE to spare) 47 (19 to spare)
|
|
59
|
+
* nested 54 83 (refuses) 65 (1 to spare)
|
|
60
|
+
*
|
|
61
|
+
* The middle row is why this is not cosmetic. Naming the root after the check spent the budget on a
|
|
62
|
+
* label, and left an ordinary single-level suite run one character from refusing — a host with a
|
|
63
|
+
* temp directory one character longer, or this one renamed a level deeper, and every browser check
|
|
64
|
+
* would refuse at once while the tree looked broken. So the root is named for what it IS, and the
|
|
65
|
+
* check that made it is named by the directory INSIDE it, where length costs nothing.
|
|
66
|
+
*
|
|
67
|
+
* A leaked root is still sweepable by name, which was the point of naming it at all, and now it is
|
|
68
|
+
* attributable too: open it and the profile directory says which check left it.
|
|
69
|
+
*/
|
|
70
|
+
export const ROOT_PREFIX = "ctb-";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Why `dir` cannot be a browser temp root, or `null` if it can.
|
|
74
|
+
*
|
|
75
|
+
* Returned rather than thrown so a caller can decide: a check that has already produced a result
|
|
76
|
+
* should say this and exit, not lose the result to an exception.
|
|
77
|
+
*/
|
|
78
|
+
export function rootRefusal(dir) {
|
|
79
|
+
if (typeof dir !== "string" || dir === "") return "a browser temp root must be a non-empty path";
|
|
80
|
+
if (dir.length <= MAX_ROOT_LENGTH) return null;
|
|
81
|
+
return (
|
|
82
|
+
`browser temp root is ${dir.length} characters and the limit is ${MAX_ROOT_LENGTH}: ${dir}\n` +
|
|
83
|
+
` A browser writes its process-singleton socket at <root>${LOCK_SUFFIX}, and a unix socket ` +
|
|
84
|
+
`address holds ${SUN_PATH_MAX} characters. Over that the browser aborts with a fatal error about ` +
|
|
85
|
+
`the socket path rather than reporting anything this check could read.\n` +
|
|
86
|
+
` Run from a shorter path, or set TMPDIR to one.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The refusal as an exception, for a caller with nothing to lose by throwing. */
|
|
91
|
+
export function assertRootFits(dir) {
|
|
92
|
+
const why = rootRefusal(dir);
|
|
93
|
+
if (why) throw new Error(why);
|
|
94
|
+
return dir;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A temp root for one browser run, removed when this process exits by any route it can observe.
|
|
99
|
+
*
|
|
100
|
+
* Rooted at the ambient temp directory rather than anywhere relative to the checkout, and that is a
|
|
101
|
+
* decision rather than a convenience: the ambient root is short by construction, while a checkout,
|
|
102
|
+
* a worktree or a per-session scratch directory is routinely past the budget above. Nesting is
|
|
103
|
+
* still correct — under a test runner that has already exported its own root, this lands inside it
|
|
104
|
+
* and is carried away with it.
|
|
105
|
+
*/
|
|
106
|
+
export function browserTempRoot() {
|
|
107
|
+
const root = mkdtempSync(join(tmpdir(), ROOT_PREFIX));
|
|
108
|
+
assertRootFits(root);
|
|
109
|
+
removeOnExit(root);
|
|
110
|
+
return root;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The environment a browser must be spawned with so its singleton lock lands under `root`.
|
|
115
|
+
*
|
|
116
|
+
* `TMPDIR` is the whole mechanism, so this refuses rather than passing a root that cannot work.
|
|
117
|
+
*/
|
|
118
|
+
export function browserEnv(root, env = process.env) {
|
|
119
|
+
assertRootFits(root);
|
|
120
|
+
return { ...env, TMPDIR: root };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A temp root and a profile directory inside it, plus the environment to spawn with.
|
|
125
|
+
*
|
|
126
|
+
* The profile goes INSIDE the root so one removal covers both. Several call sites used to make the
|
|
127
|
+
* profile directly in the shared temp directory and remove it on their success path; that left the
|
|
128
|
+
* singleton lock behind on every other path, which is the leak this module exists for.
|
|
129
|
+
*/
|
|
130
|
+
export function browserRun(label = "browser") {
|
|
131
|
+
const root = mkdtempSync(join(tmpdir(), ROOT_PREFIX));
|
|
132
|
+
assertRootFits(root);
|
|
133
|
+
const keep = removeOnExit(root);
|
|
134
|
+
// The CALLER'S NAME GOES HERE, inside the root, where it costs nothing against the socket budget.
|
|
135
|
+
// Callers pass the name they used to prefix the root with, trailing dash and all, so this reads as
|
|
136
|
+
// `<root>/home-check` and an operator opening a leaked root learns which check left it.
|
|
137
|
+
const profile = join(root, label.replace(/-+$/, "") || "browser");
|
|
138
|
+
mkdirSync(profile, { recursive: true });
|
|
139
|
+
// `keep` deregisters the root from the exit sweep. A caller with a `--keep` flag MUST call it, or
|
|
140
|
+
// the flag reads as working while the directory it promised goes at exit.
|
|
141
|
+
return { root, profile, env: browserEnv(root), keep };
|
|
142
|
+
}
|
package/shared/checkout-move.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// so that one line decides which tree the whole deployment executes — and connect said nothing about
|
|
10
10
|
// having written it.
|
|
11
11
|
//
|
|
12
|
-
// Driven on
|
|
12
|
+
// Driven on the test deployment: a lane made a worktree to test a branch, ran `connect` from it once, and
|
|
13
13
|
//
|
|
14
14
|
// · the next deploy tick fast-forwarded the DETACHED worktree and failed with "You are not currently
|
|
15
15
|
// on a branch" — a message about branches, for a box that was silently a merge behind; and
|
package/shared/client-door.mjs
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
import { join } from "node:path";
|
|
43
43
|
import { challengeVerdict, blockedByAccessChallenge, challengeNote } from "./mcp-challenge.mjs"; // — F57
|
|
44
|
+
import { whatHoldsPort } from "./os-advice.mjs";
|
|
44
45
|
|
|
45
46
|
/** The unit that runs the client door. Installed like any other; started only by this module. */
|
|
46
47
|
export const CLIENT_DOOR_UNIT = "clearotron-client-mcp.service";
|
|
@@ -162,7 +163,7 @@ export function clientDoorState({ env = {}, unitDir, exists, active = null, list
|
|
|
162
163
|
const standing = fenceOn && unitInstalled;
|
|
163
164
|
// ── CONFIGURED IS NOT RUNNING ──────────────────────────────────────────────
|
|
164
165
|
//
|
|
165
|
-
// Measured
|
|
166
|
+
// Measured in testing: a `connect` that died at `daemon-reload` had ALREADY written the denylist and
|
|
166
167
|
// installed both unit files, and `doctor` then said "the client door is on" while the unit was
|
|
167
168
|
// inactive and nothing listened on its port. The trigger was a shell without a session bus, which is
|
|
168
169
|
// not the product's fault — but any failure at that step leaves the same half-applied state
|
|
@@ -187,7 +188,7 @@ export function clientDoorState({ env = {}, unitDir, exists, active = null, list
|
|
|
187
188
|
* FIVE answers, and the two that matter are the two the caller used to get wrong in opposite directions.
|
|
188
189
|
*
|
|
189
190
|
* CONFIGURED IS NOT RUNNING. A `connect` that died at `daemon-reload` has already
|
|
190
|
-
* written the fence and placed both units, so every angle reads as set up;
|
|
191
|
+
* written the fence and placed both units, so every angle reads as set up; testing measured `doctor`
|
|
191
192
|
* saying "the client door is on" over an inactive unit with nothing on its port. That state is a
|
|
192
193
|
* problem and it is the reason this function exists.
|
|
193
194
|
*
|
|
@@ -278,7 +279,7 @@ export function describeDoorState(door, {
|
|
|
278
279
|
+ "That is very likely why it cannot start: the product's ports are fixed defaults, so another "
|
|
279
280
|
+ "install or a stray process holds it, and the unit fails over and over against a port it will "
|
|
280
281
|
+ `never get. Find the holder before re-applying anything — \`${startCmd}\`'s own refusal names `
|
|
281
|
-
+
|
|
282
|
+
+ `the port and the variable, and \`${whatHoldsPort("<the door's port>")}\` names the process.` };
|
|
282
283
|
}
|
|
283
284
|
if (looping) {
|
|
284
285
|
return { level: "problem",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// `process.env.PROFILE_DIR || join(HERE, "profiles")` while the runs, the roster, the account picker and
|
|
8
8
|
// the artifacts door read `CLEAROTRON_CUSTOMERS_DIR`. Nothing set `PROFILE_DIR` — not `onboard`, not
|
|
9
9
|
// `.env.example`, not the box's own env — so the settings surface served the product's BUNDLED DEMO
|
|
10
|
-
// BUNDLE
|
|
10
|
+
// BUNDLE while the runs served the real store.
|
|
11
11
|
//
|
|
12
12
|
// WHAT THE CLIENT SAW: every brand owner added the documented way was "These settings are not available
|
|
13
13
|
// to you." The clearance itself submitted and ran under their framework. Only the surface that
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// So the rules live here and nowhere else. Anything that wants to know whether a line is clean imports
|
|
14
14
|
// from this file.
|
|
15
15
|
//
|
|
16
|
-
// THE TABLE IS NOT HERE. shared/identifier-
|
|
16
|
+
// THE TABLE IS NOT HERE. shared/identifier-sentinels.mjs owns the names (sentinels, or sentinels ∪ the
|
|
17
17
|
// private roster). This file owns only the matching — it names no identity of its own beyond the
|
|
18
18
|
// platform tokens ALLOWED_CONTEXT has to spell out, which is why it declares itself in
|
|
19
19
|
// DECLARATION_SOURCES.
|
package/shared/install-auth.mjs
CHANGED
|
@@ -124,3 +124,38 @@ export function describeAuthGaps({ gaps, modes }, envFile) {
|
|
|
124
124
|
}
|
|
125
125
|
return out;
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
// ── WHAT PUTS SOMETHING IN FRONT OF A DOOR ───────────────────────────────────────────────────────────
|
|
129
|
+
//
|
|
130
|
+
// A different question from the one above, and kept apart from it deliberately: `authRequirements`
|
|
131
|
+
// answers "what will this unit refuse to start without", which includes values that say nothing about
|
|
132
|
+
// what is in front of it — a grants file, a local passphrase user, an allowed-domain list. This answers
|
|
133
|
+
// "does something OUTSIDE this deployment resolve to these port numbers", which is the only question a
|
|
134
|
+
// launcher may move a door on.
|
|
135
|
+
//
|
|
136
|
+
// Every name here is an alternative in some door's `oneOf`: a Cloudflare Access team, or that door's own
|
|
137
|
+
// OIDC issuer. The AUDIENCES are deliberately absent — they are `all` entries, not alternatives, and an
|
|
138
|
+
// audience set with neither a team nor an issuer refuses to start on every face, so it can never be the
|
|
139
|
+
// only evidence of a proxy.
|
|
140
|
+
//
|
|
141
|
+
// THE CLIENT DOOR HAS ITS OWN ISSUER SPELLING and it is not legacy: `mcp-server/http-server-client.mjs`
|
|
142
|
+
// reads `CLIENT_MCP_OIDC_ISSUER || TRADEMARK_MCP_OIDC_ISSUER`, and its fail-closed admits a start on the
|
|
143
|
+
// client spelling alone with no team set. A deployment fronting only its client door that way once read
|
|
144
|
+
// as unfronted here, which is the state that would have moved a door behind a proxy addressed to the old
|
|
145
|
+
// number — up, and unreachable.
|
|
146
|
+
//
|
|
147
|
+
// An arm holds this list to the doors themselves rather than to this comment: it reads the entrypoints
|
|
148
|
+
// `bin/start.mjs` spawns and asserts every team-or-issuer name they read appears below.
|
|
149
|
+
export const FRONTING_VARIABLES = Object.freeze([
|
|
150
|
+
"CF_ACCESS_TEAM",
|
|
151
|
+
"PORTAL_OIDC_ISSUER",
|
|
152
|
+
"TRADEMARK_MCP_OIDC_ISSUER",
|
|
153
|
+
"CLIENT_MCP_OIDC_ISSUER",
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The fronting values this environment has set — empty means nothing outside resolves to these doors.
|
|
158
|
+
*
|
|
159
|
+
* @returns {string[]} the names that are set, in the order above
|
|
160
|
+
*/
|
|
161
|
+
export const frontingVariablesSet = (env = {}) => FRONTING_VARIABLES.filter((k) => SET(env, k));
|
package/shared/invocation.mjs
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
import { existsSync } from "node:fs";
|
|
40
40
|
import { basename, sep } from "node:path";
|
|
41
41
|
import { INSTALL_DIR, inspectShim, pathPosition, shimDir, shimPath } from "./verb-shim.mjs";
|
|
42
|
+
import { chdirPrefix } from "./os-advice.mjs";
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* Filesystem reads, injectable so the arms can drive a machine that is not this one.
|
|
@@ -92,6 +93,34 @@ export function standFrom(dir) {
|
|
|
92
93
|
return at === -1 ? s : s.slice(0, at);
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
/**
|
|
97
|
+
* WHICH ROUTE THIS INSTALL ARRIVED BY — a package, or a source checkout.
|
|
98
|
+
*
|
|
99
|
+
* The setup wizard has two spellings and only one of them works for any given reader. Someone who ran
|
|
100
|
+
* `npm i clearotron` has no package scripts, so `npm run setup` is a command they cannot type; someone
|
|
101
|
+
* working in a clone of the source has the scripts but no `clearotron` binary linked for them, because
|
|
102
|
+
* npm links a package's bin for its dependencies and never for itself. Naming one of the two on a
|
|
103
|
+
* screen is a coin flip, and the screen that names it is read by people who have just installed and
|
|
104
|
+
* have nothing else to go on.
|
|
105
|
+
*
|
|
106
|
+
* The answer is a property of where this file sits on disk, so it is derived from that. A package —
|
|
107
|
+
* local or global — is unpacked inside a `node_modules` directory; a checkout is not. `standFrom`
|
|
108
|
+
* already draws that line for the sibling question of where to stand, and reusing it is deliberate:
|
|
109
|
+
* two predicates for "am I a package" would be two chances to disagree.
|
|
110
|
+
*
|
|
111
|
+
* NO PATH IS RETURNED, and that is the point of splitting it out from `invocationForm`. That function's
|
|
112
|
+
* `prefix` can be `cd /srv/whatever && npx `, which is the server's own layout, its account name and
|
|
113
|
+
* its directory structure. This answers the same question with a word, so the answer can travel to a
|
|
114
|
+
* browser without carrying the machine with it.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} dir where this module is installed
|
|
117
|
+
* @returns {'packaged'|'checkout'}
|
|
118
|
+
*/
|
|
119
|
+
export function installRoute(dir = INSTALL_DIR) {
|
|
120
|
+
const s = String(dir ?? "");
|
|
121
|
+
return standFrom(s) === s ? "checkout" : "packaged";
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
/**
|
|
96
125
|
* WHERE npm PUT THE EXECUTABLE for a GLOBAL install of this package —.
|
|
97
126
|
*
|
|
@@ -181,7 +210,10 @@ export function invocationForm(env = process.env, io = FS, installDir = INSTALL_
|
|
|
181
210
|
// to fix it — through the very shim it had just called broken. The advice for repairing a route
|
|
182
211
|
// cannot travel that route. Both remaining forms go around it.
|
|
183
212
|
return {
|
|
184
|
-
|
|
213
|
+
// THE SEPARATOR IS THE PART THAT FAILS OFF POSIX, not the path. `chdirPrefix` answers for the
|
|
214
|
+
// shell the reader is actually in; on Windows PowerShell 5.1 there is no `&&` at all, so this
|
|
215
|
+
// line was a parse error there before its backslashes mattered. Reported from a real run.
|
|
216
|
+
form: "in-place", prefix: `${chdirPrefix(standFrom(installDir))}npx `, shim: path, dir,
|
|
185
217
|
onPath: false, shadowedBy: null, shimKind: shim.kind, otherInstall: shim.installDir,
|
|
186
218
|
staleInterpreter: shim.interpreterMissing === true ? shim.interpreter : null,
|
|
187
219
|
};
|