etymd 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +47 -23
- package/dist/{approve-FISVPFL2.js → approve-ZLOW4A7R.js} +6 -6
- package/dist/audit-ICT2JK7A.js +12 -0
- package/dist/{brief-UF6KV7SG.js → brief-PDUGGMLI.js} +6 -6
- package/dist/{chunk-TADKOW6P.js → chunk-5BVKFJWM.js} +7 -3
- package/dist/{chunk-IGHES6AK.js → chunk-6DUDIVIC.js} +1 -1
- package/dist/{chunk-4AEYMVZK.js → chunk-BQGCLPHS.js} +2 -2
- package/dist/{chunk-R74SJ7HS.js → chunk-C7LBUFNU.js} +1 -1
- package/dist/{chunk-WWV5W2MM.js → chunk-HLWCODYX.js} +1 -1
- package/dist/{chunk-YLNQQZ5F.js → chunk-IV3FYVTS.js} +11 -1
- package/dist/{chunk-NMZ3RHWW.js → chunk-K7QBTANO.js} +2 -2
- package/dist/{chunk-OX3LUOZR.js → chunk-KTIEYKFK.js} +46 -22
- package/dist/{chunk-WWBF4Y7K.js → chunk-OAFYLBVG.js} +1 -1
- package/dist/{chunk-6G3JJMZ3.js → chunk-OESQNO2J.js} +77 -9
- package/dist/{chunk-LT67FU2Y.js → chunk-OKU3HXIH.js} +2 -2
- package/dist/{chunk-HX5IYDCU.js → chunk-ULEWJ3ZK.js} +3 -3
- package/dist/{chunk-MMPV67FW.js → chunk-V2SOHPXF.js} +2 -2
- package/dist/cli.js +18 -18
- package/dist/config-UJRSCDMD.js +4 -0
- package/dist/{context-4DGXO7JR.js → context-ZEUI5ESZ.js} +5 -5
- package/dist/doctor-PORXFSYT.js +19 -0
- package/dist/{fleet-UTFZKYNH.js → fleet-JHNQOLSY.js} +71 -22
- package/dist/{gates-YUUV7YI4.js → gates-KHC7HPIF.js} +21 -13
- package/dist/generate-UTKQ4GE6.js +6 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +138 -31
- package/dist/{init-Q5Q7IGZS.js → init-IPUPP4GB.js} +8 -8
- package/dist/ledger-B2XGANTH.js +5 -0
- package/dist/{scan-UVNX52J5.js → scan-6L4L2GXT.js} +6 -6
- package/dist/scan-D4M7JXE3.js +5 -0
- package/dist/{screen-U7JMWZPS.js → screen-6R6NXDSY.js} +2 -2
- package/package.json +3 -4
- package/dist/audit-GUG2QFZM.js +0 -12
- package/dist/config-MX7OYI7F.js +0 -4
- package/dist/doctor-SOLNC37X.js +0 -19
- package/dist/generate-CBMTNN7P.js +0 -6
- package/dist/ledger-TMIV45AA.js +0 -5
- package/dist/scan-NWZ65YWB.js +0 -5
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { measureContext } from './chunk-
|
|
3
|
-
import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-
|
|
4
|
-
import { DEFAULT_CONFIG, readConfig, CONFIG_FILE } from './chunk-
|
|
5
|
-
import { scanProject, expandFileGlobs } from './chunk-
|
|
6
|
-
import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-
|
|
7
|
-
import { PACK_VERSION } from './chunk-
|
|
8
|
-
import { pathExists, readJson, git, readText, isDirectory, matchesAnyGlob, isCiEnvironment, normalizeRelPath } from './chunk-
|
|
2
|
+
import { measureContext } from './chunk-OKU3HXIH.js';
|
|
3
|
+
import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-OAFYLBVG.js';
|
|
4
|
+
import { DEFAULT_CONFIG, readConfig, CONFIG_FILE } from './chunk-K7QBTANO.js';
|
|
5
|
+
import { scanProject, expandFileGlobs } from './chunk-ULEWJ3ZK.js';
|
|
6
|
+
import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-C7LBUFNU.js';
|
|
7
|
+
import { PACK_VERSION } from './chunk-6DUDIVIC.js';
|
|
8
|
+
import { pathExists, readJson, git, readText, isDirectory, matchesAnyGlob, isCiEnvironment, normalizeRelPath, isExecutable } from './chunk-IV3FYVTS.js';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import YAML from 'yaml';
|
|
11
11
|
import { promises } from 'node:fs';
|
|
@@ -302,13 +302,43 @@ function parseGithubWorkflow(text, file, scripts) {
|
|
|
302
302
|
}
|
|
303
303
|
return { jobs, parseErrors };
|
|
304
304
|
}
|
|
305
|
+
async function companionOf(root, hooksDir, hook, hookText, scripts) {
|
|
306
|
+
const rel = path.posix.join(hooksDir.split(path.sep).join("/"), `${hook}.local`);
|
|
307
|
+
if (!hookText.includes(`${hook}.local`)) return null;
|
|
308
|
+
const abs = path.join(root, hooksDir, `${hook}.local`);
|
|
309
|
+
const text = await readText(abs);
|
|
310
|
+
if (text === null) return null;
|
|
311
|
+
const [executable, hookExecutable] = await Promise.all([
|
|
312
|
+
isExecutable(abs),
|
|
313
|
+
isExecutable(path.join(root, hooksDir, hook))
|
|
314
|
+
]);
|
|
315
|
+
if (executable === false && hookExecutable === true) return { rel, tools: [], state: "inert" };
|
|
316
|
+
return {
|
|
317
|
+
rel,
|
|
318
|
+
tools: matchTools(text, scripts),
|
|
319
|
+
state: executable === true ? "counted" : "unverified"
|
|
320
|
+
};
|
|
321
|
+
}
|
|
305
322
|
async function localHookTools(root, facts, scripts) {
|
|
306
323
|
const hooks = facts.hooks;
|
|
307
324
|
const empty = [];
|
|
325
|
+
const companions = [];
|
|
326
|
+
const unverifiedCompanions = [];
|
|
327
|
+
const inertCompanions = [];
|
|
308
328
|
const readHook = async (name) => {
|
|
309
329
|
if (!hooks.dir) return empty;
|
|
310
330
|
const text = await readText(path.join(root, hooks.dir, name));
|
|
311
|
-
|
|
331
|
+
if (!text) return empty;
|
|
332
|
+
const tools = new Set(matchTools(text, scripts));
|
|
333
|
+
const companion = await companionOf(root, hooks.dir, name, text, scripts);
|
|
334
|
+
if (companion) {
|
|
335
|
+
if (companion.state === "inert") inertCompanions.push(companion.rel);
|
|
336
|
+
else {
|
|
337
|
+
(companion.state === "counted" ? companions : unverifiedCompanions).push(companion.rel);
|
|
338
|
+
for (const t of companion.tools) tools.add(t);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return [...tools];
|
|
312
342
|
};
|
|
313
343
|
let preCommit = [];
|
|
314
344
|
let prePush = [];
|
|
@@ -352,7 +382,17 @@ async function localHookTools(root, facts, scripts) {
|
|
|
352
382
|
}
|
|
353
383
|
}
|
|
354
384
|
const wired = hooks.source === "custom" || hooks.source === "husky" || hooks.source === "husky-legacy" ? true : hooks.source === "githooks" ? facts.git.hooksPath === hooks.dir : false;
|
|
355
|
-
return {
|
|
385
|
+
return {
|
|
386
|
+
source: hooks.source,
|
|
387
|
+
wired,
|
|
388
|
+
preCommit,
|
|
389
|
+
prePush,
|
|
390
|
+
commitMsg,
|
|
391
|
+
lintStaged,
|
|
392
|
+
companions,
|
|
393
|
+
unverifiedCompanions,
|
|
394
|
+
inertCompanions
|
|
395
|
+
};
|
|
356
396
|
}
|
|
357
397
|
async function detectThresholds(root, scripts, ciJobs) {
|
|
358
398
|
const sonarConfigured = await pathExists(path.join(root, "sonar-project.properties")) || ciJobs.some((j) => j.tools.includes("sonar"));
|
|
@@ -485,6 +525,24 @@ function deriveGateFindings(inv, disclosures = []) {
|
|
|
485
525
|
);
|
|
486
526
|
}
|
|
487
527
|
}
|
|
528
|
+
if (inv.local.inertCompanions?.length) {
|
|
529
|
+
const files = inv.local.inertCompanions;
|
|
530
|
+
findings.push(
|
|
531
|
+
finding({
|
|
532
|
+
id: `${LENS_ID2}/companion-not-executable`,
|
|
533
|
+
tier: "gap",
|
|
534
|
+
kind: "truth",
|
|
535
|
+
claim: `${files.join(", ")} ${files.length === 1 ? "is" : "are"} called by a hook but not executable \u2014 the checks inside never run`,
|
|
536
|
+
evidence: files,
|
|
537
|
+
why: "The hook guards the call with `[ -x ]`, so it skips the file in silence \u2014 the repo looks guarded and is not, on every clone that carries the same mode bit.",
|
|
538
|
+
// chmod alone does not survive a fresh clone: git tracks exactly one permission bit, so
|
|
539
|
+
// the fix has to reach the index or the dead gate propagates to everyone else.
|
|
540
|
+
action: `Run \`chmod +x ${files[0]}\` and commit the mode with \`git update-index --chmod=+x ${files[0]}\`.`,
|
|
541
|
+
effort: "S",
|
|
542
|
+
confidence: "high"
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
}
|
|
488
546
|
for (const tool of CORRECTNESS_TOOLS) {
|
|
489
547
|
if (!enforcedCiTools.has(tool) || localTools.has(tool)) continue;
|
|
490
548
|
const jobs = enforcedCiJobs.filter((j) => j.tools.includes(tool));
|
|
@@ -585,6 +643,16 @@ var gateIntegrityLens = {
|
|
|
585
643
|
}
|
|
586
644
|
if (inv.ci.parseErrors.length)
|
|
587
645
|
disclosures.push(...inv.ci.parseErrors.map((e) => `CI parse: ${e}`));
|
|
646
|
+
if (inv.local.companions.length) {
|
|
647
|
+
disclosures.push(
|
|
648
|
+
`Checks in ${inv.local.companions.join(", ")} are counted as locally enforced \u2014 the generated hook calls the companion, and etymd reads it without owning it.`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (inv.local.unverifiedCompanions.length) {
|
|
652
|
+
disclosures.push(
|
|
653
|
+
`Checks in ${inv.local.unverifiedCompanions.join(", ")} are counted as locally enforced, but the execute bit could not be judged here (no POSIX mode bits, or the hook beside it is not executable either) \u2014 whether ${inv.local.unverifiedCompanions.length === 1 ? "it runs" : "they run"} was not verified.`
|
|
654
|
+
);
|
|
655
|
+
}
|
|
588
656
|
if (inv.ci.system === "none")
|
|
589
657
|
disclosures.push("No CI configuration found \u2014 CI\u2194local comparisons skipped.");
|
|
590
658
|
const findings = deriveGateFindings(inv, disclosures);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { DEFAULT_CONFIG } from './chunk-
|
|
3
|
-
import { readText, wordCount, approxTokens, pathExists } from './chunk-
|
|
2
|
+
import { DEFAULT_CONFIG } from './chunk-K7QBTANO.js';
|
|
3
|
+
import { readText, wordCount, approxTokens, pathExists } from './chunk-IV3FYVTS.js';
|
|
4
4
|
import { promises } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { VERSION } from './chunk-
|
|
3
|
-
import { PACK_VERSION } from './chunk-
|
|
4
|
-
import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-
|
|
2
|
+
import { VERSION } from './chunk-HLWCODYX.js';
|
|
3
|
+
import { PACK_VERSION } from './chunk-6DUDIVIC.js';
|
|
4
|
+
import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-IV3FYVTS.js';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { promises } from 'node:fs';
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readLedger, resolveEntry, writeLedger } from './chunk-
|
|
3
|
-
import { print, renderLedger, glyph, theme } from './chunk-
|
|
2
|
+
import { readLedger, resolveEntry, writeLedger } from './chunk-OAFYLBVG.js';
|
|
3
|
+
import { print, renderLedger, glyph, theme } from './chunk-5BVKFJWM.js';
|
|
4
4
|
|
|
5
5
|
// src/commands/ledger.ts
|
|
6
6
|
async function list(opts) {
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { VERSION } from './chunk-
|
|
2
|
+
import { VERSION } from './chunk-HLWCODYX.js';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import pc from 'picocolors';
|
|
@@ -31,7 +31,7 @@ program.command("audit").description("Verify every instruction claim against the
|
|
|
31
31
|
"exit non-zero when findings at/above this tier exist (risk|gap|polish)"
|
|
32
32
|
).action(
|
|
33
33
|
(opts, cmd) => action(async () => {
|
|
34
|
-
const { run } = await import('./audit-
|
|
34
|
+
const { run } = await import('./audit-ICT2JK7A.js');
|
|
35
35
|
await run({
|
|
36
36
|
cwd: resolveCwd(cmd),
|
|
37
37
|
json: opts.json,
|
|
@@ -44,7 +44,7 @@ program.command("audit").description("Verify every instruction claim against the
|
|
|
44
44
|
);
|
|
45
45
|
program.command("init").description("Onboard the truth guard: approve the baseline; scaffold AGENTS.md only if missing").option("-y, --yes", "accept defaults without prompting (never overwrites)").action(
|
|
46
46
|
(opts, cmd) => action(async () => {
|
|
47
|
-
const { run } = await import('./init-
|
|
47
|
+
const { run } = await import('./init-IPUPP4GB.js');
|
|
48
48
|
await run({ cwd: resolveCwd(cmd), yes: opts.yes });
|
|
49
49
|
})
|
|
50
50
|
);
|
|
@@ -52,49 +52,49 @@ program.command("approve").description(
|
|
|
52
52
|
"Re-approve the committed baseline after intentional structural changes (non-interactive)"
|
|
53
53
|
).action(
|
|
54
54
|
(_opts, cmd) => action(async () => {
|
|
55
|
-
const { run } = await import('./approve-
|
|
55
|
+
const { run } = await import('./approve-ZLOW4A7R.js');
|
|
56
56
|
await run({ cwd: resolveCwd(cmd) });
|
|
57
57
|
})
|
|
58
58
|
);
|
|
59
59
|
program.command("scan").description("Deterministically reckon the project into a facts index").option("--json", "print the raw facts as JSON").option("--no-save", "do not write the .etymd cache").action(
|
|
60
60
|
(opts, cmd) => action(async () => {
|
|
61
|
-
const { run } = await import('./scan-
|
|
61
|
+
const { run } = await import('./scan-6L4L2GXT.js');
|
|
62
62
|
await run({ cwd: resolveCwd(cmd), json: opts.json, save: opts.save });
|
|
63
63
|
})
|
|
64
64
|
);
|
|
65
65
|
program.command("doctor").description('Alias for `audit --truth` \u2014 "are the recorded instructions still true?"').option("--json", "print findings as JSON").action(
|
|
66
66
|
(opts, cmd) => action(async () => {
|
|
67
|
-
const { run } = await import('./doctor-
|
|
67
|
+
const { run } = await import('./doctor-PORXFSYT.js');
|
|
68
68
|
await run({ cwd: resolveCwd(cmd), json: opts.json });
|
|
69
69
|
})
|
|
70
70
|
);
|
|
71
71
|
program.command("context").description("Measure the always-loaded context footprint and flag extraction candidates").option("--json", "print the budget as JSON").action(
|
|
72
72
|
(opts, cmd) => action(async () => {
|
|
73
|
-
const { run } = await import('./context-
|
|
73
|
+
const { run } = await import('./context-ZEUI5ESZ.js');
|
|
74
74
|
await run({ cwd: resolveCwd(cmd), json: opts.json });
|
|
75
75
|
})
|
|
76
76
|
);
|
|
77
77
|
program.command("brief").description("Emit a grounded briefing for the in-repo agent to complete the semantic layer").option("--human", "write a human onboarding brief instead of the agent briefing").action(
|
|
78
78
|
(opts, cmd) => action(async () => {
|
|
79
|
-
const { run } = await import('./brief-
|
|
79
|
+
const { run } = await import('./brief-PDUGGMLI.js');
|
|
80
80
|
await run({ cwd: resolveCwd(cmd), human: opts.human });
|
|
81
81
|
})
|
|
82
82
|
);
|
|
83
83
|
program.command("ledger").description("List every tracked finding grouped by status (open \xB7 accepted \xB7 dismissed \xB7 \u2026)").option("--json", "print the raw ledger as JSON").action(
|
|
84
84
|
(opts, cmd) => action(async () => {
|
|
85
|
-
const { list } = await import('./ledger-
|
|
85
|
+
const { list } = await import('./ledger-B2XGANTH.js');
|
|
86
86
|
await list({ cwd: resolveCwd(cmd), json: opts.json });
|
|
87
87
|
})
|
|
88
88
|
);
|
|
89
89
|
program.command("dismiss").argument("<finding-id>", "the finding id (from `etymd audit`) to dismiss").description("Dismiss a finding as a false positive / not applicable (hidden from future audits)").requiredOption("--reason <text>", "why it is dismissed \u2014 recorded so the decision survives").action(
|
|
90
90
|
(id, opts, cmd) => action(async () => {
|
|
91
|
-
const { dismiss } = await import('./ledger-
|
|
91
|
+
const { dismiss } = await import('./ledger-B2XGANTH.js');
|
|
92
92
|
await dismiss({ cwd: resolveCwd(cmd), id, reason: opts.reason });
|
|
93
93
|
})
|
|
94
94
|
);
|
|
95
95
|
program.command("accept").argument("<finding-id>", "the finding id (from `etymd audit`) to accept").description("Accept a real finding as a known trade-off (hidden from future audits)").option("--reason <text>", "optional note on why the trade-off is accepted").action(
|
|
96
96
|
(id, opts, cmd) => action(async () => {
|
|
97
|
-
const { accept } = await import('./ledger-
|
|
97
|
+
const { accept } = await import('./ledger-B2XGANTH.js');
|
|
98
98
|
await accept({ cwd: resolveCwd(cmd), id, reason: opts.reason });
|
|
99
99
|
})
|
|
100
100
|
);
|
|
@@ -111,7 +111,7 @@ var fleet = program.command("fleet").description(
|
|
|
111
111
|
"exit non-zero when findings at/above this tier exist (risk|gap|polish)"
|
|
112
112
|
).action(
|
|
113
113
|
(opts, cmd) => action(async () => {
|
|
114
|
-
const { sweep } = await import('./fleet-
|
|
114
|
+
const { sweep } = await import('./fleet-JHNQOLSY.js');
|
|
115
115
|
await sweep({
|
|
116
116
|
cwd: resolveCwd(cmd),
|
|
117
117
|
manifest: opts.manifest,
|
|
@@ -126,7 +126,7 @@ var fleet = program.command("fleet").description(
|
|
|
126
126
|
);
|
|
127
127
|
fleet.command("check").description("Validate the manifest pair only \u2014 no lenses: dangling mappings, duplicates, leaks").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").option("--json", "print the findings as JSON (EXPERIMENTAL through 0.2.x)").action(
|
|
128
128
|
(_opts, cmd) => action(async () => {
|
|
129
|
-
const { check } = await import('./fleet-
|
|
129
|
+
const { check } = await import('./fleet-JHNQOLSY.js');
|
|
130
130
|
const opts = cmd.optsWithGlobals();
|
|
131
131
|
await check({ cwd: resolveCwd(cmd), manifest: opts.manifest, json: opts.json });
|
|
132
132
|
})
|
|
@@ -136,7 +136,7 @@ fleet.command("add").argument("<dir>", "directory of the project to register").d
|
|
|
136
136
|
"public-repo | public-bound | private \u2014 mandatory for personal entries"
|
|
137
137
|
).option("-y, --yes", "skip prompts; every mandatory value must be passed as a flag").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
|
|
138
138
|
(dir, opts, cmd) => action(async () => {
|
|
139
|
-
const { add } = await import('./fleet-
|
|
139
|
+
const { add } = await import('./fleet-JHNQOLSY.js');
|
|
140
140
|
const shared = cmd.optsWithGlobals();
|
|
141
141
|
await add({
|
|
142
142
|
cwd: resolveCwd(cmd),
|
|
@@ -159,7 +159,7 @@ fleet.command("dismiss").argument("<name>", "the registered project name").argum
|
|
|
159
159
|
"Dismiss a project's finding from any cwd \u2014 corp ledgers persist beside the manifest"
|
|
160
160
|
).requiredOption("--reason <text>", "why it is dismissed \u2014 recorded so the decision survives").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
|
|
161
161
|
(name, id, _opts, cmd) => action(async () => {
|
|
162
|
-
const { dismiss } = await import('./fleet-
|
|
162
|
+
const { dismiss } = await import('./fleet-JHNQOLSY.js');
|
|
163
163
|
const opts = cmd.optsWithGlobals();
|
|
164
164
|
await dismiss({
|
|
165
165
|
cwd: resolveCwd(cmd),
|
|
@@ -174,7 +174,7 @@ fleet.command("accept").argument("<name>", "the registered project name").argume
|
|
|
174
174
|
"Accept a project's finding as a known trade-off \u2014 corp ledgers persist beside the manifest"
|
|
175
175
|
).option("--reason <text>", "optional note on why the trade-off is accepted").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
|
|
176
176
|
(name, id, _opts, cmd) => action(async () => {
|
|
177
|
-
const { accept } = await import('./fleet-
|
|
177
|
+
const { accept } = await import('./fleet-JHNQOLSY.js');
|
|
178
178
|
const opts = cmd.optsWithGlobals();
|
|
179
179
|
await accept({ cwd: resolveCwd(cmd), manifest: opts.manifest, name, id, reason: opts.reason });
|
|
180
180
|
})
|
|
@@ -183,7 +183,7 @@ program.command("screen").description(
|
|
|
183
183
|
"Content screen: check for text that must never be published. Bring your own patterns \u2014 etymd ships none."
|
|
184
184
|
).option("--staged", "screen what a commit is about to add (default)").option("--tree", "screen every tracked file \u2014 what is about to leave the machine").option("--message <file>", "screen a commit message (the commit-msg hook's $1)").option("--dir <dir>", "screen an unpacked build artifact \u2014 what actually ships").option("--patterns <file>", "pattern file (default: ~/.config/etymd/screen-patterns)").option("--advisory", "report without failing").action(
|
|
185
185
|
(opts, cmd) => action(async () => {
|
|
186
|
-
const { run } = await import('./screen-
|
|
186
|
+
const { run } = await import('./screen-6R6NXDSY.js');
|
|
187
187
|
const scope = opts.message ? "message" : opts.dir ? "dir" : opts.tree ? "tree" : "staged";
|
|
188
188
|
await run({
|
|
189
189
|
cwd: resolveCwd(cmd),
|
|
@@ -196,7 +196,7 @@ program.command("screen").description(
|
|
|
196
196
|
);
|
|
197
197
|
program.command("gates").description("Install the local git-hook gates (process \u2192 pre-commit, correctness \u2192 pre-push)").option("--ci", "note about the CI review gate (ships later; local gates install now)").option("-y, --yes", "skip prompts; never overwrites a hand-edited hook").action(
|
|
198
198
|
(opts, cmd) => action(async () => {
|
|
199
|
-
const { run } = await import('./gates-
|
|
199
|
+
const { run } = await import('./gates-KHC7HPIF.js');
|
|
200
200
|
await run({ cwd: resolveCwd(cmd), ci: opts.ci, yes: opts.yes });
|
|
201
201
|
})
|
|
202
202
|
);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { measureContext } from './chunk-
|
|
3
|
-
import { readConfig } from './chunk-
|
|
4
|
-
import { print, renderContext } from './chunk-
|
|
5
|
-
import './chunk-
|
|
6
|
-
import './chunk-
|
|
2
|
+
import { measureContext } from './chunk-OKU3HXIH.js';
|
|
3
|
+
import { readConfig } from './chunk-K7QBTANO.js';
|
|
4
|
+
import { print, renderContext } from './chunk-5BVKFJWM.js';
|
|
5
|
+
import './chunk-C7LBUFNU.js';
|
|
6
|
+
import './chunk-IV3FYVTS.js';
|
|
7
7
|
|
|
8
8
|
// src/commands/context.ts
|
|
9
9
|
async function run(opts) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from './chunk-BQGCLPHS.js';
|
|
3
|
+
import './chunk-OESQNO2J.js';
|
|
4
|
+
import './chunk-OKU3HXIH.js';
|
|
5
|
+
import './chunk-OAFYLBVG.js';
|
|
6
|
+
import './chunk-K7QBTANO.js';
|
|
7
|
+
import './chunk-5BVKFJWM.js';
|
|
8
|
+
import './chunk-ULEWJ3ZK.js';
|
|
9
|
+
import './chunk-HLWCODYX.js';
|
|
10
|
+
import './chunk-C7LBUFNU.js';
|
|
11
|
+
import './chunk-6DUDIVIC.js';
|
|
12
|
+
import './chunk-IV3FYVTS.js';
|
|
13
|
+
|
|
14
|
+
// src/commands/doctor.ts
|
|
15
|
+
async function run2(opts) {
|
|
16
|
+
await run({ cwd: opts.cwd, json: opts.json, truth: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { run2 as run };
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { dismiss, accept } from './chunk-
|
|
3
|
-
import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import { readLedger } from './chunk-
|
|
6
|
-
import { DEFAULT_CONFIG } from './chunk-
|
|
7
|
-
import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-
|
|
8
|
-
import { scanProject } from './chunk-
|
|
9
|
-
import './chunk-
|
|
10
|
-
import { ETYMD_DIR } from './chunk-
|
|
11
|
-
import './chunk-
|
|
12
|
-
import { readText, pathExists, git, isDirectory } from './chunk-
|
|
2
|
+
import { dismiss, accept } from './chunk-V2SOHPXF.js';
|
|
3
|
+
import { parseFailOnTier, meetsFailOn, runAudit } from './chunk-OESQNO2J.js';
|
|
4
|
+
import './chunk-OKU3HXIH.js';
|
|
5
|
+
import { readLedger } from './chunk-OAFYLBVG.js';
|
|
6
|
+
import { DEFAULT_CONFIG } from './chunk-K7QBTANO.js';
|
|
7
|
+
import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-5BVKFJWM.js';
|
|
8
|
+
import { scanProject } from './chunk-ULEWJ3ZK.js';
|
|
9
|
+
import './chunk-HLWCODYX.js';
|
|
10
|
+
import { ETYMD_DIR } from './chunk-C7LBUFNU.js';
|
|
11
|
+
import './chunk-6DUDIVIC.js';
|
|
12
|
+
import { readText, pathExists, git, isDirectory } from './chunk-IV3FYVTS.js';
|
|
13
13
|
import { promises } from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
14
15
|
import path from 'node:path';
|
|
15
16
|
import { select, isCancel, cancel, text, confirm } from '@clack/prompts';
|
|
16
|
-
import os from 'node:os';
|
|
17
17
|
import { execFile } from 'node:child_process';
|
|
18
18
|
import { promisify } from 'node:util';
|
|
19
19
|
|
|
@@ -687,9 +687,9 @@ async function checkCorpEmails(manifest, findings, disclosures) {
|
|
|
687
687
|
}
|
|
688
688
|
}
|
|
689
689
|
async function checkGateDrift(manifest, findings, disclosures) {
|
|
690
|
-
const { planWorkflow } = await import('./generate-
|
|
691
|
-
const { scanProject: scanProject2 } = await import('./scan-
|
|
692
|
-
const { readConfig } = await import('./config-
|
|
690
|
+
const { planWorkflow } = await import('./generate-UTKQ4GE6.js');
|
|
691
|
+
const { scanProject: scanProject2 } = await import('./scan-D4M7JXE3.js');
|
|
692
|
+
const { readConfig } = await import('./config-UJRSCDMD.js');
|
|
693
693
|
for (const entry of manifest.entries) {
|
|
694
694
|
const root = entry.resolvedRoot;
|
|
695
695
|
if (!root || !await isDirectory(root)) continue;
|
|
@@ -709,7 +709,14 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
709
709
|
const { config } = await readConfig(root);
|
|
710
710
|
const planned = (await planWorkflow(root, facts, { agents: false, gates: true, gateConfig: config.gates })).filter((f) => f.executable);
|
|
711
711
|
const missing = planned.filter((f) => !f.exists).map((f) => f.path);
|
|
712
|
-
const
|
|
712
|
+
const differing = planned.filter((f) => f.exists && f.differs);
|
|
713
|
+
const stale = differing.filter((f) => f.drift !== "edited").map((f) => f.path);
|
|
714
|
+
const edited = differing.filter((f) => f.drift === "edited").map((f) => f.path);
|
|
715
|
+
if (edited.length) {
|
|
716
|
+
disclosures.push(
|
|
717
|
+
`${entry.name}: ${edited.join(", ")} edited since etymd generated ${edited.length === 1 ? "it" : "them"} \u2014 a customisation, not drift; etymd will not overwrite ${edited.length === 1 ? "it" : "them"}`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
713
720
|
if (missing.length) {
|
|
714
721
|
findings.push(
|
|
715
722
|
finding(
|
|
@@ -729,8 +736,8 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
729
736
|
"polish",
|
|
730
737
|
`\`${entry.name}\` has ${stale.length} gate${stale.length === 1 ? "" : "s"} that differ from what its own inputs generate`,
|
|
731
738
|
stale.map((s) => `${entry.name}: ${s}`),
|
|
732
|
-
"
|
|
733
|
-
"Re-run `etymd gates`
|
|
739
|
+
"An outdated hook drifts from the pack silently \u2014 the repo believes it runs checks the file no longer contains.",
|
|
740
|
+
"Re-run `etymd gates` in that repo \u2014 it regenerates a gate it provably wrote and asks before touching anything else."
|
|
734
741
|
)
|
|
735
742
|
);
|
|
736
743
|
}
|
|
@@ -1077,6 +1084,35 @@ async function check(opts) {
|
|
|
1077
1084
|
renderFindings(findings);
|
|
1078
1085
|
renderFleetNotes([], disclosures);
|
|
1079
1086
|
}
|
|
1087
|
+
async function recordCorpMapping(manifest, name, absTarget) {
|
|
1088
|
+
const home = os.homedir();
|
|
1089
|
+
const value = absTarget === home || absTarget.startsWith(home + path.sep) ? `~${absTarget.slice(home.length)}` : absTarget;
|
|
1090
|
+
const insideRepo = await git(manifest.dir, ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
1091
|
+
if (insideRepo) {
|
|
1092
|
+
const ignored = await git(manifest.dir, ["check-ignore", "-q", manifest.localPath]) !== null;
|
|
1093
|
+
if (!ignored) {
|
|
1094
|
+
throw new Error(
|
|
1095
|
+
`${path.basename(manifest.localPath)} is not gitignored \u2014 refusing to write employer directory names into a file git would track. Add it to .gitignore, then re-run.`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const existing = await readText(manifest.localPath);
|
|
1100
|
+
let doc = {};
|
|
1101
|
+
if (existing) {
|
|
1102
|
+
try {
|
|
1103
|
+
doc = JSON.parse(existing);
|
|
1104
|
+
} catch {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
`${path.basename(manifest.localPath)} is not valid JSON \u2014 fix it before registering, so the mapping is not written over hand-maintained entries that cannot be re-derived.`
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const dirs = doc.dirs ?? {};
|
|
1111
|
+
dirs[name] = value;
|
|
1112
|
+
doc.dirs = dirs;
|
|
1113
|
+
await promises.writeFile(manifest.localPath, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
1114
|
+
return value;
|
|
1115
|
+
}
|
|
1080
1116
|
async function add(opts) {
|
|
1081
1117
|
const { manifest, autoNote } = await loadManifest(opts.cwd, opts.manifest);
|
|
1082
1118
|
if (autoNote) print(` ${theme.dim(`\u25E6 ${autoNote}`)}`);
|
|
@@ -1190,10 +1226,14 @@ async function add(opts) {
|
|
|
1190
1226
|
path: path.relative(manifest.root ?? manifest.dir, absTarget),
|
|
1191
1227
|
trust,
|
|
1192
1228
|
contract: {},
|
|
1193
|
-
links: {}
|
|
1194
|
-
// The
|
|
1195
|
-
//
|
|
1196
|
-
|
|
1229
|
+
links: {}
|
|
1230
|
+
// The remote is deliberately NOT recorded. Nothing reads it — it was persisted only
|
|
1231
|
+
// because it happened to be derivable — and it is the field that turns a mis-profiled
|
|
1232
|
+
// entry into a disclosure: a raw URL carries the host AND the internal group path,
|
|
1233
|
+
// while `path` carries a bare directory name. Removing a field no consumer reads
|
|
1234
|
+
// retires that whole class without any host-matching guess, and keeps working on a
|
|
1235
|
+
// machine that has no local manifest for the corp-host guard above to read.
|
|
1236
|
+
// It stays derivable at any time from the checkout itself, which is where it came from.
|
|
1197
1237
|
};
|
|
1198
1238
|
print(` ${glyph.bullet} ${theme.dim("entry")}`);
|
|
1199
1239
|
print(
|
|
@@ -1208,11 +1248,20 @@ async function add(opts) {
|
|
|
1208
1248
|
return;
|
|
1209
1249
|
}
|
|
1210
1250
|
}
|
|
1251
|
+
let mappedTo;
|
|
1252
|
+
if (profile === "corp") {
|
|
1253
|
+
mappedTo = await recordCorpMapping(manifest, name, absTarget);
|
|
1254
|
+
}
|
|
1211
1255
|
const doc = JSON.parse(raw);
|
|
1212
1256
|
if (!Array.isArray(doc.projects)) throw new Error("manifest has no `projects` array to append to");
|
|
1213
1257
|
doc.projects.push(entry);
|
|
1214
1258
|
await promises.writeFile(manifest.manifestPath, JSON.stringify(doc, null, 2) + "\n", "utf8");
|
|
1215
1259
|
print(` ${glyph.ok} ${theme.dim("registered")} ${theme.info(name)}`);
|
|
1260
|
+
if (mappedTo) {
|
|
1261
|
+
print(
|
|
1262
|
+
` ${glyph.ok} ${theme.dim("mapped")} ${theme.info(`${name} \u2192 ${mappedTo}`)} ${theme.dim(`in ${path.basename(manifest.localPath)} (gitignored)`)}`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1216
1265
|
print(` ${theme.dim(`verify with \`etymd fleet check --manifest ${manifest.manifestPath}\``)}`);
|
|
1217
1266
|
}
|
|
1218
1267
|
async function resolveFleetFinding(opts, status) {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { applyFiles } from './chunk-EF6BPBG6.js';
|
|
3
|
-
import { derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-
|
|
4
|
-
import { readConfig, CONFIG_FILE, configPath } from './chunk-
|
|
5
|
-
import { section, print, glyph, theme, renderPlan } from './chunk-
|
|
6
|
-
import { scanProject } from './chunk-
|
|
7
|
-
import './chunk-
|
|
8
|
-
import './chunk-
|
|
9
|
-
import './chunk-
|
|
10
|
-
import { readText, git } from './chunk-
|
|
3
|
+
import { derivedCommands, planWorkflow, runPrefix, isSafeGateCommand } from './chunk-KTIEYKFK.js';
|
|
4
|
+
import { readConfig, CONFIG_FILE, configPath } from './chunk-K7QBTANO.js';
|
|
5
|
+
import { section, print, glyph, theme, renderPlan } from './chunk-5BVKFJWM.js';
|
|
6
|
+
import { scanProject } from './chunk-ULEWJ3ZK.js';
|
|
7
|
+
import './chunk-HLWCODYX.js';
|
|
8
|
+
import './chunk-C7LBUFNU.js';
|
|
9
|
+
import './chunk-6DUDIVIC.js';
|
|
10
|
+
import { readText, git } from './chunk-IV3FYVTS.js';
|
|
11
11
|
import { promises } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { confirm, isCancel, cancel, select, multiselect } from '@clack/prompts';
|
|
@@ -185,7 +185,7 @@ async function run(opts) {
|
|
|
185
185
|
return files.filter((f) => f.executable);
|
|
186
186
|
};
|
|
187
187
|
let gateFiles = await plan();
|
|
188
|
-
renderPlan(gateFiles);
|
|
188
|
+
renderPlan(gateFiles, { regeneratesStale: true });
|
|
189
189
|
printGateSummary(gateConfig, facts, configSource);
|
|
190
190
|
if (!opts.yes) {
|
|
191
191
|
const choice = await select({
|
|
@@ -217,13 +217,13 @@ async function run(opts) {
|
|
|
217
217
|
const overwrite = /* @__PURE__ */ new Set();
|
|
218
218
|
for (const f of gateFiles) {
|
|
219
219
|
if (!f.exists) continue;
|
|
220
|
-
if (!f.differs) {
|
|
220
|
+
if (!f.differs || f.drift === "stale") {
|
|
221
221
|
overwrite.add(f.path);
|
|
222
222
|
continue;
|
|
223
223
|
}
|
|
224
224
|
if (opts.yes) continue;
|
|
225
225
|
const ow = await confirm({
|
|
226
|
-
message: `${f.path}
|
|
226
|
+
message: f.drift === "edited" ? `${f.path} has been edited since etymd generated it. Overwrite with the pack version?` : `${f.path} carries no etymd generation stamp, so it may be hand-written. Overwrite with the pack version?`,
|
|
227
227
|
initialValue: false
|
|
228
228
|
});
|
|
229
229
|
if (isCancel(ow)) {
|
|
@@ -236,8 +236,16 @@ async function run(opts) {
|
|
|
236
236
|
await git(opts.cwd, ["config", "core.hooksPath", ".githooks"]);
|
|
237
237
|
section("Done");
|
|
238
238
|
for (const w of result.written) print(` ${glyph.ok} ${theme.dim("wrote")} ${theme.info(w)}`);
|
|
239
|
-
for (const sk of result.skipped)
|
|
240
|
-
|
|
239
|
+
for (const sk of result.skipped) {
|
|
240
|
+
const drift = gateFiles.find((f) => f.path === sk)?.drift;
|
|
241
|
+
const reason = drift === "edited" ? "kept (edited since generated)" : "kept (no etymd stamp)";
|
|
242
|
+
print(` ${glyph.bullet} ${theme.dim(reason)} ${theme.dim(sk)}`);
|
|
243
|
+
if (opts.yes) {
|
|
244
|
+
print(
|
|
245
|
+
` ${theme.dim("its content differs from what this repo now generates \u2014 re-run without --yes to review and overwrite")}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
241
249
|
print(` ${glyph.ok} ${theme.dim("set")} ${theme.code("core.hooksPath = .githooks")}`);
|
|
242
250
|
const wrotePublishGate = result.written.includes(PUBLISH_GATE_SCRIPT);
|
|
243
251
|
if (wrotePublishGate) {
|
package/dist/index.d.ts
CHANGED
|
@@ -256,8 +256,15 @@ interface GeneratedFile {
|
|
|
256
256
|
contents: string;
|
|
257
257
|
/** Present on disk already — apply will skip or ask before overwriting. */
|
|
258
258
|
exists: boolean;
|
|
259
|
-
/** The existing file's content differs from what the pack would generate
|
|
259
|
+
/** The existing file's content differs from what the pack would generate. */
|
|
260
260
|
differs?: boolean;
|
|
261
|
+
/**
|
|
262
|
+
* WHY it differs, when it does — the two cases pull in opposite directions and must not share
|
|
263
|
+
* a bucket. `stale` is provably etymd's own output against inputs that have since moved (an
|
|
264
|
+
* older pack, a renamed script, a changed package manager): regenerating it destroys nothing.
|
|
265
|
+
* `edited` and `unstamped` may hold someone's work, so they are kept.
|
|
266
|
+
*/
|
|
267
|
+
drift?: "stale" | "edited" | "unstamped";
|
|
261
268
|
/** Hooks need the executable bit. */
|
|
262
269
|
executable?: boolean;
|
|
263
270
|
label: string;
|
|
@@ -745,6 +752,18 @@ interface GateInventory {
|
|
|
745
752
|
prePush: GateTool[];
|
|
746
753
|
commitMsg: GateTool[];
|
|
747
754
|
lintStaged: GateTool[];
|
|
755
|
+
/** Companion files the hooks call and whose checks ARE counted above. */
|
|
756
|
+
companions: string[];
|
|
757
|
+
/**
|
|
758
|
+
* Counted, but the execute bit could not be judged here — no POSIX mode bits, or a hook that
|
|
759
|
+
* is not itself executable, which means this filesystem's bits prove nothing either way.
|
|
760
|
+
*/
|
|
761
|
+
unverifiedCompanions: string[];
|
|
762
|
+
/**
|
|
763
|
+
* Companions a hook calls that exist and provably lack the execute bit while the hook beside
|
|
764
|
+
* them has it. NOT counted: the call skips them on every run.
|
|
765
|
+
*/
|
|
766
|
+
inertCompanions: string[];
|
|
748
767
|
};
|
|
749
768
|
ci: {
|
|
750
769
|
system: ProjectFacts["ci"]["system"];
|
|
@@ -769,7 +788,7 @@ declare const gateIntegrityLens: Lens;
|
|
|
769
788
|
* change meaning. Stamped into facts, baselines, and generated artifacts so drift against the
|
|
770
789
|
* pack is computable and `harvest` has something to diff.
|
|
771
790
|
*/
|
|
772
|
-
declare const PACK_VERSION = "
|
|
791
|
+
declare const PACK_VERSION = "6";
|
|
773
792
|
|
|
774
793
|
declare const VERSION: string;
|
|
775
794
|
declare const NAME: string;
|