@williamthorsen/nmr 0.15.0 → 0.16.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/README.md CHANGED
@@ -3,27 +3,19 @@
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.15.0 (2026-06-27)
6
+ ## Release notes — v0.16.0 (2026-06-30)
7
7
 
8
8
  ### 🎉 Features
9
9
 
10
- - Centralize the per-package build as an nmr-compile bin (#419)
10
+ - 🚨 **Breaking:** Auto-activate integration test variant from config presence (#448)
11
11
 
12
- Introduces `nmr-compile`, a single command shipped with `@williamthorsen/nmr` that compiles each workspace package and now backs the default build. Consuming repos can delete their own per-package build script and pick up future build fixes just by upgrading nmr. Repeated builds with unchanged source now reliably skip recompiling instead of occasionally rebuilding for no reason, and import aliases now resolve correctly in symlinked checkouts.
12
+ A package can now separate its integration tests from its standalone suite simply by including a `vitest.integration.config.ts` (alongside a `vitest.standalone.config.ts`). The `--int-test` flag that previously enabled this is removed — that config-file pairing is now the only way to activate the separation. In such a package, `test` and `test:coverage` run only the standalone suite and skip integration tests, while a new `test:all` runs both suites together. The separation now holds even when tests run across every package at once, so a full-workspace `test` run still keeps integration tests out of the default suite. Packages that previously hand-copied these test scripts no longer need to.
13
13
 
14
14
  ### 🐛 Bug fixes
15
15
 
16
- - Write the build cache only after a successful compile (#421)
16
+ - Prevent CLI output truncation when piped before exit (#446)
17
17
 
18
- Fixes an issue where a package compile that failed partway — from a crash, disk error, or transient build failure — could leave stale or incomplete output that the next build skipped over as unchanged. The next build now retries the failed compile, so recovering no longer requires a manual `nmr clean` or a source-file edit.
19
-
20
- - Stop sync-agent-files --check failing on version-only bumps (#424)
21
-
22
- Fixes an issue where `nmr sync-agent-files --check` failed after an nmr upgrade even when the managed agent guidance was identical. The check now passes whenever the guidance content is current, even if the version has changed.
23
-
24
- - Remove inapplicable bootstrap fallback from agent guidance (#425)
25
-
26
- Fixes an issue where the agent guidance bundled with nmr told coding agents to run a recovery step that exists only in the nmr repo itself.
18
+ Fixes an issue where large output from the `nmr` and `v11y` commands could be truncated when captured through a pipe — for example, by a CI job.
27
19
  <!-- /section:release-notes -->
28
20
 
29
21
  ## Installation
@@ -176,11 +168,12 @@ These scripts are available out of the box. Repo-wide config (tier 2) and per-pa
176
168
 
177
169
  #### Integration test variant
178
170
 
179
- Packages with a `vitest.integration.config.ts` file get different test commands. Use the `--int-test` flag to select this variant.
171
+ A package gets this variant automatically when it contains a `vitest.integration.config.ts` file: `test` and `test:coverage` run the standalone (non-integration) suite, and integration tests run only when invoked explicitly. Activation is detected per package, so it applies under the recursive `nmr ci` fan-out. Opting in requires **both** `vitest.integration.config.ts` and `vitest.standalone.config.ts`, since the standalone scripts reference the latter.
180
172
 
181
173
  | Command | Runs |
182
174
  | ------------------ | ------------------------------------------------------------------ |
183
175
  | `test` | `pnpm exec vitest --config=vitest.standalone.config.ts` |
176
+ | `test:all` | `pnpm exec vitest` |
184
177
  | `test:coverage` | `pnpm exec vitest --config=vitest.standalone.config.ts --coverage` |
185
178
  | `test:integration` | `pnpm exec vitest --config=vitest.integration.config.ts` |
186
179
  | `test:watch` | `pnpm exec vitest --config=vitest.standalone.config.ts --watch` |
@@ -294,7 +287,6 @@ nmr [flags] <command> [args...]
294
287
  | `-q, --quiet` | Suppress info messages; show full output on failure | — |
295
288
  | `-?, --help` | Show available commands | — |
296
289
  | `-V, --version` | Show version number | — |
297
- | `--int-test` | Use integration test scripts | — |
298
290
 
299
291
  ### Examples
300
292
 
package/dist/esm/.cache CHANGED
@@ -1 +1 @@
1
- d2f01850cfe4f42bf00cb8b8eaf3d3fab438916c419957842d927d22fb9d67cf
1
+ 47af840104236371ef384e3368ae463ca3d05d525fcffd96134d4c9c11257796
@@ -2,6 +2,7 @@ import { buildPackage } from "./commands/build.js";
2
2
  try {
3
3
  await buildPackage(process.cwd());
4
4
  } catch (error) {
5
- console.error(error instanceof Error ? error.message : String(error));
6
- process.exit(1);
5
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
6
+ `);
7
+ process.exitCode = 1;
7
8
  }
@@ -1,31 +1,12 @@
1
+ import { parseArgsOrExit, reportError } from "@williamthorsen/nmr-core";
1
2
  import { DEFAULT_HOOK, ensurePrepublishHooks } from "./commands/ensure-prepublish-hooks.js";
2
3
  import { findMonorepoRoot } from "./context.js";
3
- let fix = false;
4
- let dryRun = false;
5
- let command;
6
- const args = process.argv.slice(2);
7
- for (let i = 0; i < args.length; i++) {
8
- const arg = args[i];
9
- switch (arg) {
10
- case "--fix":
11
- fix = true;
12
- break;
13
- case "--dry-run":
14
- dryRun = true;
15
- break;
16
- case "--command":
17
- i++;
18
- command = args[i];
19
- if (!command) {
20
- console.error("--command requires a value");
21
- process.exit(1);
22
- }
23
- break;
24
- default:
25
- console.error(`Unknown argument: ${arg}`);
26
- process.exit(1);
27
- }
28
- }
4
+ const flagSchema = {
5
+ fix: { long: "--fix", type: "boolean" },
6
+ dryRun: { long: "--dry-run", type: "boolean" },
7
+ command: { long: "--command", type: "string" }
8
+ };
9
+ const { fix, dryRun, command } = parseArgsOrExit(process.argv.slice(2), flagSchema).flags;
29
10
  try {
30
11
  const monorepoRoot = findMonorepoRoot();
31
12
  const hookCommand = command ?? DEFAULT_HOOK;
@@ -52,11 +33,12 @@ try {
52
33
  }
53
34
  if (result.hasFailures) {
54
35
  const missing = result.packages.filter((p) => p.action === "missing").length;
55
- console.error(`
56
- ${missing} package(s) missing prepublishOnly. Use --fix to add it.`);
57
- process.exit(1);
36
+ process.stderr.write(`
37
+ ${missing} package(s) missing prepublishOnly. Use --fix to add it.
38
+ `);
39
+ process.exitCode = 1;
58
40
  }
59
41
  } catch (error) {
60
- console.error(error instanceof Error ? error.message : String(error));
61
- process.exit(1);
42
+ reportError(error instanceof Error ? error.message : String(error));
43
+ process.exitCode = 1;
62
44
  }
@@ -1,9 +1,10 @@
1
+ import { reportError } from "@williamthorsen/nmr-core";
1
2
  import { reportOverrides } from "./commands/report-overrides.js";
2
3
  import { findMonorepoRoot } from "./context.js";
3
4
  try {
4
5
  const monorepoRoot = findMonorepoRoot();
5
6
  reportOverrides(monorepoRoot);
6
7
  } catch (error) {
7
- console.error(error instanceof Error ? error.message : String(error));
8
- process.exit(1);
8
+ reportError(error instanceof Error ? error.message : String(error));
9
+ process.exitCode = 1;
9
10
  }
@@ -1,24 +1,16 @@
1
- import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
1
+ import { parseArgsOrExit, reportError } from "@williamthorsen/nmr-core";
2
2
  import { check, sync } from "./commands/sync-agent-files.js";
3
3
  import { findMonorepoRoot } from "./context.js";
4
4
  const flagSchema = {
5
5
  check: { long: "--check", type: "boolean" }
6
6
  };
7
- const { flags } = parseArgsOrExit(process.argv.slice(2));
7
+ const { flags } = parseArgsOrExit(process.argv.slice(2), flagSchema);
8
8
  try {
9
9
  runCommand(flags.check);
10
10
  } catch (error) {
11
- console.error(error instanceof Error ? error.message : String(error));
11
+ reportError(error instanceof Error ? error.message : String(error));
12
12
  process.exit(1);
13
13
  }
14
- function parseArgsOrExit(argv) {
15
- try {
16
- return parseArgs(argv, flagSchema);
17
- } catch (error) {
18
- console.error(`Error: ${translateParseError(error)}`);
19
- process.exit(1);
20
- }
21
- }
22
14
  function runCommand(checkOnly) {
23
15
  const monorepoRoot = findMonorepoRoot();
24
16
  if (!checkOnly) {
@@ -33,6 +25,7 @@ function runCommand(checkOnly) {
33
25
  console.info("\u2713 .agents/nmr/AGENTS.md is in sync");
34
26
  process.exit(0);
35
27
  }
36
- console.error(result.reason);
28
+ process.stderr.write(`${result.reason}
29
+ `);
37
30
  process.exit(1);
38
31
  }
@@ -1,9 +1,10 @@
1
+ import { reportError } from "@williamthorsen/nmr-core";
1
2
  import { syncPnpmVersion } from "./commands/sync-pnpm-version.js";
2
3
  import { findMonorepoRoot } from "./context.js";
3
4
  try {
4
5
  const monorepoRoot = findMonorepoRoot();
5
6
  syncPnpmVersion(monorepoRoot);
6
7
  } catch (error) {
7
- console.error(error instanceof Error ? error.message : String(error));
8
- process.exit(1);
8
+ reportError(error instanceof Error ? error.message : String(error));
9
+ process.exitCode = 1;
9
10
  }
package/dist/esm/cli.js CHANGED
@@ -8,9 +8,9 @@ try {
8
8
  stdout: process.stdout,
9
9
  stderr: process.stderr
10
10
  });
11
- process.exit(exitCode);
11
+ process.exitCode = exitCode;
12
12
  } catch (error) {
13
13
  process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}
14
14
  `);
15
- process.exit(1);
15
+ process.exitCode = 1;
16
16
  }
@@ -17,6 +17,7 @@ const commonWorkspaceScripts = {
17
17
  };
18
18
  const integrationTestScripts = {
19
19
  test: "pnpm exec vitest --config=vitest.standalone.config.ts",
20
+ "test:all": "pnpm exec vitest",
20
21
  "test:coverage": "pnpm exec vitest --config=vitest.standalone.config.ts --coverage",
21
22
  "test:integration": "pnpm exec vitest --config=vitest.integration.config.ts",
22
23
  "test:watch": "pnpm exec vitest --config=vitest.standalone.config.ts --watch"
package/dist/esm/help.js CHANGED
@@ -16,7 +16,6 @@ function generateHelp(config, packageDir, useRoot) {
16
16
  " -w, --workspace-root Run root command regardless of cwd",
17
17
  " -q, --quiet Suppress output on success; show full output on failure",
18
18
  " -?, --help Show this help",
19
- " --int-test Use integration test scripts",
20
19
  "",
21
20
  "Workspace commands:"
22
21
  ];
@@ -8,6 +8,7 @@ export interface ResolvedScript {
8
8
  export declare function expandScript(script: ScriptValue, workspaceRoot: boolean): string;
9
9
  export declare function describeScript(script: ScriptValue): string;
10
10
  export declare function readPackageJsonScripts(packageDir: string): Record<string, string> | undefined;
11
+ export declare function hasIntegrationTestConfig(packageDir: string): boolean;
11
12
  export declare function buildWorkspaceRegistry(config: NmrConfig, useIntTests: boolean): ScriptRegistry;
12
13
  export declare function buildRootRegistry(config: NmrConfig): ScriptRegistry;
13
14
  export declare function isSelfReferential(script: string, commandName: string): boolean;
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { isObject } from "./helpers/type-guards.js";
4
4
  import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
@@ -54,6 +54,9 @@ function readPackageJsonScripts(packageDir) {
54
54
  throw error;
55
55
  }
56
56
  }
57
+ function hasIntegrationTestConfig(packageDir) {
58
+ return existsSync(path.join(packageDir, "vitest.integration.config.ts"));
59
+ }
57
60
  function buildWorkspaceRegistry(config, useIntTests) {
58
61
  return {
59
62
  ...getDefaultWorkspaceScripts(useIntTests),
@@ -92,6 +95,7 @@ export {
92
95
  buildWorkspaceRegistry,
93
96
  describeScript,
94
97
  expandScript,
98
+ hasIntegrationTestConfig,
95
99
  isSelfReferential,
96
100
  readPackageJsonScripts,
97
101
  resolveScript
@@ -1,17 +1,22 @@
1
1
  import path from "node:path";
2
- import { readPackageVersion } from "@williamthorsen/nmr-core";
2
+ import { readPackageVersion, reportError } from "@williamthorsen/nmr-core";
3
3
  import { resolveContext } from "./context.js";
4
4
  import { generateHelp } from "./help.js";
5
5
  import { isHookName } from "./helpers/hook-name.js";
6
- import { applyDevBin, buildRootRegistry, buildWorkspaceRegistry, resolveScript } from "./resolver.js";
6
+ import {
7
+ applyDevBin,
8
+ buildRootRegistry,
9
+ buildWorkspaceRegistry,
10
+ hasIntegrationTestConfig,
11
+ resolveScript
12
+ } from "./resolver.js";
7
13
  import { runCommand } from "./runner.js";
8
14
  const VERSION = readPackageVersion(import.meta.url);
9
15
  async function runCli(options) {
10
16
  const { args, cwd, env, stdout, stderr } = options;
11
17
  const parseResult = parseArgs(args);
12
18
  if (!parseResult.ok) {
13
- stderr.write(`${parseResult.error}
14
- `);
19
+ reportError(parseResult.error, stderr);
15
20
  return { exitCode: 1 };
16
21
  }
17
22
  const { parsed } = parseResult;
@@ -42,14 +47,13 @@ async function runCli(options) {
42
47
  const exitCode2 = runCommand(delegateCmd, context.monorepoRoot, { ...runOptions, env: childEnv });
43
48
  return { exitCode: exitCode2 };
44
49
  }
45
- const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config, parsed.intTest);
50
+ const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config, hasIntegrationTestConfig(packageDir));
46
51
  const resolved = resolveScript(command, registry, packageDir, parsed.workspaceRoot);
47
52
  if (!resolved) {
48
53
  if (env.NMR_RUN_IF_PRESENT === "1") {
49
54
  return { exitCode: 0 };
50
55
  }
51
- stderr.write(`Unknown command: ${command}
52
- `);
56
+ reportError(`Unknown command: ${command}`, stderr);
53
57
  return { exitCode: 1 };
54
58
  }
55
59
  const skipExitCode = handleSkipMessage(resolved.command, packageDir, parsed.quiet, stdout);
@@ -74,7 +78,6 @@ function parseArgs(args) {
74
78
  workspaceRoot: false,
75
79
  help: false,
76
80
  version: false,
77
- intTest: false,
78
81
  passthrough: []
79
82
  };
80
83
  let i = 0;
@@ -85,7 +88,7 @@ function parseArgs(args) {
85
88
  i++;
86
89
  const filterValue = args[i];
87
90
  if (filterValue === void 0) {
88
- return { ok: false, error: "Error: -F/--filter requires a pattern argument" };
91
+ return { ok: false, error: "-F/--filter requires a pattern argument" };
89
92
  }
90
93
  parsed.filter = filterValue;
91
94
  i++;
@@ -116,11 +119,6 @@ function parseArgs(args) {
116
119
  i++;
117
120
  continue;
118
121
  }
119
- if (arg === "--int-test") {
120
- parsed.intTest = true;
121
- i++;
122
- continue;
123
- }
124
122
  parsed.command = arg;
125
123
  parsed.passthrough = args.slice(i + 1);
126
124
  break;
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import process from "node:process";
3
+ import { reportError } from "@williamthorsen/nmr-core";
3
4
  function runCommand(command, cwd, options) {
4
5
  const quiet = options?.quiet === true;
5
6
  const stdout = options?.stdout ?? process.stdout;
@@ -14,8 +15,7 @@ function runCommand(command, cwd, options) {
14
15
  env
15
16
  });
16
17
  if (result.error) {
17
- stderr.write(`${result.error.message}
18
- `);
18
+ reportError(result.error.message, stderr);
19
19
  return 1;
20
20
  }
21
21
  if (quiet) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@williamthorsen/nmr",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "description": "Context-aware script runner for PNPM monorepos",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  "jiti": "2.7.0",
46
46
  "yaml": "2.9.0",
47
47
  "zod": "4.4.3",
48
- "@williamthorsen/nmr-core": "0.4.0"
48
+ "@williamthorsen/nmr-core": "0.5.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@williamthorsen/toolbelt.strings": "3.1.4"