@williamthorsen/nmr 0.40.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.meta/changelog.json +17 -0
- package/.readyup/kits/default.js +1 -1
- package/.readyup/manifest.json +2 -2
- package/CHANGELOG.md +6 -0
- package/README.md +3 -10
- package/dist/esm/runCli.js +83 -6
- package/dist/esm/steps.d.ts +2 -0
- package/dist/esm/steps.js +6 -6
- package/package.json +2 -2
package/.meta/changelog.json
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.41.0",
|
|
4
|
+
"date": "2026-10-02",
|
|
5
|
+
"sections": [
|
|
6
|
+
{
|
|
7
|
+
"title": "🎉 Features",
|
|
8
|
+
"audience": "all",
|
|
9
|
+
"items": [
|
|
10
|
+
{
|
|
11
|
+
"description": "Adds a hint to `nmr` that, when a command fails, names any of nmr's own flags written after the command name and shows the corrected invocation, such as `nmr -q check`. (#956)",
|
|
12
|
+
"hash": "21e1759f8eb8257d81bc5bf1e533dbd496d99017",
|
|
13
|
+
"entry": 1
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
2
19
|
{
|
|
3
20
|
"version": "0.40.0",
|
|
4
21
|
"date": "2026-10-01",
|
package/.readyup/kits/default.js
CHANGED
|
@@ -617,7 +617,7 @@ function formatPaths(paths) {
|
|
|
617
617
|
${paths.map((path2) => ` ${path2}`).join("\n")}`;
|
|
618
618
|
}
|
|
619
619
|
function getMinVersion() {
|
|
620
|
-
const pickedFields = { "version": "0.
|
|
620
|
+
const pickedFields = { "version": "0.41.0" };
|
|
621
621
|
if (typeof pickedFields["version"] !== "string") {
|
|
622
622
|
throw new TypeError("nmr/package.json: 'version' must be a string");
|
|
623
623
|
}
|
package/.readyup/manifest.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"path": "kits/default.ts"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
"hash": "
|
|
19
|
+
"hash": "00e9a998",
|
|
20
20
|
"kind": "inline",
|
|
21
21
|
"path": "../package.json",
|
|
22
22
|
"paths": [
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"readyupVersion": "0.38.0",
|
|
50
50
|
"source": "kits/default.ts",
|
|
51
51
|
"sourceHash": "5f7bd316",
|
|
52
|
-
"targetHash": "
|
|
52
|
+
"targetHash": "69936c4f"
|
|
53
53
|
}
|
|
54
54
|
]
|
|
55
55
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.41.0 — 2026-10-02
|
|
6
|
+
|
|
7
|
+
### 🎉 Features
|
|
8
|
+
|
|
9
|
+
- Adds a hint to `nmr` that, when a command fails, names any of nmr's own flags written after the command name and shows the corrected invocation, such as `nmr -q check`. (#956)
|
|
10
|
+
|
|
5
11
|
## 0.40.0 — 2026-10-01
|
|
6
12
|
|
|
7
13
|
### 🎉 Features
|
package/README.md
CHANGED
|
@@ -5,18 +5,11 @@
|
|
|
5
5
|
Context-aware script runner for pnpm monorepos. Includes an `nmr` (node-monorepo run) binary that provides centralized, consistent script execution across workspace packages and the monorepo root.
|
|
6
6
|
|
|
7
7
|
<!-- section:release-notes -->
|
|
8
|
-
## Release notes — v0.
|
|
8
|
+
## Release notes — v0.41.0 (2026-10-02)
|
|
9
9
|
|
|
10
10
|
### 🎉 Features
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
- Excludes untracked paths that git ignores from collection in `defineVitestConfig` and `defineRootVitestConfig`, so that Vitest does not run a test file that the conventions check skips. (#898)
|
|
14
|
-
- Rewords the `nmr --log` refusal for a run that the check-result cache does not cover to `the check-result cache does not apply here (NMR_DEBUG=1 reports why)`, and renames the matching `docs/check-cache.md` heading to "When the cache does not apply", which changes its anchor. (#923)
|
|
15
|
-
|
|
16
|
-
### 🐛 Bug fixes
|
|
17
|
-
|
|
18
|
-
- Rewords the messages printed by `nmr`, its bin wrappers, and its readyup kit, which used figures such as "sits under" and "holds", em-dashes, and "no" as a determiner, so that a diagnosis such as "-F/--filter did not match any workspace" states its condition directly. (#932)
|
|
19
|
-
- Fixes the issue that the `nmr` rulebook recommended `-q` without stating that nmr reads its own flags only before the command name, which led agents to run `nmr ci -q` and fail when `nmr-fmt` rejected the flag. (#948)
|
|
12
|
+
- Adds a hint to `nmr` that, when a command fails, names any of nmr's own flags written after the command name and shows the corrected invocation, such as `nmr -q check`. (#956)
|
|
20
13
|
<!-- /section:release-notes -->
|
|
21
14
|
|
|
22
15
|
## Installation
|
|
@@ -66,7 +59,7 @@ nmr --recursive lint # Lint all workspace packages
|
|
|
66
59
|
|
|
67
60
|
`--recursive`, and a `--filter` that selects more than one package, do not give any package terminal input. An interactive command such as `test:watch` needs a filter that selects a single package.
|
|
68
61
|
|
|
69
|
-
Position determines ownership: Flags before the command name are nmr's own, and everything after the command name is forwarded untouched to the resolved command.
|
|
62
|
+
Position determines ownership: Flags before the command name are nmr's own, and everything after the command name is forwarded untouched to the resolved command. When a command fails and one of nmr's own flags follows its name, nmr names the flag and prints the corrected invocation.
|
|
70
63
|
|
|
71
64
|
`nmr --help` lists every flag and command, with the shell command to which each command resolves. Per-package `package.json` files do not need any script entries.
|
|
72
65
|
|
package/dist/esm/runCli.js
CHANGED
|
@@ -16,9 +16,9 @@ import { isTerminalStream, OUTPUT_STYLE_ENV_VAR, OUTPUT_STYLE_FLAG, resolveOutpu
|
|
|
16
16
|
import { renderRecording, renderRefusal, resolveRecording } from "./recording.js";
|
|
17
17
|
import { assembleReplay } from "./replay-assembly.js";
|
|
18
18
|
import { readReportFormatEnv, REPORT_FORMAT_ENV_VAR, resolveReportFormat } from "./report-format.js";
|
|
19
|
-
import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, findChainedSelfReference, resolveScript, } from "./resolver.js";
|
|
19
|
+
import { applyDevBinToSteps, buildRootRegistry, buildWorkspaceRegistry, expandScript, findChainedSelfReference, readPackageJsonScripts, resolveScript, } from "./resolver.js";
|
|
20
20
|
import { resolveChannel, runSteps } from "./runner.js";
|
|
21
|
-
import { composeNmrStep, dropRecursiveSteps, findNmrCrossing, renderChain } from "./steps.js";
|
|
21
|
+
import { composeNmrStep, dropRecursiveSteps, findNmrCrossing, quoteToken, renderChain } from "./steps.js";
|
|
22
22
|
import { UserError } from "./UserError.js";
|
|
23
23
|
import { COMMAND_VERBOSITY_ENV_VAR, readVerbosityEnv, resolveVerbosity, } from "./verbosity.js";
|
|
24
24
|
import { writeVerdict } from "./verdict.js";
|
|
@@ -34,7 +34,7 @@ const NAMED_ESCAPES = new Map([
|
|
|
34
34
|
]);
|
|
35
35
|
export const RUN_IF_PRESENT_ENV_VAR = 'NMR_RUN_IF_PRESENT';
|
|
36
36
|
export async function runCli(options) {
|
|
37
|
-
const { args,
|
|
37
|
+
const { args, env, stdout, stderr } = options;
|
|
38
38
|
const parseResult = parseArgs(args);
|
|
39
39
|
if (!parseResult.ok) {
|
|
40
40
|
reportError(parseResult.error, stderr);
|
|
@@ -52,6 +52,18 @@ export async function runCli(options) {
|
|
|
52
52
|
return { exitCode: 1 };
|
|
53
53
|
}
|
|
54
54
|
const { styles } = presentationRead;
|
|
55
|
+
const { exitCode, isUnknownCommand = false } = await runInvocation({ ...options, parsedArgs, presentationRead });
|
|
56
|
+
if (exitCode !== 0 && !isUnknownCommand && isTopLevelInvocation(env)) {
|
|
57
|
+
const hint = formatMisplacedFlagHint(args, parsedArgs.passthrough, styles.stderr);
|
|
58
|
+
if (hint !== undefined) {
|
|
59
|
+
stderr.write(`${hint}\n`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { exitCode };
|
|
63
|
+
}
|
|
64
|
+
async function runInvocation(options) {
|
|
65
|
+
const { cwd, env, parsedArgs, presentationRead, stderr, stdout } = options;
|
|
66
|
+
const { styles } = presentationRead;
|
|
55
67
|
if (parsedArgs.shouldShowVersion) {
|
|
56
68
|
stdout.write(`${VERSION}\n`);
|
|
57
69
|
return { exitCode: 0 };
|
|
@@ -103,7 +115,8 @@ export async function runCli(options) {
|
|
|
103
115
|
};
|
|
104
116
|
const delegation = composeDelegation({ childEnv, command, parsedArgs });
|
|
105
117
|
if (delegation !== undefined) {
|
|
106
|
-
|
|
118
|
+
const result = await runDelegation({ context, delegation, parsedArgs, runOptions, stderr });
|
|
119
|
+
return { ...result, isUnknownCommand: result.exitCode !== 0 && !isDefinedInAnyScope(command, context) };
|
|
107
120
|
}
|
|
108
121
|
const registry = shouldUseRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config);
|
|
109
122
|
assertNoSelfReference({
|
|
@@ -120,7 +133,7 @@ export async function runCli(options) {
|
|
|
120
133
|
return { exitCode: 0 };
|
|
121
134
|
}
|
|
122
135
|
reportError(`Unknown command: ${command}`, stderr);
|
|
123
|
-
return { exitCode: 1 };
|
|
136
|
+
return { exitCode: 1, isUnknownCommand: true };
|
|
124
137
|
}
|
|
125
138
|
const { isEmptiedByWorkspace, steps: runnableSteps } = readRunnableSteps(resolvedScript, context.workspacePackageDirs);
|
|
126
139
|
const resolvedCommand = renderChain(runnableSteps);
|
|
@@ -200,6 +213,7 @@ export async function runCli(options) {
|
|
|
200
213
|
reportVerdict({ command, scope, ...outcome }, stdout, format, styles.stdout);
|
|
201
214
|
return { exitCode };
|
|
202
215
|
}
|
|
216
|
+
const FILTER_FLAGS = new Set(['-F', '--filter']);
|
|
203
217
|
const BOOLEAN_FLAGS = new Map([
|
|
204
218
|
['-?', 'shouldShowHelp'],
|
|
205
219
|
['--help', 'shouldShowHelp'],
|
|
@@ -587,7 +601,7 @@ function parseArgs(args) {
|
|
|
587
601
|
const arg = args[index];
|
|
588
602
|
if (arg === undefined)
|
|
589
603
|
break;
|
|
590
|
-
if (arg
|
|
604
|
+
if (FILTER_FLAGS.has(arg)) {
|
|
591
605
|
index++;
|
|
592
606
|
const filterValue = args[index];
|
|
593
607
|
if (!filterValue) {
|
|
@@ -862,6 +876,69 @@ async function runGated(options) {
|
|
|
862
876
|
outcome: exitCode === 0 ? { outcome: 'passed', durationMs } : { outcome: 'failed', durationMs, exitCode },
|
|
863
877
|
};
|
|
864
878
|
}
|
|
879
|
+
function formatMisplacedFlagHint(args, passthrough, style) {
|
|
880
|
+
const { flagNames, movedTokens, remainingTokens } = splitMisplacedFlags(passthrough);
|
|
881
|
+
if (flagNames.length === 0) {
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
884
|
+
const commandIndex = args.length - passthrough.length - 1;
|
|
885
|
+
const correctedTokens = [
|
|
886
|
+
'nmr',
|
|
887
|
+
...args.slice(0, commandIndex),
|
|
888
|
+
...movedTokens,
|
|
889
|
+
...args.slice(commandIndex, commandIndex + 1),
|
|
890
|
+
...remainingTokens,
|
|
891
|
+
];
|
|
892
|
+
const subject = flagNames.length === 1 ? 'is an nmr flag' : 'are nmr flags';
|
|
893
|
+
return (`${STATUS_GLYPHS[style].warning.text} ${renderQuotedList(flagNames)} ${subject}; ` +
|
|
894
|
+
`nmr reads its flags only before the command: \`${correctedTokens.map(quoteToken).join(' ')}\``);
|
|
895
|
+
}
|
|
896
|
+
function isDefinedInAnyScope(command, context) {
|
|
897
|
+
const registries = [buildRootRegistry(context.config), buildWorkspaceRegistry(context.config)];
|
|
898
|
+
if (registries.some((registry) => Object.hasOwn(registry, command))) {
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
return [context.monorepoRoot, ...context.workspacePackageDirs].some((dir) => Object.hasOwn(readPackageJsonScripts(dir) ?? {}, command));
|
|
902
|
+
}
|
|
903
|
+
function isTopLevelInvocation(env) {
|
|
904
|
+
const runId = env[RUN_ID_ENV_VAR];
|
|
905
|
+
return runId === undefined || runId === '';
|
|
906
|
+
}
|
|
907
|
+
function readMisplacedFlagLength(args, index) {
|
|
908
|
+
const arg = args[index] ?? '';
|
|
909
|
+
if (FILTER_FLAGS.has(arg)) {
|
|
910
|
+
return (args[index + 1] ?? '') === '' ? undefined : 2;
|
|
911
|
+
}
|
|
912
|
+
const booleanFlag = BOOLEAN_FLAGS.get(arg);
|
|
913
|
+
if (booleanFlag !== undefined) {
|
|
914
|
+
return booleanFlag === 'shouldShowHelp' || booleanFlag === 'shouldShowVersion' ? undefined : 1;
|
|
915
|
+
}
|
|
916
|
+
const styleArgument = readOutputStyleArgument(args, index);
|
|
917
|
+
return styleArgument?.ok === true ? styleArgument.consumedCount : undefined;
|
|
918
|
+
}
|
|
919
|
+
function splitMisplacedFlags(passthrough) {
|
|
920
|
+
const flagNames = new Set();
|
|
921
|
+
const movedTokens = [];
|
|
922
|
+
const remainingTokens = [];
|
|
923
|
+
let index = 0;
|
|
924
|
+
while (index < passthrough.length) {
|
|
925
|
+
const arg = passthrough[index] ?? '';
|
|
926
|
+
if (arg === '--') {
|
|
927
|
+
remainingTokens.push(...passthrough.slice(index));
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
const flagLength = readMisplacedFlagLength(passthrough, index);
|
|
931
|
+
if (flagLength === undefined) {
|
|
932
|
+
remainingTokens.push(arg);
|
|
933
|
+
index++;
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
flagNames.add(arg.startsWith(`${OUTPUT_STYLE_FLAG}=`) ? OUTPUT_STYLE_FLAG : arg);
|
|
937
|
+
movedTokens.push(...passthrough.slice(index, index + flagLength));
|
|
938
|
+
index += flagLength;
|
|
939
|
+
}
|
|
940
|
+
return { flagNames: [...flagNames], movedTokens, remainingTokens };
|
|
941
|
+
}
|
|
865
942
|
function quoteForShell(arg) {
|
|
866
943
|
return "'" + arg.replace(/'/g, String.raw `'\''`) + "'";
|
|
867
944
|
}
|
package/dist/esm/steps.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export declare function findNmrCrossing(steps: readonly Step[]): string | undefi
|
|
|
75
75
|
* literally would be quoted whole by the rendering, turning the element into a command that nobody wrote.
|
|
76
76
|
*/
|
|
77
77
|
export declare function findUnexpressibleToken(element: string): string | undefined;
|
|
78
|
+
/** Quotes a token that the shell would not read literally, and leaves every other token bare. */
|
|
79
|
+
export declare function quoteToken(token: string): string;
|
|
78
80
|
/**
|
|
79
81
|
* Returns the command that a structural step's nmr process runs, whether the step hands that command to other
|
|
80
82
|
* scopes rather than running it in its own scope, and whether it anchors the command at the monorepo root.
|
package/dist/esm/steps.js
CHANGED
|
@@ -33,6 +33,12 @@ export function findNmrCrossing(steps) {
|
|
|
33
33
|
export function findUnexpressibleToken(element) {
|
|
34
34
|
return tokenize(element).find((token) => !SHELL_SAFE_TOKEN.test(token));
|
|
35
35
|
}
|
|
36
|
+
export function quoteToken(token) {
|
|
37
|
+
if (SHELL_SAFE_TOKEN.test(token)) {
|
|
38
|
+
return token;
|
|
39
|
+
}
|
|
40
|
+
return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
|
|
41
|
+
}
|
|
36
42
|
export function readNmrStep(step) {
|
|
37
43
|
if (step.kind !== 'structural') {
|
|
38
44
|
return undefined;
|
|
@@ -66,12 +72,6 @@ function isRedirectionOperator(char, precedingText, nextChar) {
|
|
|
66
72
|
}
|
|
67
73
|
return (char === '&' && nextChar === '>') || /[<>]\s*$/.test(precedingText);
|
|
68
74
|
}
|
|
69
|
-
function quoteToken(token) {
|
|
70
|
-
if (SHELL_SAFE_TOKEN.test(token)) {
|
|
71
|
-
return token;
|
|
72
|
-
}
|
|
73
|
-
return "'" + token.replaceAll("'", String.raw `'\''`) + "'";
|
|
74
|
-
}
|
|
75
75
|
function readNmrTail(segment) {
|
|
76
76
|
const [head, ...rest] = dropLeadingAssignments(tokenizeSegment(segment));
|
|
77
77
|
if (head === undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/nmr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Context-aware script runner for pnpm monorepos",
|
|
6
6
|
"keywords": [
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"@williamthorsen/nmr-core": "0.13.1",
|
|
81
81
|
"@williamthorsen/toolbelt.errors": "0.8.1",
|
|
82
82
|
"glob": "13.0.6",
|
|
83
|
-
"prettier-plugin-sh": "0.
|
|
83
|
+
"prettier-plugin-sh": "0.20.2",
|
|
84
84
|
"taze": "21.1.0"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|