@williamthorsen/nmr 0.36.0 → 0.38.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 (62) hide show
  1. package/.meta/changelog.json +113 -0
  2. package/.readyup/kits/default.js +98 -10
  3. package/.readyup/manifest.json +7 -7
  4. package/CHANGELOG.md +82 -0
  5. package/README.md +107 -1135
  6. package/agents/README.md +1 -1
  7. package/agents/guidance/rulebooks/nmr.md +4 -4
  8. package/dist/esm/check-cache.d.ts +2 -1
  9. package/dist/esm/check-cache.js +4 -4
  10. package/dist/esm/cli-build.js +3 -0
  11. package/dist/esm/cli-clean.js +3 -1
  12. package/dist/esm/cli-ensure-prepublish-hooks.js +3 -1
  13. package/dist/esm/cli-report-catalog.js +2 -1
  14. package/dist/esm/cli-report-overrides.js +2 -1
  15. package/dist/esm/commands/build.d.ts +6 -1
  16. package/dist/esm/commands/build.js +9 -10
  17. package/dist/esm/commands/clean.d.ts +3 -2
  18. package/dist/esm/commands/clean.js +15 -14
  19. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +3 -2
  20. package/dist/esm/commands/ensure-prepublish-hooks.js +8 -6
  21. package/dist/esm/commands/report-catalog.d.ts +2 -1
  22. package/dist/esm/commands/report-catalog.js +5 -4
  23. package/dist/esm/commands/report-overrides.d.ts +2 -1
  24. package/dist/esm/commands/report-overrides.js +5 -4
  25. package/dist/esm/config.js +14 -4
  26. package/dist/esm/context.d.ts +2 -0
  27. package/dist/esm/context.js +1 -0
  28. package/dist/esm/default-scripts.d.ts +2 -2
  29. package/dist/esm/default-scripts.js +5 -5
  30. package/dist/esm/glyphs.d.ts +9 -0
  31. package/dist/esm/glyphs.js +9 -0
  32. package/dist/esm/help.js +15 -9
  33. package/dist/esm/helpers/filter-selection.d.ts +42 -0
  34. package/dist/esm/helpers/filter-selection.js +61 -0
  35. package/dist/esm/output-style.d.ts +41 -0
  36. package/dist/esm/output-style.js +17 -0
  37. package/dist/esm/recording.d.ts +3 -0
  38. package/dist/esm/recording.js +6 -3
  39. package/dist/esm/resolveBinStyles.d.ts +12 -0
  40. package/dist/esm/resolveBinStyles.js +16 -0
  41. package/dist/esm/resolver.js +2 -2
  42. package/dist/esm/runCli.js +152 -62
  43. package/dist/esm/runner.js +6 -5
  44. package/dist/esm/steps.d.ts +8 -4
  45. package/dist/esm/steps.js +6 -2
  46. package/dist/esm/verdict.d.ts +7 -3
  47. package/dist/esm/verdict.js +16 -10
  48. package/dist/esm/vitest-source-resolution.d.ts +37 -0
  49. package/dist/esm/vitest-source-resolution.js +205 -0
  50. package/dist/esm/vitest.d.ts +3 -2
  51. package/dist/esm/vitest.js +7 -6
  52. package/dist/esm/workspace.d.ts +9 -0
  53. package/dist/esm/workspace.js +17 -0
  54. package/docs/check-cache.md +176 -0
  55. package/docs/prettier.md +47 -0
  56. package/docs/reporting.md +104 -0
  57. package/docs/scripts.md +168 -0
  58. package/docs/upgrades.md +110 -0
  59. package/docs/utilities.md +64 -0
  60. package/docs/vitest.md +233 -0
  61. package/docs/workspace.md +11 -0
  62. package/package.json +11 -9
package/dist/esm/help.js CHANGED
@@ -1,19 +1,25 @@
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
4
  export function generateHelp(config, packageDir, useRoot) {
4
5
  const lines = [
5
6
  'Usage: nmr [flags] <command> [args...]',
6
7
  '',
7
8
  'Flags:',
8
- ' -F, --filter <pattern> Run command in matching packages',
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
  ];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Asks pnpm what a `-F` pattern selects, so an invocation that would select nothing is caught before it
3
+ * delegates, and one selecting several packages is told from one selecting a single package.
4
+ *
5
+ * pnpm's own selector answers rather than a matcher of nmr's: `-F` means the whole selector language, names
6
+ * and globs alongside `./path`, `{dir}`, `pkg...`, `!negation`, and `[since]`, and a matcher here would leave
7
+ * every form but the simplest silently unchecked.
8
+ *
9
+ * Runs from the monorepo root, where the delegate runs, so a path pattern resolves against the same directory
10
+ * in the probe as in the run it stands in for.
11
+ *
12
+ * The listing counts the root project wherever the filter leaves it standing, while the `exec` a delegation
13
+ * composes runs there only where the pattern selects the root positively: `-F '!./packages/*'` lists the root
14
+ * and executes nowhere. A listing of the root alone is therefore the one reading the listing cannot settle,
15
+ * and it is put to `exec` itself. Every other listing settles on its own, and counts the packages beside the
16
+ * root: two of them run in two scopes, while one beside the root may run in that package alone.
17
+ */
18
+ export declare function readFilterSelection(pattern: string, monorepoRoot: string): FilterSelection;
19
+ /**
20
+ * Reduces a listing to what it proves about the selection.
21
+ *
22
+ * Every outcome the probe cannot read resolves to `unresolved`, which claims nothing about the selection, and the
23
+ * invocation delegates as it would have. A pattern pnpm rejects rather than resolves, such as a bad git ref in
24
+ * a changed-since selector, is pnpm's to report, and reporting it here as a match failure would name a
25
+ * different fault than the one the user has.
26
+ */
27
+ export declare function interpretSelectionProbe(probe: SelectionProbe, monorepoRoot: string): ProbeReading;
28
+ /**
29
+ * Reduces a marked delegate run to what it proves: a scope that wrote the marker is a scope the run reached. The
30
+ * run is made only for a listing of the root alone, so that scope is the only one.
31
+ */
32
+ export declare function interpretDelegateProbe(probe: SelectionProbe): FilterSelection;
33
+ /** What a `-F` pattern selected, as far as pnpm could be asked. `multiple` is two or more packages beside the root. */
34
+ export type FilterSelection = 'empty' | 'multiple' | 'single' | 'unresolved';
35
+ /** What a listing proves, `root-only` being the reading that only the delegate can settle. */
36
+ export type ProbeReading = FilterSelection | 'root-only';
37
+ /** The part of a probe's completion a selection is read from. */
38
+ export interface SelectionProbe {
39
+ error: Error | undefined;
40
+ status: number | null;
41
+ stdout: string;
42
+ }
@@ -0,0 +1,61 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { realpathSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import { isObject } from "./type-guards.js";
6
+ const SELECTION_MARKER = 'nmr-selected-scope';
7
+ export function readFilterSelection(pattern, monorepoRoot) {
8
+ const listing = spawnSync('pnpm', ['ls', '--filter', pattern, '--depth', '-1', '--json'], {
9
+ cwd: monorepoRoot,
10
+ encoding: 'utf8',
11
+ });
12
+ const reading = interpretSelectionProbe({ error: listing.error, status: listing.status, stdout: listing.stdout }, monorepoRoot);
13
+ return reading === 'root-only' ? readDelegateSelection(pattern, monorepoRoot) : reading;
14
+ }
15
+ export function interpretSelectionProbe(probe, monorepoRoot) {
16
+ if (probe.error !== undefined || probe.status !== 0) {
17
+ return 'unresolved';
18
+ }
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(probe.stdout);
22
+ }
23
+ catch {
24
+ return 'unresolved';
25
+ }
26
+ if (!Array.isArray(parsed)) {
27
+ return 'unresolved';
28
+ }
29
+ if (parsed.length === 0) {
30
+ return 'empty';
31
+ }
32
+ const packageCount = parsed.filter((entry) => !isRootEntry(entry, monorepoRoot)).length;
33
+ if (packageCount === 0) {
34
+ return 'root-only';
35
+ }
36
+ return packageCount === 1 ? 'single' : 'multiple';
37
+ }
38
+ export function interpretDelegateProbe(probe) {
39
+ if (probe.error !== undefined || probe.status !== 0) {
40
+ return 'unresolved';
41
+ }
42
+ return probe.stdout.includes(SELECTION_MARKER) ? 'single' : 'empty';
43
+ }
44
+ function isRootEntry(entry, monorepoRoot) {
45
+ if (!isObject(entry) || typeof entry['path'] !== 'string') {
46
+ return false;
47
+ }
48
+ return readRealPath(entry['path']) === readRealPath(monorepoRoot);
49
+ }
50
+ function readRealPath(target) {
51
+ try {
52
+ return realpathSync(target);
53
+ }
54
+ catch {
55
+ return path.resolve(target);
56
+ }
57
+ }
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 });
61
+ }
@@ -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
+ }
@@ -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
@@ -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.
@@ -1,14 +1,17 @@
1
+ import { formatGlyphLine, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
1
2
  import { readCheckCacheEntry, readTranscript } from "./check-cache.js";
3
+ import { NMR_GLYPHS } from "./glyphs.js";
2
4
  import { formatDuration } from "./helpers/duration.js";
3
5
  export function renderRecording(options) {
4
6
  const { entry } = options.recording;
5
7
  const age = formatDuration(Math.max(0, Date.now() - Date.parse(entry.recordedAt)));
6
- const header = `📼 ${options.scope}: ${options.command} — recorded ${entry.recordedAt} (${age} ago), ` +
7
- `ran in ${formatDuration(entry.durationMs)}\n$ ${entry.commandString}\n\n`;
8
+ const header = formatGlyphLine(NMR_GLYPHS, options.style, 'recording', `${options.scope}: ${options.command} — recorded ${entry.recordedAt} (${age} ago), ` +
9
+ `ran in ${formatDuration(entry.durationMs)}`) + `\n$ ${entry.commandString}\n\n`;
8
10
  return `${header}${appendNewline(renderBody(options.recording))}`;
9
11
  }
10
12
  export function renderRefusal(options) {
11
- return `📭 ${options.scope}: ${options.command}: no recording; ${describeRefusal(options.command, options.refusal)}`;
13
+ const { text } = STATUS_GLYPHS[options.style].warning;
14
+ return `${text} ${options.scope}: ${options.command}: no recording; ${describeRefusal(options.command, options.refusal)}`;
12
15
  }
13
16
  export async function resolveRecording(options) {
14
17
  const { anchorDir, command, monorepoRoot } = options;
@@ -0,0 +1,12 @@
1
+ import { type StreamStyles } from '@williamthorsen/nmr-core';
2
+ /**
3
+ * Resolves the style of the running process's own output streams, and rejects a variable naming no style.
4
+ *
5
+ * What a standalone bin calls. A bin carries no `--output-style` of its own: the flag is `nmr`'s, and what
6
+ * reaches a bin is the variable that `nmr <command>` exports to it, so a bin run through nmr renders as nmr
7
+ * does and one run on its own detects its streams.
8
+ *
9
+ * Throws rather than exiting, so the bin's own boundary reports the message and sets the exit code, as it does
10
+ * for every other failure in what the caller declared.
11
+ */
12
+ export declare function resolveBinStyles(): StreamStyles;
@@ -0,0 +1,16 @@
1
+ import process from 'node:process';
2
+ import { describeInvalidOutputStyle } from '@williamthorsen/nmr-core';
3
+ import { resolveOutputStyles } from "./output-style.js";
4
+ import { UserError } from "./UserError.js";
5
+ export function resolveBinStyles() {
6
+ const { invalid, styles } = resolveOutputStyles({
7
+ env: process.env,
8
+ flagValue: undefined,
9
+ stderrIsTty: process.stderr.isTTY,
10
+ stdoutIsTty: process.stdout.isTTY,
11
+ });
12
+ if (invalid !== undefined) {
13
+ throw new UserError(describeInvalidOutputStyle(invalid));
14
+ }
15
+ return styles;
16
+ }
@@ -46,7 +46,7 @@ export function expandScript(script, workspaceRoot) {
46
46
  }
47
47
  return script.map((element) => typeof element === 'string'
48
48
  ? composeNmrStep(element, workspaceRoot)
49
- : composeNmrStep(element.run, workspaceRoot, element.declinesArgs));
49
+ : composeNmrStep(element.run, workspaceRoot, element.shouldDeclineArguments));
50
50
  }
51
51
  export function describeScript(script) {
52
52
  return typeof script === 'string' ? script : `[${script.map(describeElement).join(', ')}]`;
@@ -55,7 +55,7 @@ function describeElement(element) {
55
55
  if (typeof element === 'string') {
56
56
  return element;
57
57
  }
58
- return element.declinesArgs === true ? `${element.run} (no args)` : element.run;
58
+ return element.shouldDeclineArguments === true ? `${element.run} (no args)` : element.run;
59
59
  }
60
60
  export function readPackageJsonScripts(packageDir) {
61
61
  const file = resolvePackageJsonPath(packageDir);