@williamthorsen/nmr 0.25.0 → 0.26.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.
@@ -40,8 +40,7 @@ var workspaceScripts = {
40
40
  "test:unit": "pnpm exec vitest --project unit",
41
41
  "test:watch": `pnpm exec vitest ${GATE_PROJECTS} --watch`,
42
42
  typecheck: "tsgo --noEmit",
43
- // `--include-locked` is required rather than stylistic: a repo that pins exactly (pnpm's
44
- // `savePrefix: ''`) has no dependency taze considers unlocked, so without it nothing is reported.
43
+ // Without `--include-locked`, nothing would be reported in a repo that pins exact version numbers.
45
44
  upgrade: "nmr-taze --include-locked",
46
45
  "view-coverage": "open coverage/index.html"
47
46
  };
@@ -51,21 +50,17 @@ var rootScripts = {
51
50
  "audit:prod": "pnpm exec v11y --prod",
52
51
  build: "pnpm --recursive exec nmr build",
53
52
  check: ["typecheck", "fmt:check", "lint:check", "test"],
54
- "check:agent-files": "nmr-sync-agent-files --check",
55
- "check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage", "check:agent-files"],
56
- // Excludes the audit, which has a workflow of its own.
53
+ "check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage"],
54
+ // Excludes the audit, which in CI has a workflow of its own.
57
55
  ci: ["build", "check:strict"],
58
56
  clean: "nmr-clean",
59
57
  fix: ["lint", "fmt"],
60
58
  "fix:check": ["fmt:check", "lint:check"],
61
- // Identical to the workspace entries: the bin scopes its own selection to the working directory, so
62
- // neither registry needs the `sh -c '… "${@:-.}"' --` wrapper that gave the root form a default path.
63
59
  fmt: "nmr-fmt --write",
64
60
  "fmt:check": "nmr-fmt --check",
65
61
  lint: "nmr root:lint && pnpm --recursive exec nmr lint",
66
62
  "lint:check": "nmr root:lint:check && pnpm --recursive exec nmr lint:check",
67
63
  "lint:strict": "nmr root:lint:strict && pnpm --recursive exec nmr lint:strict",
68
- // Everything the remote runs: the code-quality gate plus the audit workflow.
69
64
  prepush: ["ci", "audit"],
70
65
  "report-overrides": "nmr-report-overrides",
71
66
  "root:check": ["root:typecheck", "fmt:check", "root:lint:check", "root:test"],
@@ -78,23 +73,16 @@ var rootScripts = {
78
73
  "root:test:unit": "vitest --config ./vitest.root.config.ts --project unit",
79
74
  "root:typecheck": "tsgo --noEmit",
80
75
  "root:upgrade": "nmr-taze --include-locked",
81
- "sync-agent-files": "nmr-sync-agent-files",
82
76
  test: "nmr root:test && pnpm --recursive exec nmr test",
83
77
  "test:all": "nmr root:test:all && pnpm --recursive exec nmr test:all",
84
- // Chains `root:test`, not a `root:test:coverage`: the root config reports no coverage of its own.
85
78
  "test:coverage": "nmr root:test && pnpm --recursive exec nmr test:coverage",
86
79
  "test:tool": "nmr root:test:tool && pnpm --recursive exec nmr test:tool",
87
80
  "test:unit": "nmr root:test:unit && pnpm --recursive exec nmr test:unit",
88
- // Omitting `--config` is deliberate: bare `vitest` at the monorepo root resolves the root `vitest.config.ts`,
89
- // covering the whole tree in one process. A chain like the others would never advance past its first watcher.
81
+ // Bare `vitest` at the monorepo root resolves the root `vitest.config.ts`, covering the whole tree in one
82
+ // process; a chain like the others would never advance past its first watcher.
90
83
  "test:watch": `vitest ${GATE_PROJECTS} --watch`,
91
84
  typecheck: "nmr root:typecheck && pnpm --recursive exec nmr typecheck",
92
- // A string rather than a composite because passthrough args attach to the chain's last command — as a
93
- // composite, `nmr upgrade major` would hand `major` to the override report instead of the upgrade tool.
94
- //
95
- // Overrides print first, as context for the report that follows: a pinned transitive dependency is why
96
- // an expected upgrade may be missing from it. taze handles pnpm workspaces natively, so this needs no
97
- // `pnpm --recursive` fan-out: one process covers the root and every package.
85
+ // A string rather than a composite: Passthrough args attach to the chain's last command.
98
86
  upgrade: "nmr-report-overrides && nmr-taze --include-locked --recursive"
99
87
  };
100
88
 
@@ -375,7 +363,7 @@ function formatPaths(paths) {
375
363
  ${paths.map((path2) => ` ${path2}`).join("\n")}`;
376
364
  }
377
365
  function getMinVersion() {
378
- const picked = { "version": "0.25.0" };
366
+ const picked = { "version": "0.26.1" };
379
367
  if (typeof picked.version !== "string") {
380
368
  throw new TypeError("nmr/package.json: 'version' must be a string");
381
369
  }
@@ -10,7 +10,7 @@
10
10
  "readyupVersion": "0.23.0",
11
11
  "source": "kits/default.ts",
12
12
  "sourceHash": "f646e7fb",
13
- "targetHash": "514c9b6f"
13
+ "targetHash": "8631c00a"
14
14
  }
15
15
  ]
16
16
  }
package/README.md CHANGED
@@ -3,20 +3,13 @@
3
3
  Context-aware script runner for pnpm monorepos. Ships an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
4
4
 
5
5
  <!-- section:release-notes -->
6
- ## Release notes — v0.25.0 (2026-08-03)
6
+ ## Release notes — v0.26.1 (2026-08-04)
7
7
 
8
- ### 🎉 Features
8
+ ### 🐛 Bug fixes
9
9
 
10
- - Mark a cache hit's time saving with a speed-boost icon (#579)
10
+ - Prevent unnecessary bypasses of the check-result cache (#595)
11
11
 
12
- Time saved due to `nmr`'s cache feature now earns a rocket emoji in the terminal output.
13
- Savings less than 1 second are no longer reported.
14
-
15
- - Publish nmr's readyup kit with a test-tier conformance check (#580)
16
-
17
- `@williamthorsen/nmr` is now bundled with ReadyUp kits that check a repo's alignment with `nmr` requirements and recommendations.
18
-
19
- `rdy run --from npm:@williamthorsen/nmr` checks the repo against the kits without prior configuration. Once the package is added to the ReadyUp config, `rdy run --packages` will run the `nmr` kits along with those of any other registered package. Requires `readyup` v0.23 or greater.
12
+ Agents are now instructed never to bypass `nmr`'s check-result cache in order to force a supposed "real" run of a check that has already passed in the working tree, unless the output (and not only the result) of that check is needed. The "Re-run with --no-cache" hint, which was being treated as an instruction by agents, has been removed.
20
13
  <!-- /section:release-notes -->
21
14
 
22
15
  ## Installation
@@ -201,23 +194,23 @@ import { defineConfig } from '@williamthorsen/nmr/config';
201
194
 
202
195
  export default defineConfig({
203
196
  workspaceScripts: {
204
- 'build:pre': 'npx rdy compile',
197
+ 'build:pre': 'node scripts/generate-manifest.ts',
205
198
  },
206
199
  });
207
200
  ```
208
201
 
209
- Attach a post-build step to one package:
202
+ Attach a step to one package's hook:
210
203
 
211
204
  ```jsonc
212
- // packages/nmr/package.json
205
+ // packages/my-package/package.json
213
206
  {
214
207
  "scripts": {
215
- "build:post": "nmr-sync-agent-files",
208
+ "upgrade:post": "nmr-report-overrides",
216
209
  },
217
210
  }
218
211
  ```
219
212
 
220
- The second example calls the bin directly, which sidesteps the workspace-versus-root registry distinction.
213
+ The second example calls the bin directly: `report-overrides` is a root-registry command, so `nmr report-overrides` from inside a package fails with `Unknown command`, while the bin runs from either scope.
221
214
 
222
215
  ## Check-result cache
223
216
 
@@ -228,7 +221,7 @@ $ nmr ci
228
221
  # ... four minutes of build, typecheck, lint, and coverage ...
229
222
 
230
223
  $ nmr ci
231
- ⏭️ ci: passed 2m ago on this tree (🚀 saved ~4m). Re-run with --no-cache.
224
+ ⏭️ ci: passed 2m ago on this tree (🚀 saved ~4m).
232
225
  ```
233
226
 
234
227
  Composite commands expand into child `nmr` processes, so one green `nmr ci` records a pass for every cacheable command in the chain, at every scope it ran at. A later `nmr check`, `nmr typecheck`, or `nmr -F core test` on the same tree skips too. The skip line is suppressed by `-q`.
@@ -237,7 +230,7 @@ The cache is a working-tree cache, not a build cache: it answers "has this exact
237
230
 
238
231
  ### What is cached
239
232
 
240
- Cacheable by default: `check`, `check:agent-files`, `check:strict`, `ci`, `fix:check`, `fmt:check`, `lint:check`, `lint:strict`, `test`, `test:coverage`, `test:tool`, `test:unit`, `typecheck`, and the `root:` variants `root:check`, `root:lint:check`, `root:lint:strict`, `root:test`, `root:test:tool`, `root:test:unit`, and `root:typecheck`.
233
+ Cacheable by default: `check`, `check:strict`, `ci`, `fix:check`, `fmt:check`, `lint:check`, `lint:strict`, `test`, `test:coverage`, `test:tool`, `test:unit`, `typecheck`, and the `root:` variants `root:check`, `root:lint:check`, `root:lint:strict`, `root:test`, `root:test:tool`, `root:test:unit`, and `root:typecheck`.
241
234
 
242
235
  Everything else runs every time:
243
236
 
@@ -437,11 +430,10 @@ Neither is bound to a git hook. `prepush` is named for when you run it, not for
437
430
 
438
431
  #### Check and quality
439
432
 
440
- | Command | Runs |
441
- | ------------------- | ----------------------------------------------------------------------------- |
442
- | `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
443
- | `check:agent-files` | `nmr-sync-agent-files --check` |
444
- | `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage`, `check:agent-files` |
433
+ | Command | Runs |
434
+ | -------------- | -------------------------------------------------------- |
435
+ | `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
436
+ | `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
445
437
 
446
438
  #### Fix
447
439
 
@@ -531,7 +523,6 @@ These scripts operate on root-level code only (not workspace packages):
531
523
  | Command | Runs |
532
524
  | ------------------ | ---------------------- |
533
525
  | `report-overrides` | `nmr-report-overrides` |
534
- | `sync-agent-files` | `nmr-sync-agent-files` |
535
526
 
536
527
  ## CLI reference
537
528
 
@@ -585,35 +576,33 @@ Report any active `pnpm.overrides` in the root `package.json`, reminding develop
585
576
  nmr report-overrides
586
577
  ```
587
578
 
588
- ### `sync-agent-files`
579
+ ## Agent guidance
589
580
 
590
- Sync the agent-facing guidance shipped with nmr into the consuming repo.
581
+ nmr ships the rules an agent needs in order to invoke it, as [CodeAssembly](https://github.com/williamthorsen/codeassembly) package content. Adopt it by naming the package in the consuming repo's `.agents/codeassembly.yaml`:
591
582
 
592
- ```bash
593
- nmr sync-agent-files # write .agents/nmr/AGENTS.md, stamped with the installed nmr version
594
- nmr sync-agent-files --check # verify the stamp matches; exit 1 with a fix message if not
583
+ ```yaml
584
+ packages:
585
+ use:
586
+ - '@williamthorsen/nmr'
595
587
  ```
596
588
 
597
- Run `nmr sync-agent-files` once after upgrading nmr. The generated file is committed to the consuming repo; do not edit it by hand.
598
-
599
- The default root `check:strict` composite includes `check:agent-files`, which runs `--check` automatically — so any CI pipeline already running `check:strict` catches drift without per-consumer wiring.
589
+ Then add `codeassembly` as a devDependency and run `codeassembly sync`. The guidance is injected into the machine-local guidance file each harness loads at launch; nothing is copied into the repo or committed there. It resolves from the installed package, so upgrading nmr updates it with no second step.
600
590
 
601
- To expose the synced guidance to Claude Code sessions, add this include to the consuming repo's `.agents/PROJECT.md`:
591
+ Wiring the sync to `postinstall` keeps it current without a hand-run command:
602
592
 
603
- ```markdown
604
- @nmr/AGENTS.md
593
+ ```json
594
+ {
595
+ "scripts": {
596
+ "postinstall": "codeassembly sync --warn-only"
597
+ }
598
+ }
605
599
  ```
606
600
 
607
- #### What belongs in the synced file
608
-
609
- The synced file is injected at launch in every consuming repo, so every line is paid for on every task whether or not it is relevant. It is a cheatsheet, not a manual.
610
-
611
- A line earns a place in `AGENTS.md` only if both hold:
601
+ `--warn-only` reports a sync failure and exits 0; without it, a failed sync aborts `pnpm install`.
612
602
 
613
- 1. It is absent from nmr's own output. Bare `nmr` already prints the flags, every command, and the shell command each resolves to; failing checks already name their own fix. Repeating any of that costs launch tokens to say something the agent will be told anyway, at the moment it matters.
614
- 2. The obvious action goes wrong without it. Guidance that prevents a silent mistake earns its place; guidance that prevents a mistake the next error message would diagnose does not.
603
+ ### Migrating from `sync-agent-files`
615
604
 
616
- Everything else belongs in this README, reachable on demand at `node_modules/@williamthorsen/nmr/README.md`, with a pointer from `AGENTS.md` naming the topic so an agent knows to look. When adding a feature, document it here and extend that pointer rather than the cheatsheet.
605
+ The `nmr sync-agent-files` command, its `nmr-sync-agent-files` bin, and the `check:agent-files` script were removed in favor of the above. After upgrading, delete the generated `.agents/nmr/` directory and the `@nmr/AGENTS.md` line it was exposed through, drop `check:agent-files` from any `check:strict` override that enumerates it, and remove any direct call to the bin.
617
606
 
618
607
  ## Standalone utilities
619
608
 
@@ -0,0 +1,24 @@
1
+ # Authoring nmr's agent guidance
2
+
3
+ This directory is a CodeAssembly content root, declared by the `codeassembly.content` key in `packages/nmr/package.json`.
4
+
5
+ ## The ambient cheatsheet
6
+
7
+ `guidance/rulebooks/nmr.md` carries `delivery: ambient`, so its body is injected at launch in every consuming repo. Every line is paid for on every task, whether or not it is relevant. It is a cheatsheet, not a manual.
8
+
9
+ A line earns a place in it only if both hold:
10
+
11
+ 1. **It is absent from nmr's own output.** Bare `nmr` already prints the flags, every command, and the shell command each resolves to; failing checks already name their own fix. Repeating any of that costs launch tokens to say something the agent will be told anyway, at the moment it matters.
12
+ 2. **The obvious action goes wrong without it.** Guidance that prevents a silent mistake earns its place; guidance that prevents a mistake the next error message would diagnose does not.
13
+
14
+ Everything else belongs in `packages/nmr/README.md`, reachable on demand at `node_modules/@williamthorsen/nmr/README.md`, with a pointer from the cheatsheet naming the topic so an agent knows to look. When adding a feature, document it there and extend that pointer rather than the cheatsheet.
15
+
16
+ ## Constraints on rulebook bodies
17
+
18
+ Rulebook bodies carry no links to other rulebooks. CodeAssembly's renderer rewrites link targets under `skills/` and `scripts/` and rejects everything else, so a cross-reference names the deployed `consult-<slug>` in prose and declares the edge under `dependencies:`.
19
+
20
+ Anchor-only links are checked against the body they appear in, so a fragment naming a heading that lives in another document fails the run. Check `](#` before moving a section between bodies.
21
+
22
+ ## Publishing
23
+
24
+ The directory is published because `files` in `packages/nmr/package.json` lists it. Dropping that entry fails nothing locally, because this repo resolves the guidance through a `workspace:*` self-link that `files` does not govern. `packaging.tool.test.ts` is what catches it.
@@ -1,12 +1,14 @@
1
1
  ---
2
- source: '@williamthorsen/nmr@0.0.0-source'
2
+ slug: nmr
3
+ description: Invocation rules for the nmr script runner in a pnpm monorepo.
4
+ delivery: ambient
3
5
  ---
4
6
 
5
- # nmr: agent guidance (generated; do not edit)
7
+ # nmr: agent guidance
6
8
 
7
9
  - Use `nmr <command>`, not `pnpm run <command>`, for anything nmr provides. Run bare `nmr` to list every command and the shell command it resolves to; a wrong guess reports only `Unknown command`, with no list.
8
10
  - Scope follows cwd, and bare `nmr` lists both registries: from the repo root a command covers root files and every workspace; from inside a package, that package alone. `nmr -F <pkg> <command>` targets one package from anywhere; `nmr root:<command>` targets root files alone, which isolates a failure to root code.
9
11
  - Never `npx nmr`: inside a git worktree it can resolve a different nmr from outside the tree. Fall back to `pnpm exec nmr`.
10
12
  - `nmr ci` runs what a code-quality workflow runs (build, then strict checks) and not the dependency audit, which belongs to a workflow of its own; `nmr prepush` runs both. Neither installs or binds a git hook.
11
- - A check that already passed on the current working tree skips, printing `⏭️ <command>: passed …`. That is a real pass, not a stale one: any edit, addition, or deletion re-runs the command. A skipped command produces no files, so where you need an artifact rather than an exit status (a fresh `coverage/`), run `nmr --no-cache <command>`.
13
+ - A check that already passed on the current working tree skips, printing `⏭️ <command>: passed …`. Any edit, addition, or deletion re-runs it, so a skip is a real pass and needs no confirming: never pass `--no-cache` to get a "real" run, which costs the command's full runtime to reach the same answer. Its one use is a file rather than an exit status -- a skipped command writes none, so a fresh `coverage/` needs `nmr --no-cache test:coverage`.
12
14
  - The rest is in `node_modules/@williamthorsen/nmr/README.md`: pre/post hooks (every `nmr X` auto-wraps as `nmr X:pre && nmr X && nmr X:post`), script overrides and their `""`/`":"` skip values, `nmr upgrade` ceilings (report-only until `nmr upgrade --write`), the `nmr-compile` build (there is no repo-local build script), and the check-result cache's key, its `checkCache` config, and every condition that disables it.
@@ -18,7 +18,6 @@ export const CURRENT_RUNTIME = {
18
18
  export const DEBUG_ENV_VAR = 'NMR_DEBUG';
19
19
  export const DEFAULT_CACHEABLE_COMMANDS = [
20
20
  'check',
21
- 'check:agent-files',
22
21
  'check:strict',
23
22
  'ci',
24
23
  'fix:check',
@@ -94,7 +93,7 @@ export function formatSkipLine(command, entry, now) {
94
93
  const age = formatDuration(Math.max(0, now - Date.parse(entry.recordedAt)));
95
94
  const saving = formatSaving(entry.durationMs);
96
95
  const savingClause = saving === undefined ? '' : ` (${saving})`;
97
- return `⏭️ ${command}: passed ${age} ago on this tree${savingClause}. Re-run with --no-cache.`;
96
+ return `⏭️ ${command}: passed ${age} ago on this tree${savingClause}.`;
98
97
  }
99
98
  export async function readBuildOutputState(monorepoRoot, config) {
100
99
  const state = { missing: [], digests: {} };
@@ -28,8 +28,7 @@ export const rootScripts = {
28
28
  'audit:prod': 'pnpm exec v11y --prod',
29
29
  build: 'pnpm --recursive exec nmr build',
30
30
  check: ['typecheck', 'fmt:check', 'lint:check', 'test'],
31
- 'check:agent-files': 'nmr-sync-agent-files --check',
32
- 'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage', 'check:agent-files'],
31
+ 'check:strict': ['typecheck', 'fmt:check', 'lint:strict', 'test:coverage'],
33
32
  ci: ['build', 'check:strict'],
34
33
  clean: 'nmr-clean',
35
34
  fix: ['lint', 'fmt'],
@@ -51,7 +50,6 @@ export const rootScripts = {
51
50
  'root:test:unit': 'vitest --config ./vitest.root.config.ts --project unit',
52
51
  'root:typecheck': 'tsgo --noEmit',
53
52
  'root:upgrade': 'nmr-taze --include-locked',
54
- 'sync-agent-files': 'nmr-sync-agent-files',
55
53
  test: 'nmr root:test && pnpm --recursive exec nmr test',
56
54
  'test:all': 'nmr root:test:all && pnpm --recursive exec nmr test:all',
57
55
  'test:coverage': 'nmr root:test && pnpm --recursive exec nmr test:coverage',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for pnpm monorepos",
6
6
  "keywords": [
@@ -54,13 +54,12 @@
54
54
  "nmr-compile": "bin/nmr-compile.js",
55
55
  "nmr-fmt": "bin/nmr-fmt.js",
56
56
  "nmr-report-overrides": "bin/nmr-report-overrides.js",
57
- "nmr-sync-agent-files": "bin/nmr-sync-agent-files.js",
58
57
  "nmr-taze": "bin/nmr-taze.js"
59
58
  },
60
59
  "files": [
61
60
  ".readyup/kits/*.js",
62
61
  ".readyup/manifest.json",
63
- "AGENTS.md",
62
+ "agents",
64
63
  "bin",
65
64
  "dist"
66
65
  ],
@@ -69,7 +68,7 @@
69
68
  "prettier-plugin-sh": "0.19.0",
70
69
  "taze": "19.16.0",
71
70
  "yaml": "2.9.0",
72
- "@williamthorsen/nmr-core": "0.9.0"
71
+ "@williamthorsen/nmr-core": "0.9.1"
73
72
  },
74
73
  "devDependencies": {
75
74
  "prettier": "3.9.6",
@@ -96,7 +95,11 @@
96
95
  "publishConfig": {
97
96
  "access": "public"
98
97
  },
98
+ "codeassembly": {
99
+ "content": "agents"
100
+ },
99
101
  "scripts": {
100
- "build:post": "nmr-sync-agent-files && rdy compile"
102
+ "build:post": "rdy compile",
103
+ "build:pre": "rdy verify"
101
104
  }
102
105
  }
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env node
2
- try {
3
- await import('../dist/esm/cli-sync-agent-files.js');
4
- } catch (error) {
5
- if (error.code === 'ERR_MODULE_NOT_FOUND') {
6
- process.stderr.write('nmr-sync-agent-files: build output not found — run `pnpm run build` first\n');
7
- } else {
8
- process.stderr.write(`nmr-sync-agent-files: failed to load: ${error.message}\n`);
9
- }
10
- process.exit(1);
11
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,29 +0,0 @@
1
- import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
2
- import { check, sync } from "./commands/sync-agent-files.js";
3
- import { findMonorepoRoot } from "./workspace.js";
4
- const flagSchema = {
5
- check: { long: '--check', type: 'boolean' },
6
- };
7
- const { flags } = parseArgsOrExit(process.argv.slice(2), flagSchema);
8
- try {
9
- runCommand(flags.check);
10
- }
11
- catch (error) {
12
- reportError(error instanceof Error ? error.message : String(error));
13
- process.exit(1);
14
- }
15
- function runCommand(checkOnly) {
16
- const monorepoRoot = findMonorepoRoot();
17
- if (!checkOnly) {
18
- const { path: destinationPath, packageSpecifier, changed } = sync(monorepoRoot);
19
- console.info(changed ? `✓ Wrote ${destinationPath} (${packageSpecifier})` : `✓ ${destinationPath} already up to date`);
20
- process.exit(0);
21
- }
22
- const result = check(monorepoRoot);
23
- if (result.ok) {
24
- console.info('✓ .agents/nmr/AGENTS.md is in sync');
25
- process.exit(0);
26
- }
27
- process.stderr.write(`${result.reason}\n`);
28
- process.exit(1);
29
- }
@@ -1,13 +0,0 @@
1
- export declare function check(destinationDir: string): CheckResult;
2
- export interface SyncResult {
3
- path: string;
4
- packageSpecifier: string;
5
- changed: boolean;
6
- }
7
- export declare function sync(destinationDir: string): SyncResult;
8
- export type CheckResult = {
9
- ok: true;
10
- } | {
11
- ok: false;
12
- reason: string;
13
- };
@@ -1,58 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { readPackageVersion } from '@williamthorsen/nmr-core';
5
- const VERSION = readPackageVersion(import.meta.url);
6
- const PACKAGE_NAME = '@williamthorsen/nmr';
7
- const DESTINATION_RELATIVE_PATH = '.agents/nmr/AGENTS.md';
8
- const SOURCE_FILENAME = 'AGENTS.md';
9
- const FRONTMATTER_REGEX = /^---\n((?:.*\n)*?)---\n/;
10
- export function check(destinationDir) {
11
- const destinationPath = getDestinationPath(destinationDir);
12
- if (!existsSync(destinationPath)) {
13
- return {
14
- ok: false,
15
- reason: `${DESTINATION_RELATIVE_PATH} is missing. Run \`nmr sync-agent-files\`.`,
16
- };
17
- }
18
- const expectedBody = stripFrontmatter(readFileSync(getSourcePath(), 'utf8'));
19
- const foundBody = stripFrontmatter(readFileSync(destinationPath, 'utf8'));
20
- if (foundBody !== expectedBody) {
21
- return {
22
- ok: false,
23
- reason: `${DESTINATION_RELATIVE_PATH} content is out of date. Run \`nmr sync-agent-files\`.`,
24
- };
25
- }
26
- return { ok: true };
27
- }
28
- export function sync(destinationDir) {
29
- const body = stripFrontmatter(readFileSync(getSourcePath(), 'utf8'));
30
- const packageSpecifier = buildPackageSpecifier();
31
- const destinationPath = getDestinationPath(destinationDir);
32
- if (existsSync(destinationPath) && stripFrontmatter(readFileSync(destinationPath, 'utf8')) === body) {
33
- return { path: destinationPath, packageSpecifier, changed: false };
34
- }
35
- const output = `---\nsource: '${packageSpecifier}'\n---\n${body}`;
36
- mkdirSync(path.dirname(destinationPath), { recursive: true });
37
- writeFileSync(destinationPath, output, 'utf8');
38
- return { path: destinationPath, packageSpecifier, changed: true };
39
- }
40
- function buildPackageSpecifier() {
41
- return `${PACKAGE_NAME}@${VERSION}`;
42
- }
43
- function getDestinationPath(destinationDir) {
44
- return path.join(destinationDir, DESTINATION_RELATIVE_PATH);
45
- }
46
- function getSourcePath() {
47
- let dir = path.dirname(fileURLToPath(import.meta.url));
48
- while (dir !== path.dirname(dir)) {
49
- const candidate = path.join(dir, SOURCE_FILENAME);
50
- if (existsSync(candidate))
51
- return candidate;
52
- dir = path.dirname(dir);
53
- }
54
- throw new Error(`Could not locate ${SOURCE_FILENAME} in any parent of ${fileURLToPath(import.meta.url)}`);
55
- }
56
- function stripFrontmatter(content) {
57
- return content.replace(FRONTMATTER_REGEX, '');
58
- }