etymd 0.2.1 → 0.3.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +71 -2
  2. package/README.md +276 -61
  3. package/dist/{approve-X3AW7KLD.js → approve-JEOEC2MV.js} +6 -4
  4. package/dist/audit-GYZ5YZ7U.js +12 -0
  5. package/dist/{brief-RT6KE4CN.js → brief-ZK7TXAC4.js} +6 -4
  6. package/dist/{chunk-4UZBCCFJ.js → chunk-3NYDVB4Z.js} +123 -25
  7. package/dist/chunk-CRJHIXJH.js +5 -0
  8. package/dist/chunk-EF6BPBG6.js +22 -0
  9. package/dist/chunk-LT67FU2Y.js +59 -0
  10. package/dist/{chunk-E2Q7WFPN.js → chunk-MKYPLNLH.js} +8 -5
  11. package/dist/{chunk-EUIHYD6F.js → chunk-MMPV67FW.js} +2 -2
  12. package/dist/{chunk-C4Z6NXDO.js → chunk-NFENQ3IK.js} +1 -1
  13. package/dist/{chunk-ANCCGYSH.js → chunk-NMZ3RHWW.js} +31 -59
  14. package/dist/{chunk-FZSIOCOZ.js → chunk-R74SJ7HS.js} +1 -1
  15. package/dist/{chunk-LBRNZZZF.js → chunk-TADKOW6P.js} +1 -99
  16. package/dist/{chunk-SS7VGLJ5.js → chunk-V3DPLOOM.js} +71 -15
  17. package/dist/{chunk-KBF3SW3N.js → chunk-WWBF4Y7K.js} +1 -1
  18. package/dist/{chunk-6W3J4PJ6.js → chunk-XTAX4YNT.js} +2 -2
  19. package/dist/chunk-YLNQQZ5F.js +102 -0
  20. package/dist/cli.js +50 -16
  21. package/dist/config-MX7OYI7F.js +4 -0
  22. package/dist/{context-GYCLR3TG.js → context-4DGXO7JR.js} +5 -3
  23. package/dist/doctor-C2RTL2QV.js +19 -0
  24. package/dist/{fleet-IS6YWDDV.js → fleet-YZGR4UC2.js} +243 -10
  25. package/dist/gates-ILE4PXDA.js +272 -0
  26. package/dist/generate-IYCQETQB.js +4 -0
  27. package/dist/index.d.ts +113 -33
  28. package/dist/index.js +777 -267
  29. package/dist/{init-ISEFZWPV.js → init-OVLO4OBZ.js} +8 -5
  30. package/dist/ledger-TMIV45AA.js +5 -0
  31. package/dist/{scan-ADZ2RKYY.js → scan-RAJUFTQR.js} +6 -4
  32. package/dist/scan-ZYZ7XDSJ.js +5 -0
  33. package/dist/screen-U7JMWZPS.js +134 -0
  34. package/package.json +2 -2
  35. package/dist/audit-YH74YZFQ.js +0 -9
  36. package/dist/doctor-EAA4IP4Y.js +0 -16
  37. package/dist/gates-3NPKF4IZ.js +0 -71
  38. package/dist/ledger-RHQJH5EA.js +0 -4
@@ -1,26 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { PACK_VERSION } from './chunk-E2Q7WFPN.js';
3
- import { pathExists, readText } from './chunk-LBRNZZZF.js';
4
- import { promises } from 'node:fs';
2
+ import { PACK_VERSION } from './chunk-CRJHIXJH.js';
3
+ import { pathExists, readText } from './chunk-YLNQQZ5F.js';
5
4
  import path from 'node:path';
6
5
 
7
- async function applyFiles(root, files, overwrite = /* @__PURE__ */ new Set()) {
8
- const written = [];
9
- const skipped = [];
10
- for (const file of files) {
11
- if (file.exists && !overwrite.has(file.path)) {
12
- skipped.push(file.path);
13
- continue;
14
- }
15
- const abs = path.join(root, file.path);
16
- await promises.mkdir(path.dirname(abs), { recursive: true });
17
- await promises.writeFile(abs, file.contents, "utf8");
18
- if (file.executable) await promises.chmod(abs, 493);
19
- written.push(file.path);
20
- }
21
- return { written, skipped };
22
- }
23
-
24
6
  // src/pack/templates.ts
25
7
  function runPrefix(pm) {
26
8
  switch (pm) {
@@ -128,22 +110,119 @@ ${[
128
110
  <!-- etymd pack v${PACK_VERSION} -->
129
111
  `;
130
112
  }
113
+ var CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(command -v etymd || true)}"`;
114
+ function localHookCall(hook) {
115
+ return `# Repo-owned checks. This file is generated and will be overwritten; \`.githooks/${hook}.local\`
116
+ # is yours \u2014 etymd never reads, writes, or regenerates it. Put project-specific guards there.
117
+ LOCAL="$(dirname "$0")/${hook}.local"
118
+ if [ -x "$LOCAL" ]; then
119
+ "$LOCAL" "$@" || exit 1
120
+ fi`;
121
+ }
131
122
  function generatePreCommitHook() {
132
123
  return `#!/usr/bin/env sh
133
124
  # etymd: process gate. Cheap, locally-knowable checks belong here (fast, blocks the commit).
125
+
126
+ ${localHookCall("pre-commit")}
127
+
128
+ # Content screen \u2014 staged file bytes. Refuses to commit environment, employer or identity
129
+ # detail into a repo whose history is (or could become) public. The checker and its patterns
130
+ # are machine-local by design, so this is a NO-OP wherever no checker is installed: safe to
131
+ # commit anywhere, active only where you opted in.
132
+ #
133
+ # Bypass, with a reason: git commit --no-verify
134
+ ${CONTENT_GATE_RESOLUTION}
135
+ if [ -x "$GATE" ]; then
136
+ "$GATE" screen --staged || exit 1
137
+ fi
138
+
139
+ exit 0
140
+ `;
141
+ }
142
+ function generateCommitMsgHook() {
143
+ return `#!/usr/bin/env sh
144
+ # etymd: content screen \u2014 the commit message itself.
145
+ #
146
+ # The staged-content gate reads file bytes and never sees the message, yet a message is as
147
+ # permanently published as any file. No-op where no checker is installed.
148
+ #
149
+ # Bypass, with a reason: git commit --no-verify
150
+ GATE="\${COMMIT_MSG_GATE:-$(command -v etymd || true)}"
151
+ if [ -x "$GATE" ]; then
152
+ "$GATE" screen --message "$1" || exit 1
153
+ fi
154
+
155
+ ${localHookCall("commit-msg")}
156
+
134
157
  exit 0
135
158
  `;
136
159
  }
137
- function generatePrePushHook(facts) {
160
+ function generatePrePushHook(facts, gates) {
138
161
  const run = runPrefix(facts.packageManager);
139
162
  const c = facts.commands;
140
- const candidates = [c.formatCheck, c.typecheck, c.lint];
141
- const steps = candidates.filter((key) => Boolean(key) && isSafeGateCommand(c.raw[key])).map((key) => `${run} ${key}`);
163
+ const candidates = gates?.commands.length ? gates.commands : [c.formatCheck, c.typecheck, c.lint];
164
+ const allowed = new Set(gates?.allowWriting ?? []);
165
+ const steps = candidates.filter(
166
+ (key) => Boolean(key) && (allowed.has(key) || isSafeGateCommand(c.raw[key]))
167
+ ).map((key) => `${run} ${key}`);
142
168
  const body = steps.length ? steps.map((s) => `echo "\u203A ${s}"
143
169
  ${s} || exit 1`).join("\n") : 'echo "etymd: no correctness commands detected \u2014 add format:check / typecheck / lint"';
170
+ const failOn = gates?.failOn ?? "risk";
171
+ const auditStep = `
172
+ if command -v etymd >/dev/null 2>&1; then
173
+ echo "\u203A etymd audit --fail-on ${failOn}"
174
+ etymd audit --no-ledger --fail-on ${failOn} || exit 1
175
+ else
176
+ echo "\u203A etymd audit skipped (not on PATH)"
177
+ fi`;
144
178
  return `#!/usr/bin/env sh
145
179
  # etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
180
+
181
+ ${localHookCall("pre-push")}
146
182
  ${body}
183
+ ${auditStep}
184
+
185
+ # Content screen, second pass \u2014 the WHOLE TREE rather than one diff. Catches anything committed
186
+ # with --no-verify and anything a rebase or merge brought in from elsewhere. Advisory here (it
187
+ # never blocks the push): the blocking decision belongs at commit time, where the fix is cheap.
188
+ ${CONTENT_GATE_RESOLUTION}
189
+ if [ -x "$GATE" ] && [ "\${CONTENT_GATE_PREPUSH:-1}" = "1" ]; then
190
+ "$GATE" screen --tree --advisory || true
191
+ fi
192
+
193
+ exit 0
194
+ `;
195
+ }
196
+ function generateArtifactCheckScript() {
197
+ return `#!/usr/bin/env sh
198
+ # etymd: content screen \u2014 the published ARTIFACT, not the repository.
199
+ #
200
+ # Wire it into the irreversible moment:
201
+ # package.json \u2192 "prepublishOnly": "./scripts/artifact-check.sh"
202
+ #
203
+ # No-op where no checker is installed. Bypass is deliberate and loud: ARTIFACT_CHECK_SKIP=1.
204
+ set -eu
205
+
206
+ if [ "\${ARTIFACT_CHECK_SKIP:-0}" = "1" ]; then
207
+ echo "\u203A artifact-check: SKIPPED by ARTIFACT_CHECK_SKIP=1"
208
+ exit 0
209
+ fi
210
+
211
+ ${CONTENT_GATE_RESOLUTION}
212
+ [ -x "$GATE" ] || { echo "\u203A artifact-check: no checker installed \u2014 skipping."; exit 0; }
213
+
214
+ WORK=$(mktemp -d)
215
+ trap 'rm -rf "$WORK"' EXIT INT TERM
216
+
217
+ # Pack exactly what would ship, then screen the unpacked bytes.
218
+ if [ -f package.json ]; then
219
+ npm pack --pack-destination "$WORK" >/dev/null 2>&1 || {
220
+ echo "\u203A artifact-check: npm pack failed \u2014 cannot verify what would ship" >&2; exit 1; }
221
+ tar -xzf "$WORK"/*.tgz -C "$WORK" 2>/dev/null || true
222
+ fi
223
+
224
+ "$GATE" screen --dir "$WORK" || exit 1
225
+ exit 0
147
226
  `;
148
227
  }
149
228
 
@@ -168,9 +247,28 @@ async function planWorkflow(root, facts, opts) {
168
247
  }
169
248
  if (opts.gates) {
170
249
  await add(".githooks/pre-commit", generatePreCommitHook(), "Process gate (pre-commit)", true);
171
- await add(".githooks/pre-push", generatePrePushHook(facts), "Correctness gate (pre-push)", true);
250
+ await add(
251
+ ".githooks/commit-msg",
252
+ generateCommitMsgHook(),
253
+ "Content screen (commit message)",
254
+ true
255
+ );
256
+ await add(
257
+ ".githooks/pre-push",
258
+ generatePrePushHook(facts, opts.gateConfig),
259
+ "Correctness gate (pre-push)",
260
+ true
261
+ );
262
+ if (opts.gateConfig?.publishGate ?? opts.publishGate ?? facts.publishable) {
263
+ await add(
264
+ "scripts/artifact-check.sh",
265
+ generateArtifactCheckScript(),
266
+ "Content screen (published artifact)",
267
+ true
268
+ );
269
+ }
172
270
  }
173
271
  return out;
174
272
  }
175
273
 
176
- export { applyFiles, planWorkflow };
274
+ export { isSafeGateCommand, planWorkflow, runPrefix };
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // src/pack/version.ts
3
+ var PACK_VERSION = "3";
4
+
5
+ export { PACK_VERSION };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import { promises } from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ async function applyFiles(root, files, overwrite = /* @__PURE__ */ new Set()) {
6
+ const written = [];
7
+ const skipped = [];
8
+ for (const file of files) {
9
+ if (file.exists && !overwrite.has(file.path)) {
10
+ skipped.push(file.path);
11
+ continue;
12
+ }
13
+ const abs = path.join(root, file.path);
14
+ await promises.mkdir(path.dirname(abs), { recursive: true });
15
+ await promises.writeFile(abs, file.contents, "utf8");
16
+ if (file.executable) await promises.chmod(abs, 493);
17
+ written.push(file.path);
18
+ }
19
+ return { written, skipped };
20
+ }
21
+
22
+ export { applyFiles };
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { DEFAULT_CONFIG } from './chunk-NMZ3RHWW.js';
3
+ import { readText, wordCount, approxTokens, pathExists } from './chunk-YLNQQZ5F.js';
4
+ import { promises } from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ var ALWAYS_LOADED = [
8
+ { path: "AGENTS.md", role: "operating contract" },
9
+ { path: "PROJECT_CONTEXT.md", role: "ground-truth state" },
10
+ { path: "CLAUDE.md", role: "Claude Code pointer" },
11
+ { path: "GEMINI.md", role: "Gemini pointer" },
12
+ { path: ".github/copilot-instructions.md", role: "Copilot instructions" },
13
+ { path: ".cursorrules", role: "Cursor rules (legacy)" }
14
+ ];
15
+ var EXTRACTION_THRESHOLD = DEFAULT_CONFIG.context.perFileWords;
16
+ function isAlwaysAppliedCursorRule(text) {
17
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
18
+ if (!fm) return true;
19
+ const frontmatter = fm[1] ?? "";
20
+ return /^\s*alwaysApply\s*:\s*true\s*$/m.test(frontmatter);
21
+ }
22
+ async function measureContext(root, perFileWords = EXTRACTION_THRESHOLD) {
23
+ const files = [];
24
+ for (const spec of ALWAYS_LOADED) {
25
+ const abs = path.join(root, spec.path);
26
+ const text = await readText(abs);
27
+ if (text === null) continue;
28
+ const words = wordCount(text);
29
+ files.push({ path: spec.path, role: spec.role, words, approxTokens: approxTokens(words) });
30
+ }
31
+ const rulesDir = path.join(root, ".cursor", "rules");
32
+ if (await pathExists(rulesDir)) {
33
+ try {
34
+ for (const entry of await promises.readdir(rulesDir)) {
35
+ if (!entry.endsWith(".mdc") && !entry.endsWith(".md")) continue;
36
+ const text = await readText(path.join(rulesDir, entry));
37
+ if (text === null || !isAlwaysAppliedCursorRule(text)) continue;
38
+ const words = wordCount(text);
39
+ files.push({
40
+ path: `.cursor/rules/${entry}`,
41
+ role: "Cursor rule (always applied)",
42
+ words,
43
+ approxTokens: approxTokens(words)
44
+ });
45
+ }
46
+ } catch {
47
+ }
48
+ }
49
+ const totalWords = files.reduce((s, f) => s + f.words, 0);
50
+ return {
51
+ files: files.sort((a, b) => b.words - a.words),
52
+ totalWords,
53
+ totalApproxTokens: approxTokens(totalWords),
54
+ perFileWords,
55
+ extractionCandidates: files.filter((f) => f.words >= perFileWords)
56
+ };
57
+ }
58
+
59
+ export { measureContext };
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-C4Z6NXDO.js';
3
- import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-LBRNZZZF.js';
2
+ import { VERSION } from './chunk-NFENQ3IK.js';
3
+ import { PACK_VERSION } from './chunk-CRJHIXJH.js';
4
+ import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-YLNQQZ5F.js';
4
5
  import path from 'node:path';
5
6
  import { promises } from 'node:fs';
6
7
 
7
- // src/pack/version.ts
8
- var PACK_VERSION = "2";
9
8
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
10
9
  "node_modules",
11
10
  ".git",
@@ -564,10 +563,14 @@ async function scanProject(root, opts = {}) {
564
563
  commands: classifyCommands(rootPkg?.scripts),
565
564
  ci,
566
565
  hooks,
566
+ publishable: Boolean(rootPkg) && rootPkg?.private !== true,
567
+ // `engines.vscode` is what makes a package an extension, and extensions publish through
568
+ // vsce — which never runs prepublishOnly.
569
+ publishRoute: !rootPkg ? "none" : rootPkg.engines?.vscode ? "vscode" : "npm",
567
570
  artifacts,
568
571
  freshness,
569
572
  tree
570
573
  };
571
574
  }
572
575
 
573
- export { PACK_VERSION, expandFileGlobs, scanProject };
576
+ export { expandFileGlobs, scanProject };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { readLedger, resolveEntry, writeLedger } from './chunk-KBF3SW3N.js';
3
- import { print, renderLedger, glyph, theme } from './chunk-LBRNZZZF.js';
2
+ import { readLedger, resolveEntry, writeLedger } from './chunk-WWBF4Y7K.js';
3
+ import { print, renderLedger, glyph, theme } from './chunk-TADKOW6P.js';
4
4
 
5
5
  // src/commands/ledger.ts
6
6
  async function list(opts) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // package.json
3
3
  var package_default = {
4
- version: "0.2.1"};
4
+ version: "0.3.0"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { ETYMD_DIR } from './chunk-FZSIOCOZ.js';
3
- import { pathExists, readText, wordCount, approxTokens } from './chunk-LBRNZZZF.js';
4
- import path2 from 'node:path';
5
- import { promises } from 'node:fs';
2
+ import { ETYMD_DIR } from './chunk-R74SJ7HS.js';
3
+ import { pathExists, readText } from './chunk-YLNQQZ5F.js';
4
+ import path from 'node:path';
6
5
 
7
- var CONFIG_FILE = path2.join(ETYMD_DIR, "config.json");
6
+ var CONFIG_FILE = path.join(ETYMD_DIR, "config.json");
8
7
  var DEFAULT_CONFIG = {
9
8
  instructions: { include: [], exclude: [] },
10
9
  context: { perFileWords: 4e3, totalWords: 8e3 },
11
- state: { staleAfterDays: 30, maxChars: 9500 }
10
+ state: { staleAfterDays: 30, maxChars: 9500 },
11
+ gates: { commands: [], failOn: "risk", allowWriting: [] }
12
12
  };
13
13
  function configPath(root) {
14
- return path2.join(root, CONFIG_FILE);
14
+ return path.join(root, CONFIG_FILE);
15
15
  }
16
16
  function readGlobList(value, key, problems) {
17
17
  if (value === void 0) return void 0;
@@ -53,6 +53,23 @@ async function readConfig(root) {
53
53
  const instructions = obj.instructions ?? {};
54
54
  const context = obj.context ?? {};
55
55
  const state = obj.state ?? {};
56
+ const gates = obj.gates ?? {};
57
+ const failOn = (() => {
58
+ if (gates.failOn === void 0) return void 0;
59
+ if (typeof gates.failOn !== "string" || !["risk", "gap", "polish"].includes(gates.failOn.toLowerCase())) {
60
+ problems.push(`${CONFIG_FILE}: \`gates.failOn\` must be risk | gap | polish \u2014 ignored.`);
61
+ return void 0;
62
+ }
63
+ return gates.failOn.toLowerCase();
64
+ })();
65
+ const publishGate = (() => {
66
+ if (gates.publishGate === void 0) return void 0;
67
+ if (typeof gates.publishGate !== "boolean") {
68
+ problems.push(`${CONFIG_FILE}: \`gates.publishGate\` must be true or false \u2014 ignored.`);
69
+ return void 0;
70
+ }
71
+ return gates.publishGate;
72
+ })();
56
73
  return {
57
74
  present: true,
58
75
  problems,
@@ -68,60 +85,15 @@ async function readConfig(root) {
68
85
  state: {
69
86
  staleAfterDays: readBudget(state.staleAfterDays, "state.staleAfterDays", problems) ?? DEFAULT_CONFIG.state.staleAfterDays,
70
87
  maxChars: readBudget(state.maxChars, "state.maxChars", problems) ?? DEFAULT_CONFIG.state.maxChars
88
+ },
89
+ gates: {
90
+ commands: readGlobList(gates.commands, "gates.commands", problems) ?? DEFAULT_CONFIG.gates.commands,
91
+ failOn: failOn ?? DEFAULT_CONFIG.gates.failOn,
92
+ publishGate,
93
+ allowWriting: readGlobList(gates.allowWriting, "gates.allowWriting", problems) ?? DEFAULT_CONFIG.gates.allowWriting
71
94
  }
72
95
  }
73
96
  };
74
97
  }
75
- var ALWAYS_LOADED = [
76
- { path: "AGENTS.md", role: "operating contract" },
77
- { path: "PROJECT_CONTEXT.md", role: "ground-truth state" },
78
- { path: "CLAUDE.md", role: "Claude Code pointer" },
79
- { path: "GEMINI.md", role: "Gemini pointer" },
80
- { path: ".github/copilot-instructions.md", role: "Copilot instructions" },
81
- { path: ".cursorrules", role: "Cursor rules (legacy)" }
82
- ];
83
- var EXTRACTION_THRESHOLD = DEFAULT_CONFIG.context.perFileWords;
84
- function isAlwaysAppliedCursorRule(text) {
85
- const fm = text.match(/^---\n([\s\S]*?)\n---/);
86
- if (!fm) return true;
87
- const frontmatter = fm[1] ?? "";
88
- return /^\s*alwaysApply\s*:\s*true\s*$/m.test(frontmatter);
89
- }
90
- async function measureContext(root, perFileWords = EXTRACTION_THRESHOLD) {
91
- const files = [];
92
- for (const spec of ALWAYS_LOADED) {
93
- const abs = path2.join(root, spec.path);
94
- const text = await readText(abs);
95
- if (text === null) continue;
96
- const words = wordCount(text);
97
- files.push({ path: spec.path, role: spec.role, words, approxTokens: approxTokens(words) });
98
- }
99
- const rulesDir = path2.join(root, ".cursor", "rules");
100
- if (await pathExists(rulesDir)) {
101
- try {
102
- for (const entry of await promises.readdir(rulesDir)) {
103
- if (!entry.endsWith(".mdc") && !entry.endsWith(".md")) continue;
104
- const text = await readText(path2.join(rulesDir, entry));
105
- if (text === null || !isAlwaysAppliedCursorRule(text)) continue;
106
- const words = wordCount(text);
107
- files.push({
108
- path: `.cursor/rules/${entry}`,
109
- role: "Cursor rule (always applied)",
110
- words,
111
- approxTokens: approxTokens(words)
112
- });
113
- }
114
- } catch {
115
- }
116
- }
117
- const totalWords = files.reduce((s, f) => s + f.words, 0);
118
- return {
119
- files: files.sort((a, b) => b.words - a.words),
120
- totalWords,
121
- totalApproxTokens: approxTokens(totalWords),
122
- perFileWords,
123
- extractionCandidates: files.filter((f) => f.words >= perFileWords)
124
- };
125
- }
126
98
 
127
- export { CONFIG_FILE, DEFAULT_CONFIG, measureContext, readConfig };
99
+ export { CONFIG_FILE, DEFAULT_CONFIG, configPath, readConfig };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readJson } from './chunk-LBRNZZZF.js';
2
+ import { readJson } from './chunk-YLNQQZ5F.js';
3
3
  import { promises } from 'node:fs';
4
4
  import path from 'node:path';
5
5
 
@@ -1,104 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { execFile } from 'node:child_process';
3
- import { promises } from 'node:fs';
4
- import { promisify } from 'node:util';
5
2
  import pc from 'picocolors';
6
3
 
7
- var pExecFile = promisify(execFile);
8
- async function pathExists(p) {
9
- try {
10
- await promises.access(p);
11
- return true;
12
- } catch {
13
- return false;
14
- }
15
- }
16
- async function readJson(p) {
17
- try {
18
- const raw = await promises.readFile(p, "utf8");
19
- return JSON.parse(raw);
20
- } catch {
21
- return null;
22
- }
23
- }
24
- async function readText(p) {
25
- try {
26
- return await promises.readFile(p, "utf8");
27
- } catch {
28
- return null;
29
- }
30
- }
31
- async function isDirectory(p) {
32
- try {
33
- return (await promises.stat(p)).isDirectory();
34
- } catch {
35
- return false;
36
- }
37
- }
38
- async function git(root, args) {
39
- try {
40
- const { stdout } = await pExecFile("git", args, { cwd: root, timeout: 4e3 });
41
- return stdout.trim();
42
- } catch {
43
- return null;
44
- }
45
- }
46
- function wordCount(text) {
47
- const m = text.trim().match(/\S+/g);
48
- return m ? m.length : 0;
49
- }
50
- function approxTokens(words) {
51
- return Math.round(words / 0.75);
52
- }
53
- var CI_ENV_VARS = [
54
- "CI",
55
- "GITHUB_ACTIONS",
56
- "GITLAB_CI",
57
- "BUILDKITE",
58
- "CIRCLECI",
59
- "JENKINS_URL",
60
- "TF_BUILD"
61
- ];
62
- function isCiEnvironment() {
63
- return CI_ENV_VARS.some((v) => Boolean(process.env[v]));
64
- }
65
- function normalizeRelPath(p) {
66
- return p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
67
- }
68
- function globToRegExp(glob) {
69
- let re = "";
70
- for (let i = 0; i < glob.length; i++) {
71
- const c = glob[i];
72
- if (c === "*") {
73
- if (glob[i + 1] === "*") {
74
- i += 1;
75
- if (glob[i + 1] === "/") {
76
- i += 1;
77
- re += "(?:.*/)?";
78
- } else {
79
- re += ".*";
80
- }
81
- } else {
82
- re += "[^/]*";
83
- }
84
- } else if (c === "?") {
85
- re += "[^/]";
86
- } else {
87
- re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
88
- }
89
- }
90
- return new RegExp(`^${re}$`);
91
- }
92
- function matchesGlob(p, glob) {
93
- const target = normalizeRelPath(p);
94
- const pattern = normalizeRelPath(glob);
95
- if (!pattern) return false;
96
- if (!/[*?]/.test(pattern)) return target === pattern || target.startsWith(pattern + "/");
97
- return globToRegExp(pattern).test(target);
98
- }
99
- function matchesAnyGlob(p, globs) {
100
- return globs.some((g) => matchesGlob(p, g));
101
- }
102
4
  var theme = {
103
5
  brand: (s) => pc.magenta(pc.bold(s)),
104
6
  heading: (s) => pc.bold(s),
@@ -356,4 +258,4 @@ function renderLedger(ledger) {
356
258
  }
357
259
  }
358
260
 
359
- export { TIER_BADGE, approxTokens, git, glyph, isCiEnvironment, isDirectory, matchesAnyGlob, normalizeRelPath, pathExists, print, readJson, readText, renderBaselineDrift, renderContext, renderFacts, renderFindings, renderFleetNotes, renderFleetRows, renderLedger, renderLedgerDiff, renderLensCoverage, renderPlan, section, theme, wordCount };
261
+ export { TIER_BADGE, glyph, print, renderBaselineDrift, renderContext, renderFacts, renderFindings, renderFleetNotes, renderFleetRows, renderLedger, renderLedgerDiff, renderLensCoverage, renderPlan, section, theme };
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-KBF3SW3N.js';
3
- import { DEFAULT_CONFIG, readConfig, CONFIG_FILE, measureContext } from './chunk-ANCCGYSH.js';
4
- import { scanProject, PACK_VERSION, expandFileGlobs } from './chunk-E2Q7WFPN.js';
5
- import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-FZSIOCOZ.js';
6
- import { pathExists, readJson, git, readText, isDirectory, matchesAnyGlob, isCiEnvironment, normalizeRelPath } from './chunk-LBRNZZZF.js';
2
+ import { measureContext } from './chunk-LT67FU2Y.js';
3
+ import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-WWBF4Y7K.js';
4
+ import { DEFAULT_CONFIG, readConfig, CONFIG_FILE } from './chunk-NMZ3RHWW.js';
5
+ import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-R74SJ7HS.js';
6
+ import { scanProject, expandFileGlobs } from './chunk-MKYPLNLH.js';
7
+ import { PACK_VERSION } from './chunk-CRJHIXJH.js';
8
+ import { pathExists, readJson, git, readText, isDirectory, matchesAnyGlob, isCiEnvironment, normalizeRelPath } from './chunk-YLNQQZ5F.js';
7
9
  import path from 'node:path';
8
10
  import YAML from 'yaml';
9
11
  import { promises } from 'node:fs';
@@ -91,24 +93,78 @@ var TOOL_MATCHERS = [
91
93
  ["lint", /\beslint\b/],
92
94
  ["test", /\bjest\b|\bvitest\b|react-scripts test|\bnx\b[^&|;]*\btest\b/]
93
95
  ];
96
+ var PM_SUBCOMMANDS = /* @__PURE__ */ new Set([
97
+ "run",
98
+ "run-script",
99
+ "install",
100
+ "i",
101
+ "ci",
102
+ "add",
103
+ "remove",
104
+ "rm",
105
+ "uninstall",
106
+ "un",
107
+ "update",
108
+ "up",
109
+ "upgrade",
110
+ "create",
111
+ "init",
112
+ "audit",
113
+ "publish",
114
+ "pack",
115
+ "link",
116
+ "unlink",
117
+ "why",
118
+ "list",
119
+ "ls",
120
+ "outdated",
121
+ "prune",
122
+ "dedupe",
123
+ "import",
124
+ "rebuild",
125
+ "root",
126
+ "config",
127
+ "cache",
128
+ "login",
129
+ "logout",
130
+ "whoami",
131
+ "version",
132
+ "workspace",
133
+ "workspaces",
134
+ "store",
135
+ "fund",
136
+ "deprecate",
137
+ "access",
138
+ "bin",
139
+ "env",
140
+ "help"
141
+ ]);
142
+ var PM_BINARY_RUNNERS = /* @__PURE__ */ new Set(["exec", "dlx", "x"]);
143
+ var NPX_SCRIPT_RUNNERS = /* @__PURE__ */ new Set(["run-s", "run-p", "npm-run-all"]);
94
144
  function expandScriptRefs(command, scripts, depth = 2) {
95
145
  if (depth <= 0) return command;
96
146
  let expanded = command;
97
- for (const m of command.matchAll(
98
- /(?:yarn|pnpm|npm run|bun run|npx run-s|npx run-p)\s+([A-Za-z0-9:._-]+)/g
99
- )) {
100
- const name = m[1];
147
+ const seen = /* @__PURE__ */ new Set();
148
+ const add = (name) => {
149
+ if (seen.has(name)) return;
150
+ seen.add(name);
101
151
  const body = scripts[name];
102
152
  if (body) expanded += `
103
153
  ${expandScriptRefs(body, scripts, depth - 1)}`;
154
+ };
155
+ for (const m of command.matchAll(/\b(yarn|pnpm|npm|bun|npx)\b([^&|;]*)/g)) {
156
+ const manager = m[1];
157
+ const tokens = (m[2] ?? "").split(/\s+/).map((t) => t.replace(/["']/g, "")).filter(Boolean);
158
+ if (manager === "npx" && !(tokens[0] && NPX_SCRIPT_RUNNERS.has(tokens[0]))) continue;
159
+ for (const token of tokens) {
160
+ if (token.startsWith("-")) continue;
161
+ if (PM_BINARY_RUNNERS.has(token)) break;
162
+ if (PM_SUBCOMMANDS.has(token) || NPX_SCRIPT_RUNNERS.has(token)) continue;
163
+ add(token);
164
+ }
104
165
  }
105
166
  if (/npm-run-all|run-s|run-p/.test(command)) {
106
- for (const token of command.split(/\s+/)) {
107
- const clean = token.replace(/["']/g, "");
108
- const body = scripts[clean];
109
- if (body) expanded += `
110
- ${expandScriptRefs(body, scripts, depth - 1)}`;
111
- }
167
+ for (const token of command.split(/\s+/)) add(token.replace(/["']/g, ""));
112
168
  }
113
169
  return expanded;
114
170
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readJson } from './chunk-LBRNZZZF.js';
2
+ import { readJson } from './chunk-YLNQQZ5F.js';
3
3
  import { promises } from 'node:fs';
4
4
  import path from 'node:path';
5
5
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-SS7VGLJ5.js';
3
- import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-LBRNZZZF.js';
2
+ import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-V3DPLOOM.js';
3
+ import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-TADKOW6P.js';
4
4
 
5
5
  // src/commands/audit.ts
6
6
  async function run(opts) {