@antelopejs/tooling-configs 0.0.8 → 0.2.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.
@@ -0,0 +1,485 @@
1
+ #!/usr/bin/env node
2
+ import { a as formatCeilings, t as CEILINGS_FILE } from "../ceilings--pJmhSde.mjs";
3
+ import { a as repositoryRoot, o as runCli } from "../files-BSl0U8lT.mjs";
4
+ import { a as repositoryTools, i as parseManifest, n as managedPackage, o as toolingVersions, r as packageManager, t as execCommand } from "../package-B1aipTbn.mjs";
5
+ import { t as measureBaseline } from "../ratchet-DNsVZH9s.mjs";
6
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { parseArgs } from "node:util";
9
+ import { isMap, isScalar, isSeq, parseDocument } from "yaml";
10
+ //#region src/guardrails/agents-block.ts
11
+ const AGENTS_BLOCK_START = "<!-- antelopejs-guardrails:start -->";
12
+ const AGENTS_BLOCK_END = "<!-- antelopejs-guardrails:end -->";
13
+ const COMMENTS = `### Comments
14
+
15
+ Say it in code. A constraint belongs in a type, a lint rule, or a test. A public item's contract belongs in TSDoc (\`/** */\`) on that item. The only other comments are lint directives that name their rules, \`@ts-expect-error <reason>\`, \`SAFETY: <reason>\` above a type assertion, \`/*!\` license headers, and bundler or formatter directives.`;
16
+ const TESTS = `### Tests
17
+
18
+ Hold every test you write or keep to this checklist:
19
+
20
+ - Name the wrong implementation this test fails on. A test with no answer is deleted, not kept.
21
+ - No test reads the file it tests. Drive the behaviour through the public interface.
22
+ - An invariant over many files is a lint rule, not a test.`;
23
+ const SHARED_RULES = [
24
+ ["Formatting", "oxfmt"],
25
+ ["Lint findings are errors; no rule warns", "oxlint preset"],
26
+ ["Comments are TSDoc, directives, or `SAFETY:` only", "`antelopejs/no-comments`"],
27
+ ["Every test asserts", "`antelopejs/require-assertion`"],
28
+ ["Tests read no files outside fixtures, helpers, locales", "`antelopejs/no-fs-read-in-tests`"],
29
+ ["No assertion that restates a constant", "`antelopejs/no-constant-value-assertion`"],
30
+ ["No test whose only check is \"is a function\"", "`antelopejs/no-typeof-function-only-test`"],
31
+ ["Params ≤ 5, function ≤ 120 lines, file ≤ 500 lines, depth ≤ 4, complexity ≤ 20", "`eslint/max-*`, `eslint/complexity`"]
32
+ ];
33
+ const FULL_RULES = [
34
+ ["Files ≤ 20,000 bytes, tests included", "`antelopejs-check-size` (pre-commit)"],
35
+ ["Existing debt only shrinks", "`antelopejs-ratchet`"],
36
+ ["Types check", "`tsc` (pre-push)"],
37
+ ["No dead files, exports, or dependencies", "knip"],
38
+ ["Managed files match the pack", "`antelopejs-sync --check`"],
39
+ ["A test names the wrong implementation it catches", "review; no tool"]
40
+ ];
41
+ /** Pads each column the way oxfmt does, so formatting AGENTS.md never reports sync drift. */
42
+ function ruleTable(rows) {
43
+ const table = [["Rule", "Enforced by"], ...rows];
44
+ const widths = [0, 1].map((column) => Math.max(...table.map((row) => row[column].length)));
45
+ const line = (cells) => `| ${cells.map((cell, column) => cell.padEnd(widths[column])).join(" | ")} |`;
46
+ const [header, ...body] = table.map(line);
47
+ return [
48
+ "### What enforces what",
49
+ "",
50
+ header,
51
+ line(widths.map((width) => "-".repeat(width))),
52
+ ...body
53
+ ].join("\n");
54
+ }
55
+ function pointers(context) {
56
+ const lines = [];
57
+ if (context.verificationSkill !== void 0) lines.push(`Prove a change works with the verification skill in [\`${context.verificationSkill}\`](${context.verificationSkill}) before you call it done.`);
58
+ if (context.glossary !== void 0) lines.push(`Name things with the terms in [\`${context.glossary}\`](${context.glossary}).`);
59
+ return lines;
60
+ }
61
+ function fullBody(context) {
62
+ return [
63
+ "## Guardrails",
64
+ "",
65
+ "Finish every change with `pnpm run checks`. It runs typecheck, lint, format, knip, tests, size, ratchet, and sync, and prints only what failed. The change is done when it prints no failure.",
66
+ "",
67
+ ...pointers(context).flatMap((line) => [line, ""]),
68
+ "The tools enforce most rules, listed in the table at the end. The sections below hold the rules no tool checks.",
69
+ "",
70
+ COMMENTS,
71
+ "",
72
+ TESTS,
73
+ "",
74
+ "### Debt",
75
+ "",
76
+ "`guardrails.ratchet.json` records the lint findings and oversize files this repository already had. Its numbers only go down. After you remove debt, run `pnpm run ratchet:update` and commit the lowered file. A rule listed there is off for `oxlint`. The ratchet counts it instead, and the pre-commit hook rejects it on every line you add.",
77
+ "",
78
+ ruleTable([...SHARED_RULES, ...FULL_RULES])
79
+ ];
80
+ }
81
+ function templateBody(context) {
82
+ return [
83
+ "## Guardrails",
84
+ "",
85
+ "Lint with `oxlint` and format with `oxfmt`; both read the configs at the repository root. Fix every finding: no rule warns.",
86
+ "",
87
+ ...pointers(context).flatMap((line) => [line, ""]),
88
+ COMMENTS,
89
+ "",
90
+ TESTS,
91
+ "",
92
+ ruleTable(SHARED_RULES)
93
+ ];
94
+ }
95
+ /** The managed block, markers included, ending with a newline. */
96
+ function agentsBlock(context) {
97
+ const body = context.profile === "full" ? fullBody(context) : templateBody(context);
98
+ return [
99
+ AGENTS_BLOCK_START,
100
+ "<!-- Managed by @antelopejs/tooling-configs. `antelopejs-sync` rewrites everything between these markers; write repository-specific guidance outside them. -->",
101
+ "",
102
+ ...body,
103
+ "",
104
+ AGENTS_BLOCK_END,
105
+ ""
106
+ ].join("\n");
107
+ }
108
+ //#endregion
109
+ //#region src/guardrails/templates.ts
110
+ const PRESET_CONFIGS = [
111
+ {
112
+ path: "oxlint.config.ts",
113
+ legacy: [".oxlintrc.json"],
114
+ specifier: "@antelopejs/tooling-configs/oxc/lint",
115
+ content: "import { defineConfig } from \"oxlint\";\nimport { antelopePreset } from \"@antelopejs/tooling-configs/oxc/lint\";\n\nexport default defineConfig({\n extends: [antelopePreset()],\n options: { typeAware: true },\n});\n"
116
+ },
117
+ {
118
+ path: "oxfmt.config.ts",
119
+ legacy: [".oxfmtrc.json"],
120
+ specifier: "@antelopejs/tooling-configs/oxc/fmt",
121
+ content: "import { antelopeFmtPreset } from \"@antelopejs/tooling-configs/oxc/fmt\";\n\nexport default antelopeFmtPreset();\n"
122
+ },
123
+ {
124
+ path: "knip.config.ts",
125
+ legacy: [
126
+ "knip.json",
127
+ "knip.jsonc",
128
+ ".knip.json"
129
+ ],
130
+ specifier: "@antelopejs/tooling-configs/knip",
131
+ content: "import { antelopeKnipConfig } from \"@antelopejs/tooling-configs/knip\";\n\nexport default antelopeKnipConfig();\n"
132
+ }
133
+ ];
134
+ function presetConfig(current, preset, tools) {
135
+ const target = tools.presets[preset.path];
136
+ const specifier = target.replaceAll("/", "\\/").replaceAll(".", "\\.");
137
+ const imported = new RegExp(`(?:\\bfrom\\s*|\\bimport\\s*\\(?\\s*)["']${specifier}["']`);
138
+ if (current === void 0) return preset.content.replace(preset.specifier, target);
139
+ if (imported.test(current)) return current;
140
+ const importLine = preset.content.split("\n").filter((line) => line.includes(preset.specifier)).join("\n").replace(preset.specifier, target);
141
+ throw new Error(`${preset.path} does not import its preset. Add \`${importLine}\` and extend that preset, then rerun antelopejs-sync. The file was not replaced.`);
142
+ }
143
+ function lefthook(current, manager, tools) {
144
+ const exec = execCommand(manager);
145
+ const document = parseDocument(current ?? "{}\n");
146
+ if (document.errors.length) throw new Error(`lefthook.yml: ${document.errors.map(({ message }) => message).join("; ")}`);
147
+ if (!isMap(document.contents)) throw new Error("lefthook.yml must contain a YAML mapping. Merge the guardrail hooks manually and rerun antelopejs-sync.");
148
+ if (current === void 0) document.contents.flow = false;
149
+ const glob = "*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue}";
150
+ const hooks = {
151
+ "pre-commit": {
152
+ "antelopejs-format": {
153
+ run: `${exec} oxfmt --no-error-on-unmatched-pattern {staged_files}`,
154
+ stage_fixed: true
155
+ },
156
+ "antelopejs-lint": {
157
+ glob,
158
+ run: `${exec} oxlint --no-error-on-unmatched-pattern {staged_files}`
159
+ },
160
+ "antelopejs-size": { run: `${tools.hookPrefix}${tools.commands.size} --staged {staged_files}` },
161
+ "antelopejs-ratchet": {
162
+ glob,
163
+ run: `${tools.hookPrefix}${tools.commands.ratchet} --staged {staged_files}`
164
+ }
165
+ },
166
+ "pre-push": { "antelopejs-typecheck": { run: `${manager} run typecheck` } }
167
+ };
168
+ for (const [hook, commands] of Object.entries(hooks)) {
169
+ for (const path of [[hook], [hook, "commands"]]) {
170
+ if (!document.hasIn(path)) document.setIn(path, document.createNode({}));
171
+ if (!isMap(document.getIn(path))) throw new Error(`lefthook.yml: ${path.join(".")} must be a mapping. Preserve the existing hook and merge its commands before rerunning antelopejs-sync.`);
172
+ }
173
+ for (const [key, command] of Object.entries(commands)) document.setIn([
174
+ hook,
175
+ "commands",
176
+ key
177
+ ], document.createNode(command));
178
+ }
179
+ return document.toString({ lineWidth: 0 });
180
+ }
181
+ const SETUP_SCRIPT = `#!/usr/bin/env bash
182
+ set -euo pipefail
183
+ cd "$(dirname "$BASH_SOURCE")/.."
184
+ export CI=1 COREPACK_ENABLE_DOWNLOAD_PROMPT=0
185
+ major="$(tr -d 'v[:space:]' < .nvmrc)"
186
+ if [[ "$(node -p 'process.versions.node.split(".")[0]')" != "$major" ]]; then
187
+ if command -v fnm >/dev/null 2>&1; then
188
+ eval "$(fnm env --shell bash)"
189
+ fnm install "$major"
190
+ fnm use "$major"
191
+ else
192
+ export NVM_DIR="\${NVM_DIR:-$HOME/.nvm}"
193
+ if [[ ! -s "$NVM_DIR/nvm.sh" ]]; then
194
+ git clone --quiet --depth 1 --branch v0.40.3 https://github.com/nvm-sh/nvm.git "$NVM_DIR"
195
+ fi
196
+ source "$NVM_DIR/nvm.sh" --no-use
197
+ nvm install "$major"
198
+ nvm use "$major"
199
+ fi
200
+ fi
201
+ pin="$(node -p 'JSON.parse(require("node:fs").readFileSync("package.json", "utf8")).packageManager || "pnpm"')"
202
+ manager="\${pin%%@*}"
203
+ corepack enable
204
+ if [[ "$pin" == *@* ]]; then
205
+ corepack prepare "$pin" --activate
206
+ fi
207
+ case "$manager" in
208
+ pnpm) pnpm install --frozen-lockfile ;;
209
+ yarn) YARN_ENABLE_IMMUTABLE_INSTALLS=true yarn install --frozen-lockfile ;;
210
+ npm) npm ci ;;
211
+ bun) bun install --frozen-lockfile ;;
212
+ *) printf 'Unsupported package manager %s\\n' "$manager" >&2; exit 1 ;;
213
+ esac
214
+ `;
215
+ //#endregion
216
+ //#region src/guardrails/workflow.ts
217
+ const MISSING_CHECKS_WORKFLOW = "No workflow in .github/workflows runs pnpm run checks. Add a step running `pnpm run checks` (or `pnpm checks`), or a job using AntelopeJS/.github/.github/workflows/ci-node.yml@<ref>.";
218
+ function hasChecksWorkflow(root) {
219
+ const directory = join(root, ".github/workflows");
220
+ if (!existsSync(directory)) return false;
221
+ for (const file of readdirSync(directory).filter((name) => /\.ya?ml$/.test(name))) {
222
+ const document = parseDocument(readFileSync(join(directory, file), "utf8"));
223
+ if (document.errors.length) continue;
224
+ const jobs = document.get("jobs", true);
225
+ if (!isMap(jobs)) continue;
226
+ for (const { value: job } of jobs.items) {
227
+ if (!isMap(job)) continue;
228
+ const uses = job.get("uses", true);
229
+ if (isScalar(uses) && /^AntelopeJS\/\.github\/\.github\/workflows\/ci-node\.yml@\S+$/.test(String(uses.value))) return true;
230
+ const steps = job.get("steps", true);
231
+ if (!isSeq(steps)) continue;
232
+ for (const step of steps.items) {
233
+ if (!isMap(step)) continue;
234
+ const run = step.get("run", true);
235
+ if (isScalar(run) && /(?:^|\n|&&|;)\s*pnpm\s+(?:run\s+)?checks(?=\s*(?:$|\n|&&|;|#))/.test(String(run.value))) return true;
236
+ }
237
+ }
238
+ }
239
+ return false;
240
+ }
241
+ //#endregion
242
+ //#region src/guardrails/sync.ts
243
+ const LEGACY_COMMENT_INSTRUCTION = "- Explain non-obvious constraints and decisions in comments; avoid restating the code. Document public contracts with TSDoc.";
244
+ function managedAgents(current = "", context) {
245
+ const start = current.indexOf(AGENTS_BLOCK_START);
246
+ const end = current.indexOf(AGENTS_BLOCK_END);
247
+ if (start === -1 !== (end === -1) || end < start) throw new Error("AGENTS.md has incomplete guardrails markers. Repair the markers before syncing.");
248
+ const notes = [];
249
+ const clean = (text) => text.split("\n").filter((line) => {
250
+ if (line.replace(/\r$/, "") !== LEGACY_COMMENT_INSTRUCTION) return true;
251
+ notes.push("Removed the legacy comment instruction from AGENTS.md's hand-written part.");
252
+ return false;
253
+ }).join("\n");
254
+ let before = clean(start === -1 ? current : current.slice(0, start));
255
+ const after = start === -1 ? "" : clean(current.slice(end + 34));
256
+ if (context.claude) before = `${before.trimEnd()}${before.trim() ? "\n\n" : ""}${clean(context.claude).trimEnd()}\n\n`;
257
+ const block = agentsBlock(context);
258
+ return {
259
+ content: start === -1 ? `${before}${before && !before.endsWith("\n") ? "\n" : ""}${before && !before.endsWith("\n\n") ? "\n" : ""}${block}` : `${before}${block.trimEnd()}${after}`,
260
+ notes
261
+ };
262
+ }
263
+ const MANAGED_ARTIFACTS = [
264
+ ...PRESET_CONFIGS.map((preset) => ({
265
+ path: preset.path,
266
+ profiles: ["full", "template"],
267
+ desired: (current, context) => ({
268
+ content: presetConfig(current, preset, context.tools),
269
+ notes: []
270
+ })
271
+ })),
272
+ {
273
+ path: "lefthook.yml",
274
+ profiles: ["full"],
275
+ desired: (current, context) => ({
276
+ content: lefthook(current, context.manager, context.tools),
277
+ notes: []
278
+ })
279
+ },
280
+ {
281
+ path: "package.json",
282
+ profiles: ["full"],
283
+ desired: (current, context) => ({
284
+ content: managedPackage(current, context.versions, context),
285
+ notes: []
286
+ })
287
+ },
288
+ {
289
+ path: ".nvmrc",
290
+ profiles: ["full"],
291
+ desired: () => ({
292
+ content: "24\n",
293
+ notes: []
294
+ })
295
+ },
296
+ {
297
+ path: ".agents/setup",
298
+ profiles: ["full"],
299
+ desired: () => ({
300
+ content: SETUP_SCRIPT,
301
+ notes: []
302
+ }),
303
+ mode: 493
304
+ },
305
+ {
306
+ path: "AGENTS.md",
307
+ profiles: ["full", "template"],
308
+ desired: managedAgents
309
+ },
310
+ {
311
+ path: "CLAUDE.md",
312
+ profiles: ["full"],
313
+ desired: (_current, context) => ({
314
+ content: "@AGENTS.md\n",
315
+ notes: context.claude ? ["Moved CLAUDE.md content into AGENTS.md's hand-written part before the managed block."] : []
316
+ })
317
+ },
318
+ {
319
+ path: CEILINGS_FILE,
320
+ profiles: ["full"],
321
+ desired: (current, context) => ({
322
+ content: current ?? context.baseline,
323
+ notes: []
324
+ })
325
+ }
326
+ ];
327
+ function planSync(current, modes, context) {
328
+ const plan = [];
329
+ for (const preset of PRESET_CONFIGS) for (const file of preset.legacy) if (current.has(file)) plan.push({
330
+ kind: "error",
331
+ path: file,
332
+ message: `${file}: move its settings into ${preset.path} extending "${context.tools.presets[preset.path]}". Delete ${file}, then rerun antelopejs-sync.`
333
+ });
334
+ for (const artifact of MANAGED_ARTIFACTS) {
335
+ if (!artifact.profiles.includes(context.profile)) continue;
336
+ try {
337
+ const { content, notes } = artifact.desired(current.get(artifact.path), context);
338
+ const changed = current.get(artifact.path) !== content || artifact.mode !== void 0 && (artifact.mode & 73) !== 0 && ((modes.get(artifact.path) ?? 0) & 73) === 0;
339
+ if (notes.length) plan.push({
340
+ kind: "migrated",
341
+ path: artifact.path,
342
+ content,
343
+ mode: artifact.mode,
344
+ notes
345
+ });
346
+ else if (changed) plan.push({
347
+ kind: "write",
348
+ path: artifact.path,
349
+ content,
350
+ mode: artifact.mode
351
+ });
352
+ else plan.push({
353
+ kind: "unchanged",
354
+ path: artifact.path
355
+ });
356
+ } catch (error) {
357
+ plan.push({
358
+ kind: "error",
359
+ path: artifact.path,
360
+ message: error instanceof Error ? error.message : String(error)
361
+ });
362
+ }
363
+ }
364
+ return plan;
365
+ }
366
+ function verificationSkill(root) {
367
+ for (const directory of [
368
+ ".agents/skills",
369
+ ".claude/skills",
370
+ "skills"
371
+ ]) {
372
+ if (!existsSync(join(root, directory))) continue;
373
+ for (const entry of readdirSync(join(root, directory), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
374
+ const path = `${directory}/${entry.name}/SKILL.md`;
375
+ if (entry.isDirectory() && entry.name.toLowerCase().includes("verif") && existsSync(join(root, path))) return path;
376
+ }
377
+ }
378
+ }
379
+ function repositorySnapshot(root) {
380
+ const current = /* @__PURE__ */ new Map();
381
+ const modes = /* @__PURE__ */ new Map();
382
+ for (const file of [...MANAGED_ARTIFACTS.map(({ path }) => path), ...PRESET_CONFIGS.flatMap(({ legacy }) => legacy)]) {
383
+ const path = join(root, file);
384
+ if (existsSync(path)) {
385
+ current.set(file, readFileSync(path, "utf8"));
386
+ modes.set(file, statSync(path).mode & 511);
387
+ }
388
+ }
389
+ return {
390
+ current,
391
+ modes
392
+ };
393
+ }
394
+ function linksTo(root, link, target) {
395
+ const path = join(root, link);
396
+ return lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink() === true && resolve(root, readlinkSync(path)) === join(root, target);
397
+ }
398
+ function claudeProse(root, current) {
399
+ if (linksTo(root, "CLAUDE.md", "AGENTS.md") || linksTo(root, "AGENTS.md", "CLAUDE.md")) return "";
400
+ const claude = current.get("CLAUDE.md")?.trim() ?? "";
401
+ if (!claude.includes("\n") && /^(?:@?(?:\.\/)?AGENTS\.md|(?:See |Refer to )?\[AGENTS\.md\]\((?:\.\/)?AGENTS\.md\)(?: for .*)?\.?)$/i.test(claude)) return "";
402
+ return claude;
403
+ }
404
+ function repositoryContext(root, current, profile) {
405
+ const manifest = parseManifest(current.get("package.json") ?? "{}");
406
+ const lint = PRESET_CONFIGS[0];
407
+ const desiredLint = current.get(lint.path) ?? lint.content;
408
+ return {
409
+ profile,
410
+ manager: packageManager(manifest),
411
+ versions: toolingVersions(),
412
+ tools: repositoryTools(manifest),
413
+ typeAware: /\btypeAware["']?\s*:\s*true\b/.test(desiredLint),
414
+ nuxt: Boolean(manifest.dependencies?.nuxt || manifest.devDependencies?.nuxt || readdirSync(root).some((name) => name.startsWith("nuxt.config."))),
415
+ claude: profile === "full" ? claudeProse(root, current) : "",
416
+ baseline: "",
417
+ verificationSkill: verificationSkill(root),
418
+ glossary: ["GLOSSARY.md", "docs/GLOSSARY.md"].find((file) => existsSync(join(root, file)))
419
+ };
420
+ }
421
+ function writeArtifacts(root, artifacts) {
422
+ for (const artifact of artifacts) {
423
+ const path = join(root, artifact.path);
424
+ mkdirSync(dirname(path), { recursive: true });
425
+ if (lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink()) unlinkSync(path);
426
+ writeFileSync(path, artifact.content);
427
+ if (artifact.mode !== void 0) chmodSync(path, artifact.mode);
428
+ }
429
+ }
430
+ function syncRepository(root, options = {}) {
431
+ const profile = options.profile ?? "full";
432
+ const { current, modes } = repositorySnapshot(root);
433
+ const manifest = parseManifest(current.get("package.json") ?? "{}");
434
+ const context = repositoryContext(root, current, profile);
435
+ let plan = planSync(current, modes, context);
436
+ const errors = plan.filter((artifact) => artifact.kind === "error").map(({ message }) => message);
437
+ const notes = plan.flatMap((artifact) => artifact.kind === "migrated" ? artifact.notes : []);
438
+ if (profile === "full" && !hasChecksWorkflow(root)) {
439
+ if (options.check) errors.push(MISSING_CHECKS_WORKFLOW);
440
+ else notes.push(MISSING_CHECKS_WORKFLOW);
441
+ }
442
+ if (!errors.length && profile === "full" && !options.check && !current.has("guardrails.ratchet.json")) {
443
+ const lint = PRESET_CONFIGS[0];
444
+ const desired = presetConfig(current.get(lint.path), lint, context.tools);
445
+ context.baseline = formatCeilings(measureBaseline(root, desired === current.get(lint.path) ? void 0 : desired));
446
+ plan = planSync(current, modes, context);
447
+ }
448
+ const writes = plan.filter((artifact) => artifact.kind === "write" || artifact.kind === "migrated");
449
+ if (!options.check && !errors.length) writeArtifacts(root, writes);
450
+ const requiredDependencies = ["lefthook", "knip"];
451
+ if (context.typeAware) requiredDependencies.push("oxlint-tsgolint");
452
+ const dependenciesAdded = profile === "full" && requiredDependencies.some((name) => !manifest.devDependencies?.[name]);
453
+ return {
454
+ artifacts: plan,
455
+ paths: errors.length ? [] : writes.map(({ path }) => path),
456
+ errors,
457
+ notes,
458
+ dependenciesAdded
459
+ };
460
+ }
461
+ //#endregion
462
+ //#region src/bin/sync.ts
463
+ runCli(() => {
464
+ const { values } = parseArgs({ options: {
465
+ check: { type: "boolean" },
466
+ profile: {
467
+ type: "string",
468
+ default: "full"
469
+ }
470
+ } });
471
+ if (values.profile !== "full" && values.profile !== "template") throw new Error("--profile must be full or template.");
472
+ const result = syncRepository(repositoryRoot(), {
473
+ check: values.check,
474
+ profile: values.profile
475
+ });
476
+ for (const note of result.notes) process.stdout.write(`Note: ${values.check || result.errors.length ? "Planned migration. " : ""}${note}\n`);
477
+ for (const error of result.errors) process.stderr.write(`${error}\n`);
478
+ if (result.errors.length) return 1;
479
+ for (const path of result.paths) process.stdout.write(`${values.check ? "Drift" : "Updated"}: ${path}${values.check ? "; run antelopejs-sync" : ""}\n`);
480
+ if (!result.paths.length) process.stdout.write("Guardrails are in sync.\n");
481
+ if (!values.check && result.dependenciesAdded) process.stdout.write("Added devDependencies. Run install with your package manager before using the hooks or checks.\n");
482
+ return values.check && result.paths.length ? 1 : 0;
483
+ });
484
+ //#endregion
485
+ export {};
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ //#region src/guardrails/ceilings.ts
4
+ /** The per-repository ratchet file, at the repository root. */
5
+ const CEILINGS_FILE = "guardrails.ratchet.json";
6
+ /**
7
+ * Set by the ratchet when it measures: the oxlint preset then ignores the
8
+ * ceilings and reports every rule, so the counts include the existing debt.
9
+ */
10
+ const MEASURE_ENV = "ANTELOPEJS_RATCHET_MEASURE";
11
+ /** Largest authored file, in bytes, tests included. */
12
+ const MAX_FILE_BYTES = 2e4;
13
+ const EMPTY_CEILINGS = {
14
+ rules: {},
15
+ oversizeFiles: {}
16
+ };
17
+ /** Finds the nearest git directory or worktree marker, falling back to cwd outside git. */
18
+ function findGitRoot(cwd = process.cwd()) {
19
+ let directory = resolve(cwd);
20
+ while (!existsSync(join(directory, ".git"))) {
21
+ const parent = dirname(directory);
22
+ if (parent === directory) return resolve(cwd);
23
+ directory = parent;
24
+ }
25
+ return directory;
26
+ }
27
+ /** Reads the ceilings at `root`; a repository without the file has no debt. */
28
+ function readCeilings(root) {
29
+ const path = join(root, CEILINGS_FILE);
30
+ if (!existsSync(path)) return EMPTY_CEILINGS;
31
+ return parseCeilings(readFileSync(path, "utf8"), path);
32
+ }
33
+ /** Parses the ceilings file, rejecting anything but non-negative integer counts. */
34
+ function parseCeilings(text, source) {
35
+ const parsed = JSON.parse(text);
36
+ if (parsed === null || Array.isArray(parsed)) throw new Error(`${source}: expected an object with "rules" and "oversizeFiles"`);
37
+ return {
38
+ rules: parseCounts(parsed.rules, `${source}: rules`),
39
+ oversizeFiles: parseCounts(parsed.oversizeFiles, `${source}: oversizeFiles`)
40
+ };
41
+ }
42
+ function parseCounts(value, source) {
43
+ if (value === void 0) return {};
44
+ if (value === null || Array.isArray(value)) throw new Error(`${source}: expected an object of counts`);
45
+ const counts = {};
46
+ for (const [key, count] of Object.entries(value)) {
47
+ if (!Number.isInteger(count) || count < 0) throw new Error(`${source}: "${key}" must be a non-negative integer`);
48
+ counts[key] = count;
49
+ }
50
+ return counts;
51
+ }
52
+ function sortedCounts(counts) {
53
+ return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)));
54
+ }
55
+ /** Serializes ceilings with sorted keys, so rewriting an unchanged file is a no-op. */
56
+ function formatCeilings(ceilings) {
57
+ const sorted = {
58
+ rules: sortedCounts(ceilings.rules),
59
+ oversizeFiles: sortedCounts(ceilings.oversizeFiles)
60
+ };
61
+ return `${JSON.stringify(sorted, null, 2)}\n`;
62
+ }
63
+ //#endregion
64
+ export { formatCeilings as a, findGitRoot as i, MAX_FILE_BYTES as n, readCeilings as o, MEASURE_ENV as r, CEILINGS_FILE as t };
package/dist/eslint.mjs CHANGED
@@ -10,13 +10,18 @@ import { createConfigForNuxt } from "@nuxt/eslint-config/flat";
10
10
  * layers carry today) would fight it on every save.
11
11
  */
12
12
  const ANTELOPE_NUXT_RULES = {
13
+ /** Import order follows side-effectful setup and belongs to the formatter. */
13
14
  "import/first": "off",
14
15
  "import/order": "off",
16
+ /** Single-word pages are intentional; withDefaults supplies prop defaults. */
15
17
  "vue/multi-word-component-names": "off",
16
18
  "vue/require-default-prop": "off",
19
+ /** CMS blocks and forms require structural types in the interface layer. */
17
20
  "@typescript-eslint/no-explicit-any": "off",
18
21
  "@typescript-eslint/no-empty-object-type": "off",
22
+ /** Separate event signatures preserve their documentation. */
19
23
  "@typescript-eslint/unified-signatures": "off",
24
+ /** Underscores mark intentionally unused bindings. */
20
25
  "@typescript-eslint/no-unused-vars": ["error", {
21
26
  argsIgnorePattern: "^_",
22
27
  varsIgnorePattern: "^_",
@@ -35,13 +40,28 @@ const ANTELOPE_NUXT_RULES = {
35
40
  * ```
36
41
  */
37
42
  function antelopeNuxtConfig(options = {}) {
43
+ const overrides = {
44
+ ...ANTELOPE_NUXT_RULES,
45
+ ...options.rules
46
+ };
38
47
  return createConfigForNuxt({
39
48
  features: { tooling: true },
40
49
  dirs: { src: options.dirs ?? [] }
41
- }).overrideRules({
42
- ...ANTELOPE_NUXT_RULES,
43
- ...options.rules
44
- });
50
+ }).onResolved((configs) => configs.map((config) => {
51
+ if (config.rules === void 0) return config;
52
+ const rules = { ...config.rules };
53
+ for (const [name, original] of Object.entries(rules)) {
54
+ const rule = overrides[name] === void 0 ? original : overrides[name];
55
+ if (rule === null) delete rules[name];
56
+ else if (rule === "warn" || rule === 1) rules[name] = "error";
57
+ else if (Array.isArray(rule) && (rule[0] === "warn" || rule[0] === 1)) rules[name] = ["error", ...rule.slice(1)];
58
+ else rules[name] = rule;
59
+ }
60
+ return {
61
+ ...config,
62
+ rules
63
+ };
64
+ }));
45
65
  }
46
66
  //#endregion
47
67
  export { ANTELOPE_NUXT_RULES, antelopeNuxtConfig };