@williamthorsen/nmr 0.37.0 → 0.39.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.
Files changed (82) hide show
  1. package/.meta/changelog.json +114 -0
  2. package/.readyup/kits/default.js +92 -87
  3. package/.readyup/manifest.json +8 -8
  4. package/CHANGELOG.md +89 -0
  5. package/README.md +37 -28
  6. package/agents/guidance/rulebooks/nmr.md +2 -2
  7. package/dist/esm/check-cache.d.ts +4 -3
  8. package/dist/esm/check-cache.js +28 -28
  9. package/dist/esm/cli-build.js +3 -0
  10. package/dist/esm/cli-clean.js +3 -1
  11. package/dist/esm/cli-ensure-prepublish-hooks.js +7 -5
  12. package/dist/esm/cli-report-catalog.js +2 -1
  13. package/dist/esm/cli-report-overrides.js +2 -1
  14. package/dist/esm/commands/build-output.d.ts +2 -2
  15. package/dist/esm/commands/build-output.js +9 -9
  16. package/dist/esm/commands/build.d.ts +6 -1
  17. package/dist/esm/commands/build.js +73 -72
  18. package/dist/esm/commands/clean.d.ts +3 -2
  19. package/dist/esm/commands/clean.js +25 -24
  20. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -4
  21. package/dist/esm/commands/ensure-prepublish-hooks.js +44 -42
  22. package/dist/esm/commands/fmt.js +33 -33
  23. package/dist/esm/commands/report-catalog.d.ts +2 -1
  24. package/dist/esm/commands/report-catalog.js +14 -11
  25. package/dist/esm/commands/report-overrides.d.ts +2 -1
  26. package/dist/esm/commands/report-overrides.js +11 -10
  27. package/dist/esm/config.js +39 -29
  28. package/dist/esm/context.js +8 -8
  29. package/dist/esm/default-scripts.d.ts +2 -2
  30. package/dist/esm/default-scripts.js +5 -5
  31. package/dist/esm/glyphs.d.ts +9 -0
  32. package/dist/esm/glyphs.js +9 -0
  33. package/dist/esm/help.d.ts +2 -2
  34. package/dist/esm/help.js +36 -30
  35. package/dist/esm/helpers/clampToBytes.js +3 -3
  36. package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
  37. package/dist/esm/helpers/createBoundedBuffer.js +2 -2
  38. package/dist/esm/helpers/duration.d.ts +2 -2
  39. package/dist/esm/helpers/duration.js +7 -7
  40. package/dist/esm/helpers/filter-selection.js +12 -12
  41. package/dist/esm/helpers/findClosestName.js +15 -15
  42. package/dist/esm/helpers/package-json.d.ts +2 -2
  43. package/dist/esm/helpers/package-json.js +26 -25
  44. package/dist/esm/helpers/transcript.d.ts +2 -2
  45. package/dist/esm/helpers/transcript.js +14 -14
  46. package/dist/esm/output-style.d.ts +41 -0
  47. package/dist/esm/output-style.js +17 -0
  48. package/dist/esm/prettier.js +5 -5
  49. package/dist/esm/recording.d.ts +8 -5
  50. package/dist/esm/recording.js +27 -17
  51. package/dist/esm/report-format.d.ts +1 -1
  52. package/dist/esm/report-format.js +7 -7
  53. package/dist/esm/resolveBinStyles.d.ts +12 -0
  54. package/dist/esm/resolveBinStyles.js +16 -0
  55. package/dist/esm/resolver.d.ts +2 -2
  56. package/dist/esm/resolver.js +16 -16
  57. package/dist/esm/runCli.js +309 -221
  58. package/dist/esm/runner.d.ts +1 -1
  59. package/dist/esm/runner.js +18 -18
  60. package/dist/esm/steps.d.ts +17 -5
  61. package/dist/esm/steps.js +37 -28
  62. package/dist/esm/tests.d.ts +7 -7
  63. package/dist/esm/tests.js +20 -8
  64. package/dist/esm/tiers.d.ts +1 -1
  65. package/dist/esm/tiers.js +8 -8
  66. package/dist/esm/verbosity.d.ts +1 -1
  67. package/dist/esm/verbosity.js +7 -7
  68. package/dist/esm/verdict.d.ts +15 -4
  69. package/dist/esm/verdict.js +78 -59
  70. package/dist/esm/vitest-source-resolution.js +48 -46
  71. package/dist/esm/vitest.d.ts +2 -2
  72. package/dist/esm/vitest.js +74 -25
  73. package/dist/esm/workspace.d.ts +2 -18
  74. package/dist/esm/workspace.js +9 -67
  75. package/docs/check-cache.md +5 -5
  76. package/docs/reporting.md +20 -2
  77. package/docs/scripts.md +2 -2
  78. package/docs/vitest.md +6 -6
  79. package/docs/workspace.md +24 -1
  80. package/package.json +9 -10
  81. package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
  82. package/dist/esm/helpers/workspace-patterns.js +0 -29
package/dist/esm/help.js CHANGED
@@ -1,35 +1,41 @@
1
1
  import { isHookName } from "./helpers/hook-name.js";
2
+ import { OUTPUT_STYLE_ENV_VAR, OUTPUT_STYLE_FLAG } from "./output-style.js";
2
3
  import { buildRootRegistry, buildWorkspaceRegistry, describeScript, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
3
- export function generateHelp(config, packageDir, useRoot) {
4
+ export function generateHelp(config, packageDir, shouldUseRoot) {
4
5
  const lines = [
5
6
  'Usage: nmr [flags] <command> [args...]',
6
7
  '',
7
8
  'Flags:',
8
- ' -F, --filter <pattern> Run command in packages whose manifest name matches',
9
- ' -R, --recursive Run command in all packages',
10
- ' -w, --workspace-root Use root scripts, running at the monorepo root',
11
- " -q, --quiet Suppress command output, keeping nmr's verdicts",
12
- ' --json Report one JSON object per command, withholding command output',
13
- ' --log Print the recorded run instead of running (up to 256 KiB)',
14
- ' --no-cache Run even if this tree already passed; record the result',
15
- ' -?, --help Show this help',
16
- ' -V, --version Show version number',
9
+ ' -F, --filter <pattern> Run command in packages whose manifest name matches',
10
+ ' -R, --recursive Run command in all packages',
11
+ ' -w, --workspace-root Use root scripts, running at the monorepo root',
12
+ " -q, --quiet Suppress command output, keeping nmr's verdicts",
13
+ ' --json Report one JSON object per command, withholding command output',
14
+ ' --log Print the recorded run instead of running (up to 256 KiB)',
15
+ ' --no-cache Run even if this tree already passed; record the result',
16
+ ` ${OUTPUT_STYLE_FLAG} <style> Output style: auto (default), plain, or rich`,
17
+ ' -?, --help Show this help',
18
+ ' -V, --version Show version number',
19
+ '',
20
+ 'Environment:',
21
+ ` ${OUTPUT_STYLE_ENV_VAR} Output style: auto (default), plain, or rich. auto prints`,
22
+ ' plain, without emoji, when CI is set or the stream is not a terminal.',
17
23
  '',
18
24
  'Workspace commands:',
19
25
  ];
20
26
  const overrides = packageDir === undefined ? {} : collectOverrides(packageDir);
21
27
  let hadOverride = false;
22
28
  const workspaceRegistry = filterHooks(buildWorkspaceRegistry(config));
23
- const workspaceMarked = !useRoot ? applyOverrides(workspaceRegistry, overrides) : new Set();
24
- if (workspaceMarked.size > 0)
29
+ const workspaceMarkedNames = !shouldUseRoot ? applyOverrides(workspaceRegistry, overrides) : new Set();
30
+ if (workspaceMarkedNames.size > 0)
25
31
  hadOverride = true;
26
- formatRegistry(workspaceRegistry, workspaceMarked, lines);
32
+ formatRegistry(workspaceRegistry, workspaceMarkedNames, lines);
27
33
  lines.push('', 'Root commands:');
28
34
  const rootRegistry = filterHooks(buildRootRegistry(config));
29
- const rootMarked = useRoot ? applyOverrides(rootRegistry, overrides) : new Set();
30
- if (rootMarked.size > 0)
35
+ const rootMarkedNames = shouldUseRoot ? applyOverrides(rootRegistry, overrides) : new Set();
36
+ if (rootMarkedNames.size > 0)
31
37
  hadOverride = true;
32
- formatRegistry(rootRegistry, rootMarked, lines);
38
+ formatRegistry(rootRegistry, rootMarkedNames, lines);
33
39
  if (hadOverride) {
34
40
  lines.push('', '* Overridden by package.json');
35
41
  }
@@ -39,43 +45,43 @@ function collectOverrides(packageDir) {
39
45
  const scripts = readPackageJsonScripts(packageDir);
40
46
  if (!scripts)
41
47
  return {};
42
- const filtered = {};
48
+ const candidateOverrides = {};
43
49
  for (const [name, value] of Object.entries(scripts)) {
44
50
  if (isHookName(name))
45
51
  continue;
46
52
  if (isSelfReferential(value, name, packageDir))
47
53
  continue;
48
- filtered[name] = value;
54
+ candidateOverrides[name] = value;
49
55
  }
50
- return filtered;
56
+ return candidateOverrides;
51
57
  }
52
58
  function applyOverrides(registry, overrides) {
53
- const marked = new Set();
59
+ const markedNames = new Set();
54
60
  for (const [name, value] of Object.entries(overrides)) {
55
61
  if (!Object.hasOwn(registry, name)) {
56
62
  continue;
57
63
  }
58
64
  registry[name] = value;
59
- marked.add(name);
65
+ markedNames.add(name);
60
66
  }
61
- return marked;
67
+ return markedNames;
62
68
  }
63
69
  function filterHooks(registry) {
64
- const filtered = {};
70
+ const hooklessRegistry = {};
65
71
  for (const [key, value] of Object.entries(registry)) {
66
72
  if (!isHookName(key))
67
- filtered[key] = value;
73
+ hooklessRegistry[key] = value;
68
74
  }
69
- return filtered;
75
+ return hooklessRegistry;
70
76
  }
71
- function formatRegistry(registry, marked, lines) {
77
+ function formatRegistry(registry, markedNames, lines) {
72
78
  const keys = Object.keys(registry);
73
79
  if (keys.length === 0)
74
80
  return;
75
- const maxKeyLen = Math.max(...keys.map((k) => k.length));
76
- const pad = Math.max(maxKeyLen + 1 + 2, 20);
81
+ const maxKeyLength = Math.max(...keys.map((k) => k.length));
82
+ const padWidth = Math.max(maxKeyLength + 1 + 2, 20);
77
83
  for (const [key, value] of Object.entries(registry)) {
78
- const marker = marked.has(key) ? '*' : ' ';
79
- lines.push(` ${(key + marker).padEnd(pad)} ${describeScript(value)}`);
84
+ const marker = markedNames.has(key) ? '*' : ' ';
85
+ lines.push(` ${(key + marker).padEnd(padWidth)} ${describeScript(value)}`);
80
86
  }
81
87
  }
@@ -7,14 +7,14 @@ export function clampToBytes(value, budgetBytes) {
7
7
  if (budget < 0) {
8
8
  return '';
9
9
  }
10
- let kept = '';
10
+ let keptText = '';
11
11
  let bytes = 0;
12
12
  for (const character of value) {
13
13
  const size = Buffer.byteLength(character);
14
14
  if (bytes + size > budget)
15
15
  break;
16
- kept += character;
16
+ keptText += character;
17
17
  bytes += size;
18
18
  }
19
- return `${kept}${TRUNCATION_MARK}`;
19
+ return `${keptText}${TRUNCATION_MARK}`;
20
20
  }
@@ -6,7 +6,7 @@ export interface BoundedBuffer {
6
6
  /** Retains a chunk, dropping bytes from the middle once both ends are full. */
7
7
  append: (chunk: Buffer) => void;
8
8
  /** Renders the retained bytes, with a marker naming the dropped count standing in for what was dropped. */
9
- toBuffer: () => Buffer;
9
+ readBuffer: () => Buffer;
10
10
  }
11
11
  /**
12
12
  * Creates an accumulator that retains the first and last bytes it is fed and drops the middle.
@@ -40,7 +40,7 @@ export function createBoundedBuffer(options = {}) {
40
40
  tailOffset = (tailOffset + chunk.length) % tailLimitBytes;
41
41
  tailBytes = Math.min(tailLimitBytes, tailBytes + chunk.length);
42
42
  }
43
- function toBuffer() {
43
+ function readBuffer() {
44
44
  const headBuffer = Buffer.concat(headChunks);
45
45
  if (tailRing === undefined) {
46
46
  return headBuffer;
@@ -53,5 +53,5 @@ export function createBoundedBuffer(options = {}) {
53
53
  }
54
54
  return Buffer.concat([headBuffer, Buffer.from(formatElisionMarker(elidedBytes)), tailBuffer]);
55
55
  }
56
- return { append, toBuffer };
56
+ return { append, readBuffer };
57
57
  }
@@ -5,7 +5,7 @@
5
5
  * `1m 30s`, where rounding would claim `2m`. Below a minute the tenth is kept, since that is the scale at which
6
6
  * one check's runtime differs from another's; a whole number sheds the `.0` rather than reading `12.0s`.
7
7
  */
8
- export declare function formatDuration(milliseconds: number): string;
8
+ export declare function formatDuration(durationMs: number): string;
9
9
  /**
10
10
  * Renders the clause a skip spends on the time it saved, or `undefined` when there is no saving worth naming.
11
11
  * A skip that can say what it saved earns the clause; one that cannot says nothing, and the caller drops it
@@ -14,4 +14,4 @@ export declare function formatDuration(milliseconds: number): string;
14
14
  * Guards its own input rather than trusting the caller's, because the clause is shared: a non-finite duration
15
15
  * compares false against the threshold, and would reach a reader as `saved ~NaNs`.
16
16
  */
17
- export declare function formatSaving(milliseconds: number): string | undefined;
17
+ export declare function formatSaving(durationMs: number): string | undefined;
@@ -1,5 +1,5 @@
1
- export function formatDuration(milliseconds) {
2
- const total = Math.max(0, milliseconds);
1
+ export function formatDuration(durationMs) {
2
+ const total = Math.max(0, durationMs);
3
3
  if (total < MILLISECONDS_PER_MINUTE) {
4
4
  return `${truncateToTenth(total)}s`;
5
5
  }
@@ -10,11 +10,11 @@ export function formatDuration(milliseconds) {
10
10
  }
11
11
  return joinUnits(`${Math.floor(totalMinutes / MINUTES_PER_HOUR)}h`, totalMinutes % MINUTES_PER_HOUR, 'm');
12
12
  }
13
- export function formatSaving(milliseconds) {
14
- if (!Number.isFinite(milliseconds) || milliseconds < MILLISECONDS_PER_SECOND) {
13
+ export function formatSaving(durationMs) {
14
+ if (!Number.isFinite(durationMs) || durationMs < MILLISECONDS_PER_SECOND) {
15
15
  return undefined;
16
16
  }
17
- return `saved ~${formatDuration(milliseconds)}`;
17
+ return `saved ~${formatDuration(durationMs)}`;
18
18
  }
19
19
  const MILLISECONDS_PER_MINUTE = 60_000;
20
20
  const MILLISECONDS_PER_SECOND = 1_000;
@@ -23,6 +23,6 @@ const SECONDS_PER_MINUTE = 60;
23
23
  function joinUnits(major, minor, minorUnit) {
24
24
  return minor === 0 ? major : `${major} ${minor}${minorUnit}`;
25
25
  }
26
- function truncateToTenth(milliseconds) {
27
- return Math.floor(milliseconds / 100) / 10;
26
+ function truncateToTenth(durationMs) {
27
+ return Math.floor(durationMs / 100) / 10;
28
28
  }
@@ -5,31 +5,31 @@ import process from 'node:process';
5
5
  import { isObject } from "./type-guards.js";
6
6
  const SELECTION_MARKER = 'nmr-selected-scope';
7
7
  export function readFilterSelection(pattern, monorepoRoot) {
8
- const listing = spawnSync('pnpm', ['ls', '--filter', pattern, '--depth', '-1', '--json'], {
8
+ const listingRun = spawnSync('pnpm', ['ls', '--filter', pattern, '--depth', '-1', '--json'], {
9
9
  cwd: monorepoRoot,
10
10
  encoding: 'utf8',
11
11
  });
12
- const reading = interpretSelectionProbe({ error: listing.error, status: listing.status, stdout: listing.stdout }, monorepoRoot);
13
- return reading === 'root-only' ? readDelegateSelection(pattern, monorepoRoot) : reading;
12
+ const probeReading = interpretSelectionProbe({ error: listingRun.error, status: listingRun.status, stdout: listingRun.stdout }, monorepoRoot);
13
+ return probeReading === 'root-only' ? readDelegateSelection(pattern, monorepoRoot) : probeReading;
14
14
  }
15
15
  export function interpretSelectionProbe(probe, monorepoRoot) {
16
16
  if (probe.error !== undefined || probe.status !== 0) {
17
17
  return 'unresolved';
18
18
  }
19
- let parsed;
19
+ let parsedListing;
20
20
  try {
21
- parsed = JSON.parse(probe.stdout);
21
+ parsedListing = JSON.parse(probe.stdout);
22
22
  }
23
23
  catch {
24
24
  return 'unresolved';
25
25
  }
26
- if (!Array.isArray(parsed)) {
26
+ if (!Array.isArray(parsedListing)) {
27
27
  return 'unresolved';
28
28
  }
29
- if (parsed.length === 0) {
29
+ if (parsedListing.length === 0) {
30
30
  return 'empty';
31
31
  }
32
- const packageCount = parsed.filter((entry) => !isRootEntry(entry, monorepoRoot)).length;
32
+ const packageCount = parsedListing.filter((entry) => !isRootEntry(entry, monorepoRoot)).length;
33
33
  if (packageCount === 0) {
34
34
  return 'root-only';
35
35
  }
@@ -45,9 +45,9 @@ function isRootEntry(entry, monorepoRoot) {
45
45
  if (!isObject(entry) || typeof entry['path'] !== 'string') {
46
46
  return false;
47
47
  }
48
- return readRealPath(entry['path']) === readRealPath(monorepoRoot);
48
+ return resolveRealPath(entry['path']) === resolveRealPath(monorepoRoot);
49
49
  }
50
- function readRealPath(target) {
50
+ function resolveRealPath(target) {
51
51
  try {
52
52
  return realpathSync(target);
53
53
  }
@@ -56,6 +56,6 @@ function readRealPath(target) {
56
56
  }
57
57
  }
58
58
  function readDelegateSelection(pattern, monorepoRoot) {
59
- const probe = spawnSync('pnpm', ['--filter', pattern, 'exec', process.execPath, '--eval', `process.stdout.write('${SELECTION_MARKER}')`], { cwd: monorepoRoot, encoding: 'utf8' });
60
- return interpretDelegateProbe({ error: probe.error, status: probe.status, stdout: probe.stdout });
59
+ const probeRun = spawnSync('pnpm', ['--filter', pattern, 'exec', process.execPath, '--eval', `process.stdout.write('${SELECTION_MARKER}')`], { cwd: monorepoRoot, encoding: 'utf8' });
60
+ return interpretDelegateProbe({ error: probeRun.error, status: probeRun.status, stdout: probeRun.stdout });
61
61
  }
@@ -1,33 +1,33 @@
1
1
  export function findClosestName(name, candidates) {
2
2
  const ceiling = Math.max(1, Math.floor(name.length / 4));
3
- let closest;
4
- let shortest = Infinity;
3
+ let closestName;
4
+ let shortestDistance = Infinity;
5
5
  for (const candidate of candidates) {
6
6
  const distance = measureDistance(name, candidate);
7
- if (distance < shortest) {
8
- shortest = distance;
9
- closest = candidate;
7
+ if (distance < shortestDistance) {
8
+ shortestDistance = distance;
9
+ closestName = candidate;
10
10
  }
11
11
  }
12
- return shortest <= ceiling ? closest : undefined;
12
+ return shortestDistance <= ceiling ? closestName : undefined;
13
13
  }
14
14
  function measureDistance(first, second) {
15
15
  if (first.length === 0 || second.length === 0) {
16
16
  return Math.max(first.length, second.length);
17
17
  }
18
- let previous = Array.from({ length: second.length + 1 }, (_unused, column) => column);
19
- let current = Array.from({ length: second.length + 1 }, () => 0);
18
+ let previousRow = Array.from({ length: second.length + 1 }, (_unused, column) => column);
19
+ let currentRow = Array.from({ length: second.length + 1 }, () => 0);
20
20
  for (let row = 1; row <= first.length; row += 1) {
21
- current[0] = row;
21
+ currentRow[0] = row;
22
22
  for (let column = 1; column <= second.length; column += 1) {
23
- const substitution = readCell(previous, column - 1) + (first[row - 1] === second[column - 1] ? 0 : 1);
24
- current[column] = Math.min(readCell(current, column - 1) + 1, readCell(previous, column) + 1, substitution);
23
+ const substitution = readCell(previousRow, column - 1) + (first[row - 1] === second[column - 1] ? 0 : 1);
24
+ currentRow[column] = Math.min(readCell(currentRow, column - 1) + 1, readCell(previousRow, column) + 1, substitution);
25
25
  }
26
- const completed = current;
27
- current = previous;
28
- previous = completed;
26
+ const completedRow = currentRow;
27
+ currentRow = previousRow;
28
+ previousRow = completedRow;
29
29
  }
30
- return readCell(previous, second.length);
30
+ return readCell(previousRow, second.length);
31
31
  }
32
32
  function readCell(row, index) {
33
33
  const value = row[index];
@@ -25,14 +25,14 @@ export declare function readPackageJson(dir: string): PackageJson;
25
25
  /**
26
26
  * Returns the `pnpm.overrides` block a package.json declares, every key it holds and each value as written.
27
27
  */
28
- export declare function getPnpmOverrides(pkg: PackageJson): Record<string, unknown> | undefined;
28
+ export declare function getPnpmOverrides(packageJson: PackageJson): Record<string, unknown> | undefined;
29
29
  /**
30
30
  * Parses a `package.json`'s text, rejecting what does not parse.
31
31
  *
32
32
  * A file the user wrote by hand is theirs to fix, so the failure names it rather than reporting the parser's
33
33
  * own stack.
34
34
  */
35
- export declare function parsePackageJson(raw: string, file: string): unknown;
35
+ export declare function parsePackageJson(rawText: string, file: string): unknown;
36
36
  /**
37
37
  * Narrows a `scripts` record to strings, rejecting any value that is not one.
38
38
  *
@@ -14,45 +14,46 @@ export const DEPENDENCY_FIELDS = [
14
14
  ];
15
15
  export function readPackageJson(dir) {
16
16
  const file = resolvePackageJsonPath(dir);
17
- const parsed = parsePackageJson(readFileSync(file, 'utf8'), file);
18
- if (!isObject(parsed)) {
17
+ const parsedManifest = parsePackageJson(readFileSync(file, 'utf8'), file);
18
+ if (!isObject(parsedManifest)) {
19
19
  throw new UserError(`Invalid package.json in ${dir}: expected an object`);
20
20
  }
21
- const pkg = {};
22
- if (typeof parsed['name'] === 'string')
23
- pkg.name = parsed['name'];
24
- if (parsed['private'] === true)
25
- pkg.private = true;
26
- if (typeof parsed['version'] === 'string')
27
- pkg.version = parsed['version'];
28
- if (typeof parsed['packageManager'] === 'string')
29
- pkg.packageManager = parsed['packageManager'];
30
- if (isObject(parsed['scripts'])) {
31
- pkg.scripts = readScriptRecord(dir, parsed['scripts']);
21
+ const packageJson = {};
22
+ if (typeof parsedManifest['name'] === 'string')
23
+ packageJson.name = parsedManifest['name'];
24
+ if (parsedManifest['private'] === true)
25
+ packageJson.private = true;
26
+ if (typeof parsedManifest['version'] === 'string')
27
+ packageJson.version = parsedManifest['version'];
28
+ if (typeof parsedManifest['packageManager'] === 'string') {
29
+ packageJson.packageManager = parsedManifest['packageManager'];
32
30
  }
33
- if (isObject(parsed['pnpm'])) {
34
- const pnpm = parsed['pnpm'];
31
+ if (isObject(parsedManifest['scripts'])) {
32
+ packageJson.scripts = readScriptRecord(dir, parsedManifest['scripts']);
33
+ }
34
+ if (isObject(parsedManifest['pnpm'])) {
35
+ const pnpm = parsedManifest['pnpm'];
35
36
  if (isObject(pnpm['overrides'])) {
36
- pkg.pnpm = { overrides: pnpm['overrides'] };
37
+ packageJson.pnpm = { overrides: pnpm['overrides'] };
37
38
  }
38
39
  }
39
40
  for (const field of DEPENDENCY_FIELDS) {
40
- const declared = parsed[field];
41
- if (isObject(declared)) {
42
- pkg[field] = readStringValues(declared);
41
+ const declaredDependencies = parsedManifest[field];
42
+ if (isObject(declaredDependencies)) {
43
+ packageJson[field] = readStringValues(declaredDependencies);
43
44
  }
44
45
  }
45
- return pkg;
46
+ return packageJson;
46
47
  }
47
- export function getPnpmOverrides(pkg) {
48
- if (!isObject(pkg.pnpm))
48
+ export function getPnpmOverrides(packageJson) {
49
+ if (!isObject(packageJson.pnpm))
49
50
  return undefined;
50
- const overrides = pkg.pnpm.overrides;
51
+ const overrides = packageJson.pnpm.overrides;
51
52
  return isObject(overrides) ? overrides : undefined;
52
53
  }
53
- export function parsePackageJson(raw, file) {
54
+ export function parsePackageJson(rawText, file) {
54
55
  try {
55
- return JSON.parse(raw);
56
+ return JSON.parse(rawText);
56
57
  }
57
58
  catch (error) {
58
59
  throw new UserError(`Invalid package.json at ${file}: ${describeError(error)}`);
@@ -16,7 +16,7 @@ export declare const TRANSCRIPT_LIMIT_BYTES = 262144;
16
16
  * stderr on failure is the command's own output going to a stream that may be a terminal, and stripping its
17
17
  * color there would be a loss.
18
18
  */
19
- export declare function cleanTranscript(raw: string): string;
19
+ export declare function cleanTranscript(rawText: string): string;
20
20
  /**
21
21
  * Composes what a pass persists from what its command wrote, or `undefined` where it wrote nothing worth
22
22
  * keeping.
@@ -25,6 +25,6 @@ export declare function cleanTranscript(raw: string): string;
25
25
  * failure: nmr reads each through a pipe of its own, so their true interleaving is not among the facts it
26
26
  * holds. A stream carrying nothing contributes neither content nor marker.
27
27
  */
28
- export declare function composeTranscript(retained: RetainedOutput): string | undefined;
28
+ export declare function composeTranscript(retainedOutput: RetainedOutput): string | undefined;
29
29
  /** Renders the line that stands in for dropped bytes, in nmr's voice so it is not mistaken for the tool's. */
30
30
  export declare function formatElisionMarker(byteCount: number): string;
@@ -2,15 +2,15 @@ export const TRANSCRIPT_LIMIT_BYTES = 262_144;
2
2
  const ANSI_PATTERN = /\u{1B}(?:\[[\u{30}-\u{3F}]*[\u{20}-\u{2F}]*[\u{40}-\u{7E}]|\][^\u{7}\u{1B}]*(?:\u{7}|\u{1B}\\))/gu;
3
3
  const END_LIMIT_BYTES = TRANSCRIPT_LIMIT_BYTES / 2;
4
4
  const STREAM_MARKER = '\n… nmr: stderr …\n';
5
- export function cleanTranscript(raw) {
6
- return raw
5
+ export function cleanTranscript(rawText) {
6
+ return rawText
7
7
  .replaceAll(ANSI_PATTERN, '')
8
8
  .split('\n')
9
9
  .map((line) => renderCarriageReturns(line))
10
10
  .join('\n');
11
11
  }
12
- export function composeTranscript(retained) {
13
- const streams = [retained.stdout, retained.stderr]
12
+ export function composeTranscript(retainedOutput) {
13
+ const streams = [retainedOutput.stdout, retainedOutput.stderr]
14
14
  .map((stream) => cleanTranscript(stream.toString('utf8')))
15
15
  .filter((stream) => stream.trim() !== '');
16
16
  if (streams.length === 0) {
@@ -22,19 +22,19 @@ export function formatElisionMarker(byteCount) {
22
22
  const unit = byteCount === 1 ? 'byte' : 'bytes';
23
23
  return `\n… nmr elided ${byteCount.toLocaleString('en-US')} ${unit} …\n`;
24
24
  }
25
- function alignEnd(buffer, end) {
26
- let aligned = Math.min(buffer.length, end);
27
- while (aligned > 0 && isContinuationByte(buffer[aligned])) {
28
- aligned--;
25
+ function alignEnd(buffer, endOffset) {
26
+ let alignedOffset = Math.min(buffer.length, endOffset);
27
+ while (alignedOffset > 0 && isContinuationByte(buffer[alignedOffset])) {
28
+ alignedOffset--;
29
29
  }
30
- return aligned;
30
+ return alignedOffset;
31
31
  }
32
- function alignStart(buffer, start) {
33
- let aligned = Math.max(0, start);
34
- while (aligned < buffer.length && isContinuationByte(buffer[aligned])) {
35
- aligned++;
32
+ function alignStart(buffer, startOffset) {
33
+ let alignedOffset = Math.max(0, startOffset);
34
+ while (alignedOffset < buffer.length && isContinuationByte(buffer[alignedOffset])) {
35
+ alignedOffset++;
36
36
  }
37
- return aligned;
37
+ return alignedOffset;
38
38
  }
39
39
  function clampTranscript(transcript) {
40
40
  const buffer = Buffer.from(transcript, 'utf8');
@@ -0,0 +1,41 @@
1
+ import type { Writable } from 'node:stream';
2
+ import { type StreamStyleResolution } from '@williamthorsen/nmr-core';
3
+ /**
4
+ * Reports whether a stream is backed by a terminal, which is what detection reads where no source names a
5
+ * style. A stream carrying no `isTTY`, which is the one a test injects, is not one.
6
+ */
7
+ export declare function isTerminalStream(stream: Writable): boolean;
8
+ /**
9
+ * Carries the resolved style down the spawned chain, so the flag reaches every process rather than the first
10
+ * and a child writing to a pipe renders as its parent does rather than detecting plain on its own descriptor.
11
+ *
12
+ * Deliberately not a keyed variable, on neither the pass key nor the retention key: it changes how a run
13
+ * renders and never what a command concludes, so folding it into either would stop a plain run from hitting a
14
+ * pass a rich one recorded.
15
+ */
16
+ export declare const OUTPUT_STYLE_ENV_VAR = "NMR_OUTPUT_STYLE";
17
+ /**
18
+ * The flag naming a style for one invocation, which outranks `OUTPUT_STYLE_ENV_VAR`.
19
+ *
20
+ * `nmr` alone carries it. A standalone bin takes the variable, which `nmr <command>` exports to it, so adding
21
+ * a style to a bin does not mean adding a flag to each bin's own parser.
22
+ */
23
+ export declare const OUTPUT_STYLE_FLAG = "--output-style";
24
+ /**
25
+ * Resolves the style of each output stream: the flag, then `NMR_OUTPUT_STYLE`, then detection from `CI`, the
26
+ * stream's terminal state, and `TERM`.
27
+ *
28
+ * Never throws. A value naming no style is returned in `invalid`, for the caller to report with
29
+ * `describeInvalidOutputStyle`, because the caller has to render that complaint in some style.
30
+ *
31
+ * The flag's raw value is handed to nmr-core as an assignment rather than narrowed here, so nmr rejects a
32
+ * misspelling in the words its siblings use and no second ladder can come to name a different one.
33
+ */
34
+ export declare function resolveOutputStyles(options: ResolveOutputStylesOptions): StreamStyleResolution;
35
+ export interface ResolveOutputStylesOptions {
36
+ env: NodeJS.ProcessEnv;
37
+ /** The value the flag was given, absent where the invocation carries none, as a bin's invocation always does. */
38
+ flagValue?: string | undefined;
39
+ stderrIsTty: boolean;
40
+ stdoutIsTty: boolean;
41
+ }
@@ -0,0 +1,17 @@
1
+ import { resolveStreamStyles } from '@williamthorsen/nmr-core';
2
+ export function isTerminalStream(stream) {
3
+ return 'isTTY' in stream && stream.isTTY === true;
4
+ }
5
+ export const OUTPUT_STYLE_ENV_VAR = 'NMR_OUTPUT_STYLE';
6
+ export const OUTPUT_STYLE_FLAG = '--output-style';
7
+ export function resolveOutputStyles(options) {
8
+ const { env, flagValue, stderrIsTty, stdoutIsTty } = options;
9
+ return resolveStreamStyles({
10
+ argv: flagValue === undefined ? [] : [`${OUTPUT_STYLE_FLAG}=${flagValue}`],
11
+ env,
12
+ envVar: OUTPUT_STYLE_ENV_VAR,
13
+ flag: OUTPUT_STYLE_FLAG,
14
+ stderrIsTty,
15
+ stdoutIsTty,
16
+ });
17
+ }
@@ -57,23 +57,23 @@ export function definePrettierConfig(options = {}) {
57
57
  function buildShellPlugin() {
58
58
  return {
59
59
  ...shPlugin,
60
- languages: shPlugin.languages.filter(isInferredLanguage).map(withoutExcludedFilenames),
60
+ languages: shPlugin.languages.filter(isInferredLanguage).map(dropExcludedFilenames),
61
61
  };
62
62
  }
63
63
  function isInferredLanguage(language) {
64
64
  return INFERRED_LANGUAGES.has(language.name);
65
65
  }
66
- function withoutExcludedFilenames(language) {
66
+ function dropExcludedFilenames(language) {
67
67
  if (language.filenames === undefined)
68
68
  return language;
69
69
  return { ...language, filenames: language.filenames.filter((name) => !EXCLUDED_FILENAMES.has(name)) };
70
70
  }
71
71
  function assertNoReplacement(options) {
72
72
  if ('overrides' in options)
73
- throw ownedKeyError('overrides', 'additionalOverrides');
73
+ throw buildOwnedKeyError('overrides', 'additionalOverrides');
74
74
  if ('plugins' in options)
75
- throw ownedKeyError('plugins', 'additionalPlugins');
75
+ throw buildOwnedKeyError('plugins', 'additionalPlugins');
76
76
  }
77
- function ownedKeyError(key, seam) {
77
+ function buildOwnedKeyError(key, seam) {
78
78
  return new TypeError(`definePrettierConfig: \`${key}\` is owned by this config. Use \`${seam}\` to append to it.`);
79
79
  }
@@ -1,3 +1,4 @@
1
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
1
2
  import { type CheckCacheEntry } from './check-cache.js';
2
3
  /**
3
4
  * What this invocation would be recorded under, insofar as an entry records the same facts. A mismatch is read
@@ -27,12 +28,12 @@ export type KeyDifference = {
27
28
  ingredient: 'command-string';
28
29
  } | {
29
30
  ingredient: 'nmr-version';
30
- current: string;
31
- recorded: string;
31
+ currentVersion: string;
32
+ recordedVersion: string;
32
33
  } | {
33
34
  ingredient: 'node-version';
34
- current: string;
35
- recorded: string;
35
+ currentVersion: string;
36
+ recordedVersion: string;
36
37
  } | {
37
38
  ingredient: 'other';
38
39
  };
@@ -75,6 +76,7 @@ export declare function renderRecording(options: {
75
76
  command: string;
76
77
  recording: Recording;
77
78
  scope: string;
79
+ style: OutputStyle;
78
80
  }): string;
79
81
  /**
80
82
  * Renders a refusal on the one line a fan-out can attribute, in the grammar a verdict uses: the scope, the
@@ -84,6 +86,7 @@ export declare function renderRefusal(options: {
84
86
  command: string;
85
87
  refusal: RecordingRefusal;
86
88
  scope: string;
89
+ style: OutputStyle;
87
90
  }): string;
88
91
  /**
89
92
  * Resolves what one scope has to show for one command.
@@ -96,7 +99,7 @@ export declare function renderRefusal(options: {
96
99
  export declare function resolveRecording(options: {
97
100
  anchorDir: string;
98
101
  command: string;
99
- current: RunIdentity;
102
+ currentIdentity: RunIdentity;
100
103
  isCacheable: boolean;
101
104
  key: string | undefined;
102
105
  monorepoRoot: string;