@williamthorsen/nmr 0.11.0 → 0.12.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/AGENTS.md +41 -0
- package/README.md +48 -6
- package/bin/nmr-sync-agent-files.js +11 -0
- package/dist/esm/.cache +1 -1
- package/dist/esm/cli-sync-agent-files.d.ts +1 -0
- package/dist/esm/cli-sync-agent-files.js +31 -0
- package/dist/esm/commands/sync-agent-files.d.ts +14 -0
- package/dist/esm/commands/sync-agent-files.js +77 -0
- package/dist/esm/default-scripts.js +5 -3
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +3 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
source: '@williamthorsen/nmr@0.0.0-source'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# nmr: agent guidance
|
|
6
|
+
|
|
7
|
+
This file is managed by `@williamthorsen/nmr`. Do not edit — re-run `pnpm exec nmr sync-agent-files` after an nmr upgrade to refresh it.
|
|
8
|
+
|
|
9
|
+
## Discover scripts by running nmr
|
|
10
|
+
|
|
11
|
+
Run `nmr` with no command (from the monorepo root or any workspace package) to list every available script, including composite expansions and resolved shell commands. Check this before guessing a script name from another repo — the registry is authoritative.
|
|
12
|
+
|
|
13
|
+
## Invocation rules
|
|
14
|
+
|
|
15
|
+
- Use `nmr <command>` for anything nmr provides. Do not use `pnpm run <command>`.
|
|
16
|
+
- Use `pnpm exec nmr`, not `npx nmr`. Inside git worktrees, `npx` can resolve a different nmr binary from outside the working tree.
|
|
17
|
+
- If `nmr` itself fails to run (fresh clone, missing build output), run `pnpm run bootstrap` from the repo root first.
|
|
18
|
+
|
|
19
|
+
## Root vs. workspace context
|
|
20
|
+
|
|
21
|
+
nmr walks up to find `pnpm-workspace.yaml`, then decides which registry to use based on whether your cwd is inside a workspace package. The same command name (e.g. `build`, `test`, `check:strict`) often exists in both registries with different behavior — the root version typically delegates across all workspaces. Use `-w` to force the root registry from inside a package dir, and `-F <pkg>` to run a single package's script from anywhere.
|
|
22
|
+
|
|
23
|
+
## Composite scripts
|
|
24
|
+
|
|
25
|
+
A script value shown in `nmr` output as `[a, b, c]` is a composite: it runs `nmr a && nmr b && nmr c`, stopping at the first failure. Composites can reference other composites.
|
|
26
|
+
|
|
27
|
+
## `root:*` siblings
|
|
28
|
+
|
|
29
|
+
Some root scripts (e.g. `lint`, `typecheck`, `test`) expand to `nmr root:X && pnpm --recursive exec nmr X`. The `root:X` variant runs only against root-level files; the plain name runs everywhere. Use `root:X` directly when you want to isolate a failure to the root code.
|
|
30
|
+
|
|
31
|
+
## Override behaviors
|
|
32
|
+
|
|
33
|
+
In `.config/nmr.config.ts` or a package's `package.json`, override values have special semantics:
|
|
34
|
+
|
|
35
|
+
- `""` (empty string) — skip the script with a "Skipping" message; exit 0.
|
|
36
|
+
- `":"` — no-op; exit 0. Prefer this over `""` if your repo enforces non-empty script values.
|
|
37
|
+
- Any other string — runs in place of the default.
|
|
38
|
+
|
|
39
|
+
## Agent-file sync
|
|
40
|
+
|
|
41
|
+
The presence and version stamp of `.agents/nmr/AGENTS.md` is verified by `check:agent-files`, which is part of the default root `check:strict` composite. If it fails, run `nmr sync-agent-files`.
|
package/README.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
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
|
+
<!-- section:release-notes -->
|
|
6
|
+
## Release notes — v0.12.0 (2026-04-23)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- Add agent-facing AGENTS.md and sync-agent-files command (#263)
|
|
11
|
+
|
|
12
|
+
Ships nmr-owned agent guidance alongside the `@williamthorsen/nmr` package so consuming repos stop hand-maintaining their own copies of the runner's invocation rules. A new `nmr sync-agent-files` command pulls that guidance into `.agents/nmr/AGENTS.md` in the consuming repo, stamped with the installed nmr version; a companion `--check` variant verifies the stamp against the installed version on every root `check:strict` run. Drift between installed nmr and the committed guidance now fails the quality gate automatically with a single actionable fix message, without per-consumer wiring.
|
|
13
|
+
|
|
14
|
+
- Rename check:fixable script to fix:check (#266)
|
|
15
|
+
|
|
16
|
+
Renames the `check:fixable` convenience script to `fix:check` in both the workspace and root default script registries. The new name mirrors the existing `fmt` / `fmt:check` pattern, aligning the read-only variant with its mutating counterpart (`fix`) so the command's read-only semantics are recognizable from the name alone.
|
|
17
|
+
|
|
18
|
+
The script's expansion (`fmt:check`, `lint:check`) is unchanged.
|
|
19
|
+
<!-- /section:release-notes -->
|
|
20
|
+
|
|
5
21
|
## Installation
|
|
6
22
|
|
|
7
23
|
```bash
|
|
@@ -133,11 +149,11 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
|
|
|
133
149
|
| ------------------ | -------------------------------------------------------- |
|
|
134
150
|
| `build` | `compile`, `generate-typings` |
|
|
135
151
|
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
136
|
-
| `check:fixable` | `fmt:check`, `lint:check` |
|
|
137
152
|
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage` |
|
|
138
153
|
| `clean` | `pnpm exec rimraf dist/*` |
|
|
139
154
|
| `compile` | `tsx ../../config/build.ts` |
|
|
140
155
|
| `fix` | `lint`, `fmt` |
|
|
156
|
+
| `fix:check` | `fmt:check`, `lint:check` |
|
|
141
157
|
| `fmt` | `prettier --list-different --write .` |
|
|
142
158
|
| `fmt:check` | `prettier --check .` |
|
|
143
159
|
| `generate-typings` | `tsc --project tsconfig.generate-typings.json` |
|
|
@@ -173,11 +189,17 @@ Packages with a `vitest.integration.config.ts` file get different test commands.
|
|
|
173
189
|
|
|
174
190
|
#### Check and quality
|
|
175
191
|
|
|
176
|
-
| Command
|
|
177
|
-
|
|
|
178
|
-
| `check`
|
|
179
|
-
| `check:
|
|
180
|
-
| `check:strict`
|
|
192
|
+
| Command | Runs |
|
|
193
|
+
| ------------------- | ----------------------------------------------------------------------------- |
|
|
194
|
+
| `check` | `typecheck`, `fmt:check`, `lint:check`, `test` |
|
|
195
|
+
| `check:agent-files` | `nmr-sync-agent-files --check` |
|
|
196
|
+
| `check:strict` | `typecheck`, `fmt:check`, `lint:strict`, `test:coverage`, `check:agent-files` |
|
|
197
|
+
|
|
198
|
+
#### Fix
|
|
199
|
+
|
|
200
|
+
| Command | Runs |
|
|
201
|
+
| ----------- | ------------------------- |
|
|
202
|
+
| `fix:check` | `fmt:check`, `lint:check` |
|
|
181
203
|
|
|
182
204
|
#### Test
|
|
183
205
|
|
|
@@ -245,6 +267,7 @@ These scripts operate on root-level code only (not workspace packages):
|
|
|
245
267
|
| Command | Runs |
|
|
246
268
|
| ------------------- | ----------------------- |
|
|
247
269
|
| `report-overrides` | `nmr-report-overrides` |
|
|
270
|
+
| `sync-agent-files` | `nmr-sync-agent-files` |
|
|
248
271
|
| `sync-pnpm-version` | `nmr-sync-pnpm-version` |
|
|
249
272
|
|
|
250
273
|
## CLI reference
|
|
@@ -296,6 +319,25 @@ Report any active `pnpm.overrides` in the root `package.json`. Useful as a `post
|
|
|
296
319
|
nmr report-overrides
|
|
297
320
|
```
|
|
298
321
|
|
|
322
|
+
### `sync-agent-files`
|
|
323
|
+
|
|
324
|
+
Sync the agent-facing guidance shipped with nmr into the consuming repo.
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
nmr sync-agent-files # write .agents/nmr/AGENTS.md, stamped with the installed nmr version
|
|
328
|
+
nmr sync-agent-files --check # verify the stamp matches; exit 1 with a fix message if not
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Run `nmr sync-agent-files` once after upgrading nmr. The generated file is committed to the consuming repo; do not edit it by hand.
|
|
332
|
+
|
|
333
|
+
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.
|
|
334
|
+
|
|
335
|
+
To expose the synced guidance to Claude Code sessions, add this include to the consuming repo's `.agents/PROJECT.md`:
|
|
336
|
+
|
|
337
|
+
```markdown
|
|
338
|
+
@.agents/nmr/AGENTS.md
|
|
339
|
+
```
|
|
340
|
+
|
|
299
341
|
### `sync-pnpm-version`
|
|
300
342
|
|
|
301
343
|
Synchronize the pnpm version from the root `package.json` `packageManager` field into the GitHub `code-quality.yaml` workflow file.
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
}
|
package/dist/esm/.cache
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
d28e365779e353583b02eb587a2dfe64797c3a00544b42b8b18922aff2959d34
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { check, sync } from "./commands/sync-agent-files.js";
|
|
2
|
+
import { findMonorepoRoot } from "./context.js";
|
|
3
|
+
function parseArgs(argv) {
|
|
4
|
+
const args = argv.slice(2);
|
|
5
|
+
if (args.length === 0) return { mode: "sync" };
|
|
6
|
+
if (args.length === 1 && args[0] === "--check") return { mode: "check" };
|
|
7
|
+
return { error: `Usage: nmr sync-agent-files [--check]` };
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const parsed = parseArgs(process.argv);
|
|
11
|
+
if ("error" in parsed) {
|
|
12
|
+
console.error(parsed.error);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
const monorepoRoot = findMonorepoRoot();
|
|
16
|
+
if (parsed.mode === "sync") {
|
|
17
|
+
const { written, stamp } = sync(monorepoRoot);
|
|
18
|
+
console.info(`\u2713 Wrote ${written} (${stamp})`);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
const result = check(monorepoRoot);
|
|
22
|
+
if (result.ok) {
|
|
23
|
+
console.info(`\u2713 .agents/nmr/AGENTS.md is in sync (${result.stamp})`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
console.error(result.reason);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function parseSourceStamp(content: string): string | null;
|
|
2
|
+
export interface SyncResult {
|
|
3
|
+
written: string;
|
|
4
|
+
stamp: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function sync(destinationDir: string): SyncResult;
|
|
7
|
+
export type CheckResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
stamp: string;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
reason: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function check(destinationDir: string): CheckResult;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { VERSION } from "../version.js";
|
|
5
|
+
const PACKAGE_NAME = "@williamthorsen/nmr";
|
|
6
|
+
const DESTINATION_RELATIVE_PATH = ".agents/nmr/AGENTS.md";
|
|
7
|
+
const SOURCE_FILENAME = "AGENTS.md";
|
|
8
|
+
function getSourcePath() {
|
|
9
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
while (dir !== path.dirname(dir)) {
|
|
11
|
+
const candidate = path.join(dir, SOURCE_FILENAME);
|
|
12
|
+
if (existsSync(candidate)) return candidate;
|
|
13
|
+
dir = path.dirname(dir);
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`Could not locate ${SOURCE_FILENAME} in any parent of ${fileURLToPath(import.meta.url)}`);
|
|
16
|
+
}
|
|
17
|
+
function getDestinationPath(destinationDir) {
|
|
18
|
+
return path.join(destinationDir, DESTINATION_RELATIVE_PATH);
|
|
19
|
+
}
|
|
20
|
+
function currentSourceStamp() {
|
|
21
|
+
return `${PACKAGE_NAME}@${VERSION}`;
|
|
22
|
+
}
|
|
23
|
+
const FRONTMATTER_REGEX = /^---\n((?:.*\n)*?)---\n/;
|
|
24
|
+
function stripFrontmatter(content) {
|
|
25
|
+
return content.replace(FRONTMATTER_REGEX, "");
|
|
26
|
+
}
|
|
27
|
+
function parseSourceStamp(content) {
|
|
28
|
+
const frontmatterMatch = FRONTMATTER_REGEX.exec(content);
|
|
29
|
+
if (!frontmatterMatch) return null;
|
|
30
|
+
const frontmatterBody = frontmatterMatch[1] ?? "";
|
|
31
|
+
const sourceMatch = /^source:\s*['"]([^'"]+)['"]\s*$/m.exec(frontmatterBody);
|
|
32
|
+
return sourceMatch?.[1] ?? null;
|
|
33
|
+
}
|
|
34
|
+
function sync(destinationDir) {
|
|
35
|
+
const sourcePath = getSourcePath();
|
|
36
|
+
const sourceContent = readFileSync(sourcePath, "utf8");
|
|
37
|
+
const body = stripFrontmatter(sourceContent);
|
|
38
|
+
const stamp = currentSourceStamp();
|
|
39
|
+
const output = `---
|
|
40
|
+
source: '${stamp}'
|
|
41
|
+
---
|
|
42
|
+
${body}`;
|
|
43
|
+
const destinationPath = getDestinationPath(destinationDir);
|
|
44
|
+
mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
45
|
+
writeFileSync(destinationPath, output, "utf8");
|
|
46
|
+
return { written: destinationPath, stamp };
|
|
47
|
+
}
|
|
48
|
+
function check(destinationDir) {
|
|
49
|
+
const destinationPath = getDestinationPath(destinationDir);
|
|
50
|
+
const expected = currentSourceStamp();
|
|
51
|
+
if (!existsSync(destinationPath)) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
reason: `${DESTINATION_RELATIVE_PATH} is missing. Run \`nmr sync-agent-files\`.`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const content = readFileSync(destinationPath, "utf8");
|
|
58
|
+
const found = parseSourceStamp(content);
|
|
59
|
+
if (found === null) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
reason: `Cannot parse version stamp in ${DESTINATION_RELATIVE_PATH}. Run \`nmr sync-agent-files\`.`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (found !== expected) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
reason: `${DESTINATION_RELATIVE_PATH} is out of sync (file: ${found}, installed: ${expected}). Run \`nmr sync-agent-files\`.`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { ok: true, stamp: expected };
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
check,
|
|
75
|
+
parseSourceStamp,
|
|
76
|
+
sync
|
|
77
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const commonWorkspaceScripts = {
|
|
2
2
|
build: ["compile", "generate-typings"],
|
|
3
3
|
check: ["typecheck", "fmt:check", "lint:check", "test"],
|
|
4
|
-
"check:fixable": ["fmt:check", "lint:check"],
|
|
5
4
|
"check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage"],
|
|
6
5
|
clean: "pnpm exec rimraf dist/*",
|
|
7
6
|
compile: "tsx ../../config/build.ts",
|
|
8
7
|
fix: ["lint", "fmt"],
|
|
8
|
+
"fix:check": ["fmt:check", "lint:check"],
|
|
9
9
|
fmt: "prettier --list-different --write .",
|
|
10
10
|
"fmt:check": "prettier --check .",
|
|
11
11
|
"generate-typings": "tsc --project tsconfig.generate-typings.json",
|
|
@@ -32,11 +32,12 @@ const rootScripts = {
|
|
|
32
32
|
"audit:prod": "pnpm exec audit-deps --prod",
|
|
33
33
|
build: "pnpm --recursive exec nmr build",
|
|
34
34
|
check: ["typecheck", "fmt:check", "lint:check", "test"],
|
|
35
|
-
"check:
|
|
36
|
-
"check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage"],
|
|
35
|
+
"check:agent-files": "nmr-sync-agent-files --check",
|
|
36
|
+
"check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage", "check:agent-files"],
|
|
37
37
|
ci: ["build", "check:strict", "audit"],
|
|
38
38
|
clean: "pnpm --recursive exec nmr clean",
|
|
39
39
|
fix: ["lint", "fmt"],
|
|
40
|
+
"fix:check": ["fmt:check", "lint:check"],
|
|
40
41
|
fmt: `sh -c 'prettier --list-different --write "\${@:-.}"' --`,
|
|
41
42
|
"fmt:all": ["fmt", "fmt:sh"],
|
|
42
43
|
"fmt:check": `sh -c 'prettier --check "\${@:-.}"' --`,
|
|
@@ -53,6 +54,7 @@ const rootScripts = {
|
|
|
53
54
|
"root:lint:strict": "strict-lint --ignore-pattern 'packages/**' .",
|
|
54
55
|
"root:test": "vitest --config ./vitest.root.config.ts",
|
|
55
56
|
"root:typecheck": "tsgo --noEmit",
|
|
57
|
+
"sync-agent-files": "nmr-sync-agent-files",
|
|
56
58
|
"sync-pnpm-version": "nmr-sync-pnpm-version",
|
|
57
59
|
test: "nmr root:test && pnpm --recursive exec nmr test",
|
|
58
60
|
"test:coverage": "nmr root:test && pnpm --recursive exec nmr test:coverage",
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.12.0";
|
package/dist/esm/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for PNPM monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -30,9 +30,11 @@
|
|
|
30
30
|
"ensure-prepublish-hooks": "bin/ensure-prepublish-hooks.js",
|
|
31
31
|
"nmr": "bin/nmr.js",
|
|
32
32
|
"nmr-report-overrides": "bin/nmr-report-overrides.js",
|
|
33
|
+
"nmr-sync-agent-files": "bin/nmr-sync-agent-files.js",
|
|
33
34
|
"nmr-sync-pnpm-version": "bin/nmr-sync-pnpm-version.js"
|
|
34
35
|
},
|
|
35
36
|
"files": [
|
|
37
|
+
"AGENTS.md",
|
|
36
38
|
"bin",
|
|
37
39
|
"dist"
|
|
38
40
|
],
|