@williamthorsen/nmr 1.0.0 → 2.0.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.
@@ -1,4 +1,34 @@
1
1
  [
2
+ {
3
+ "version": "2.0.0",
4
+ "date": "2026-11-10",
5
+ "sections": [
6
+ {
7
+ "title": "🪦 Removed",
8
+ "audience": "all",
9
+ "items": [
10
+ {
11
+ "description": "Removes the `devBin` root-config field of `@williamthorsen/nmr`, which replaced the first token of a resolved command with a command that ran a bin from source. (#1269)",
12
+ "breaking": true,
13
+ "migration": "Replace each `devBin` entry by overriding the script that runs the bin in `rootScripts` or `workspaceScripts` with `node <path to the bin's source>`, and build an absolute path for a workspace script, because nmr no longer resolves relative paths from the monorepo root and a workspace script runs in each package's directory; a `package.json` script that runs the bin outranks the config, so change it in that `package.json`.",
14
+ "hash": "5a95865afe2560a4bb9e3f66448bd68f6a32c312",
15
+ "entry": 1
16
+ }
17
+ ]
18
+ },
19
+ {
20
+ "title": "♻️ Refactoring",
21
+ "audience": "dev",
22
+ "items": [
23
+ {
24
+ "description": "Move the duplicated output-style resolver into nmr-core's cli entry (#1248)",
25
+ "body": "Replaces the duplicate `resolveStyles` modules in `release-kit` and `v11y-check` with one `resolveCliStyles` function, exported from `@williamthorsen/nmr-core/cli`. Each package still reads its own `RELEASE_KIT_OUTPUT_STYLE` or `V11Y_CHECK_OUTPUT_STYLE` variable and still reports a usage error for a value other than `auto`, `plain`, or `rich`.",
26
+ "hash": "c5f4ac68360ce550a7e53006a2a633b79f179f5f"
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ },
2
32
  {
3
33
  "version": "1.0.0",
4
34
  "date": "2026-11-08",
@@ -8733,11 +8733,6 @@ function readPackageScripts(packageDir) {
8733
8733
  }
8734
8734
  }
8735
8735
 
8736
- // src/helpers/type-guards.ts
8737
- function isStringRecord(value) {
8738
- return isRecord2(value) && Object.values(value).every((entry) => typeof entry === "string");
8739
- }
8740
-
8741
8736
  // src/verbosity.ts
8742
8737
  var COMMAND_VERBOSITIES = ["full", "quiet"];
8743
8738
  function formatVerbosityRejection(source, value) {
@@ -8752,7 +8747,7 @@ function isCommandVerbosity(value) {
8752
8747
  var CONFIG_TIERS = {
8753
8748
  root: {
8754
8749
  label: "a monorepo-root config",
8755
- honoredKeys: ["checkCache", "delegate", "devBin", "output", "rootScripts", "selfHostedRunner", "workspaceScripts"],
8750
+ honoredKeys: ["checkCache", "delegate", "output", "rootScripts", "selfHostedRunner", "workspaceScripts"],
8756
8751
  elsewhere: "the package's own config"
8757
8752
  },
8758
8753
  workspace: {
@@ -8788,8 +8783,6 @@ function validateConfig(value, configPath, baseDir) {
8788
8783
  const checkCache = validateCheckCacheField(value, configPath);
8789
8784
  if (checkCache) config.checkCache = checkCache;
8790
8785
  if (value["delegate"] !== void 0) config.delegate = validateDelegateField(value["delegate"], configPath);
8791
- const devBin = validateStringRecordField(value, "devBin", configPath);
8792
- if (devBin) config.devBin = devBin;
8793
8786
  const output = validateOutputField(value, configPath);
8794
8787
  if (output) config.output = output;
8795
8788
  const workspaceScripts2 = validateScriptField(value, "workspaceScripts", configPath);
@@ -8960,15 +8953,6 @@ function validateScriptField(value, fieldName, configPath) {
8960
8953
  assertValidElements(scripts, fieldName, configPath);
8961
8954
  return scripts;
8962
8955
  }
8963
- function validateStringRecordField(value, fieldName, configPath) {
8964
- if (!Object.hasOwn(value, fieldName) || value[fieldName] === void 0) {
8965
- return void 0;
8966
- }
8967
- if (!isStringRecord(value[fieldName])) {
8968
- throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string>`);
8969
- }
8970
- return value[fieldName];
8971
- }
8972
8956
 
8973
8957
  // src/helpers/escapeControlCharacters.ts
8974
8958
  var NAMED_ESCAPES = /* @__PURE__ */ new Map([
@@ -9611,7 +9595,7 @@ function hasSupportedStrictLintVersion() {
9611
9595
  });
9612
9596
  }
9613
9597
  function getMinVersion() {
9614
- const pickedFields = { "version": "1.0.0" };
9598
+ const pickedFields = { "version": "2.0.0" };
9615
9599
  if (typeof pickedFields["version"] !== "string") {
9616
9600
  throw new TypeError("nmr/package.json: 'version' must be a string");
9617
9601
  }
@@ -32,7 +32,7 @@
32
32
  "checklist": "nmr",
33
33
  "checks": [
34
34
  "@williamthorsen/nmr in devDependencies",
35
- "@williamthorsen/nmr >= 1.0.0",
35
+ "@williamthorsen/nmr >= 2.0.0",
36
36
  "pnpm-workspace.yaml exists",
37
37
  "catalogued dependencies use catalog:",
38
38
  "package.json has packageManager field",
@@ -86,7 +86,7 @@
86
86
  "path": "../../nmr-core/dist/esm/cache/cache-store.js"
87
87
  },
88
88
  {
89
- "hash": "d2edf9e9",
89
+ "hash": "6ea2c3c0",
90
90
  "kind": "module",
91
91
  "path": "../../nmr-core/dist/esm/cli.js"
92
92
  },
@@ -236,7 +236,7 @@
236
236
  "path": "lib/workspace-build.ts"
237
237
  },
238
238
  {
239
- "hash": "2afa0f3c",
239
+ "hash": "6933f29e",
240
240
  "kind": "inline",
241
241
  "path": "../package.json",
242
242
  "paths": [
@@ -254,12 +254,12 @@
254
254
  "path": "../src/config-check-cache.ts"
255
255
  },
256
256
  {
257
- "hash": "378fb666",
257
+ "hash": "d25aef4f",
258
258
  "kind": "module",
259
259
  "path": "../src/config-validation.ts"
260
260
  },
261
261
  {
262
- "hash": "8cb3c082",
262
+ "hash": "d345a285",
263
263
  "kind": "module",
264
264
  "path": "../src/config.ts"
265
265
  },
@@ -308,11 +308,6 @@
308
308
  "kind": "module",
309
309
  "path": "../src/helpers/reportClosing.ts"
310
310
  },
311
- {
312
- "hash": "3f8448c9",
313
- "kind": "module",
314
- "path": "../src/helpers/type-guards.ts"
315
- },
316
311
  {
317
312
  "hash": "77f6bb50",
318
313
  "kind": "module",
@@ -324,7 +319,7 @@
324
319
  "path": "../src/resolve-scripts.ts"
325
320
  },
326
321
  {
327
- "hash": "5112becc",
322
+ "hash": "0112952e",
328
323
  "kind": "module",
329
324
  "path": "../src/resolver.ts"
330
325
  },
@@ -364,7 +359,7 @@
364
359
  "readyupVersion": "0.45.0",
365
360
  "source": "kits/default.ts",
366
361
  "sourceHash": "9fd61bf4",
367
- "targetHash": "5818d99b"
362
+ "targetHash": "7ddb0518"
368
363
  }
369
364
  ]
370
365
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 2.0.0 — 2026-11-10
6
+
7
+ ### 🪦 Removed
8
+
9
+ - 🚨 **Breaking:** Removes the `devBin` root-config field of `@williamthorsen/nmr`, which replaced the first token of a resolved command with a command that ran a bin from source. (#1269)
10
+
11
+ ### ♻️ Refactoring
12
+
13
+ - Move the duplicated output-style resolver into nmr-core's cli entry (#1248)
14
+
15
+ Replaces the duplicate `resolveStyles` modules in `release-kit` and `v11y-check` with one `resolveCliStyles` function, exported from `@williamthorsen/nmr-core/cli`. Each package still reads its own `RELEASE_KIT_OUTPUT_STYLE` or `V11Y_CHECK_OUTPUT_STYLE` variable and still reports a usage error for a value other than `auto`, `plain`, or `rich`.
16
+
5
17
  ## 1.0.0 — 2026-11-08
6
18
 
7
19
  ### 🧪 Tests
package/README.md CHANGED
@@ -4,7 +4,13 @@
4
4
 
5
5
  Context-aware script runner for pnpm monorepos. Includes an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
6
6
 
7
- <!-- section:release-notes --><!-- /section:release-notes -->
7
+ <!-- section:release-notes -->
8
+ ## Release notes — v2.0.0 (2026-11-10)
9
+
10
+ ### 🪦 Removed
11
+
12
+ - 🚨 **Breaking:** Removes the `devBin` root-config field of `@williamthorsen/nmr`, which replaced the first token of a resolved command with a command that ran a bin from source. (#1269)
13
+ <!-- /section:release-notes -->
8
14
 
9
15
  ## Installation
10
16
 
@@ -141,7 +147,7 @@ export default defineConfig({
141
147
  });
142
148
  ```
143
149
 
144
- [Scripts and configuration](docs/scripts.md#configuration) lists every `defineConfig` field and covers package-level configuration and `devBin`.
150
+ [Scripts and configuration](docs/scripts.md#configuration) lists every `defineConfig` field, covers package-level configuration, and shows how to run a workspace bin from source.
145
151
 
146
152
  ## What nmr reports
147
153
 
@@ -304,7 +310,7 @@ rdy list --from npm:@williamthorsen/nmr # what nmr publishes
304
310
 
305
311
  ## Documentation
306
312
 
307
- - [Scripts and configuration](docs/scripts.md): Override tiers, script values, hooks, `defineConfig` fields, `devBin`, and the default commands
313
+ - [Scripts and configuration](docs/scripts.md): Override tiers, script values, hooks, `defineConfig` fields, running a workspace bin from source, and the default commands
308
314
  - [What nmr reports](docs/reporting.md): Verdicts, `--json`, output style, and verbosity
309
315
  - [Check-result cache](docs/check-cache.md): What is skipped, what a hit requires, and replaying, reading, and bypassing recorded runs
310
316
  - [Machine lock](docs/machine-lock.md): Which commands wait for one another, where the lock lives, and bypassing it
@@ -25,8 +25,6 @@ export declare function openGate(options: {
25
25
  }): TreeSnapshot | undefined;
26
26
  /**
27
27
  * Computes the key under which this invocation would be recorded, or `undefined` when the cache does not apply.
28
- * The cache does not apply to a `devBin` substitution: the substitute is built from a location that the tree id
29
- * does not describe, so a pass by it is not a pass by the command that the key names.
30
28
  */
31
29
  export declare function resolveCacheKey(options: {
32
30
  anchorDir: string;
@@ -36,7 +34,6 @@ export declare function resolveCacheKey(options: {
36
34
  monorepoRoot: string;
37
35
  snapshot: TreeSnapshot | undefined;
38
36
  stderr: Writable;
39
- substitution: string | undefined;
40
37
  }): string | undefined;
41
38
  /**
42
39
  * Runs the resolved steps behind the check-result cache: skips them when a recorded pass covers this
@@ -33,14 +33,10 @@ export function openGate(options) {
33
33
  return snapshot.snapshot;
34
34
  }
35
35
  export function resolveCacheKey(options) {
36
- const { env, snapshot, stderr, substitution } = options;
36
+ const { env, snapshot, stderr } = options;
37
37
  if (snapshot === undefined) {
38
38
  return undefined;
39
39
  }
40
- if (substitution !== undefined) {
41
- writeDebugNote(`gate disabled: devBin substituted \`${substitution}\``, env, stderr);
42
- return undefined;
43
- }
44
40
  const result = computeCacheKey({
45
41
  anchorDir: options.anchorDir,
46
42
  command: options.command,
@@ -8,7 +8,7 @@ import { findContainingPackageDir } from "../context.js";
8
8
  import { NMR_GLYPHS } from "../glyphs.js";
9
9
  import { reportClosing } from "../helpers/reportClosing.js";
10
10
  import { resolveChannel } from "../output-channel.js";
11
- import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
11
+ import { buildWorkspaceRegistry, resolveScript } from "../resolver.js";
12
12
  import { runCommand } from "../runner.js";
13
13
  import { renderChain } from "../steps.js";
14
14
  import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
@@ -67,13 +67,12 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
67
67
  cleanedCount++;
68
68
  continue;
69
69
  }
70
- const command = applyDevBin(resolvedCommand, config.devBin, monorepoRoot);
71
- const { exitCode } = await runCommand(command, packageDir, {
70
+ const { exitCode } = await runCommand(resolvedCommand, packageDir, {
72
71
  bin: NMR_CLEAN_BIN,
73
72
  channels: { stderr: resolveChannel(process.stderr, false), stdout: resolveChannel(process.stdout, false) },
74
73
  });
75
74
  if (exitCode !== 0) {
76
- throw new UserError(`\`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
75
+ throw new UserError(`\`${resolvedCommand}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
77
76
  }
78
77
  cleanedCount++;
79
78
  }
@@ -1,12 +1,11 @@
1
1
  import { isRecord, UserError } from '@williamthorsen/nmr-core';
2
2
  import { assertResolvableCheckCacheCommands } from "./config-check-cache.js";
3
- import { isStringRecord } from "./helpers/type-guards.js";
4
3
  import { findUnexpressibleToken } from "./steps.js";
5
4
  import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
6
5
  export const CONFIG_TIERS = {
7
6
  root: {
8
7
  label: 'a monorepo-root config',
9
- honoredKeys: ['checkCache', 'delegate', 'devBin', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
8
+ honoredKeys: ['checkCache', 'delegate', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
10
9
  elsewhere: "the package's own config",
11
10
  },
12
11
  workspace: {
@@ -44,9 +43,6 @@ export function validateConfig(value, configPath, baseDir) {
44
43
  config.checkCache = checkCache;
45
44
  if (value['delegate'] !== undefined)
46
45
  config.delegate = validateDelegateField(value['delegate'], configPath);
47
- const devBin = validateStringRecordField(value, 'devBin', configPath);
48
- if (devBin)
49
- config.devBin = devBin;
50
46
  const output = validateOutputField(value, configPath);
51
47
  if (output)
52
48
  config.output = output;
@@ -225,12 +221,3 @@ function validateScriptField(value, fieldName, configPath) {
225
221
  assertValidElements(scripts, fieldName, configPath);
226
222
  return scripts;
227
223
  }
228
- function validateStringRecordField(value, fieldName, configPath) {
229
- if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
230
- return undefined;
231
- }
232
- if (!isStringRecord(value[fieldName])) {
233
- throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string>`);
234
- }
235
- return value[fieldName];
236
- }
@@ -6,7 +6,7 @@ import type { NmrConfig } from './types.js';
6
6
  */
7
7
  export declare function loadConfig(baseDir: string): Promise<NmrConfig>;
8
8
  /**
9
- * Loads the monorepo-root `.config/nmr.config.ts`, the tier that feeds script resolution and `devBin`.
9
+ * Loads the monorepo-root `.config/nmr.config.ts`, the tier that feeds script resolution.
10
10
  * Returns an empty config if the file doesn't exist.
11
11
  *
12
12
  * Throws on a recognized key that this tier does not honor. `build` governs one package's compile, which reads the
@@ -24,7 +24,7 @@ export declare function assertNoSelfReference(options: {
24
24
  *
25
25
  * Ahead of the gate, so that a command that usually skips still reports its crossing. A `--log` reports
26
26
  * none, running nothing that could cross. Reads the resolved steps rather than the full chain: the line names
27
- * a declaration to edit, and a hook, a passthrough, and a `devBin` substitution are none.
27
+ * a declaration to edit, and neither a hook nor a passthrough is one.
28
28
  */
29
29
  export declare function reportNmrCrossing(options: {
30
30
  config: NmrConfig;
@@ -1,18 +1,6 @@
1
1
  import { type ScriptRegistry, type ScriptValue } from './resolve-scripts.js';
2
2
  import { type Step } from './steps.js';
3
3
  import type { NmrConfig } from './types.js';
4
- /**
5
- * Replaces the first token of a command with a `devBin` substitute.
6
- * Relative paths in the replacement are resolved from `monorepoRoot`.
7
- */
8
- export declare function applyDevBin(command: string, devBin: Record<string, string> | undefined, monorepoRoot: string): string;
9
- /**
10
- * Applies a `devBin` substitution to each opaque step, leaving nmr's own compositions alone.
11
- *
12
- * A structural step names a command that nmr resolves for itself, so substituting its first token would replace
13
- * the `nmr` that runs the composite rather than the leaf tool that `devBin` documents replacing.
14
- */
15
- export declare function applyDevBinToSteps(steps: readonly Step[], devBin: Record<string, string> | undefined, monorepoRoot: string): readonly Step[];
16
4
  /**
17
5
  * Where a resolved script was read from.
18
6
  *
@@ -1,44 +1,9 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
2
  import { hasErrnoCode, isRecord } from '@williamthorsen/nmr-core';
4
3
  import { parsePackageJson, readScriptRecord, resolvePackageJsonPath } from "./helpers/package-json.js";
5
4
  import { getDefaultRootScripts, getDefaultWorkspaceScripts, } from "./resolve-scripts.js";
6
5
  import { composeNmrStep, readSelfReference } from "./steps.js";
7
6
  import { isMonorepoRoot } from "./workspace.js";
8
- export function applyDevBin(command, devBin, monorepoRoot) {
9
- if (!devBin) {
10
- return command;
11
- }
12
- const spaceIndex = command.indexOf(' ');
13
- const firstToken = spaceIndex === -1 ? command : command.slice(0, spaceIndex);
14
- const restOfCommand = spaceIndex === -1 ? '' : command.slice(spaceIndex);
15
- const replacement = devBin[firstToken];
16
- if (replacement === undefined) {
17
- return command;
18
- }
19
- const resolvedReplacement = resolveReplacementPaths(replacement, monorepoRoot);
20
- return resolvedReplacement + restOfCommand;
21
- }
22
- export function applyDevBinToSteps(steps, devBin, monorepoRoot) {
23
- if (!devBin) {
24
- return steps;
25
- }
26
- return steps.map((step) => step.kind === 'opaque' ? { kind: 'opaque', command: applyDevBin(step.command, devBin, monorepoRoot) } : step);
27
- }
28
- function resolveReplacementPaths(replacement, monorepoRoot) {
29
- const tokens = replacement.split(' ');
30
- return tokens
31
- .map((token, index) => {
32
- if (index === 0)
33
- return token;
34
- if (token.startsWith('-'))
35
- return token;
36
- if (!token.includes('/'))
37
- return token;
38
- return path.resolve(monorepoRoot, token);
39
- })
40
- .join(' ');
41
- }
42
7
  export function expandScript(script, isWorkspaceRoot) {
43
8
  if (typeof script === 'string') {
44
9
  return [{ kind: 'opaque', command: script }];
@@ -2,7 +2,7 @@ import { type OutputStyle } from '@williamthorsen/nmr-core';
2
2
  import type { TreeSnapshot } from './check-cache.js';
3
3
  import { type Invocation, type PhaseResult, type RunPlan } from './invocation.js';
4
4
  import type { ScriptRegistry } from './resolve-scripts.js';
5
- import { type ResolvedScript } from './resolver.js';
5
+ import type { ResolvedScript } from './resolver.js';
6
6
  /**
7
7
  * Assembles the chain that a resolved script runs at this scope, the hooks wrapped around it included, and the key
8
8
  * under which a pass by that chain is recorded. Settles a no-op override and an argument that no step can take.
@@ -9,7 +9,6 @@ import { quoteForShell } from "./helpers/quoteForShell.js";
9
9
  import { wrapWithHooks } from "./hooks.js";
10
10
  import { USAGE_EXIT_CODE } from "./invocation.js";
11
11
  import { reportVerdict } from "./presentation.js";
12
- import { applyDevBinToSteps } from "./resolver.js";
13
12
  import { dropRecursiveSteps, renderChain } from "./steps.js";
14
13
  export function assembleChain(options) {
15
14
  const { invocation, registry, resolvedScript, snapshot } = options;
@@ -21,9 +20,7 @@ export function assembleChain(options) {
21
20
  reportVerdict({ command, scope: invocation.scope, outcome: 'no-op', reason: noOpReason }, stdout, invocation.format, styles.stdout);
22
21
  return { settled: true, result: { exitCode: 0 } };
23
22
  }
24
- const substitutedSteps = applyDevBinToSteps(runnableSteps, context.config.devBin, context.monorepoRoot);
25
- const substitutedCommand = renderChain(substitutedSteps);
26
- const bindResult = bindPassthrough(substitutedSteps, parsedArgs.passthrough, command);
23
+ const bindResult = bindPassthrough(runnableSteps, parsedArgs.passthrough, command);
27
24
  if (!bindResult.ok) {
28
25
  reportError(NMR_BIN, bindResult.error, stderr);
29
26
  return { settled: true, result: { exitCode: USAGE_EXIT_CODE } };
@@ -53,7 +50,6 @@ export function assembleChain(options) {
53
50
  monorepoRoot: context.monorepoRoot,
54
51
  snapshot,
55
52
  stderr,
56
- substitution: substitutedCommand === resolvedCommand ? undefined : substitutedCommand,
57
53
  });
58
54
  return { settled: false, value: { commandString: fullCommand, key, ownSteps: mainSteps, steps: fullSteps } };
59
55
  }
@@ -59,7 +59,6 @@ export interface NmrConfig {
59
59
  * config says. A delegation flag still sends a run elsewhere.
60
60
  */
61
61
  delegate?: false;
62
- devBin?: Record<string, string>;
63
62
  output?: OutputConfig;
64
63
  workspaceScripts?: Record<string, ScriptValue>;
65
64
  rootScripts?: Record<string, ScriptValue>;
@@ -63,7 +63,6 @@ The gate never wrongly skips. When it cannot be sure, it does nothing and the co
63
63
  - When the monorepo root is not the git toplevel.
64
64
  - When a changed path cannot be read, contains a newline or bytes that are not UTF-8, or is neither a file nor a symlink (an untracked nested repository, for instance).
65
65
  - When the repository does not have a pnpm install to fingerprint.
66
- - Under a `devBin` substitution, which runs a binary built from a location that the tree id does not describe.
67
66
  - For any invocation carrying arguments after the command name, and for every step below it: the arguments narrow what runs, and a step served from a recorded pass is a step that did not.
68
67
 
69
68
  `NMR_DEBUG=1` reports why a run did not skip and why the cache did not apply.
package/docs/scripts.md CHANGED
@@ -79,7 +79,7 @@ The line names the declaration to which it is an edit (the file, the field, and
79
79
 
80
80
  One kind of `package.json` entry is resolved in its own file: an entry whose whole value is `nmr <name>`, without flags, arguments, assignments, or a launcher, where `<name>` is a script in the same `package.json` that runs a command other than nmr and does not have a `:pre` or `:post` hook. Replace the entry's value with that command, and delete the script: `"build:pre": "nmr compile:ports"` beside `"compile:ports": "node scripts/compile-ports.ts"` becomes `"build:pre": "node scripts/compile-ports.ts"`. Keep the script when the registry also defines `<name>`, because the script then overrides nmr's own command. The script must come from the same file: Inlining a command that resolves from the registry would copy nmr's own command text into the package, where nmr's later changes do not reach it.
81
81
 
82
- nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, `|`, or a newline, past any leading `NAME=value` assignments, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. This line reports a boundary rather than enforcing a rule: It covers a step that nmr runs as written, and only the reporting of the nested run is degraded. The line is also partial in three ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), a launcher outside that set goes unreported, and a `devBin` substitution that introduces `nmr` itself is not reported, the line being an edit to a declaration rather than to a substitution.
82
+ nmr recognizes `nmr` in command position: at the start of a step, after an unquoted `&&`, `||`, `;`, `|`, or a newline, past any leading `NAME=value` assignments, and behind `npx`, `bunx`, or an `exec`, `dlx`, or `x` subcommand of `npm`, `pnpm`, `yarn`, or `bun`. This line reports a boundary rather than enforcing a rule: It covers a step that nmr runs as written, and only the reporting of the nested run is degraded. The line is also partial in two ways: a value-taking flag standing immediately before the program name hides it (`npx -p foo nmr`), and a launcher outside that set goes unreported.
83
83
 
84
84
  A [self-referential entry](#three-tier-override-system), in `package.json` or in `.config/nmr.config.ts`, is the shape that nmr cannot run as written, and it is rejected rather than reported. The same recognition finds it, so the same residuals apply: What goes unrecognized there re-enters nmr once, and the run then fails with an error naming the command and the directory that it re-entered.
85
85
 
@@ -125,7 +125,6 @@ A minimal `.config/nmr.config.ts` is in the [README](../README.md#configuration)
125
125
  | `delegate` | `false` | Keeps every check on the machine that has the tree, whatever `NMR_DELEGATE` or the [user config](delegate.md#the-user-config) says; a delegation flag still sends a run elsewhere (root config only) |
126
126
  | `workspaceScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the workspace registry (tier 2) |
127
127
  | `rootScripts` | `Record<string, ScriptValue>` | Scripts added or overridden in the root registry (tier 2) |
128
- | `devBin` | `Record<string, string>` | Map binary names to source-repo replacement commands |
129
128
  | `output` | `{ commandVerbosity?: CommandVerbosity; extraAgentEnvVars?: string[] }` | How loudly runs report command output; see [configuring](reporting.md#configuring) |
130
129
  | `selfHostedRunner` | `{ exempt: string }` | Why a qualifying repo keeps CI off the self-hosted runners; stops [nmr's readyup kit](../README.md#conformance-checks) from recommending `CI_RUNS_ON` (root config only). The reason is structural: a circular dependency, a platform or capacity the runners lack, or network reach. A missing tool is installed by a workflow step instead |
131
130
 
@@ -135,7 +134,7 @@ Each field belongs to exactly one tier, and nmr loads from a config only the fie
135
134
 
136
135
  ### Package-level configuration
137
136
 
138
- A package may have its own `.config/nmr.config.ts`, which `nmr-compile` reads from the package that it is compiling. This tier honors `build` alone; declaring `workspaceScripts`, `rootScripts`, or `devBin` there fails with a message naming them, rather than being quietly ignored: Those belong in the monorepo-root config, and a package that appeared to set them would otherwise build on settings that nothing applied. The monorepo-root config has the mirror restriction: `build` there fails the same way, since only a package's own config affects the compile.
137
+ A package may have its own `.config/nmr.config.ts`, which `nmr-compile` reads from the package that it is compiling. This tier honors `build` alone; declaring `workspaceScripts`, `rootScripts`, or any other root-config field there fails with a message naming them, rather than being quietly ignored: Those belong in the monorepo-root config, and a package that appeared to set them would otherwise build on settings that nothing applied. The monorepo-root config has the mirror restriction: `build` there fails the same way, since only a package's own config affects the compile.
139
138
 
140
139
  ```ts
141
140
  // packages/my-package/.config/nmr.config.ts
@@ -150,23 +149,29 @@ export default defineConfig({
150
149
 
151
150
  > **Building nmr from source:** A config file that has to load while nmr itself is being built (before its `dist` exists) must use the type-only form, `import type { NmrConfig } from '@williamthorsen/nmr/config'` with `satisfies NmrConfig`, which does not produce a runtime import. `defineConfig` is unavailable at that moment, because it resolves through nmr's build output. Every other config, including every consumer's, imports nmr from an installed tarball that contains `dist`, and can use `defineConfig` freely.
152
151
 
153
- ### `devBin`: source-repo binary substitution
152
+ ### Running a workspace bin from source
154
153
 
155
- During development of a CLI tool inside the monorepo, the published binary may not reflect the latest source changes. `devBin` maps a binary name to a replacement command that runs from source:
154
+ A script that runs a bin built in this monorepo runs its build output, which can lag the source or be missing before the first build. To run the source instead, override the script whose value runs the bin, and call `node` on the bin's source file: Node runs TypeScript directly.
155
+
156
+ A resolved script runs in the directory of its scope: A root script runs from the monorepo root, so a path relative to the root works there, and a workspace script runs in each package's directory, so the config builds an absolute path from `import.meta.dirname`, the config's own `.config` directory.
156
157
 
157
158
  ```ts
159
+ // .config/nmr.config.ts
160
+ import { defineConfig } from '@williamthorsen/nmr/config';
161
+
162
+ const myCli = `${import.meta.dirname}/../packages/my-cli/src/cli.ts`;
163
+
158
164
  export default defineConfig({
159
- devBin: {
160
- 'my-cli': 'tsx packages/my-cli/src/cli.ts',
165
+ rootScripts: {
166
+ 'build:post': 'node packages/my-cli/src/cli.ts compile',
167
+ },
168
+ workspaceScripts: {
169
+ lint: `node "${myCli}" lint`,
161
170
  },
162
171
  });
163
172
  ```
164
173
 
165
- When nmr resolves a command whose first token matches a `devBin` key, it replaces that token with the mapped command. Arguments are preserved. Relative paths in the replacement are resolved from the monorepo root.
166
-
167
- For example, if a workspace script resolves to `my-cli --verbose`, nmr rewrites it to `tsx /absolute/path/to/packages/my-cli/src/cli.ts --verbose`.
168
-
169
- > **Note:** Path resolution uses a heuristic: Any non-flag token containing `/` is treated as a relative path. This works well for typical dev-tool commands but may incorrectly resolve URL-like values or glob patterns. Flags using `--flag=value` syntax are not resolved; use the spaced form `--flag value` for paths that need resolution.
174
+ A package's own `package.json` script outranks the config (tier 3), so a package script that runs the bin is changed in that `package.json`.
170
175
 
171
176
  ## Default commands
172
177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for pnpm monorepos",
6
6
  "keywords": [
@@ -80,7 +80,7 @@
80
80
  "!docs/*.v*.md"
81
81
  ],
82
82
  "dependencies": {
83
- "@williamthorsen/nmr-core": "0.16.0",
83
+ "@williamthorsen/nmr-core": "0.16.1",
84
84
  "@williamthorsen/toolbelt.errors": "0.9.0",
85
85
  "@williamthorsen/toolbelt.filesystem": "0.10.0",
86
86
  "glob": "14.0.4",