@williamthorsen/nmr 0.48.0 → 0.50.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 (111) hide show
  1. package/.meta/changelog.json +218 -0
  2. package/.readyup/kits/default.js +1527 -1460
  3. package/.readyup/manifest.json +96 -11
  4. package/CHANGELOG.md +65 -0
  5. package/README.md +19 -8
  6. package/agents/guidance/rulebooks/nmr.md +2 -2
  7. package/dist/esm/check-cache-build-output.d.ts +24 -0
  8. package/dist/esm/check-cache-build-output.js +49 -0
  9. package/dist/esm/check-cache-delegation.d.ts +27 -0
  10. package/dist/esm/check-cache-delegation.js +92 -0
  11. package/dist/esm/check-cache-entries.d.ts +109 -0
  12. package/dist/esm/check-cache-entries.js +87 -0
  13. package/dist/esm/check-cache-gate.d.ts +64 -0
  14. package/dist/esm/check-cache-gate.js +226 -0
  15. package/dist/esm/check-cache-key.d.ts +47 -0
  16. package/dist/esm/check-cache-key.js +77 -0
  17. package/dist/esm/check-cache-record.d.ts +69 -0
  18. package/dist/esm/check-cache-record.js +167 -0
  19. package/dist/esm/check-cache.d.ts +8 -176
  20. package/dist/esm/check-cache.js +9 -214
  21. package/dist/esm/child-env.d.ts +57 -0
  22. package/dist/esm/child-env.js +59 -0
  23. package/dist/esm/child-process.d.ts +55 -0
  24. package/dist/esm/child-process.js +112 -0
  25. package/dist/esm/commands/build.d.ts +0 -16
  26. package/dist/esm/commands/build.js +9 -214
  27. package/dist/esm/commands/clean.js +3 -2
  28. package/dist/esm/commands/compiler.d.ts +18 -0
  29. package/dist/esm/commands/compiler.js +49 -0
  30. package/dist/esm/commands/fmt.d.ts +0 -39
  31. package/dist/esm/commands/fmt.js +4 -81
  32. package/dist/esm/commands/format-targets.d.ts +39 -0
  33. package/dist/esm/commands/format-targets.js +83 -0
  34. package/dist/esm/commands/rewrite-specifiers.d.ts +17 -0
  35. package/dist/esm/commands/rewrite-specifiers.js +122 -0
  36. package/dist/esm/commands/tsconfig-chain.d.ts +8 -0
  37. package/dist/esm/commands/tsconfig-chain.js +63 -0
  38. package/dist/esm/config-check-cache.d.ts +17 -0
  39. package/dist/esm/config-check-cache.js +63 -0
  40. package/dist/esm/config-validation.d.ts +20 -0
  41. package/dist/esm/config-validation.js +236 -0
  42. package/dist/esm/config.js +4 -286
  43. package/dist/esm/crossing-diagnostics.d.ts +39 -0
  44. package/dist/esm/crossing-diagnostics.js +132 -0
  45. package/dist/esm/default-scripts.js +1 -1
  46. package/dist/esm/delegate-host.d.ts +43 -0
  47. package/dist/esm/delegate-host.js +191 -0
  48. package/dist/esm/delegation-target.d.ts +52 -0
  49. package/dist/esm/delegation-target.js +71 -0
  50. package/dist/esm/fan-out.d.ts +57 -0
  51. package/dist/esm/fan-out.js +146 -0
  52. package/dist/esm/glyphs.d.ts +1 -1
  53. package/dist/esm/help.js +10 -3
  54. package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
  55. package/dist/esm/helpers/deriveExcerpt.js +1 -1
  56. package/dist/esm/helpers/renderQuotedList.d.ts +2 -0
  57. package/dist/esm/helpers/renderQuotedList.js +9 -0
  58. package/dist/esm/hooks.d.ts +17 -0
  59. package/dist/esm/hooks.js +23 -0
  60. package/dist/esm/invocation-args.d.ts +40 -0
  61. package/dist/esm/invocation-args.js +178 -0
  62. package/dist/esm/invocation.d.ts +67 -0
  63. package/dist/esm/invocation.js +3 -0
  64. package/dist/esm/machine-lock.d.ts +2 -0
  65. package/dist/esm/machine-lock.js +1 -1
  66. package/dist/esm/output-channel.d.ts +23 -0
  67. package/dist/esm/output-channel.js +14 -0
  68. package/dist/esm/pass-manifest.d.ts +53 -0
  69. package/dist/esm/pass-manifest.js +89 -0
  70. package/dist/esm/presentation.d.ts +47 -0
  71. package/dist/esm/presentation.js +32 -0
  72. package/dist/esm/recording.d.ts +1 -1
  73. package/dist/esm/recording.js +2 -2
  74. package/dist/esm/remote-host-session.d.ts +62 -0
  75. package/dist/esm/remote-host-session.js +277 -0
  76. package/dist/esm/remote-probe-host.d.ts +22 -0
  77. package/dist/esm/remote-probe-host.js +50 -0
  78. package/dist/esm/remote-probe.d.ts +63 -0
  79. package/dist/esm/remote-probe.js +88 -0
  80. package/dist/esm/remote-run.d.ts +45 -65
  81. package/dist/esm/remote-run.js +128 -162
  82. package/dist/esm/remote-session.d.ts +40 -0
  83. package/dist/esm/remote-session.js +151 -0
  84. package/dist/esm/remote-snapshot.d.ts +21 -0
  85. package/dist/esm/remote-snapshot.js +80 -0
  86. package/dist/esm/replay-assembly.d.ts +2 -2
  87. package/dist/esm/replay-assembly.js +1 -1
  88. package/dist/esm/run-chain.d.ts +21 -0
  89. package/dist/esm/run-chain.js +108 -0
  90. package/dist/esm/runCli.d.ts +1 -9
  91. package/dist/esm/runCli.js +39 -1002
  92. package/dist/esm/runner.d.ts +1 -54
  93. package/dist/esm/runner.js +2 -125
  94. package/dist/esm/shell-syntax.d.ts +30 -0
  95. package/dist/esm/shell-syntax.js +120 -0
  96. package/dist/esm/steps.d.ts +0 -2
  97. package/dist/esm/steps.js +3 -119
  98. package/dist/esm/types.d.ts +5 -0
  99. package/dist/esm/user-config.d.ts +34 -0
  100. package/dist/esm/user-config.js +93 -0
  101. package/dist/esm/verbosity.d.ts +6 -0
  102. package/dist/esm/verbosity.js +7 -7
  103. package/dist/esm/verdict.d.ts +1 -1
  104. package/dist/esm/verdict.js +1 -1
  105. package/docs/check-cache.md +28 -12
  106. package/docs/delegate.md +103 -17
  107. package/docs/reporting.md +6 -1
  108. package/docs/scripts.md +2 -1
  109. package/package.json +4 -4
  110. /package/dist/esm/{prettier-default-config.d.ts → prettier-default.config.d.ts} +0 -0
  111. /package/dist/esm/{prettier-default-config.js → prettier-default.config.js} +0 -0
@@ -0,0 +1,236 @@
1
+ import { assertResolvableCheckCacheCommands } from "./config-check-cache.js";
2
+ import { isObject, isStringRecord } from "./helpers/type-guards.js";
3
+ import { findUnexpressibleToken } from "./steps.js";
4
+ import { UserError } from "./UserError.js";
5
+ import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
6
+ export const CONFIG_TIERS = {
7
+ root: {
8
+ label: 'a monorepo-root config',
9
+ honoredKeys: ['checkCache', 'delegate', 'devBin', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
10
+ elsewhere: "the package's own config",
11
+ },
12
+ workspace: {
13
+ label: 'a package config',
14
+ honoredKeys: ['build'],
15
+ elsewhere: 'the monorepo-root config',
16
+ },
17
+ };
18
+ const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
19
+ const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
20
+ const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
21
+ const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
22
+ const RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ['exempt'];
23
+ const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments', 'shouldRunInBackground'];
24
+ const RETIRED_STEP_KEYS = new Map([['declinesArgs', 'shouldDeclineArguments']]);
25
+ export function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
26
+ const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
27
+ if (unrecognizedKeys.length === 0) {
28
+ return;
29
+ }
30
+ throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? 'key' : 'keys'} ` +
31
+ `${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
32
+ }
33
+ export function validateConfig(value, configPath, baseDir) {
34
+ if (!isObject(value)) {
35
+ throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
36
+ }
37
+ assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
38
+ const config = {};
39
+ const build = validateBuildField(value, configPath);
40
+ if (build)
41
+ config.build = build;
42
+ const checkCache = validateCheckCacheField(value, configPath);
43
+ if (checkCache)
44
+ config.checkCache = checkCache;
45
+ if (value['delegate'] !== undefined)
46
+ config.delegate = validateDelegateField(value['delegate'], configPath);
47
+ const devBin = validateStringRecordField(value, 'devBin', configPath);
48
+ if (devBin)
49
+ config.devBin = devBin;
50
+ const output = validateOutputField(value, configPath);
51
+ if (output)
52
+ config.output = output;
53
+ const workspaceScripts = validateScriptField(value, 'workspaceScripts', configPath);
54
+ if (workspaceScripts)
55
+ config.workspaceScripts = workspaceScripts;
56
+ const rootScripts = validateScriptField(value, 'rootScripts', configPath);
57
+ if (rootScripts)
58
+ config.rootScripts = rootScripts;
59
+ const selfHostedRunner = validateSelfHostedRunnerField(value, configPath);
60
+ if (selfHostedRunner)
61
+ config.selfHostedRunner = selfHostedRunner;
62
+ assertResolvableCheckCacheCommands(config, configPath, baseDir);
63
+ return config;
64
+ }
65
+ function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
66
+ for (const key of Object.keys(value)) {
67
+ const replacement = retiredKeys.get(key);
68
+ if (replacement === undefined)
69
+ continue;
70
+ throw new UserError(`Invalid nmr config at ${configPath}: \`${prefix}${key}\` was renamed to \`${prefix}${replacement}\`.`);
71
+ }
72
+ }
73
+ function assertValidElements(scripts, fieldName, configPath) {
74
+ for (const [command, script] of Object.entries(scripts)) {
75
+ if (typeof script === 'string')
76
+ continue;
77
+ for (const spec of script) {
78
+ if (typeof spec !== 'string') {
79
+ assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
80
+ assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
81
+ }
82
+ const element = typeof spec === 'string' ? spec : spec.run;
83
+ const token = findUnexpressibleToken(element);
84
+ if (token === undefined)
85
+ continue;
86
+ throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` contains ` +
87
+ `\`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and ` +
88
+ `reference that name here.`);
89
+ }
90
+ }
91
+ }
92
+ function formatKeyList(keys, prefix) {
93
+ return keys
94
+ .toSorted()
95
+ .map((key) => `\`${prefix}${key}\``)
96
+ .join(', ');
97
+ }
98
+ function isOptionalBoolean(value) {
99
+ return value === undefined || typeof value === 'boolean';
100
+ }
101
+ function isScriptRecord(value) {
102
+ if (!isObject(value))
103
+ return false;
104
+ for (const v of Object.values(value)) {
105
+ if (typeof v === 'string')
106
+ continue;
107
+ if (!Array.isArray(v) || !v.every(isScriptElement))
108
+ return false;
109
+ }
110
+ return true;
111
+ }
112
+ function isScriptElement(value) {
113
+ if (typeof value === 'string')
114
+ return true;
115
+ if (!isObject(value) || typeof value['run'] !== 'string')
116
+ return false;
117
+ return isOptionalBoolean(value['shouldDeclineArguments']) && isOptionalBoolean(value['shouldRunInBackground']);
118
+ }
119
+ function isStringArray(value) {
120
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
121
+ }
122
+ function validateBuildField(value, configPath) {
123
+ const build = value['build'];
124
+ if (build === undefined) {
125
+ return undefined;
126
+ }
127
+ if (!isObject(build)) {
128
+ throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
129
+ }
130
+ assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, 'build.');
131
+ const extraIgnorePatterns = build['extraIgnorePatterns'];
132
+ if (extraIgnorePatterns !== undefined && !isStringArray(extraIgnorePatterns)) {
133
+ throw new UserError(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
134
+ }
135
+ return extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns };
136
+ }
137
+ function validateCheckCacheField(value, configPath) {
138
+ const checkCache = value['checkCache'];
139
+ if (checkCache === undefined) {
140
+ return undefined;
141
+ }
142
+ if (!isObject(checkCache)) {
143
+ throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache\` must be an object`);
144
+ }
145
+ assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
146
+ const config = {};
147
+ const enabledValue = checkCache['enabled'];
148
+ if (enabledValue !== undefined) {
149
+ if (typeof enabledValue !== 'boolean') {
150
+ throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
151
+ }
152
+ config.enabled = enabledValue;
153
+ }
154
+ for (const field of ['excludeCommands', 'extraCommands']) {
155
+ const commands = checkCache[field];
156
+ if (commands === undefined) {
157
+ continue;
158
+ }
159
+ if (!isStringArray(commands)) {
160
+ throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${field}\` must be a string[]`);
161
+ }
162
+ config[field] = commands;
163
+ }
164
+ return config;
165
+ }
166
+ function validateDelegateField(delegate, configPath) {
167
+ if (delegate === false) {
168
+ return false;
169
+ }
170
+ throw new UserError(`Invalid nmr config at ${configPath}: \`delegate\` accepts only \`false\`, which keeps the repo's checks on the ` +
171
+ 'machine that has the tree. Hosts and `when` belong in the user config, `~/.config/nmr/config.ts`.');
172
+ }
173
+ function validateOutputField(value, configPath) {
174
+ const output = value['output'];
175
+ if (output === undefined) {
176
+ return undefined;
177
+ }
178
+ if (!isObject(output)) {
179
+ throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
180
+ }
181
+ assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, 'output.');
182
+ const config = {};
183
+ const commandVerbosity = output['commandVerbosity'];
184
+ if (commandVerbosity !== undefined) {
185
+ if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
186
+ const renderedValue = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
187
+ throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`', renderedValue)}`);
188
+ }
189
+ config.commandVerbosity = commandVerbosity;
190
+ }
191
+ const extraAgentEnvVars = output['extraAgentEnvVars'];
192
+ if (extraAgentEnvVars !== undefined) {
193
+ if (!isStringArray(extraAgentEnvVars)) {
194
+ throw new UserError(`Invalid nmr config at ${configPath}: \`output.extraAgentEnvVars\` must be a string[]`);
195
+ }
196
+ config.extraAgentEnvVars = extraAgentEnvVars;
197
+ }
198
+ return config;
199
+ }
200
+ function validateSelfHostedRunnerField(value, configPath) {
201
+ const selfHostedRunner = value['selfHostedRunner'];
202
+ if (selfHostedRunner === undefined) {
203
+ return undefined;
204
+ }
205
+ if (!isObject(selfHostedRunner)) {
206
+ throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner\` must be an object`);
207
+ }
208
+ assertRecognizedKeys(selfHostedRunner, RECOGNIZED_SELF_HOSTED_RUNNER_KEYS, configPath, 'selfHostedRunner.');
209
+ const exempt = selfHostedRunner['exempt'];
210
+ if (typeof exempt !== 'string' || exempt.trim() === '') {
211
+ throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner.exempt\` must be a non-empty string giving the reason`);
212
+ }
213
+ return { exempt };
214
+ }
215
+ function validateScriptField(value, fieldName, configPath) {
216
+ if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
217
+ return undefined;
218
+ }
219
+ const scripts = value[fieldName];
220
+ if (!isScriptRecord(scripts)) {
221
+ throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
222
+ 'where an element is a command string or ' +
223
+ '`{ run: string, shouldDeclineArguments?: boolean, shouldRunInBackground?: boolean }`');
224
+ }
225
+ assertValidElements(scripts, fieldName, configPath);
226
+ return scripts;
227
+ }
228
+ function validateStringRecordField(value, fieldName, configPath) {
229
+ if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
230
+ return undefined;
231
+ }
232
+ if (!isStringRecord(value[fieldName])) {
233
+ throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string>`);
234
+ }
235
+ return value[fieldName];
236
+ }
@@ -8,279 +8,19 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import { existsSync } from 'node:fs';
10
10
  import { pathToFileURL } from 'node:url';
11
+ import { CONFIG_TIERS, validateConfig } from "./config-validation.js";
11
12
  import { resolveConfigPath } from "./helpers/config-path.js";
12
13
  import { escapeControlCharacters } from "./helpers/escapeControlCharacters.js";
13
- import { findClosestName } from "./helpers/findClosestName.js";
14
14
  import { isHookName } from "./helpers/hook-name.js";
15
- import { isObject, isStringRecord } from "./helpers/type-guards.js";
15
+ import { isObject } from "./helpers/type-guards.js";
16
16
  import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
17
- import { buildRootRegistry, buildWorkspaceRegistry, describeScript, readPackageJsonScripts } from "./resolver.js";
18
- import { findUnexpressibleToken, readSelfReference, readStepListSelfReference, } from "./steps.js";
17
+ import { describeScript } from "./resolver.js";
18
+ import { readSelfReference, readStepListSelfReference } from "./steps.js";
19
19
  import { UserError } from "./UserError.js";
20
- import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
21
- import { getWorkspacePackageDirs, isMonorepoRoot } from "./workspace.js";
22
- const CONFIG_TIERS = {
23
- root: {
24
- label: 'a monorepo-root config',
25
- honoredKeys: ['checkCache', 'devBin', 'output', 'rootScripts', 'selfHostedRunner', 'workspaceScripts'],
26
- elsewhere: "the package's own config",
27
- },
28
- workspace: {
29
- label: 'a package config',
30
- honoredKeys: ['build'],
31
- elsewhere: 'the monorepo-root config',
32
- },
33
- };
34
- const RECOGNIZED_KEYS = [...CONFIG_TIERS.root.honoredKeys, ...CONFIG_TIERS.workspace.honoredKeys];
35
- const RECOGNIZED_BUILD_KEYS = ['extraIgnorePatterns'];
36
- const RECOGNIZED_CHECK_CACHE_KEYS = ['enabled', 'excludeCommands', 'extraCommands'];
37
- const RECOGNIZED_OUTPUT_KEYS = ['commandVerbosity', 'extraAgentEnvVars'];
38
- const RECOGNIZED_SELF_HOSTED_RUNNER_KEYS = ['exempt'];
39
- const RECOGNIZED_STEP_KEYS = ['run', 'shouldDeclineArguments', 'shouldRunInBackground'];
40
20
  const SCRIPT_FIELDS = [
41
21
  { field: 'rootScripts', scope: 'root' },
42
22
  { field: 'workspaceScripts', scope: 'workspaceRegistry' },
43
23
  ];
44
- const RETIRED_STEP_KEYS = new Map([['declinesArgs', 'shouldDeclineArguments']]);
45
- function isScriptRecord(value) {
46
- if (!isObject(value))
47
- return false;
48
- for (const v of Object.values(value)) {
49
- if (typeof v === 'string')
50
- continue;
51
- if (!Array.isArray(v) || !v.every(isScriptElement))
52
- return false;
53
- }
54
- return true;
55
- }
56
- function isScriptElement(value) {
57
- if (typeof value === 'string')
58
- return true;
59
- if (!isObject(value) || typeof value['run'] !== 'string')
60
- return false;
61
- return isOptionalBoolean(value['shouldDeclineArguments']) && isOptionalBoolean(value['shouldRunInBackground']);
62
- }
63
- function validateScriptField(value, fieldName, configPath) {
64
- if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
65
- return undefined;
66
- }
67
- const scripts = value[fieldName];
68
- if (!isScriptRecord(scripts)) {
69
- throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string | element[]>, ` +
70
- 'where an element is a command string or ' +
71
- '`{ run: string, shouldDeclineArguments?: boolean, shouldRunInBackground?: boolean }`');
72
- }
73
- assertValidElements(scripts, fieldName, configPath);
74
- return scripts;
75
- }
76
- function assertValidElements(scripts, fieldName, configPath) {
77
- for (const [command, script] of Object.entries(scripts)) {
78
- if (typeof script === 'string')
79
- continue;
80
- for (const spec of script) {
81
- if (typeof spec !== 'string') {
82
- assertNoRetiredKeys(spec, RETIRED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
83
- assertRecognizedKeys(spec, RECOGNIZED_STEP_KEYS, configPath, `${fieldName}.${command}.`);
84
- }
85
- const element = typeof spec === 'string' ? spec : spec.run;
86
- const token = findUnexpressibleToken(element);
87
- if (token === undefined)
88
- continue;
89
- throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}.${command}\` element \`${element}\` contains ` +
90
- `\`${token}\`, which is neither a command name nor an nmr flag. Name it as a script of its own and ` +
91
- `reference that name here.`);
92
- }
93
- }
94
- }
95
- function isOptionalBoolean(value) {
96
- return value === undefined || typeof value === 'boolean';
97
- }
98
- function isStringArray(value) {
99
- return Array.isArray(value) && value.every((item) => typeof item === 'string');
100
- }
101
- function validateBuildField(value, configPath) {
102
- const build = value['build'];
103
- if (build === undefined) {
104
- return undefined;
105
- }
106
- if (!isObject(build)) {
107
- throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
108
- }
109
- assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, 'build.');
110
- const extraIgnorePatterns = build['extraIgnorePatterns'];
111
- if (extraIgnorePatterns !== undefined && !isStringArray(extraIgnorePatterns)) {
112
- throw new UserError(`Invalid nmr config at ${configPath}: \`build.extraIgnorePatterns\` must be a string[]`);
113
- }
114
- return extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns };
115
- }
116
- function validateCheckCacheField(value, configPath) {
117
- const checkCache = value['checkCache'];
118
- if (checkCache === undefined) {
119
- return undefined;
120
- }
121
- if (!isObject(checkCache)) {
122
- throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache\` must be an object`);
123
- }
124
- assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
125
- const config = {};
126
- const enabledValue = checkCache['enabled'];
127
- if (enabledValue !== undefined) {
128
- if (typeof enabledValue !== 'boolean') {
129
- throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.enabled\` must be a boolean`);
130
- }
131
- config.enabled = enabledValue;
132
- }
133
- for (const field of ['excludeCommands', 'extraCommands']) {
134
- const commands = checkCache[field];
135
- if (commands === undefined) {
136
- continue;
137
- }
138
- if (!isStringArray(commands)) {
139
- throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${field}\` must be a string[]`);
140
- }
141
- config[field] = commands;
142
- }
143
- return config;
144
- }
145
- function validateSelfHostedRunnerField(value, configPath) {
146
- const selfHostedRunner = value['selfHostedRunner'];
147
- if (selfHostedRunner === undefined) {
148
- return undefined;
149
- }
150
- if (!isObject(selfHostedRunner)) {
151
- throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner\` must be an object`);
152
- }
153
- assertRecognizedKeys(selfHostedRunner, RECOGNIZED_SELF_HOSTED_RUNNER_KEYS, configPath, 'selfHostedRunner.');
154
- const exempt = selfHostedRunner['exempt'];
155
- if (typeof exempt !== 'string' || exempt.trim() === '') {
156
- throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner.exempt\` must be a non-empty string giving the reason`);
157
- }
158
- return { exempt };
159
- }
160
- function assertResolvableCheckCacheCommands(config, configPath, baseDir) {
161
- const { checkCache } = config;
162
- if (checkCache === undefined || !isMonorepoRoot(baseDir)) {
163
- return;
164
- }
165
- const fields = ['excludeCommands', 'extraCommands'];
166
- const entries = fields.flatMap((field) => (checkCache[field] ?? []).map((command) => ({ command, field })));
167
- const hook = entries.find(({ command }) => isHookName(command));
168
- if (hook !== undefined) {
169
- throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${hook.field}\` names the hook \`${hook.command}\`, ` +
170
- 'which is never gated on its own: It runs as part of the chain of the command that it wraps.');
171
- }
172
- const registeredNames = new Set([
173
- ...Object.keys(buildRootRegistry(config, baseDir)),
174
- ...Object.keys(buildWorkspaceRegistry(config)),
175
- ]);
176
- const missingEntries = entries.filter(({ command }) => !registeredNames.has(command));
177
- if (missingEntries.length === 0) {
178
- return;
179
- }
180
- const declaredNames = readDeclaredScriptNames(baseDir);
181
- const unresolvableEntry = missingEntries.find(({ command }) => !declaredNames.has(command));
182
- if (unresolvableEntry === undefined) {
183
- return;
184
- }
185
- const closestName = findClosestName(unresolvableEntry.command, [...registeredNames, ...declaredNames]);
186
- throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache.${unresolvableEntry.field}\` does not name any command: ` +
187
- `\`${unresolvableEntry.command}\`.${closestName === undefined ? '' : ` Did you mean \`${closestName}\`?`}`);
188
- }
189
- function readDeclaredScriptNames(monorepoRoot) {
190
- let packageDirs;
191
- try {
192
- packageDirs = getWorkspacePackageDirs(monorepoRoot);
193
- }
194
- catch {
195
- packageDirs = [];
196
- }
197
- const names = new Set();
198
- const dirs = [monorepoRoot, ...packageDirs];
199
- for (const dir of dirs) {
200
- const scripts = readPackageScripts(dir);
201
- for (const name of Object.keys(scripts)) {
202
- names.add(name);
203
- }
204
- }
205
- return names;
206
- }
207
- function readPackageScripts(packageDir) {
208
- try {
209
- return readPackageJsonScripts(packageDir) ?? {};
210
- }
211
- catch (error) {
212
- if (error instanceof UserError) {
213
- return {};
214
- }
215
- throw error;
216
- }
217
- }
218
- function validateOutputField(value, configPath) {
219
- const output = value['output'];
220
- if (output === undefined) {
221
- return undefined;
222
- }
223
- if (!isObject(output)) {
224
- throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
225
- }
226
- assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, 'output.');
227
- const config = {};
228
- const commandVerbosity = output['commandVerbosity'];
229
- if (commandVerbosity !== undefined) {
230
- if (typeof commandVerbosity !== 'string' || !isCommandVerbosity(commandVerbosity)) {
231
- const renderedValue = typeof commandVerbosity === 'string' ? commandVerbosity : JSON.stringify(commandVerbosity);
232
- throw new UserError(`Invalid nmr config at ${configPath}: ${formatVerbosityRejection('`output.commandVerbosity`', renderedValue)}`);
233
- }
234
- config.commandVerbosity = commandVerbosity;
235
- }
236
- const extraAgentEnvVars = output['extraAgentEnvVars'];
237
- if (extraAgentEnvVars !== undefined) {
238
- if (!isStringArray(extraAgentEnvVars)) {
239
- throw new UserError(`Invalid nmr config at ${configPath}: \`output.extraAgentEnvVars\` must be a string[]`);
240
- }
241
- config.extraAgentEnvVars = extraAgentEnvVars;
242
- }
243
- return config;
244
- }
245
- function validateStringRecordField(value, fieldName, configPath) {
246
- if (!Object.hasOwn(value, fieldName) || value[fieldName] === undefined) {
247
- return undefined;
248
- }
249
- if (!isStringRecord(value[fieldName])) {
250
- throw new UserError(`Invalid nmr config at ${configPath}: \`${fieldName}\` must be a Record<string, string>`);
251
- }
252
- return value[fieldName];
253
- }
254
- function validateConfig(value, configPath, baseDir) {
255
- if (!isObject(value)) {
256
- throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
257
- }
258
- assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
259
- const config = {};
260
- const build = validateBuildField(value, configPath);
261
- if (build)
262
- config.build = build;
263
- const checkCache = validateCheckCacheField(value, configPath);
264
- if (checkCache)
265
- config.checkCache = checkCache;
266
- const devBin = validateStringRecordField(value, 'devBin', configPath);
267
- if (devBin)
268
- config.devBin = devBin;
269
- const output = validateOutputField(value, configPath);
270
- if (output)
271
- config.output = output;
272
- const workspaceScripts = validateScriptField(value, 'workspaceScripts', configPath);
273
- if (workspaceScripts)
274
- config.workspaceScripts = workspaceScripts;
275
- const rootScripts = validateScriptField(value, 'rootScripts', configPath);
276
- if (rootScripts)
277
- config.rootScripts = rootScripts;
278
- const selfHostedRunner = validateSelfHostedRunnerField(value, configPath);
279
- if (selfHostedRunner)
280
- config.selfHostedRunner = selfHostedRunner;
281
- assertResolvableCheckCacheCommands(config, configPath, baseDir);
282
- return config;
283
- }
284
24
  export async function loadConfig(baseDir) {
285
25
  const configPath = resolveConfigPath(baseDir);
286
26
  if (!existsSync(configPath)) {
@@ -302,14 +42,6 @@ export async function loadWorkspaceConfig(packageDir) {
302
42
  assertTierKeys(config, CONFIG_TIERS.workspace, resolveConfigPath(packageDir));
303
43
  return config;
304
44
  }
305
- function assertNoRetiredKeys(value, retiredKeys, configPath, prefix) {
306
- for (const key of Object.keys(value)) {
307
- const replacement = retiredKeys.get(key);
308
- if (replacement === undefined)
309
- continue;
310
- throw new UserError(`Invalid nmr config at ${configPath}: \`${prefix}${key}\` was renamed to \`${prefix}${replacement}\`.`);
311
- }
312
- }
313
45
  function assertNoSelfReferences(config, configPath, monorepoRoot) {
314
46
  for (const { field, scope } of SCRIPT_FIELDS) {
315
47
  const scripts = config[field] ?? {};
@@ -334,14 +66,6 @@ function assertNoSelfReferences(config, configPath, monorepoRoot) {
334
66
  }
335
67
  }
336
68
  }
337
- function assertRecognizedKeys(value, recognizedKeys, configPath, prefix = '') {
338
- const unrecognizedKeys = Object.keys(value).filter((key) => !recognizedKeys.includes(key));
339
- if (unrecognizedKeys.length === 0) {
340
- return;
341
- }
342
- throw new UserError(`Invalid nmr config at ${configPath}: unrecognized ${unrecognizedKeys.length === 1 ? 'key' : 'keys'} ` +
343
- `${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
344
- }
345
69
  function assertTierKeys(config, tier, configPath) {
346
70
  const unsupportedKeys = Object.keys(config).filter((key) => !tier.honoredKeys.includes(key));
347
71
  if (unsupportedKeys.length === 0) {
@@ -350,12 +74,6 @@ function assertTierKeys(config, tier, configPath) {
350
74
  throw new UserError(`Invalid nmr config at ${configPath}: ${tier.label} honors ${tier.honoredKeys.toSorted().join(', ')} alone, ` +
351
75
  `not ${unsupportedKeys.toSorted().join(', ')}. Move those keys to ${tier.elsewhere}.`);
352
76
  }
353
- function formatKeyList(keys, prefix) {
354
- return keys
355
- .toSorted()
356
- .map((key) => `\`${prefix}${key}\``)
357
- .join(', ');
358
- }
359
77
  function formatSelfReferenceRemedy(options) {
360
78
  const { isDefault, key, selfReference } = options;
361
79
  if (isDefault) {
@@ -0,0 +1,39 @@
1
+ import type { Writable } from 'node:stream';
2
+ import { type OutputStyle } from '@williamthorsen/nmr-core';
3
+ import type { ScriptRegistry } from './resolve-scripts.js';
4
+ import { type ResolvedScript } from './resolver.js';
5
+ import type { NmrConfig } from './types.js';
6
+ /**
7
+ * Throws a `UserError` for a `package.json` entry that chains steps onto a re-invocation of its own command,
8
+ * which resolution discards, leaving those steps to run nowhere. Accepts every other entry in silence.
9
+ *
10
+ * Runs ahead of resolution, so that a command that the registry does not define reports the declaration that
11
+ * names it rather than the name that resolution could not find. A `--log` rejects none, running nothing whose
12
+ * steps could go missing.
13
+ */
14
+ export declare function assertNoSelfReference(options: {
15
+ anchorDir: string;
16
+ command: string;
17
+ isReading: boolean;
18
+ isWorkspaceRoot: boolean;
19
+ monorepoRoot: string;
20
+ registry: ScriptRegistry;
21
+ }): void;
22
+ /**
23
+ * Reports a step that reaches nmr through a shell, when the resolved script contains one.
24
+ *
25
+ * Ahead of the gate, so that a command that usually skips still reports its crossing. A `--log` reports
26
+ * none, running nothing that could cross. Reads the resolved steps rather than the full chain: the line names
27
+ * a declaration to edit, and a hook, a passthrough, and a `devBin` substitution are none.
28
+ */
29
+ export declare function reportNmrCrossing(options: {
30
+ config: NmrConfig;
31
+ isReading: boolean;
32
+ isWorkspaceRoot: boolean;
33
+ monorepoRoot: string;
34
+ registry: ScriptRegistry;
35
+ resolvedScript: ResolvedScript;
36
+ shouldUseRoot: boolean;
37
+ stderr: Writable;
38
+ style: OutputStyle;
39
+ }): void;