@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.
- package/.meta/changelog.json +124 -0
- package/.readyup/kits/default.js +667 -167
- package/.readyup/manifest.json +77 -46
- package/CHANGELOG.md +40 -0
- package/README.md +1 -14
- package/agents/guidance/rulebooks/nmr.md +1 -1
- package/bin/ensure-prepublish-hooks.js +1 -1
- package/bin/nmr-actionlint.js +1 -1
- package/bin/nmr-clean.js +1 -1
- package/bin/nmr-compile.js +1 -1
- package/bin/nmr-fmt.js +1 -1
- package/bin/nmr-report-catalog.js +1 -1
- package/bin/nmr-report-overrides.js +1 -1
- package/bin/nmr-shellcheck.js +1 -1
- package/bin/nmr-taze.js +1 -1
- package/bin/nmr-vitest.js +1 -1
- package/bin/nmr.js +1 -1
- package/dist/esm/bins.d.ts +11 -0
- package/dist/esm/bins.js +11 -0
- package/dist/esm/check-cache-entries.js +5 -5
- package/dist/esm/check-cache-gate.js +2 -1
- package/dist/esm/child-env.js +2 -1
- package/dist/esm/child-process.d.ts +2 -0
- package/dist/esm/child-process.js +3 -3
- package/dist/esm/cli-actionlint.js +2 -1
- package/dist/esm/cli-build.js +2 -1
- package/dist/esm/cli-clean.js +2 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +29 -16
- package/dist/esm/cli-fmt.js +2 -1
- package/dist/esm/cli-report-catalog.js +2 -1
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/cli-shellcheck.js +2 -1
- package/dist/esm/cli-taze.js +2 -1
- package/dist/esm/cli-vitest.js +2 -1
- package/dist/esm/cli.js +2 -1
- package/dist/esm/commands/actionlint.d.ts +3 -3
- package/dist/esm/commands/actionlint.js +5 -5
- package/dist/esm/commands/build.js +6 -6
- package/dist/esm/commands/clean.js +3 -1
- package/dist/esm/commands/compiler.js +3 -3
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +1 -1
- package/dist/esm/commands/ensure-prepublish-hooks.js +4 -5
- package/dist/esm/commands/fmt.d.ts +3 -3
- package/dist/esm/commands/fmt.js +33 -52
- package/dist/esm/commands/format-targets.js +1 -13
- package/dist/esm/commands/node-pin.js +3 -4
- package/dist/esm/commands/rewrite-specifiers.js +2 -2
- package/dist/esm/commands/run-vitest.js +5 -5
- package/dist/esm/commands/shellcheck.d.ts +3 -3
- package/dist/esm/commands/shellcheck.js +8 -11
- package/dist/esm/commands/taze.js +5 -4
- package/dist/esm/commands/tsconfig-chain.js +3 -3
- package/dist/esm/commands/workflow-tools.d.ts +12 -3
- package/dist/esm/commands/workflow-tools.js +29 -14
- package/dist/esm/config-validation.js +9 -9
- package/dist/esm/config.js +2 -3
- package/dist/esm/delegate-host.d.ts +1 -1
- package/dist/esm/delegate-host.js +5 -3
- package/dist/esm/entry-chain.js +2 -3
- package/dist/esm/fan-out.d.ts +3 -3
- package/dist/esm/fan-out.js +14 -6
- package/dist/esm/git-ignored-paths.js +3 -3
- package/dist/esm/help.d.ts +14 -15
- package/dist/esm/help.js +29 -54
- package/dist/esm/helpers/filter-selection.js +2 -2
- package/dist/esm/helpers/package-json.js +8 -9
- package/dist/esm/helpers/resolveXdgBaseDir.d.ts +5 -0
- package/dist/esm/helpers/resolveXdgBaseDir.js +12 -0
- package/dist/esm/helpers/type-guards.d.ts +0 -2
- package/dist/esm/helpers/type-guards.js +2 -4
- package/dist/esm/invocation-args.d.ts +0 -32
- package/dist/esm/invocation-args.js +23 -121
- package/dist/esm/invocation.d.ts +3 -1
- package/dist/esm/invocation.js +1 -0
- package/dist/esm/machine-lock.js +3 -4
- package/dist/esm/nmr-flags.d.ts +109 -0
- package/dist/esm/nmr-flags.js +120 -0
- package/dist/esm/output-style.d.ts +1 -1
- package/dist/esm/output-style.js +2 -1
- package/dist/esm/pass-manifest.js +2 -2
- package/dist/esm/presentation.d.ts +1 -1
- package/dist/esm/remote-host-session.d.ts +2 -0
- package/dist/esm/remote-host-session.js +1 -0
- package/dist/esm/remote-probe.js +2 -2
- package/dist/esm/remote-run.d.ts +3 -3
- package/dist/esm/remote-run.js +6 -4
- package/dist/esm/remote-session.js +4 -4
- package/dist/esm/remote-snapshot.js +8 -22
- package/dist/esm/resolver.js +3 -4
- package/dist/esm/route.d.ts +29 -0
- package/dist/esm/route.js +94 -0
- package/dist/esm/run-chain.d.ts +1 -1
- package/dist/esm/run-chain.js +4 -2
- package/dist/esm/runCli.d.ts +2 -0
- package/dist/esm/runCli.js +36 -78
- package/dist/esm/stated-outcome.js +3 -3
- package/dist/esm/steps.js +6 -30
- package/dist/esm/user-config.js +7 -14
- package/dist/esm/vitest-source-resolution.js +5 -6
- package/dist/esm/vitest-temp-isolation.d.ts +2 -0
- package/dist/esm/vitest-temp-isolation.js +44 -0
- package/dist/esm/vitest.d.ts +9 -0
- package/dist/esm/vitest.js +8 -5
- package/docs/reporting.md +3 -3
- package/docs/scripts.md +1 -1
- package/docs/utilities.md +5 -5
- package/docs/vitest.md +11 -6
- 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 (!
|
|
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 (
|
|
30
|
+
if (isRecord(parsedManifest['scripts'])) {
|
|
32
31
|
packageJson.scripts = readScriptRecord(dir, parsedManifest['scripts']);
|
|
33
32
|
}
|
|
34
|
-
if (
|
|
33
|
+
if (isRecord(parsedManifest['pnpm'])) {
|
|
35
34
|
const pnpm = parsedManifest['pnpm'];
|
|
36
|
-
if (
|
|
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 (
|
|
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 (!
|
|
48
|
+
if (!isRecord(packageJson.pnpm))
|
|
50
49
|
return undefined;
|
|
51
50
|
const overrides = packageJson.pnpm.overrides;
|
|
52
|
-
return
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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 (
|
|
44
|
-
|
|
45
|
-
arg ===
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
127
|
-
return
|
|
40
|
+
function isNmrFlagKey(key) {
|
|
41
|
+
return Object.hasOwn(NMR_FLAGS, key);
|
|
128
42
|
}
|
|
129
|
-
function
|
|
43
|
+
function readMisplacedFlagLength(args, index) {
|
|
130
44
|
const arg = args[index] ?? '';
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
45
|
+
const key = findFlagKey(arg);
|
|
46
|
+
if (key === undefined) {
|
|
133
47
|
return undefined;
|
|
134
48
|
}
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
return
|
|
49
|
+
const isAssignment = arg.includes('=');
|
|
50
|
+
if (NMR_FLAGS[key].type === 'boolean') {
|
|
51
|
+
return isAssignment ? undefined : 1;
|
|
138
52
|
}
|
|
139
|
-
|
|
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
|
-
|
|
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();
|
package/dist/esm/invocation.d.ts
CHANGED
|
@@ -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 './
|
|
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;
|
package/dist/esm/invocation.js
CHANGED
package/dist/esm/machine-lock.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
|
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`.
|
package/dist/esm/output-style.js
CHANGED
|
@@ -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 = '
|
|
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 (
|
|
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
|
|
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
|
}
|