@williamthorsen/nmr 0.12.1 → 0.13.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 +36 -0
- package/README.md +19 -1
- package/dist/esm/.cache +1 -1
- package/dist/esm/cli.js +46 -19
- package/dist/esm/commands/sync-agent-files.js +2 -1
- package/dist/esm/help.d.ts +1 -1
- package/dist/esm/help.js +57 -9
- package/dist/esm/helpers/hook-name.d.ts +1 -0
- package/dist/esm/helpers/hook-name.js +6 -0
- package/dist/esm/resolver.d.ts +4 -2
- package/dist/esm/resolver.js +7 -4
- package/package.json +6 -3
- package/dist/esm/version.d.ts +0 -1
- package/dist/esm/version.js +0 -4
package/AGENTS.md
CHANGED
|
@@ -36,6 +36,42 @@ In `.config/nmr.config.ts` or a package's `package.json`, override values have s
|
|
|
36
36
|
- `":"` — no-op; exit 0. Prefer this over `""` if your repo enforces non-empty script values.
|
|
37
37
|
- Any other string — runs in place of the default.
|
|
38
38
|
|
|
39
|
+
## Pre and post hooks
|
|
40
|
+
|
|
41
|
+
Every `nmr X` invocation auto-wraps as the equivalent of `nmr X:pre && nmr X && nmr X:post`. Hooks are first-class scripts that resolve through the same 3-tier registry (built-in defaults → `.config/nmr.config.ts` → per-package `package.json`). Wrapping is uniform; nested invocations from composite expansion get their own hook treatment. Hook failure short-circuits the chain via shell `&&` semantics, propagating the failing exit code.
|
|
42
|
+
|
|
43
|
+
Behaviors worth knowing:
|
|
44
|
+
|
|
45
|
+
- **Silent when absent** — missing hooks produce no error and no output.
|
|
46
|
+
- **Skip overrides apply to hooks** — a hook value of `""` or `":"` is treated the same as not defining the hook. No console message.
|
|
47
|
+
- **Skipping the main command skips its hooks** — when `X` is overridden to `""` or `":"`, neither `X:pre` nor `X:post` fires.
|
|
48
|
+
- **Recursion guard** — direct invocation of a hook (e.g., `nmr build:pre`) is treated as a leaf operation. It does NOT itself attempt to resolve `build:pre:pre` or `build:pre:post`.
|
|
49
|
+
- **Passthrough args attach only to the main command** — `nmr X --flag value` runs hooks without `--flag value`.
|
|
50
|
+
|
|
51
|
+
Worked examples:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// .config/nmr.config.ts — extend `nmr build` with a pre-build compile step
|
|
55
|
+
import { defineConfig } from '@williamthorsen/nmr';
|
|
56
|
+
|
|
57
|
+
export default defineConfig({
|
|
58
|
+
workspaceScripts: {
|
|
59
|
+
'build:pre': 'npx rdy compile',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```jsonc
|
|
65
|
+
// packages/nmr/package.json — re-stamp .agents/nmr/AGENTS.md after every build
|
|
66
|
+
{
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build:post": "nmr-sync-agent-files",
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The second example calls the bin directly to sidestep the workspace-vs-root registry distinction.
|
|
74
|
+
|
|
39
75
|
## Agent-file sync
|
|
40
76
|
|
|
41
77
|
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,7 +2,25 @@
|
|
|
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
|
|
5
|
+
<!-- section:release-notes -->
|
|
6
|
+
## Release notes — v0.13.0 (2026-05-04)
|
|
7
|
+
|
|
8
|
+
### 🎉 Features
|
|
9
|
+
|
|
10
|
+
- Add :pre and :post hook conventions to nmr commands (#339)
|
|
11
|
+
|
|
12
|
+
Adds `:pre` and `:post` hook conventions to nmr's command runner. Consumers can declare `X:pre` or `X:post` scripts that nmr runs automatically before and after script `X`. The hook commands are optional and ignored if missing. Hooks fire even when the main command is overridden, and direct invocations like `nmr build:pre` are treated as leaf operations rather than recursively cascading into `build:pre:pre` lookups.
|
|
13
|
+
|
|
14
|
+
- Filter `nmr --help` to nmr commands (#348)
|
|
15
|
+
|
|
16
|
+
Restricts `nmr --help` to nmr commands only. Hook scripts (names ending in `:pre` or `:post`) are now hidden from help, and generic `package.json` lifecycle scripts (`prepare`, `postinstall`, `bootstrap`) no longer appear. When a `package.json` script overrides a built-in nmr command, the override is shown inline alongside the command name with a `*` marker, and a single `* Overridden by package.json` footnote is appended whenever any marker is rendered. Help also now reflects the active resolution context: invoked from a subpackage you see workspace-level overrides; invoked from the repo root (or with `-w`) you see root-level overrides.
|
|
17
|
+
|
|
18
|
+
### 🐛 Bug fixes
|
|
19
|
+
|
|
20
|
+
- Honor -w flag in composite-script step subprocesses (#346)
|
|
21
|
+
|
|
22
|
+
Fixes an issue where invoking `nmr -w <command>` from inside a workspace package, with `<command>` resolving to a composite (multi-step) script defined at the root level, failed with `Unknown command` for any step that was reachable only via the root script registry. The `-w` flag is now propagated to every step's subprocess invocation, so composite scripts run end-to-end regardless of which directory they are invoked from.
|
|
23
|
+
<!-- /section:release-notes -->
|
|
6
24
|
|
|
7
25
|
## Installation
|
|
8
26
|
|
package/dist/esm/.cache
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
01752582ad3da5f245f0d28ce77644b2b67d6be30ddb469279c38399fbd7e03b
|
package/dist/esm/cli.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import process from "node:process";
|
|
3
|
+
import { readPackageVersion } from "@williamthorsen/nmr-core";
|
|
3
4
|
import { resolveContext } from "./context.js";
|
|
4
5
|
import { generateHelp } from "./help.js";
|
|
6
|
+
import { isHookName } from "./helpers/hook-name.js";
|
|
5
7
|
import { applyDevBin, buildRootRegistry, buildWorkspaceRegistry, resolveScript } from "./resolver.js";
|
|
6
8
|
import { runCommand } from "./runner.js";
|
|
7
|
-
|
|
9
|
+
const VERSION = readPackageVersion(import.meta.url);
|
|
8
10
|
function shellQuote(arg) {
|
|
9
11
|
return "'" + arg.replace(/'/g, String.raw`'\''`) + "'";
|
|
10
12
|
}
|
|
@@ -77,8 +79,10 @@ async function main() {
|
|
|
77
79
|
process.exit(0);
|
|
78
80
|
}
|
|
79
81
|
const context = await resolveContext();
|
|
82
|
+
const useRoot = parsed.workspaceRoot || context.isRoot;
|
|
83
|
+
const packageDir = useRoot ? context.monorepoRoot : context.packageDir ?? context.monorepoRoot;
|
|
80
84
|
if (parsed.help || !parsed.command) {
|
|
81
|
-
console.info(generateHelp(context.config));
|
|
85
|
+
console.info(generateHelp(context.config, packageDir, useRoot));
|
|
82
86
|
process.exit(0);
|
|
83
87
|
}
|
|
84
88
|
const { command } = parsed;
|
|
@@ -95,10 +99,8 @@ async function main() {
|
|
|
95
99
|
const code2 = runCommand(delegateCmd, context.monorepoRoot, runOptions);
|
|
96
100
|
process.exit(code2);
|
|
97
101
|
}
|
|
98
|
-
const useRoot = parsed.workspaceRoot || context.isRoot;
|
|
99
102
|
const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config, parsed.intTest);
|
|
100
|
-
const
|
|
101
|
-
const resolved = resolveScript(command, registry, packageDir);
|
|
103
|
+
const resolved = resolveScript(command, registry, packageDir, parsed.workspaceRoot);
|
|
102
104
|
if (!resolved) {
|
|
103
105
|
if (process.env.NMR_RUN_IF_PRESENT === "1") {
|
|
104
106
|
process.exit(0);
|
|
@@ -106,27 +108,52 @@ async function main() {
|
|
|
106
108
|
console.error(`Unknown command: ${command}`);
|
|
107
109
|
process.exit(1);
|
|
108
110
|
}
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
console.info(`\u26D4 ${packageName}: Override script is defined but empty. Skipping.`);
|
|
113
|
-
}
|
|
114
|
-
process.exit(0);
|
|
115
|
-
}
|
|
116
|
-
if (resolved.command === ":") {
|
|
117
|
-
if (!parsed.quiet) {
|
|
118
|
-
console.info(`\u26D4 ${packageName}: Override script is a no-op. Skipping.`);
|
|
119
|
-
}
|
|
120
|
-
process.exit(0);
|
|
111
|
+
const skipExitCode = handleSkipMessage(resolved.command, packageDir, parsed.quiet);
|
|
112
|
+
if (skipExitCode !== void 0) {
|
|
113
|
+
process.exit(skipExitCode);
|
|
121
114
|
}
|
|
122
115
|
if (resolved.source === "package" && !parsed.quiet && registry[command] !== void 0) {
|
|
123
|
-
console.info(`\u{1F4E6} ${
|
|
116
|
+
console.info(`\u{1F4E6} ${path.basename(packageDir)}: Using override script: ${resolved.command}`);
|
|
124
117
|
}
|
|
125
118
|
const substitutedCommand = applyDevBin(resolved.command, context.config.devBin, context.monorepoRoot);
|
|
126
|
-
const
|
|
119
|
+
const mainCommand = substitutedCommand + passthrough;
|
|
120
|
+
const isHookInvocation = isHookName(command);
|
|
121
|
+
const fullCommand = isHookInvocation ? mainCommand : wrapWithHooks(command, mainCommand, registry, packageDir, parsed.workspaceRoot);
|
|
127
122
|
const code = runCommand(fullCommand, void 0, runOptions);
|
|
128
123
|
process.exit(code);
|
|
129
124
|
}
|
|
125
|
+
function handleSkipMessage(resolvedCommand, packageDir, quiet) {
|
|
126
|
+
if (resolvedCommand === "") {
|
|
127
|
+
if (!quiet) {
|
|
128
|
+
console.info(`\u26D4 ${path.basename(packageDir)}: Override script is defined but empty. Skipping.`);
|
|
129
|
+
}
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
if (resolvedCommand === ":") {
|
|
133
|
+
if (!quiet) {
|
|
134
|
+
console.info(`\u26D4 ${path.basename(packageDir)}: Override script is a no-op. Skipping.`);
|
|
135
|
+
}
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
function wrapWithHooks(command, mainCommand, registry, packageDir, workspaceRoot) {
|
|
141
|
+
const segments = [];
|
|
142
|
+
const flag = workspaceRoot ? "-w " : "";
|
|
143
|
+
if (hasRunnableHook(`${command}:pre`, registry, packageDir, workspaceRoot)) {
|
|
144
|
+
segments.push(`nmr ${flag}${command}:pre`);
|
|
145
|
+
}
|
|
146
|
+
segments.push(mainCommand);
|
|
147
|
+
if (hasRunnableHook(`${command}:post`, registry, packageDir, workspaceRoot)) {
|
|
148
|
+
segments.push(`nmr ${flag}${command}:post`);
|
|
149
|
+
}
|
|
150
|
+
return segments.join(" && ");
|
|
151
|
+
}
|
|
152
|
+
function hasRunnableHook(hookName, registry, packageDir, workspaceRoot) {
|
|
153
|
+
const resolved = resolveScript(hookName, registry, packageDir, workspaceRoot);
|
|
154
|
+
if (!resolved) return false;
|
|
155
|
+
return resolved.command !== "" && resolved.command !== ":";
|
|
156
|
+
}
|
|
130
157
|
try {
|
|
131
158
|
await main();
|
|
132
159
|
} catch (error) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { readPackageVersion } from "@williamthorsen/nmr-core";
|
|
5
|
+
const VERSION = readPackageVersion(import.meta.url);
|
|
5
6
|
const PACKAGE_NAME = "@williamthorsen/nmr";
|
|
6
7
|
const DESTINATION_RELATIVE_PATH = ".agents/nmr/AGENTS.md";
|
|
7
8
|
const SOURCE_FILENAME = "AGENTS.md";
|
package/dist/esm/help.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { NmrConfig } from './config.js';
|
|
2
|
-
export declare function generateHelp(config: NmrConfig): string;
|
|
2
|
+
export declare function generateHelp(config: NmrConfig, packageDir: string | undefined, useRoot: boolean): string;
|
package/dist/esm/help.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { isHookName } from "./helpers/hook-name.js";
|
|
2
|
+
import {
|
|
3
|
+
buildRootRegistry,
|
|
4
|
+
buildWorkspaceRegistry,
|
|
5
|
+
describeScript,
|
|
6
|
+
isSelfReferential,
|
|
7
|
+
readPackageJsonScripts
|
|
8
|
+
} from "./resolver.js";
|
|
9
|
+
function generateHelp(config, packageDir, useRoot) {
|
|
4
10
|
const lines = [
|
|
5
11
|
"Usage: nmr [flags] <command> [args...]",
|
|
6
12
|
"",
|
|
@@ -14,16 +20,58 @@ function generateHelp(config) {
|
|
|
14
20
|
"",
|
|
15
21
|
"Workspace commands:"
|
|
16
22
|
];
|
|
17
|
-
|
|
23
|
+
const overrides = packageDir === void 0 ? {} : collectOverrides(packageDir);
|
|
24
|
+
let hadOverride = false;
|
|
25
|
+
const workspaceRegistry = filterHooks(buildWorkspaceRegistry(config, false));
|
|
26
|
+
const workspaceMarked = !useRoot ? applyOverrides(workspaceRegistry, overrides) : /* @__PURE__ */ new Set();
|
|
27
|
+
if (workspaceMarked.size > 0) hadOverride = true;
|
|
28
|
+
formatRegistry(workspaceRegistry, workspaceMarked, lines);
|
|
18
29
|
lines.push("", "Root commands:");
|
|
19
|
-
|
|
30
|
+
const rootRegistry = filterHooks(buildRootRegistry(config));
|
|
31
|
+
const rootMarked = useRoot ? applyOverrides(rootRegistry, overrides) : /* @__PURE__ */ new Set();
|
|
32
|
+
if (rootMarked.size > 0) hadOverride = true;
|
|
33
|
+
formatRegistry(rootRegistry, rootMarked, lines);
|
|
34
|
+
if (hadOverride) {
|
|
35
|
+
lines.push("", "* Overridden by package.json");
|
|
36
|
+
}
|
|
20
37
|
return lines.join("\n");
|
|
21
38
|
}
|
|
22
|
-
function
|
|
23
|
-
const
|
|
24
|
-
|
|
39
|
+
function collectOverrides(packageDir) {
|
|
40
|
+
const scripts = readPackageJsonScripts(packageDir);
|
|
41
|
+
if (!scripts) return {};
|
|
42
|
+
const filtered = {};
|
|
43
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
44
|
+
if (isHookName(name)) continue;
|
|
45
|
+
if (isSelfReferential(value, name)) continue;
|
|
46
|
+
filtered[name] = value;
|
|
47
|
+
}
|
|
48
|
+
return filtered;
|
|
49
|
+
}
|
|
50
|
+
function applyOverrides(registry, overrides) {
|
|
51
|
+
const marked = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
53
|
+
if (name in registry) {
|
|
54
|
+
registry[name] = value;
|
|
55
|
+
marked.add(name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return marked;
|
|
59
|
+
}
|
|
60
|
+
function filterHooks(registry) {
|
|
61
|
+
const filtered = {};
|
|
62
|
+
for (const [key, value] of Object.entries(registry)) {
|
|
63
|
+
if (!isHookName(key)) filtered[key] = value;
|
|
64
|
+
}
|
|
65
|
+
return filtered;
|
|
66
|
+
}
|
|
67
|
+
function formatRegistry(registry, marked, lines) {
|
|
68
|
+
const keys = Object.keys(registry);
|
|
69
|
+
if (keys.length === 0) return;
|
|
70
|
+
const maxKeyLen = Math.max(...keys.map((k) => k.length));
|
|
71
|
+
const pad = Math.max(maxKeyLen + 1 + 2, 20);
|
|
25
72
|
for (const [key, value] of Object.entries(registry)) {
|
|
26
|
-
|
|
73
|
+
const marker = marked.has(key) ? "*" : " ";
|
|
74
|
+
lines.push(` ${(key + marker).padEnd(pad)} ${describeScript(value)}`);
|
|
27
75
|
}
|
|
28
76
|
}
|
|
29
77
|
export {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isHookName(key: string): boolean;
|
package/dist/esm/resolver.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ export interface ResolvedScript {
|
|
|
5
5
|
command: string;
|
|
6
6
|
source: 'default' | 'package';
|
|
7
7
|
}
|
|
8
|
-
export declare function expandScript(script: ScriptValue): string;
|
|
8
|
+
export declare function expandScript(script: ScriptValue, workspaceRoot: boolean): string;
|
|
9
9
|
export declare function describeScript(script: ScriptValue): string;
|
|
10
|
+
export declare function readPackageJsonScripts(packageDir: string): Record<string, string> | undefined;
|
|
10
11
|
export declare function buildWorkspaceRegistry(config: NmrConfig, useIntTests: boolean): ScriptRegistry;
|
|
11
12
|
export declare function buildRootRegistry(config: NmrConfig): ScriptRegistry;
|
|
12
|
-
export declare function
|
|
13
|
+
export declare function isSelfReferential(script: string, commandName: string): boolean;
|
|
14
|
+
export declare function resolveScript(commandName: string, registry: ScriptRegistry, packageDir: string | undefined, workspaceRoot: boolean): ResolvedScript | undefined;
|
package/dist/esm/resolver.js
CHANGED
|
@@ -25,11 +25,12 @@ function resolveReplacementPaths(replacement, monorepoRoot) {
|
|
|
25
25
|
return path.resolve(monorepoRoot, token);
|
|
26
26
|
}).join(" ");
|
|
27
27
|
}
|
|
28
|
-
function expandScript(script) {
|
|
28
|
+
function expandScript(script, workspaceRoot) {
|
|
29
29
|
if (typeof script === "string") {
|
|
30
30
|
return script;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
const flag = workspaceRoot ? "-w " : "";
|
|
33
|
+
return script.map((s) => `nmr ${flag}${s}`).join(" && ");
|
|
33
34
|
}
|
|
34
35
|
function describeScript(script) {
|
|
35
36
|
return typeof script === "string" ? script : `[${script.join(", ")}]`;
|
|
@@ -69,7 +70,7 @@ function isSelfReferential(script, commandName) {
|
|
|
69
70
|
const prefix = `nmr ${commandName}`;
|
|
70
71
|
return script === prefix || script.startsWith(`${prefix} `);
|
|
71
72
|
}
|
|
72
|
-
function resolveScript(commandName, registry, packageDir) {
|
|
73
|
+
function resolveScript(commandName, registry, packageDir, workspaceRoot) {
|
|
73
74
|
if (packageDir) {
|
|
74
75
|
const pkgScripts = readPackageJsonScripts(packageDir);
|
|
75
76
|
if (pkgScripts && commandName in pkgScripts) {
|
|
@@ -83,7 +84,7 @@ function resolveScript(commandName, registry, packageDir) {
|
|
|
83
84
|
if (registryEntry === void 0) {
|
|
84
85
|
return void 0;
|
|
85
86
|
}
|
|
86
|
-
return { command: expandScript(registryEntry), source: "default" };
|
|
87
|
+
return { command: expandScript(registryEntry, workspaceRoot), source: "default" };
|
|
87
88
|
}
|
|
88
89
|
export {
|
|
89
90
|
applyDevBin,
|
|
@@ -91,5 +92,7 @@ export {
|
|
|
91
92
|
buildWorkspaceRegistry,
|
|
92
93
|
describeScript,
|
|
93
94
|
expandScript,
|
|
95
|
+
isSelfReferential,
|
|
96
|
+
readPackageJsonScripts,
|
|
94
97
|
resolveScript
|
|
95
98
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for PNPM monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -41,10 +41,13 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"jiti": "2.6.1",
|
|
43
43
|
"js-yaml": "4.1.1",
|
|
44
|
-
"zod": "4.4.
|
|
44
|
+
"zod": "4.4.3",
|
|
45
|
+
"@williamthorsen/nmr-core": "0.3.1"
|
|
45
46
|
},
|
|
46
47
|
"publishConfig": {
|
|
47
48
|
"access": "public"
|
|
48
49
|
},
|
|
49
|
-
"scripts": {
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build:post": "nmr-sync-agent-files"
|
|
52
|
+
}
|
|
50
53
|
}
|
package/dist/esm/version.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const VERSION = "0.12.1";
|
package/dist/esm/version.js
DELETED