@williamthorsen/nmr 0.55.0 → 1.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.
Files changed (108) hide show
  1. package/.meta/changelog.json +124 -0
  2. package/.readyup/kits/default.js +667 -167
  3. package/.readyup/manifest.json +77 -46
  4. package/CHANGELOG.md +40 -0
  5. package/README.md +1 -14
  6. package/agents/guidance/rulebooks/nmr.md +1 -1
  7. package/bin/ensure-prepublish-hooks.js +1 -1
  8. package/bin/nmr-actionlint.js +1 -1
  9. package/bin/nmr-clean.js +1 -1
  10. package/bin/nmr-compile.js +1 -1
  11. package/bin/nmr-fmt.js +1 -1
  12. package/bin/nmr-report-catalog.js +1 -1
  13. package/bin/nmr-report-overrides.js +1 -1
  14. package/bin/nmr-shellcheck.js +1 -1
  15. package/bin/nmr-taze.js +1 -1
  16. package/bin/nmr-vitest.js +1 -1
  17. package/bin/nmr.js +1 -1
  18. package/dist/esm/bins.d.ts +11 -0
  19. package/dist/esm/bins.js +11 -0
  20. package/dist/esm/check-cache-entries.js +5 -5
  21. package/dist/esm/check-cache-gate.js +2 -1
  22. package/dist/esm/child-env.js +2 -1
  23. package/dist/esm/child-process.d.ts +2 -0
  24. package/dist/esm/child-process.js +3 -3
  25. package/dist/esm/cli-actionlint.js +2 -1
  26. package/dist/esm/cli-build.js +2 -1
  27. package/dist/esm/cli-clean.js +2 -1
  28. package/dist/esm/cli-ensure-prepublish-hooks.js +29 -16
  29. package/dist/esm/cli-fmt.js +2 -1
  30. package/dist/esm/cli-report-catalog.js +2 -1
  31. package/dist/esm/cli-report-overrides.js +2 -1
  32. package/dist/esm/cli-shellcheck.js +2 -1
  33. package/dist/esm/cli-taze.js +2 -1
  34. package/dist/esm/cli-vitest.js +2 -1
  35. package/dist/esm/cli.js +2 -1
  36. package/dist/esm/commands/actionlint.d.ts +3 -3
  37. package/dist/esm/commands/actionlint.js +5 -5
  38. package/dist/esm/commands/build.js +6 -6
  39. package/dist/esm/commands/clean.js +3 -1
  40. package/dist/esm/commands/compiler.js +3 -3
  41. package/dist/esm/commands/ensure-prepublish-hooks.d.ts +1 -1
  42. package/dist/esm/commands/ensure-prepublish-hooks.js +4 -5
  43. package/dist/esm/commands/fmt.d.ts +3 -3
  44. package/dist/esm/commands/fmt.js +33 -52
  45. package/dist/esm/commands/format-targets.js +1 -13
  46. package/dist/esm/commands/node-pin.js +3 -4
  47. package/dist/esm/commands/rewrite-specifiers.js +2 -2
  48. package/dist/esm/commands/run-vitest.js +5 -5
  49. package/dist/esm/commands/shellcheck.d.ts +3 -3
  50. package/dist/esm/commands/shellcheck.js +8 -11
  51. package/dist/esm/commands/taze.js +5 -4
  52. package/dist/esm/commands/tsconfig-chain.js +3 -3
  53. package/dist/esm/commands/workflow-tools.d.ts +12 -3
  54. package/dist/esm/commands/workflow-tools.js +29 -14
  55. package/dist/esm/config-validation.js +9 -9
  56. package/dist/esm/config.js +2 -3
  57. package/dist/esm/delegate-host.d.ts +1 -1
  58. package/dist/esm/delegate-host.js +5 -3
  59. package/dist/esm/entry-chain.js +2 -3
  60. package/dist/esm/fan-out.d.ts +3 -3
  61. package/dist/esm/fan-out.js +14 -6
  62. package/dist/esm/git-ignored-paths.js +3 -3
  63. package/dist/esm/help.d.ts +14 -15
  64. package/dist/esm/help.js +29 -54
  65. package/dist/esm/helpers/filter-selection.js +2 -2
  66. package/dist/esm/helpers/package-json.js +8 -9
  67. package/dist/esm/helpers/resolveXdgBaseDir.d.ts +5 -0
  68. package/dist/esm/helpers/resolveXdgBaseDir.js +12 -0
  69. package/dist/esm/helpers/type-guards.d.ts +0 -2
  70. package/dist/esm/helpers/type-guards.js +2 -4
  71. package/dist/esm/invocation-args.d.ts +0 -32
  72. package/dist/esm/invocation-args.js +23 -121
  73. package/dist/esm/invocation.d.ts +3 -1
  74. package/dist/esm/invocation.js +1 -0
  75. package/dist/esm/machine-lock.js +3 -4
  76. package/dist/esm/nmr-flags.d.ts +109 -0
  77. package/dist/esm/nmr-flags.js +120 -0
  78. package/dist/esm/output-style.d.ts +1 -1
  79. package/dist/esm/output-style.js +2 -1
  80. package/dist/esm/pass-manifest.js +2 -2
  81. package/dist/esm/presentation.d.ts +1 -1
  82. package/dist/esm/remote-host-session.d.ts +2 -0
  83. package/dist/esm/remote-host-session.js +1 -0
  84. package/dist/esm/remote-probe.js +2 -2
  85. package/dist/esm/remote-run.d.ts +3 -3
  86. package/dist/esm/remote-run.js +6 -4
  87. package/dist/esm/remote-session.js +4 -4
  88. package/dist/esm/remote-snapshot.js +8 -22
  89. package/dist/esm/resolver.js +3 -4
  90. package/dist/esm/route.d.ts +29 -0
  91. package/dist/esm/route.js +94 -0
  92. package/dist/esm/run-chain.d.ts +1 -1
  93. package/dist/esm/run-chain.js +4 -2
  94. package/dist/esm/runCli.d.ts +2 -0
  95. package/dist/esm/runCli.js +36 -78
  96. package/dist/esm/stated-outcome.js +3 -3
  97. package/dist/esm/steps.js +6 -30
  98. package/dist/esm/user-config.js +7 -14
  99. package/dist/esm/vitest-source-resolution.js +5 -6
  100. package/dist/esm/vitest-temp-isolation.d.ts +2 -0
  101. package/dist/esm/vitest-temp-isolation.js +44 -0
  102. package/dist/esm/vitest.d.ts +9 -0
  103. package/dist/esm/vitest.js +8 -5
  104. package/docs/reporting.md +3 -3
  105. package/docs/scripts.md +1 -1
  106. package/docs/utilities.md +5 -5
  107. package/docs/vitest.md +11 -6
  108. package/package.json +4 -4
@@ -1,11 +1,10 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { UserError } from '@williamthorsen/nmr-core';
3
+ import { isRecord, UserError } from '@williamthorsen/nmr-core';
4
4
  import { describeError } from '@williamthorsen/toolbelt.errors';
5
5
  import { isMonorepoRoot } from "../workspace.js";
6
6
  import { CONFIG_RELATIVE_PATH } from "./config-path.js";
7
7
  import { readStringValues } from "./readStringValues.js";
8
- import { isObject } from "./type-guards.js";
9
8
  export const DEPENDENCY_FIELDS = [
10
9
  'dependencies',
11
10
  'devDependencies',
@@ -15,7 +14,7 @@ export const DEPENDENCY_FIELDS = [
15
14
  export function readPackageJson(dir) {
16
15
  const file = resolvePackageJsonPath(dir);
17
16
  const parsedManifest = parsePackageJson(readFileSync(file, 'utf8'), file);
18
- if (!isObject(parsedManifest)) {
17
+ if (!isRecord(parsedManifest)) {
19
18
  throw new UserError(`Invalid package.json in ${dir}: expected an object`);
20
19
  }
21
20
  const packageJson = {};
@@ -28,28 +27,28 @@ export function readPackageJson(dir) {
28
27
  if (typeof parsedManifest['packageManager'] === 'string') {
29
28
  packageJson.packageManager = parsedManifest['packageManager'];
30
29
  }
31
- if (isObject(parsedManifest['scripts'])) {
30
+ if (isRecord(parsedManifest['scripts'])) {
32
31
  packageJson.scripts = readScriptRecord(dir, parsedManifest['scripts']);
33
32
  }
34
- if (isObject(parsedManifest['pnpm'])) {
33
+ if (isRecord(parsedManifest['pnpm'])) {
35
34
  const pnpm = parsedManifest['pnpm'];
36
- if (isObject(pnpm['overrides'])) {
35
+ if (isRecord(pnpm['overrides'])) {
37
36
  packageJson.pnpm = { overrides: pnpm['overrides'] };
38
37
  }
39
38
  }
40
39
  for (const field of DEPENDENCY_FIELDS) {
41
40
  const declaredDependencies = parsedManifest[field];
42
- if (isObject(declaredDependencies)) {
41
+ if (isRecord(declaredDependencies)) {
43
42
  packageJson[field] = readStringValues(declaredDependencies);
44
43
  }
45
44
  }
46
45
  return packageJson;
47
46
  }
48
47
  export function getPnpmOverrides(packageJson) {
49
- if (!isObject(packageJson.pnpm))
48
+ if (!isRecord(packageJson.pnpm))
50
49
  return undefined;
51
50
  const overrides = packageJson.pnpm.overrides;
52
- return isObject(overrides) ? overrides : undefined;
51
+ return isRecord(overrides) ? overrides : undefined;
53
52
  }
54
53
  export function parsePackageJson(rawText, file) {
55
54
  try {
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Returns an XDG base directory, resolved from `env` alone: the variable's value when that is an absolute path, as the
3
+ * XDG base-directory spec requires, otherwise `homeSubdir` under `HOME`. Returns `undefined` when `env` names neither.
4
+ */
5
+ export declare function resolveXdgBaseDir(env: NodeJS.ProcessEnv, variable: 'XDG_CACHE_HOME' | 'XDG_CONFIG_HOME', homeSubdir: string): string | undefined;
@@ -0,0 +1,12 @@
1
+ import path from 'node:path';
2
+ export function resolveXdgBaseDir(env, variable, homeSubdir) {
3
+ const baseDir = env[variable];
4
+ if (baseDir !== undefined && path.isAbsolute(baseDir)) {
5
+ return baseDir;
6
+ }
7
+ const home = env['HOME'];
8
+ if (home !== undefined && path.isAbsolute(home)) {
9
+ return path.join(home, homeSubdir);
10
+ }
11
+ return undefined;
12
+ }
@@ -1,4 +1,2 @@
1
- /** Narrows an unknown value to a plain object, excluding `null` and arrays. */
2
- export declare function isObject(value: unknown): value is Record<string, unknown>;
3
1
  /** Narrows an unknown value to a record of plain strings. */
4
2
  export declare function isStringRecord(value: unknown): value is Record<string, string>;
@@ -1,6 +1,4 @@
1
- export function isObject(value) {
2
- return typeof value === 'object' && value !== null && !Array.isArray(value);
3
- }
1
+ import { isRecord } from '@williamthorsen/nmr-core';
4
2
  export function isStringRecord(value) {
5
- return isObject(value) && Object.values(value).every((entry) => typeof entry === 'string');
3
+ return isRecord(value) && Object.values(value).every((entry) => typeof entry === 'string');
6
4
  }
@@ -6,35 +6,3 @@ import { type OutputStyle } from '@williamthorsen/nmr-core';
6
6
  export declare function formatMisplacedFlagHint(args: readonly string[], passthrough: readonly string[], style: OutputStyle): string | undefined;
7
7
  /** Reports whether an argument is one of nmr's delegation flags, in any spelling. */
8
8
  export declare function isDelegationFlag(arg: string): boolean;
9
- /** Parses the CLI arguments into nmr's flags, the command name, and the arguments passed through to it. */
10
- export declare function parseArgs(args: string[]): ParseResult;
11
- /** @internal */
12
- export interface ParsedArgs {
13
- /** The comma-separated hosts that `--delegate-to` names, as written. */
14
- delegateHosts?: string;
15
- filter?: string;
16
- isWorkspaceRoot: boolean;
17
- /** The flag's value as it was written, which the style resolver rather than the parser narrows. */
18
- outputStyle?: string;
19
- quiet: boolean;
20
- recursive: boolean;
21
- shouldBypassCache: boolean;
22
- shouldBypassLock: boolean;
23
- /** Set by `--delegate`, which leaves the hosts to `NMR_DELEGATE` or the user config. */
24
- shouldDelegate: boolean;
25
- shouldEmitJson: boolean;
26
- shouldPrintLog: boolean;
27
- shouldRunLocally: boolean;
28
- shouldShowHelp: boolean;
29
- shouldShowVersion: boolean;
30
- command?: string;
31
- passthrough: string[];
32
- }
33
- type ParseResult = {
34
- ok: true;
35
- parsedArgs: ParsedArgs;
36
- } | {
37
- ok: false;
38
- error: string;
39
- };
40
- export {};
@@ -1,27 +1,8 @@
1
1
  import { STATUS_GLYPHS } from '@williamthorsen/nmr-core';
2
2
  import { renderQuotedList } from "./helpers/renderQuotedList.js";
3
- import { OUTPUT_STYLE_FLAG } from "./output-style.js";
4
- import { DELEGATE_FLAG, DELEGATE_TO_FLAG, NO_DELEGATE_FLAG } from "./remote-run.js";
3
+ import { formatFlagSpelling, formatShortSpelling, NMR_FLAGS } from "./nmr-flags.js";
5
4
  import { quoteToken } from "./shell-syntax.js";
6
- const FILTER_FLAGS = new Set(['-F', '--filter']);
7
- const BOOLEAN_FLAGS = new Map([
8
- ['-?', 'shouldShowHelp'],
9
- [DELEGATE_FLAG, 'shouldDelegate'],
10
- ['--help', 'shouldShowHelp'],
11
- ['--json', 'shouldEmitJson'],
12
- ['--log', 'shouldPrintLog'],
13
- ['--no-cache', 'shouldBypassCache'],
14
- [NO_DELEGATE_FLAG, 'shouldRunLocally'],
15
- ['--no-lock', 'shouldBypassLock'],
16
- ['-q', 'quiet'],
17
- ['--quiet', 'quiet'],
18
- ['-R', 'recursive'],
19
- ['--recursive', 'recursive'],
20
- ['-V', 'shouldShowVersion'],
21
- ['--version', 'shouldShowVersion'],
22
- ['-w', 'isWorkspaceRoot'],
23
- ['--workspace-root', 'isWorkspaceRoot'],
24
- ]);
5
+ const DELEGATION_FLAG_KEYS = ['delegate', 'delegateTo', 'noDelegate'];
25
6
  export function formatMisplacedFlagHint(args, passthrough, style) {
26
7
  const { flagNames, movedTokens, remainingTokens } = splitMisplacedFlags(passthrough);
27
8
  if (flagNames.length === 0) {
@@ -40,118 +21,39 @@ export function formatMisplacedFlagHint(args, passthrough, style) {
40
21
  `nmr reads its flags only before the command: \`${correctedTokens.map(quoteToken).join(' ')}\``);
41
22
  }
42
23
  export function isDelegationFlag(arg) {
43
- return (arg === DELEGATE_FLAG ||
44
- arg === DELEGATE_TO_FLAG ||
45
- arg === NO_DELEGATE_FLAG ||
46
- arg.startsWith(`${DELEGATE_FLAG}=`) ||
47
- arg.startsWith(`${DELEGATE_TO_FLAG}=`));
24
+ return DELEGATION_FLAG_KEYS.some((key) => {
25
+ const spelling = formatFlagSpelling(key);
26
+ return arg === spelling || arg.startsWith(`${spelling}=`);
27
+ });
48
28
  }
49
- export function parseArgs(args) {
50
- const parsedArgs = {
51
- isWorkspaceRoot: false,
52
- quiet: false,
53
- recursive: false,
54
- shouldBypassCache: false,
55
- shouldBypassLock: false,
56
- shouldDelegate: false,
57
- shouldEmitJson: false,
58
- shouldPrintLog: false,
59
- shouldRunLocally: false,
60
- shouldShowHelp: false,
61
- shouldShowVersion: false,
62
- passthrough: [],
63
- };
64
- let index = 0;
65
- while (index < args.length) {
66
- const arg = args[index];
67
- if (arg === undefined)
68
- break;
69
- if (FILTER_FLAGS.has(arg)) {
70
- index++;
71
- const filterValue = args[index];
72
- if (!filterValue) {
73
- return { ok: false, error: '-F/--filter requires a pattern argument' };
74
- }
75
- parsedArgs.filter = filterValue;
76
- index++;
77
- continue;
78
- }
79
- const booleanFlag = BOOLEAN_FLAGS.get(arg);
80
- if (booleanFlag !== undefined) {
81
- parsedArgs[booleanFlag] = true;
82
- index++;
83
- continue;
84
- }
85
- if (arg.startsWith(`${DELEGATE_FLAG}=`)) {
86
- const value = arg.slice(DELEGATE_FLAG.length + 1);
87
- return {
88
- ok: false,
89
- error: `${DELEGATE_FLAG} does not take a value; name hosts with ${DELEGATE_TO_FLAG} ${value}`,
90
- };
91
- }
92
- const styleArgument = readOutputStyleArgument(args, index);
93
- if (styleArgument !== undefined) {
94
- if (!styleArgument.ok) {
95
- return styleArgument;
96
- }
97
- parsedArgs.outputStyle = styleArgument.value;
98
- index += styleArgument.consumedCount;
29
+ function findFlagKey(arg) {
30
+ const name = arg.split('=', 1)[0] ?? arg;
31
+ for (const key of Object.keys(NMR_FLAGS)) {
32
+ if (!isNmrFlagKey(key))
99
33
  continue;
34
+ if (name === formatFlagSpelling(key) || arg === formatShortSpelling(key)) {
35
+ return key;
100
36
  }
101
- const delegateArgument = readDelegateArgument(args, index);
102
- if (delegateArgument !== undefined) {
103
- if (!delegateArgument.ok) {
104
- return delegateArgument;
105
- }
106
- parsedArgs.delegateHosts = delegateArgument.value;
107
- index += delegateArgument.consumedCount;
108
- continue;
109
- }
110
- parsedArgs.command = arg;
111
- parsedArgs.passthrough = args.slice(index + 1);
112
- break;
113
- }
114
- const error = findArgError(parsedArgs);
115
- return error === undefined ? { ok: true, parsedArgs } : { ok: false, error };
116
- }
117
- function findArgError(parsedArgs) {
118
- if (parsedArgs.shouldPrintLog &&
119
- parsedArgs.command === undefined &&
120
- !parsedArgs.shouldShowHelp &&
121
- !parsedArgs.shouldShowVersion) {
122
- return '--log requires a command name: `nmr --log <command>`';
123
37
  }
124
38
  return undefined;
125
39
  }
126
- function readDelegateArgument(args, index) {
127
- return readFlagValue(args, index, DELEGATE_TO_FLAG, 'a host argument');
40
+ function isNmrFlagKey(key) {
41
+ return Object.hasOwn(NMR_FLAGS, key);
128
42
  }
129
- function readFlagValue(args, index, flag, requirement) {
43
+ function readMisplacedFlagLength(args, index) {
130
44
  const arg = args[index] ?? '';
131
- const assignment = arg.startsWith(`${flag}=`) ? arg.slice(flag.length + 1) : undefined;
132
- if (arg !== flag && assignment === undefined) {
45
+ const key = findFlagKey(arg);
46
+ if (key === undefined) {
133
47
  return undefined;
134
48
  }
135
- const value = assignment ?? args[index + 1];
136
- if (!value) {
137
- return { ok: false, error: `${flag} requires ${requirement}` };
49
+ const isAssignment = arg.includes('=');
50
+ if (NMR_FLAGS[key].type === 'boolean') {
51
+ return isAssignment ? undefined : 1;
138
52
  }
139
- return { ok: true, value, consumedCount: assignment === undefined ? 2 : 1 };
140
- }
141
- function readMisplacedFlagLength(args, index) {
142
- const arg = args[index] ?? '';
143
- if (FILTER_FLAGS.has(arg)) {
144
- return (args[index + 1] ?? '') === '' ? undefined : 2;
53
+ if (isAssignment) {
54
+ return arg.slice(arg.indexOf('=') + 1) === '' ? undefined : 1;
145
55
  }
146
- const booleanFlag = BOOLEAN_FLAGS.get(arg);
147
- if (booleanFlag !== undefined) {
148
- return booleanFlag === 'shouldShowHelp' || booleanFlag === 'shouldShowVersion' ? undefined : 1;
149
- }
150
- const valueArgument = readOutputStyleArgument(args, index) ?? readDelegateArgument(args, index);
151
- return valueArgument?.ok === true ? valueArgument.consumedCount : undefined;
152
- }
153
- function readOutputStyleArgument(args, index) {
154
- return readFlagValue(args, index, OUTPUT_STYLE_FLAG, 'a value argument: auto, plain, or rich');
56
+ return (args[index + 1] ?? '') === '' ? undefined : 2;
155
57
  }
156
58
  function splitMisplacedFlags(passthrough) {
157
59
  const flagNames = new Set();
@@ -3,7 +3,7 @@ import { type StreamStyles } from '@williamthorsen/nmr-core';
3
3
  import type { TreeSnapshot } from './check-cache.js';
4
4
  import type { ResolvedContext } from './context.js';
5
5
  import type { DelegationTarget } from './delegation-target.js';
6
- import type { ParsedArgs } from './invocation-args.js';
6
+ import type { ParsedArgs } from './nmr-flags.js';
7
7
  import type { ReportFormat } from './report-format.js';
8
8
  import type { RunStepsOptions } from './runner.js';
9
9
  import type { Step } from './steps.js';
@@ -64,4 +64,6 @@ export interface RunPlan {
64
64
  export type SettledResult = RunCliResult & {
65
65
  isUnknownCommand?: boolean;
66
66
  };
67
+ /** The exit status of an invocation that nmr refuses as written, which matches the router's for a usage error. */
68
+ export declare const USAGE_EXIT_CODE = 2;
67
69
  export declare const VERSION: string;
@@ -1,3 +1,4 @@
1
1
  import { resolveSelfVersion } from '@williamthorsen/nmr-core';
2
2
  export const RUN_IF_PRESENT_ENV_VAR = 'NMR_RUN_IF_PRESENT';
3
+ export const USAGE_EXIT_CODE = 2;
3
4
  export const VERSION = resolveSelfVersion(import.meta.url);
@@ -4,8 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import process from 'node:process';
6
6
  import { setTimeout as delay } from 'node:timers/promises';
7
- import { STATUS_GLYPHS } from '@williamthorsen/nmr-core';
8
- import { isObject } from "./helpers/type-guards.js";
7
+ import { isRecord, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
9
8
  const RECLAIM_GUARD = 'heavy.lock.reclaim';
10
9
  const RECLAIM_GUARD_STALE_AFTER_MS = 10_000;
11
10
  export const HEARTBEAT_INTERVAL_MS = 10_000;
@@ -133,7 +132,7 @@ function formatWaitingLine(holder, lockPath) {
133
132
  return `nmr: waiting for ${holder.command} in ${holder.dir} (pid ${holder.pid}) to finish`;
134
133
  }
135
134
  function isHolderRecord(value) {
136
- return (isObject(value) &&
135
+ return (isRecord(value) &&
137
136
  typeof value['command'] === 'string' &&
138
137
  typeof value['dir'] === 'string' &&
139
138
  typeof value['pid'] === 'number' &&
@@ -173,7 +172,7 @@ function parseHolder(content) {
173
172
  }
174
173
  }
175
174
  function readErrorCode(error) {
176
- return isObject(error) && typeof error['code'] === 'string' ? error['code'] : undefined;
175
+ return isRecord(error) && typeof error['code'] === 'string' ? error['code'] : undefined;
177
176
  }
178
177
  async function readLockState(lockPath) {
179
178
  try {
@@ -0,0 +1,109 @@
1
+ import { type ParsedFlags } from '@williamthorsen/nmr-core/cli';
2
+ /** nmr's own flags, each read only before the command name. */
3
+ export declare const NMR_FLAGS: {
4
+ delegate: {
5
+ type: "boolean";
6
+ description: string;
7
+ };
8
+ delegateTo: {
9
+ type: "string";
10
+ description: string;
11
+ valueHint: string;
12
+ };
13
+ filter: {
14
+ type: "string";
15
+ description: string;
16
+ short: string;
17
+ valueHint: string;
18
+ };
19
+ json: {
20
+ type: "boolean";
21
+ description: string;
22
+ };
23
+ log: {
24
+ type: "boolean";
25
+ description: string;
26
+ };
27
+ noCache: {
28
+ type: "boolean";
29
+ description: string;
30
+ };
31
+ noDelegate: {
32
+ type: "boolean";
33
+ description: string;
34
+ };
35
+ noLock: {
36
+ type: "boolean";
37
+ description: string;
38
+ };
39
+ outputStyle: {
40
+ type: "string";
41
+ description: string;
42
+ valueHint: string;
43
+ };
44
+ quiet: {
45
+ type: "boolean";
46
+ description: string;
47
+ short: string;
48
+ };
49
+ recursive: {
50
+ type: "boolean";
51
+ description: string;
52
+ short: string;
53
+ };
54
+ workspaceRoot: {
55
+ type: "boolean";
56
+ description: string;
57
+ short: string;
58
+ };
59
+ };
60
+ /** The key of one of nmr's flags. */
61
+ export type NmrFlagKey = keyof typeof NMR_FLAGS;
62
+ /** @internal */
63
+ export interface ParsedArgs {
64
+ /** The comma-separated hosts that `--delegate-to` names, as written. */
65
+ delegateHosts?: string;
66
+ filter?: string;
67
+ isWorkspaceRoot: boolean;
68
+ /** The flag's value as it was written, which the style resolver rather than the parser narrows. */
69
+ outputStyle?: string;
70
+ quiet: boolean;
71
+ recursive: boolean;
72
+ shouldBypassCache: boolean;
73
+ shouldBypassLock: boolean;
74
+ /** Set by `--delegate`, which leaves the hosts to `NMR_DELEGATE` or the user config. */
75
+ shouldDelegate: boolean;
76
+ shouldEmitJson: boolean;
77
+ shouldPrintLog: boolean;
78
+ shouldRunLocally: boolean;
79
+ shouldShowHelp: boolean;
80
+ shouldShowVersion: boolean;
81
+ command?: string;
82
+ passthrough: string[];
83
+ }
84
+ /** What the arguments ask of nmr, or that its flags do not parse. */
85
+ export type FlagReading = {
86
+ ok: true;
87
+ parsedArgs: ParsedArgs;
88
+ } | {
89
+ ok: false;
90
+ };
91
+ /** Returns the `--long` spelling of one of nmr's flags. */
92
+ export declare function formatFlagSpelling(key: NmrFlagKey): string;
93
+ /** Returns the `-x` spelling of one of nmr's flags, or `undefined` when it does not have one. */
94
+ export declare function formatShortSpelling(key: NmrFlagKey): string | undefined;
95
+ /**
96
+ * Reads nmr's flags, the command name, and the arguments passed through to it, without reporting anything.
97
+ *
98
+ * The help and version flags are read here as well, though the router reserves them, so that a caller can tell
99
+ * what the invocation asks for before it builds the command tree. Arguments whose flags do not parse report only
100
+ * that: The router parses the same arguments against the same schema and reports the error itself.
101
+ */
102
+ export declare function readInvocationFlags(args: readonly string[]): FlagReading;
103
+ /**
104
+ * Maps nmr's parsed flags, and the arguments from the command name on, to the fields that the rest of nmr reads.
105
+ * Help is false, since the parser reserves it, and version is false unless the flags include it.
106
+ */
107
+ export declare function toParsedArgs(flags: ParsedFlags<typeof NMR_FLAGS> & {
108
+ version?: boolean;
109
+ }, rest: readonly string[]): ParsedArgs;
@@ -0,0 +1,120 @@
1
+ import { parseArgs, ParseError } from '@williamthorsen/nmr-core/cli';
2
+ export const NMR_FLAGS = {
3
+ delegate: {
4
+ type: 'boolean',
5
+ description: 'Run over SSH on the first free host that NMR_DELEGATE or the user config names',
6
+ },
7
+ delegateTo: {
8
+ type: 'string',
9
+ description: 'Run over SSH on the first free of <hosts>, comma-separated',
10
+ valueHint: 'hosts',
11
+ },
12
+ filter: {
13
+ type: 'string',
14
+ description: 'Run command in packages whose manifest name matches',
15
+ short: 'F',
16
+ valueHint: 'pattern',
17
+ },
18
+ json: { type: 'boolean', description: 'Report one JSON object per command, withholding command output' },
19
+ log: { type: 'boolean', description: 'Print the recorded run instead of running (up to 256 KiB)' },
20
+ noCache: { type: 'boolean', description: 'Run even if this tree already passed; record the result' },
21
+ noDelegate: { type: 'boolean', description: 'Run here, whatever NMR_DELEGATE or the user config says' },
22
+ noLock: { type: 'boolean', description: 'Run without waiting for another heavy command on this machine' },
23
+ outputStyle: {
24
+ type: 'string',
25
+ description: 'Output style: auto (default), plain, or rich',
26
+ valueHint: 'auto|plain|rich',
27
+ },
28
+ quiet: { type: 'boolean', description: "Suppress command output, keeping nmr's verdicts", short: 'q' },
29
+ recursive: { type: 'boolean', description: 'Run command in all packages', short: 'R' },
30
+ workspaceRoot: { type: 'boolean', description: 'Use root scripts, running at the monorepo root', short: 'w' },
31
+ };
32
+ export function formatFlagSpelling(key) {
33
+ return `--${toKebabCase(key)}`;
34
+ }
35
+ export function formatShortSpelling(key) {
36
+ const definition = NMR_FLAGS[key];
37
+ return 'short' in definition ? `-${definition.short}` : undefined;
38
+ }
39
+ export function readInvocationFlags(args) {
40
+ const remaining = [...args];
41
+ let shouldShowHelp = false;
42
+ for (;;) {
43
+ try {
44
+ const parsed = parseArgs(remaining, { flags: READER_FLAGS }, { stopAtPositional: true });
45
+ return { ok: true, parsedArgs: { ...toParsedArgs(parsed.flags, parsed.rest), shouldShowHelp } };
46
+ }
47
+ catch (error) {
48
+ if (!(error instanceof ParseError)) {
49
+ throw error;
50
+ }
51
+ if (error.kind !== 'unknown-flag' ||
52
+ !HELP_SPELLINGS.has(error.token) ||
53
+ !removeHelpFlag(remaining, error.token)) {
54
+ return { ok: false };
55
+ }
56
+ shouldShowHelp = true;
57
+ }
58
+ }
59
+ }
60
+ export function toParsedArgs(flags, rest) {
61
+ const [first, ...remaining] = rest;
62
+ const hasCommand = first !== undefined && first !== '--';
63
+ return {
64
+ ...(flags.delegateTo !== undefined && { delegateHosts: flags.delegateTo }),
65
+ ...(flags.filter !== undefined && { filter: flags.filter }),
66
+ isWorkspaceRoot: flags.workspaceRoot,
67
+ ...(flags.outputStyle !== undefined && { outputStyle: flags.outputStyle }),
68
+ quiet: flags.quiet,
69
+ recursive: flags.recursive,
70
+ shouldBypassCache: flags.noCache,
71
+ shouldBypassLock: flags.noLock,
72
+ shouldDelegate: flags.delegate,
73
+ shouldEmitJson: flags.json,
74
+ shouldPrintLog: flags.log,
75
+ shouldRunLocally: flags.noDelegate,
76
+ shouldShowHelp: false,
77
+ shouldShowVersion: flags.version ?? false,
78
+ ...(hasCommand && { command: first }),
79
+ passthrough: hasCommand ? remaining : [],
80
+ };
81
+ }
82
+ const HELP_SPELLINGS = new Set(['--help', '-h']);
83
+ const READER_FLAGS = {
84
+ ...NMR_FLAGS,
85
+ version: { type: 'boolean', description: 'Print the version', short: 'V' },
86
+ };
87
+ function isBooleanCluster(arg) {
88
+ if (!/^-[^-]./.test(arg)) {
89
+ return false;
90
+ }
91
+ const booleanShorts = new Set(['h', 'V']);
92
+ for (const definition of Object.values(NMR_FLAGS)) {
93
+ if (definition.type === 'boolean' && 'short' in definition) {
94
+ booleanShorts.add(definition.short);
95
+ }
96
+ }
97
+ for (const letter of arg.slice(1)) {
98
+ if (!booleanShorts.has(letter)) {
99
+ return false;
100
+ }
101
+ }
102
+ return true;
103
+ }
104
+ function removeHelpFlag(args, token) {
105
+ const index = args.indexOf(token);
106
+ if (index !== -1) {
107
+ args.splice(index, 1);
108
+ return true;
109
+ }
110
+ const clusterIndex = args.findIndex((arg) => isBooleanCluster(arg) && arg.includes('h'));
111
+ const cluster = args[clusterIndex];
112
+ if (cluster === undefined) {
113
+ return false;
114
+ }
115
+ args[clusterIndex] = cluster.replace('h', '');
116
+ return true;
117
+ }
118
+ function toKebabCase(key) {
119
+ return key.replaceAll(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
120
+ }
@@ -19,7 +19,7 @@ export declare const OUTPUT_STYLE_ENV_VAR = "NMR_OUTPUT_STYLE";
19
19
  *
20
20
  * Only `nmr` accepts it. A standalone bin reads the variable, which `nmr <command>` exports to it.
21
21
  */
22
- export declare const OUTPUT_STYLE_FLAG = "--output-style";
22
+ export declare const OUTPUT_STYLE_FLAG: string;
23
23
  /**
24
24
  * Resolves the style of each output stream: the flag, then `NMR_OUTPUT_STYLE`, then detection from `CI`, the
25
25
  * stream's terminal state, and `TERM`.
@@ -1,9 +1,10 @@
1
1
  import { resolveStreamStyles } from '@williamthorsen/nmr-core';
2
+ import { formatFlagSpelling } from "./nmr-flags.js";
2
3
  export function isTerminalStream(stream) {
3
4
  return 'isTTY' in stream && stream.isTTY === true;
4
5
  }
5
6
  export const OUTPUT_STYLE_ENV_VAR = 'NMR_OUTPUT_STYLE';
6
- export const OUTPUT_STYLE_FLAG = '--output-style';
7
+ export const OUTPUT_STYLE_FLAG = formatFlagSpelling('outputStyle');
7
8
  export function resolveOutputStyles(options) {
8
9
  const { env, flagValue, stderrIsTty, stdoutIsTty } = options;
9
10
  return resolveStreamStyles({
@@ -2,11 +2,11 @@ import { randomUUID } from 'node:crypto';
2
2
  import { writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import process from 'node:process';
5
+ import { isRecord } from '@williamthorsen/nmr-core';
5
6
  import { describeError } from '@williamthorsen/toolbelt.errors';
6
7
  import { isCacheableCommand, PASS_MANIFEST_DIR_ENV_VAR, writeDebugNote } from "./check-cache.js";
7
8
  import { isReplayLine } from "./check-cache-entries.js";
8
9
  import { MAX_EXCERPT_BYTES } from "./helpers/deriveExcerpt.js";
9
- import { isObject } from "./helpers/type-guards.js";
10
10
  export function formatManifestScope(monorepoRoot, anchorDir) {
11
11
  const scope = path.relative(monorepoRoot, anchorDir);
12
12
  return scope === '' ? '.' : scope.split(path.sep).join('/');
@@ -70,7 +70,7 @@ function findRejection(entry, options) {
70
70
  return undefined;
71
71
  }
72
72
  function isPassManifestEntry(value) {
73
- return (isObject(value) &&
73
+ return (isRecord(value) &&
74
74
  typeof value['command'] === 'string' &&
75
75
  typeof value['commandString'] === 'string' &&
76
76
  typeof value['durationMs'] === 'number' &&
@@ -8,7 +8,7 @@ import { type Verdict } from './verdict.js';
8
8
  * output stream resolves to. Reports the first value that cannot be read.
9
9
  *
10
10
  * The style is resolved here rather than beside its use, so that a value that does not name a style is rejected
11
- * at the same point as an unreadable variable, which is ahead of `--version` and of the config load.
11
+ * at the same point as an unreadable variable, which is before any command runs.
12
12
  */
13
13
  export declare function readPresentation(options: {
14
14
  env: NodeJS.ProcessEnv;
@@ -8,6 +8,8 @@ export interface DelegationHolder {
8
8
  }
9
9
  /** What the host session needs, sent to it as JSON. Every directory is relative to the checkout. */
10
10
  export interface HostSessionParams {
11
+ /** The `maxBuffer` for the session's git calls; the session is sent as source, so it cannot import the constant. */
12
+ gitOutputLimitBytes: number;
11
13
  holder: DelegationHolder;
12
14
  lockPath: string;
13
15
  /** Begins each record line on stderr; it contains a per-delegation nonce, so that a command cannot forge one. */
@@ -160,6 +160,7 @@ export function runHostSession(params) {
160
160
  listing = childProcess.execFileSync('git', ['ls-files', '-z', '--', '*package.json'], {
161
161
  cwd: params.rootDir,
162
162
  encoding: 'utf8',
163
+ maxBuffer: params.gitOutputLimitBytes,
163
164
  stdio: ['ignore', 'pipe', 'ignore'],
164
165
  });
165
166
  }