etymd 0.2.2 → 0.3.1

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 +70 -2
  2. package/README.md +145 -37
  3. package/dist/{approve-VC4V3OPW.js → approve-3B2UL7LG.js} +6 -4
  4. package/dist/audit-4YB3K62W.js +12 -0
  5. package/dist/{brief-TZ45MNHD.js → brief-6EEXGQUW.js} +6 -4
  6. package/dist/chunk-CRJHIXJH.js +5 -0
  7. package/dist/{chunk-LVLZLCND.js → chunk-DKC7MLWG.js} +1 -1
  8. package/dist/chunk-EF6BPBG6.js +22 -0
  9. package/dist/{chunk-BH37LELV.js → chunk-HOLFW5QY.js} +8 -5
  10. package/dist/chunk-LT67FU2Y.js +59 -0
  11. package/dist/{chunk-IW6K5HLV.js → chunk-MEJ4MBVL.js} +2 -2
  12. package/dist/{chunk-EUIHYD6F.js → chunk-MMPV67FW.js} +2 -2
  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-KMAJIQHC.js → chunk-V6NZEIHF.js} +7 -5
  17. package/dist/{chunk-MWIK6ML3.js → chunk-VFUYQLW7.js} +141 -25
  18. package/dist/{chunk-KBF3SW3N.js → chunk-WWBF4Y7K.js} +1 -1
  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-T76ZNL4S.js +19 -0
  24. package/dist/{fleet-N3G5ZIGR.js → fleet-WOPKM2Z3.js} +248 -10
  25. package/dist/gates-WIGDS5VF.js +262 -0
  26. package/dist/generate-I75IEXXF.js +6 -0
  27. package/dist/index.d.ts +122 -33
  28. package/dist/index.js +737 -257
  29. package/dist/{init-4CQOLFQG.js → init-NSYVVMF2.js} +9 -5
  30. package/dist/ledger-TMIV45AA.js +5 -0
  31. package/dist/scan-BAD2UJAL.js +5 -0
  32. package/dist/{scan-CYOOGJFG.js → scan-COEPZJBO.js} +6 -4
  33. package/dist/screen-U7JMWZPS.js +134 -0
  34. package/package.json +2 -2
  35. package/dist/audit-LZJRJS5V.js +0 -9
  36. package/dist/doctor-6JDAHGDT.js +0 -16
  37. package/dist/gates-G5BCMRRV.js +0 -71
  38. package/dist/ledger-RHQJH5EA.js +0 -4
@@ -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-BH37LELV.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 { scanProject, expandFileGlobs } from './chunk-HOLFW5QY.js';
6
+ import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-R74SJ7HS.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';
@@ -1,26 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { PACK_VERSION } from './chunk-BH37LELV.js';
3
- import { pathExists, readText } from './chunk-LBRNZZZF.js';
4
- import { promises } from 'node:fs';
2
+ import { DEFAULT_CONFIG } from './chunk-NMZ3RHWW.js';
3
+ import { PACK_VERSION } from './chunk-CRJHIXJH.js';
4
+ import { readText, pathExists } from './chunk-YLNQQZ5F.js';
5
5
  import path from 'node:path';
6
6
 
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
7
  // src/pack/templates.ts
25
8
  function runPrefix(pm) {
26
9
  switch (pm) {
@@ -128,26 +111,133 @@ ${[
128
111
  <!-- etymd pack v${PACK_VERSION} -->
129
112
  `;
130
113
  }
114
+ var CONTENT_GATE_RESOLUTION = `GATE="\${CONTENT_GATE:-$(command -v etymd || true)}"`;
115
+ function localHookCall(hook) {
116
+ return `# Repo-owned checks. This file is generated and will be overwritten; \`.githooks/${hook}.local\`
117
+ # is yours \u2014 etymd never reads, writes, or regenerates it. Put project-specific guards there.
118
+ LOCAL="$(dirname "$0")/${hook}.local"
119
+ if [ -x "$LOCAL" ]; then
120
+ "$LOCAL" "$@" || exit 1
121
+ fi`;
122
+ }
131
123
  function generatePreCommitHook() {
132
124
  return `#!/usr/bin/env sh
133
125
  # etymd: process gate. Cheap, locally-knowable checks belong here (fast, blocks the commit).
126
+
127
+ ${localHookCall("pre-commit")}
128
+
129
+ # Content screen \u2014 staged file bytes. Refuses to commit environment, employer or identity
130
+ # detail into a repo whose history is (or could become) public. The checker and its patterns
131
+ # are machine-local by design, so this is a NO-OP wherever no checker is installed: safe to
132
+ # commit anywhere, active only where you opted in.
133
+ #
134
+ # Bypass, with a reason: git commit --no-verify
135
+ ${CONTENT_GATE_RESOLUTION}
136
+ if [ -x "$GATE" ]; then
137
+ "$GATE" screen --staged || exit 1
138
+ fi
139
+
134
140
  exit 0
135
141
  `;
136
142
  }
137
- function generatePrePushHook(facts) {
143
+ function generateCommitMsgHook() {
144
+ return `#!/usr/bin/env sh
145
+ # etymd: content screen \u2014 the commit message itself.
146
+ #
147
+ # The staged-content gate reads file bytes and never sees the message, yet a message is as
148
+ # permanently published as any file. No-op where no checker is installed.
149
+ #
150
+ # Bypass, with a reason: git commit --no-verify
151
+ GATE="\${COMMIT_MSG_GATE:-$(command -v etymd || true)}"
152
+ if [ -x "$GATE" ]; then
153
+ "$GATE" screen --message "$1" || exit 1
154
+ fi
155
+
156
+ ${localHookCall("commit-msg")}
157
+
158
+ exit 0
159
+ `;
160
+ }
161
+ function generatePrePushHook(facts, gates) {
138
162
  const run = runPrefix(facts.packageManager);
139
163
  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}`);
164
+ const candidates = gates?.commands?.length ? gates.commands : [c.formatCheck, c.typecheck, c.lint];
165
+ const allowed = new Set(gates?.allowWriting ?? []);
166
+ const steps = candidates.filter(
167
+ (key) => Boolean(key) && (allowed.has(key) || isSafeGateCommand(c.raw[key]))
168
+ ).map((key) => `${run} ${key}`);
142
169
  const body = steps.length ? steps.map((s) => `echo "\u203A ${s}"
143
170
  ${s} || exit 1`).join("\n") : 'echo "etymd: no correctness commands detected \u2014 add format:check / typecheck / lint"';
171
+ const failOn = gates?.failOn ?? "risk";
172
+ const auditStep = `
173
+ if command -v etymd >/dev/null 2>&1; then
174
+ echo "\u203A etymd audit --fail-on ${failOn}"
175
+ etymd audit --no-ledger --fail-on ${failOn} || exit 1
176
+ else
177
+ echo "\u203A etymd audit skipped (not on PATH)"
178
+ fi`;
144
179
  return `#!/usr/bin/env sh
145
180
  # etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
181
+
182
+ ${localHookCall("pre-push")}
146
183
  ${body}
184
+ ${auditStep}
185
+
186
+ # Content screen, second pass \u2014 the WHOLE TREE rather than one diff. Catches anything committed
187
+ # with --no-verify and anything a rebase or merge brought in from elsewhere. Advisory here (it
188
+ # never blocks the push): the blocking decision belongs at commit time, where the fix is cheap.
189
+ ${CONTENT_GATE_RESOLUTION}
190
+ if [ -x "$GATE" ] && [ "\${CONTENT_GATE_PREPUSH:-1}" = "1" ]; then
191
+ "$GATE" screen --tree --advisory || true
192
+ fi
193
+
194
+ exit 0
195
+ `;
196
+ }
197
+ function generateArtifactCheckScript() {
198
+ return `#!/usr/bin/env sh
199
+ # etymd: content screen \u2014 the published ARTIFACT, not the repository.
200
+ #
201
+ # Wire it into the irreversible moment:
202
+ # package.json \u2192 "prepublishOnly": "./scripts/artifact-check.sh"
203
+ #
204
+ # No-op where no checker is installed. Bypass is deliberate and loud: ARTIFACT_CHECK_SKIP=1.
205
+ set -eu
206
+
207
+ if [ "\${ARTIFACT_CHECK_SKIP:-0}" = "1" ]; then
208
+ echo "\u203A artifact-check: SKIPPED by ARTIFACT_CHECK_SKIP=1"
209
+ exit 0
210
+ fi
211
+
212
+ ${CONTENT_GATE_RESOLUTION}
213
+ [ -x "$GATE" ] || { echo "\u203A artifact-check: no checker installed \u2014 skipping."; exit 0; }
214
+
215
+ WORK=$(mktemp -d)
216
+ trap 'rm -rf "$WORK"' EXIT INT TERM
217
+
218
+ # Pack exactly what would ship, then screen the unpacked bytes.
219
+ if [ -f package.json ]; then
220
+ npm pack --pack-destination "$WORK" >/dev/null 2>&1 || {
221
+ echo "\u203A artifact-check: npm pack failed \u2014 cannot verify what would ship" >&2; exit 1; }
222
+ tar -xzf "$WORK"/*.tgz -C "$WORK" 2>/dev/null || true
223
+ fi
224
+
225
+ "$GATE" screen --dir "$WORK" || exit 1
226
+ exit 0
147
227
  `;
148
228
  }
149
229
 
150
230
  // src/core/generate.ts
231
+ function derivedCommands(facts, existingHook) {
232
+ const c = facts.commands;
233
+ const base = [c.formatCheck, c.typecheck, c.lint].filter(
234
+ (k) => Boolean(k) && isSafeGateCommand(c.raw[k])
235
+ );
236
+ if (c.test && existingHook && new RegExp(`\\b${c.test}\\b`).test(existingHook)) {
237
+ base.push(c.test);
238
+ }
239
+ return base;
240
+ }
151
241
  async function planWorkflow(root, facts, opts) {
152
242
  const out = [];
153
243
  const add = async (rel, contents, label, executable = false) => {
@@ -167,10 +257,36 @@ async function planWorkflow(root, facts, opts) {
167
257
  await add("AGENTS.md", generateAgentsMd(facts), "Minimal operating contract (scaffold)");
168
258
  }
169
259
  if (opts.gates) {
260
+ const existingPrePush = await readText(path.join(root, ".githooks", "pre-push"));
170
261
  await add(".githooks/pre-commit", generatePreCommitHook(), "Process gate (pre-commit)", true);
171
- await add(".githooks/pre-push", generatePrePushHook(facts), "Correctness gate (pre-push)", true);
262
+ await add(
263
+ ".githooks/commit-msg",
264
+ generateCommitMsgHook(),
265
+ "Content screen (commit message)",
266
+ true
267
+ );
268
+ const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
269
+ commands: derivedCommands(facts, existingPrePush ?? void 0),
270
+ failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
271
+ publishGate: opts.gateConfig?.publishGate,
272
+ allowWriting: opts.gateConfig?.allowWriting ?? []
273
+ };
274
+ await add(
275
+ ".githooks/pre-push",
276
+ generatePrePushHook(facts, gateConfig),
277
+ "Correctness gate (pre-push)",
278
+ true
279
+ );
280
+ if (opts.gateConfig?.publishGate ?? opts.publishGate ?? facts.publishable) {
281
+ await add(
282
+ "scripts/artifact-check.sh",
283
+ generateArtifactCheckScript(),
284
+ "Content screen (published artifact)",
285
+ true
286
+ );
287
+ }
172
288
  }
173
289
  return out;
174
290
  }
175
291
 
176
- export { applyFiles, planWorkflow };
292
+ export { derivedCommands, isSafeGateCommand, planWorkflow, runPrefix };
@@ -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
 
@@ -0,0 +1,102 @@
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
+
6
+ var pExecFile = promisify(execFile);
7
+ async function pathExists(p) {
8
+ try {
9
+ await promises.access(p);
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+ async function readJson(p) {
16
+ try {
17
+ const raw = await promises.readFile(p, "utf8");
18
+ return JSON.parse(raw);
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function readText(p) {
24
+ try {
25
+ return await promises.readFile(p, "utf8");
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+ async function isDirectory(p) {
31
+ try {
32
+ return (await promises.stat(p)).isDirectory();
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ async function git(root, args) {
38
+ try {
39
+ const { stdout } = await pExecFile("git", args, { cwd: root, timeout: 4e3 });
40
+ return stdout.trim();
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ function wordCount(text) {
46
+ const m = text.trim().match(/\S+/g);
47
+ return m ? m.length : 0;
48
+ }
49
+ function approxTokens(words) {
50
+ return Math.round(words / 0.75);
51
+ }
52
+ var CI_ENV_VARS = [
53
+ "CI",
54
+ "GITHUB_ACTIONS",
55
+ "GITLAB_CI",
56
+ "BUILDKITE",
57
+ "CIRCLECI",
58
+ "JENKINS_URL",
59
+ "TF_BUILD"
60
+ ];
61
+ function isCiEnvironment() {
62
+ return CI_ENV_VARS.some((v) => Boolean(process.env[v]));
63
+ }
64
+ function normalizeRelPath(p) {
65
+ return p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
66
+ }
67
+ function globToRegExp(glob) {
68
+ let re = "";
69
+ for (let i = 0; i < glob.length; i++) {
70
+ const c = glob[i];
71
+ if (c === "*") {
72
+ if (glob[i + 1] === "*") {
73
+ i += 1;
74
+ if (glob[i + 1] === "/") {
75
+ i += 1;
76
+ re += "(?:.*/)?";
77
+ } else {
78
+ re += ".*";
79
+ }
80
+ } else {
81
+ re += "[^/]*";
82
+ }
83
+ } else if (c === "?") {
84
+ re += "[^/]";
85
+ } else {
86
+ re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
87
+ }
88
+ }
89
+ return new RegExp(`^${re}$`);
90
+ }
91
+ function matchesGlob(p, glob) {
92
+ const target = normalizeRelPath(p);
93
+ const pattern = normalizeRelPath(glob);
94
+ if (!pattern) return false;
95
+ if (!/[*?]/.test(pattern)) return target === pattern || target.startsWith(pattern + "/");
96
+ return globToRegExp(pattern).test(target);
97
+ }
98
+ function matchesAnyGlob(p, globs) {
99
+ return globs.some((g) => matchesGlob(p, g));
100
+ }
101
+
102
+ export { approxTokens, git, isCiEnvironment, isDirectory, matchesAnyGlob, normalizeRelPath, pathExists, readJson, readText, wordCount };