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.
- package/CHANGELOG.md +71 -2
- package/README.md +276 -61
- package/dist/{approve-X3AW7KLD.js → approve-JEOEC2MV.js} +6 -4
- package/dist/audit-GYZ5YZ7U.js +12 -0
- package/dist/{brief-RT6KE4CN.js → brief-ZK7TXAC4.js} +6 -4
- package/dist/{chunk-4UZBCCFJ.js → chunk-3NYDVB4Z.js} +123 -25
- package/dist/chunk-CRJHIXJH.js +5 -0
- package/dist/chunk-EF6BPBG6.js +22 -0
- package/dist/chunk-LT67FU2Y.js +59 -0
- package/dist/{chunk-E2Q7WFPN.js → chunk-MKYPLNLH.js} +8 -5
- package/dist/{chunk-EUIHYD6F.js → chunk-MMPV67FW.js} +2 -2
- package/dist/{chunk-C4Z6NXDO.js → chunk-NFENQ3IK.js} +1 -1
- package/dist/{chunk-ANCCGYSH.js → chunk-NMZ3RHWW.js} +31 -59
- package/dist/{chunk-FZSIOCOZ.js → chunk-R74SJ7HS.js} +1 -1
- package/dist/{chunk-LBRNZZZF.js → chunk-TADKOW6P.js} +1 -99
- package/dist/{chunk-SS7VGLJ5.js → chunk-V3DPLOOM.js} +71 -15
- package/dist/{chunk-KBF3SW3N.js → chunk-WWBF4Y7K.js} +1 -1
- package/dist/{chunk-6W3J4PJ6.js → chunk-XTAX4YNT.js} +2 -2
- package/dist/chunk-YLNQQZ5F.js +102 -0
- package/dist/cli.js +50 -16
- package/dist/config-MX7OYI7F.js +4 -0
- package/dist/{context-GYCLR3TG.js → context-4DGXO7JR.js} +5 -3
- package/dist/doctor-C2RTL2QV.js +19 -0
- package/dist/{fleet-IS6YWDDV.js → fleet-YZGR4UC2.js} +243 -10
- package/dist/gates-ILE4PXDA.js +272 -0
- package/dist/generate-IYCQETQB.js +4 -0
- package/dist/index.d.ts +113 -33
- package/dist/index.js +777 -267
- package/dist/{init-ISEFZWPV.js → init-OVLO4OBZ.js} +8 -5
- package/dist/ledger-TMIV45AA.js +5 -0
- package/dist/{scan-ADZ2RKYY.js → scan-RAJUFTQR.js} +6 -4
- package/dist/scan-ZYZ7XDSJ.js +5 -0
- package/dist/screen-U7JMWZPS.js +134 -0
- package/package.json +2 -2
- package/dist/audit-YH74YZFQ.js +0 -9
- package/dist/doctor-EAA4IP4Y.js +0 -16
- package/dist/gates-3NPKF4IZ.js +0 -71
- package/dist/ledger-RHQJH5EA.js +0 -4
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { PACK_VERSION } from './chunk-
|
|
3
|
-
import { pathExists, readText } from './chunk-
|
|
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
|
|
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(
|
|
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 {
|
|
274
|
+
export { isSafeGateCommand, planWorkflow, runPrefix };
|
|
@@ -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-
|
|
3
|
-
import {
|
|
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 {
|
|
576
|
+
export { expandFileGlobs, scanProject };
|
|
@@ -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-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,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ETYMD_DIR } from './chunk-
|
|
3
|
-
import { pathExists, readText
|
|
4
|
-
import
|
|
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 =
|
|
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
|
|
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,
|
|
99
|
+
export { CONFIG_FILE, DEFAULT_CONFIG, configPath, readConfig };
|
|
@@ -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,
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { ETYMD_DIR, writeCachedFacts, readBaseline, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE } from './chunk-
|
|
6
|
-
import {
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseFailOnTier, runAudit, meetsFailOn } from './chunk-
|
|
3
|
-
import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-
|
|
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) {
|