@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,18 +1,18 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { UserError } from '@williamthorsen/nmr-core';
|
|
4
|
+
import { runGit, UserError } from '@williamthorsen/nmr-core';
|
|
5
5
|
import { describeSignalExitCode } from "../child-process.js";
|
|
6
|
-
import { ensureTool } from "./workflow-tools.js";
|
|
6
|
+
import { ensureTool, resolveToolCacheRoots } from "./workflow-tools.js";
|
|
7
7
|
export async function runShellcheck(monorepoRoot, args, options = {}) {
|
|
8
8
|
const shellcheckArgs = args.length > 0 ? [...args] : listPresentScripts(monorepoRoot, options.listFiles);
|
|
9
9
|
if (shellcheckArgs.length === 0) {
|
|
10
10
|
console.info('No shell scripts to lint: the working tree does not contain any tracked *.sh file.');
|
|
11
11
|
return 0;
|
|
12
12
|
}
|
|
13
|
-
const
|
|
14
|
-
const ensure = options.ensure ?? ((tool,
|
|
15
|
-
const shellcheckPath = await ensure('shellcheck',
|
|
13
|
+
const cacheRoots = options.cacheRoots ?? resolveToolCacheRoots(monorepoRoot);
|
|
14
|
+
const ensure = options.ensure ?? ((tool, roots) => ensureTool(tool, { cacheRoots: roots }));
|
|
15
|
+
const shellcheckPath = await ensure('shellcheck', cacheRoots);
|
|
16
16
|
const outcome = (options.spawn ?? spawnInherited)(shellcheckPath, shellcheckArgs, monorepoRoot);
|
|
17
17
|
if (outcome.error !== undefined) {
|
|
18
18
|
throw new UserError(`Cannot run shellcheck: ${outcome.error.message}`, { cause: outcome.error });
|
|
@@ -23,12 +23,9 @@ export async function runShellcheck(monorepoRoot, args, options = {}) {
|
|
|
23
23
|
return outcome.status ?? 1;
|
|
24
24
|
}
|
|
25
25
|
export function listTrackedScripts(monorepoRoot) {
|
|
26
|
-
const result =
|
|
27
|
-
if (result.
|
|
28
|
-
throw new UserError(`Cannot list tracked shell scripts: ${result.error
|
|
29
|
-
}
|
|
30
|
-
if (result.status !== 0) {
|
|
31
|
-
throw new UserError(`Cannot list tracked shell scripts: ${result.stderr.trim()}`);
|
|
26
|
+
const result = runGit(['ls-files', '-z', '--', '*.sh'], monorepoRoot);
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
throw new UserError(`Cannot list tracked shell scripts: ${result.error}`);
|
|
32
29
|
}
|
|
33
30
|
return result.stdout.split('\0').filter((file) => file !== '');
|
|
34
31
|
}
|
|
@@ -4,6 +4,7 @@ import process from 'node:process';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
6
6
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
7
|
+
import { NMR_TAZE_BIN } from "../bins.js";
|
|
7
8
|
import { loadPackageMaturityPeriod } from "./load-package-maturity-period.js";
|
|
8
9
|
import { checkNodePin as checkNodePinDefault } from "./node-pin.js";
|
|
9
10
|
import { isSilent, readFlag } from "./taze-args.js";
|
|
@@ -24,7 +25,7 @@ export async function runTaze(args, options = {}) {
|
|
|
24
25
|
}
|
|
25
26
|
catch (error) {
|
|
26
27
|
const detail = describeError(error);
|
|
27
|
-
reportError(`Could not resolve '${TAZE_CLI_SPECIFIER}' from nmr's dependencies. Reinstall the workspace to restore it. (${detail})`, stderr);
|
|
28
|
+
reportError(NMR_TAZE_BIN, `Could not resolve '${TAZE_CLI_SPECIFIER}' from nmr's dependencies. Reinstall the workspace to restore it. (${detail})`, stderr);
|
|
28
29
|
return 1;
|
|
29
30
|
}
|
|
30
31
|
let passes;
|
|
@@ -32,7 +33,7 @@ export async function runTaze(args, options = {}) {
|
|
|
32
33
|
passes = planTazePasses(args, await loadConfig(resolveConfigDir(args)));
|
|
33
34
|
}
|
|
34
35
|
catch (error) {
|
|
35
|
-
reportError(`Could not read the taze config: ${describeError(error)}`, stderr);
|
|
36
|
+
reportError(NMR_TAZE_BIN, `Could not read the taze config: ${describeError(error)}`, stderr);
|
|
36
37
|
return 1;
|
|
37
38
|
}
|
|
38
39
|
if (readFlag(args, NODE_PIN_SKIP_FLAGS) === undefined) {
|
|
@@ -45,7 +46,7 @@ export async function runTaze(args, options = {}) {
|
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
catch (error) {
|
|
48
|
-
reportError(`Could not check the nodejs pin: ${describeError(error)}`, stderr);
|
|
49
|
+
reportError(NMR_TAZE_BIN, `Could not check the nodejs pin: ${describeError(error)}`, stderr);
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
const showHeadings = !isSilent(args);
|
|
@@ -55,7 +56,7 @@ export async function runTaze(args, options = {}) {
|
|
|
55
56
|
stderr.write(`${pass.heading}\n`);
|
|
56
57
|
const outcome = spawn(process.execPath, [cliPath, ...composeRequestTimeoutArgs(pass.args), ...pass.args]);
|
|
57
58
|
if (outcome.error) {
|
|
58
|
-
reportError(`Failed to run taze: ${outcome.error.message}`, stderr);
|
|
59
|
+
reportError(NMR_TAZE_BIN, `Failed to run taze: ${outcome.error.message}`, stderr);
|
|
59
60
|
return 1;
|
|
60
61
|
}
|
|
61
62
|
const status = outcome.status ?? 1;
|
|
@@ -15,7 +15,7 @@ export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json
|
|
|
15
15
|
resolvedChain.push(normalizedPath);
|
|
16
16
|
const configFile = ts.readConfigFile(normalizedPath, (fileName) => ts.sys.readFile(fileName));
|
|
17
17
|
if (configFile.error) {
|
|
18
|
-
throw new UserError(`
|
|
18
|
+
throw new UserError(`failed to read ${normalizedPath}.\n${formatDiagnostics([configFile.error])}`);
|
|
19
19
|
}
|
|
20
20
|
for (const entry of normalizeExtendsField(configFile.config)) {
|
|
21
21
|
walk(resolveExtendsTarget(entry, normalizedPath));
|
|
@@ -48,12 +48,12 @@ function resolveExtendsTarget(extendsEntry, fromConfigPath) {
|
|
|
48
48
|
if (ts.sys.fileExists(withJsonExtension)) {
|
|
49
49
|
return withJsonExtension;
|
|
50
50
|
}
|
|
51
|
-
throw new UserError(
|
|
51
|
+
throw new UserError(`${fromConfigPath} extends '${extendsEntry}', which does not exist.`);
|
|
52
52
|
}
|
|
53
53
|
const resolvedPath = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
|
|
54
54
|
resolvePackageSpecifier(`${extendsEntry}/tsconfig.json`, fromConfigPath);
|
|
55
55
|
if (resolvedPath === undefined) {
|
|
56
|
-
throw new UserError(
|
|
56
|
+
throw new UserError(`${fromConfigPath} extends '${extendsEntry}', which does not resolve to a file.`);
|
|
57
57
|
}
|
|
58
58
|
return resolvedPath;
|
|
59
59
|
}
|
|
@@ -15,8 +15,11 @@ export type ToolPins = Record<WorkflowTool, ToolPin>;
|
|
|
15
15
|
/** The subset of `fetch` that the download uses. */
|
|
16
16
|
export type FetchFunction = (url: string, init?: RequestInit) => Promise<Response>;
|
|
17
17
|
export interface EnsureToolOptions {
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Directories under which each tool version is cached in a directory of its own, in lookup order. A miss installs
|
|
20
|
+
* into the first one that can be written.
|
|
21
|
+
*/
|
|
22
|
+
cacheRoots: readonly [string, ...string[]];
|
|
20
23
|
arch?: string;
|
|
21
24
|
fetch?: FetchFunction;
|
|
22
25
|
pins?: ToolPins;
|
|
@@ -29,11 +32,17 @@ export interface EnsureToolOptions {
|
|
|
29
32
|
*/
|
|
30
33
|
export declare const TOOL_PINS: ToolPins;
|
|
31
34
|
/**
|
|
32
|
-
* Returns the path of the tool's pinned binary,
|
|
35
|
+
* Returns the path of the tool's pinned binary, taking the first one cached under any of the roots, and otherwise
|
|
36
|
+
* downloading, verifying, and caching it under the first root that can be written.
|
|
33
37
|
*
|
|
34
38
|
* A failure at any step leaves the cache without the tool, and a concurrent run never sees a partial binary: The
|
|
35
39
|
* binary is assembled in a scratch directory beside its target and renamed into place only once verified.
|
|
36
40
|
*/
|
|
37
41
|
export declare function ensureTool(tool: WorkflowTool, options: EnsureToolOptions): Promise<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Returns the roots under which the pinned tools are cached, in lookup order: the machine cache under the XDG cache
|
|
44
|
+
* home, when `env` resolves one, then `node_modules/.cache/nmr-tools` at the monorepo root.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveToolCacheRoots(monorepoRoot: string, env?: NodeJS.ProcessEnv): [string, ...string[]];
|
|
38
47
|
/** Maps Node's platform and architecture onto a supported pair, rejecting every other pair. */
|
|
39
48
|
export declare function resolveToolPlatform(platform: NodeJS.Platform, arch: string): ToolPlatform;
|
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import { UserError } from '@williamthorsen/nmr-core';
|
|
7
7
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
8
|
+
import { resolveXdgBaseDir } from "../helpers/resolveXdgBaseDir.js";
|
|
8
9
|
const SUPPORTED_PLATFORMS = ['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64'];
|
|
9
10
|
const DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
10
11
|
const ACTIONLINT_RELEASE = 'https://github.com/rhysd/actionlint/releases/download/v1.7.12';
|
|
@@ -58,15 +59,15 @@ export const TOOL_PINS = {
|
|
|
58
59
|
export async function ensureTool(tool, options) {
|
|
59
60
|
const pin = (options.pins ?? TOOL_PINS)[tool];
|
|
60
61
|
const asset = pin.assets[resolveToolPlatform(options.platform ?? process.platform, options.arch ?? process.arch)];
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const versionDir = `${tool}-${pin.version}`;
|
|
63
|
+
for (const cacheRoot of options.cacheRoots) {
|
|
64
|
+
const cachedPath = path.join(cacheRoot, versionDir, tool);
|
|
65
|
+
if (fs.existsSync(cachedPath)) {
|
|
66
|
+
return cachedPath;
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
fs.rmSync(scratchDir, { force: true, recursive: true });
|
|
69
|
-
fs.mkdirSync(scratchDir);
|
|
69
|
+
const { scratchDir, targetDir } = createScratchDir(options.cacheRoots, versionDir);
|
|
70
|
+
const binaryPath = path.join(targetDir, tool);
|
|
70
71
|
try {
|
|
71
72
|
const archive = await download(asset.url, options.fetch ?? globalThis.fetch);
|
|
72
73
|
const actualSha256 = createHash('sha256').update(archive).digest('hex');
|
|
@@ -90,6 +91,11 @@ export async function ensureTool(tool, options) {
|
|
|
90
91
|
}
|
|
91
92
|
return binaryPath;
|
|
92
93
|
}
|
|
94
|
+
export function resolveToolCacheRoots(monorepoRoot, env = process.env) {
|
|
95
|
+
const worktreeRoot = path.join(monorepoRoot, 'node_modules', '.cache', 'nmr-tools');
|
|
96
|
+
const cacheHome = resolveXdgBaseDir(env, 'XDG_CACHE_HOME', '.cache');
|
|
97
|
+
return cacheHome === undefined ? [worktreeRoot] : [path.join(cacheHome, 'nmr', 'tools'), worktreeRoot];
|
|
98
|
+
}
|
|
93
99
|
export function resolveToolPlatform(platform, arch) {
|
|
94
100
|
const key = `${platform}-${arch}`;
|
|
95
101
|
const supported = SUPPORTED_PLATFORMS.find((candidate) => candidate === key);
|
|
@@ -98,13 +104,22 @@ export function resolveToolPlatform(platform, arch) {
|
|
|
98
104
|
}
|
|
99
105
|
return supported;
|
|
100
106
|
}
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
function createScratchDir(cacheRoots, versionDir) {
|
|
108
|
+
let lastError;
|
|
109
|
+
for (const cacheRoot of cacheRoots) {
|
|
110
|
+
const targetDir = path.join(cacheRoot, versionDir);
|
|
111
|
+
try {
|
|
112
|
+
fs.mkdirSync(cacheRoot, { recursive: true });
|
|
113
|
+
return { scratchDir: fs.mkdtempSync(`${targetDir}.tmp-`), targetDir };
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
lastError = error;
|
|
117
|
+
}
|
|
107
118
|
}
|
|
119
|
+
const lastRoot = cacheRoots.at(-1) ?? cacheRoots[0];
|
|
120
|
+
throw new UserError(`Cannot create the tool cache at ${lastRoot}: ${describeError(lastError)}`, {
|
|
121
|
+
cause: lastError,
|
|
122
|
+
});
|
|
108
123
|
}
|
|
109
124
|
async function download(url, fetchImpl) {
|
|
110
125
|
let response;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { UserError } from '@williamthorsen/nmr-core';
|
|
1
|
+
import { isRecord, UserError } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { assertResolvableCheckCacheCommands } from "./config-check-cache.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isStringRecord } from "./helpers/type-guards.js";
|
|
4
4
|
import { findUnexpressibleToken } from "./steps.js";
|
|
5
5
|
import { formatVerbosityRejection, isCommandVerbosity } from "./verbosity.js";
|
|
6
6
|
export const CONFIG_TIERS = {
|
|
@@ -31,7 +31,7 @@ export function assertRecognizedKeys(value, recognizedKeys, configPath, prefix =
|
|
|
31
31
|
`${formatKeyList(unrecognizedKeys, prefix)}. Recognized: ${formatKeyList(recognizedKeys, prefix)}.`);
|
|
32
32
|
}
|
|
33
33
|
export function validateConfig(value, configPath, baseDir) {
|
|
34
|
-
if (!
|
|
34
|
+
if (!isRecord(value)) {
|
|
35
35
|
throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
|
|
36
36
|
}
|
|
37
37
|
assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
|
|
@@ -99,7 +99,7 @@ function isOptionalBoolean(value) {
|
|
|
99
99
|
return value === undefined || typeof value === 'boolean';
|
|
100
100
|
}
|
|
101
101
|
function isScriptRecord(value) {
|
|
102
|
-
if (!
|
|
102
|
+
if (!isRecord(value))
|
|
103
103
|
return false;
|
|
104
104
|
for (const v of Object.values(value)) {
|
|
105
105
|
if (typeof v === 'string')
|
|
@@ -112,7 +112,7 @@ function isScriptRecord(value) {
|
|
|
112
112
|
function isScriptElement(value) {
|
|
113
113
|
if (typeof value === 'string')
|
|
114
114
|
return true;
|
|
115
|
-
if (!
|
|
115
|
+
if (!isRecord(value) || typeof value['run'] !== 'string')
|
|
116
116
|
return false;
|
|
117
117
|
return isOptionalBoolean(value['shouldDeclineArguments']) && isOptionalBoolean(value['shouldRunInBackground']);
|
|
118
118
|
}
|
|
@@ -124,7 +124,7 @@ function validateBuildField(value, configPath) {
|
|
|
124
124
|
if (build === undefined) {
|
|
125
125
|
return undefined;
|
|
126
126
|
}
|
|
127
|
-
if (!
|
|
127
|
+
if (!isRecord(build)) {
|
|
128
128
|
throw new UserError(`Invalid nmr config at ${configPath}: \`build\` must be an object`);
|
|
129
129
|
}
|
|
130
130
|
assertRecognizedKeys(build, RECOGNIZED_BUILD_KEYS, configPath, 'build.');
|
|
@@ -139,7 +139,7 @@ function validateCheckCacheField(value, configPath) {
|
|
|
139
139
|
if (checkCache === undefined) {
|
|
140
140
|
return undefined;
|
|
141
141
|
}
|
|
142
|
-
if (!
|
|
142
|
+
if (!isRecord(checkCache)) {
|
|
143
143
|
throw new UserError(`Invalid nmr config at ${configPath}: \`checkCache\` must be an object`);
|
|
144
144
|
}
|
|
145
145
|
assertRecognizedKeys(checkCache, RECOGNIZED_CHECK_CACHE_KEYS, configPath, 'checkCache.');
|
|
@@ -175,7 +175,7 @@ function validateOutputField(value, configPath) {
|
|
|
175
175
|
if (output === undefined) {
|
|
176
176
|
return undefined;
|
|
177
177
|
}
|
|
178
|
-
if (!
|
|
178
|
+
if (!isRecord(output)) {
|
|
179
179
|
throw new UserError(`Invalid nmr config at ${configPath}: \`output\` must be an object`);
|
|
180
180
|
}
|
|
181
181
|
assertRecognizedKeys(output, RECOGNIZED_OUTPUT_KEYS, configPath, 'output.');
|
|
@@ -202,7 +202,7 @@ function validateSelfHostedRunnerField(value, configPath) {
|
|
|
202
202
|
if (selfHostedRunner === undefined) {
|
|
203
203
|
return undefined;
|
|
204
204
|
}
|
|
205
|
-
if (!
|
|
205
|
+
if (!isRecord(selfHostedRunner)) {
|
|
206
206
|
throw new UserError(`Invalid nmr config at ${configPath}: \`selfHostedRunner\` must be an object`);
|
|
207
207
|
}
|
|
208
208
|
assertRecognizedKeys(selfHostedRunner, RECOGNIZED_SELF_HOSTED_RUNNER_KEYS, configPath, 'selfHostedRunner.');
|
package/dist/esm/config.js
CHANGED
|
@@ -8,12 +8,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import { pathToFileURL } from 'node:url';
|
|
11
|
-
import { UserError } from '@williamthorsen/nmr-core';
|
|
11
|
+
import { isRecord, UserError } from '@williamthorsen/nmr-core';
|
|
12
12
|
import { CONFIG_TIERS, validateConfig } from "./config-validation.js";
|
|
13
13
|
import { resolveConfigPath } from "./helpers/config-path.js";
|
|
14
14
|
import { escapeControlCharacters } from "./helpers/escapeControlCharacters.js";
|
|
15
15
|
import { isHookName } from "./helpers/hook-name.js";
|
|
16
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
17
16
|
import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
18
17
|
import { describeScript } from "./resolver.js";
|
|
19
18
|
import { readSelfReference, readStepListSelfReference } from "./steps.js";
|
|
@@ -27,7 +26,7 @@ export async function loadConfig(baseDir) {
|
|
|
27
26
|
return {};
|
|
28
27
|
}
|
|
29
28
|
const importedModule = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
|
|
30
|
-
const loadedConfig =
|
|
29
|
+
const loadedConfig = isRecord(importedModule) ? importedModule['default'] : undefined;
|
|
31
30
|
return validateConfig(loadedConfig, configPath, baseDir);
|
|
32
31
|
}
|
|
33
32
|
export async function loadRootConfig(monorepoRoot) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ExecutionTarget, type Invocation, type PhaseResult, type RunCliResult, type RunPlan } from './invocation.js';
|
|
2
2
|
import type { RetainedOutput } from './runner.js';
|
|
3
3
|
import type { VerdictOutcome } from './verdict.js';
|
|
4
4
|
/** How a run on the host ended, or that the invocation runs here after all. */
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import { Writable } from 'node:stream';
|
|
3
3
|
import { reportError, startTimer } from '@williamthorsen/nmr-core';
|
|
4
|
+
import { NMR_BIN } from "./bins.js";
|
|
4
5
|
import { NO_CACHE_ENV_VAR, resolveRunId, writeDebugNote } from "./check-cache.js";
|
|
5
6
|
import { buildChildEnv, buildRemoteEnv } from "./child-env.js";
|
|
6
7
|
import { resolveDelegationTarget } from "./delegation-target.js";
|
|
7
8
|
import { createBoundedBuffer } from "./helpers/createBoundedBuffer.js";
|
|
9
|
+
import { USAGE_EXIT_CODE, } from "./invocation.js";
|
|
8
10
|
import { isDelegationFlag } from "./invocation-args.js";
|
|
9
11
|
import { reportVerdict } from "./presentation.js";
|
|
10
12
|
import { chooseHost, probeHost } from "./remote-probe.js";
|
|
@@ -29,8 +31,8 @@ export async function resolveExecutionTarget(invocation) {
|
|
|
29
31
|
shouldDelegate: parsedArgs.shouldDelegate,
|
|
30
32
|
});
|
|
31
33
|
if (target.kind === 'refused') {
|
|
32
|
-
reportError(target.error, stderr);
|
|
33
|
-
return { settled: true, result: { exitCode:
|
|
34
|
+
reportError(NMR_BIN, target.error, stderr);
|
|
35
|
+
return { settled: true, result: { exitCode: USAGE_EXIT_CODE } };
|
|
34
36
|
}
|
|
35
37
|
if (target.kind === 'remote') {
|
|
36
38
|
writeDebugNote(`delegating ${command} per ${describeDelegationSource(target, parsedArgs, env)}`, env, stderr);
|
|
@@ -115,7 +117,7 @@ async function delegate(options) {
|
|
|
115
117
|
const snapshot = createSnapshot(cwd, env);
|
|
116
118
|
if (!snapshot.ok && snapshot.isUnsupported) {
|
|
117
119
|
if (target.source === 'flag') {
|
|
118
|
-
reportError(`Cannot delegate to ${listedHosts}: ${snapshot.reason}`, stderr);
|
|
120
|
+
reportError(NMR_BIN, `Cannot delegate to ${listedHosts}: ${snapshot.reason}`, stderr);
|
|
119
121
|
return { kind: 'refused', exitCode: 1 };
|
|
120
122
|
}
|
|
121
123
|
stderr.write(formatRunHereLine(command, listedHosts, snapshot.reason));
|
package/dist/esm/entry-chain.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { UserError } from '@williamthorsen/nmr-core';
|
|
4
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
3
|
+
import { isRecord, UserError } from '@williamthorsen/nmr-core';
|
|
5
4
|
export const ENTRY_CHAIN_ENV_VAR = 'NMR_ENTRY_CHAIN';
|
|
6
5
|
export function enterChain(options) {
|
|
7
6
|
const { anchorDir, command, env, monorepoRoot } = options;
|
|
@@ -40,7 +39,7 @@ function formatReentryError(chain, monorepoRoot) {
|
|
|
40
39
|
].join('\n');
|
|
41
40
|
}
|
|
42
41
|
function isChainEntry(value) {
|
|
43
|
-
return
|
|
42
|
+
return isRecord(value) && typeof value['command'] === 'string' && typeof value['dir'] === 'string';
|
|
44
43
|
}
|
|
45
44
|
function readEntryChain(value) {
|
|
46
45
|
if (value === undefined || value === '') {
|
package/dist/esm/fan-out.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
2
|
import type { ResolvedContext } from './context.js';
|
|
3
3
|
import { type RunCliResult } from './invocation.js';
|
|
4
|
-
import type { ParsedArgs } from './
|
|
4
|
+
import type { ParsedArgs } from './nmr-flags.js';
|
|
5
5
|
import { type RunStepsOptions } from './runner.js';
|
|
6
6
|
import type { Step } from './steps.js';
|
|
7
7
|
/**
|
|
@@ -26,10 +26,10 @@ export declare function composeDelegation(options: {
|
|
|
26
26
|
}>;
|
|
27
27
|
} | undefined;
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* Lists every command that a scope which a delegation can select defines. A package config does not define
|
|
30
30
|
* scripts, so the root config's registries and each `package.json` cover every scope.
|
|
31
31
|
*/
|
|
32
|
-
export declare function
|
|
32
|
+
export declare function listDefinedCommands(context: ResolvedContext): Set<string>;
|
|
33
33
|
/**
|
|
34
34
|
* Runs a `-F` or `-R` delegation, or refuses it when it would not select any scope.
|
|
35
35
|
*
|
package/dist/esm/fan-out.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { NMR_BIN } from "./bins.js";
|
|
2
3
|
import { readFilterSelection } from "./helpers/filter-selection.js";
|
|
3
4
|
import { findClosestName } from "./helpers/findClosestName.js";
|
|
4
5
|
import { renderQuotedList } from "./helpers/renderQuotedList.js";
|
|
@@ -30,19 +31,26 @@ export function composeDelegation(options) {
|
|
|
30
31
|
},
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
|
-
export function
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
export function listDefinedCommands(context) {
|
|
35
|
+
const names = new Set([
|
|
36
|
+
...Object.keys(buildRootRegistry(context.config, context.monorepoRoot)),
|
|
37
|
+
...Object.keys(buildWorkspaceRegistry(context.config)),
|
|
38
|
+
]);
|
|
39
|
+
const scopeDirs = [context.monorepoRoot, ...context.workspacePackageDirs];
|
|
40
|
+
for (const dir of scopeDirs) {
|
|
41
|
+
const scripts = readPackageJsonScripts(dir) ?? {};
|
|
42
|
+
for (const name of Object.keys(scripts)) {
|
|
43
|
+
names.add(name);
|
|
44
|
+
}
|
|
37
45
|
}
|
|
38
|
-
return
|
|
46
|
+
return names;
|
|
39
47
|
}
|
|
40
48
|
export async function runDelegation(options) {
|
|
41
49
|
const { context, delegation, parsedArgs, runOptions, stderr } = options;
|
|
42
50
|
const selection = parsedArgs.filter === undefined ? undefined : readFilterSelection(parsedArgs.filter, context.monorepoRoot);
|
|
43
51
|
const refusal = findEmptySelectionRefusal({ context, parsedArgs, selection });
|
|
44
52
|
if (refusal !== undefined) {
|
|
45
|
-
reportError(refusal, stderr);
|
|
53
|
+
reportError(NMR_BIN, refusal, stderr);
|
|
46
54
|
return { exitCode: 1 };
|
|
47
55
|
}
|
|
48
56
|
const shouldWithholdInput = parsedArgs.filter === undefined || selection === 'multiple';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { runGit } from '@williamthorsen/nmr-core';
|
|
2
2
|
export function listGitIgnoredPaths(dir) {
|
|
3
|
-
const result =
|
|
4
|
-
if (result.
|
|
3
|
+
const result = runGit(['ls-files', '-z', '--others', '--ignored', '--exclude-standard', '--directory', '--no-empty-directory'], dir);
|
|
4
|
+
if (!result.ok)
|
|
5
5
|
return [];
|
|
6
6
|
return result.stdout
|
|
7
7
|
.split('\0')
|
package/dist/esm/help.d.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import type { NmrConfig } from './types.js';
|
|
2
|
+
/** What nmr's help lists for one scope: each command that it runs, and the text that follows the options. */
|
|
3
|
+
export interface HelpListing {
|
|
4
|
+
/** Each command's name, paired with the summary that the `Commands:` block shows for it. */
|
|
5
|
+
commands: Array<[name: string, summary: string]>;
|
|
6
|
+
epilog: string;
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
9
|
+
* Describes nmr's help for the scope anchored at `packageDir`: the active registry's commands, less hooks
|
|
10
|
+
* (`*:pre`, `*:post`), then the scripts that the package's `package.json` adds to them, each summarized by the
|
|
11
|
+
* shell command to which it resolves.
|
|
6
12
|
*
|
|
7
|
-
*
|
|
8
|
-
* `package.json
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* active section is the root section when `shouldUseRoot` is true (root cwd
|
|
12
|
-
* or `-w`), otherwise the workspace section. A footnote is appended once
|
|
13
|
-
* if any override marker was rendered.
|
|
14
|
-
*
|
|
15
|
-
* Every other entry, a hook included, is listed under `Package scripts:`
|
|
16
|
-
* unless it is an npm or pnpm lifecycle script or the active registry
|
|
17
|
-
* contains its name.
|
|
13
|
+
* The active registry is the root's when `shouldUseRoot` is true (root cwd or `-w`), and the workspace's otherwise.
|
|
14
|
+
* A `package.json` entry that matches an active registry name replaces that row's value and marks it with `*`, which
|
|
15
|
+
* a footnote explains. Every other entry, a hook included, is listed unless it is an npm or pnpm lifecycle script.
|
|
16
|
+
* The epilog lists the inactive registry, the environment variables, and the files that nmr reads.
|
|
18
17
|
*/
|
|
19
|
-
export declare function
|
|
18
|
+
export declare function describeHelp(config: NmrConfig, monorepoRoot: string, packageDir: string | undefined, shouldUseRoot: boolean): HelpListing;
|
package/dist/esm/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isHookName } from "./helpers/hook-name.js";
|
|
2
|
-
import { OUTPUT_STYLE_ENV_VAR
|
|
2
|
+
import { OUTPUT_STYLE_ENV_VAR } from "./output-style.js";
|
|
3
3
|
import { DELEGATE_ENV_VAR } from "./remote-run.js";
|
|
4
4
|
import { buildRootRegistry, buildWorkspaceRegistry, describeScript, isSelfReferential, readPackageJsonScripts, } from "./resolver.js";
|
|
5
5
|
const LIFECYCLE_SCRIPT_NAMES = new Set([
|
|
@@ -24,64 +24,39 @@ const LIFECYCLE_SCRIPT_NAMES = new Set([
|
|
|
24
24
|
'uninstall',
|
|
25
25
|
'version',
|
|
26
26
|
]);
|
|
27
|
-
export function
|
|
28
|
-
const lines = [
|
|
29
|
-
'Usage: nmr [flags] <command> [args...]',
|
|
30
|
-
'',
|
|
31
|
-
'Flags:',
|
|
32
|
-
' -F, --filter <pattern> Run command in packages whose manifest name matches',
|
|
33
|
-
' -R, --recursive Run command in all packages',
|
|
34
|
-
' -w, --workspace-root Use root scripts, running at the monorepo root',
|
|
35
|
-
" -q, --quiet Suppress command output, keeping nmr's verdicts",
|
|
36
|
-
' --delegate Run over SSH on the first free host that NMR_DELEGATE or the user config names',
|
|
37
|
-
' --delegate-to <hosts> Run over SSH on the first free of <hosts>, comma-separated',
|
|
38
|
-
' --json Report one JSON object per command, withholding command output',
|
|
39
|
-
' --log Print the recorded run instead of running (up to 256 KiB)',
|
|
40
|
-
' --no-cache Run even if this tree already passed; record the result',
|
|
41
|
-
' --no-delegate Run here, whatever NMR_DELEGATE or the user config says',
|
|
42
|
-
' --no-lock Run without waiting for another heavy command on this machine',
|
|
43
|
-
` ${OUTPUT_STYLE_FLAG} <style> Output style: auto (default), plain, or rich`,
|
|
44
|
-
' -?, --help Show this help',
|
|
45
|
-
' -V, --version Show version number',
|
|
46
|
-
'',
|
|
47
|
-
'Environment:',
|
|
48
|
-
` ${DELEGATE_ENV_VAR} Hosts on which to run each command that can be delegated,`,
|
|
49
|
-
' comma-separated; nmr runs on the first free one',
|
|
50
|
-
` ${OUTPUT_STYLE_ENV_VAR} Output style: auto (default), plain, or rich. auto prints`,
|
|
51
|
-
' plain, without emoji, when CI is set or the stream is not a terminal.',
|
|
52
|
-
'',
|
|
53
|
-
'Files:',
|
|
54
|
-
" ~/.config/nmr/config.ts User config: this machine's delegation hosts, and when to use them",
|
|
55
|
-
' ($XDG_CONFIG_HOME/nmr/config.ts when that is set)',
|
|
56
|
-
' A command that it sends runs here when no host answers',
|
|
57
|
-
'',
|
|
58
|
-
'Workspace commands:',
|
|
59
|
-
];
|
|
27
|
+
export function describeHelp(config, monorepoRoot, packageDir, shouldUseRoot) {
|
|
60
28
|
const packageScripts = packageDir === undefined ? {} : readRunnableScripts(packageDir);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
if (rootMarkedNames.size > 0)
|
|
72
|
-
hadOverride = true;
|
|
73
|
-
formatRegistry(rootRegistry, rootMarkedNames, lines);
|
|
74
|
-
const activeRegistry = shouldUseRoot ? buildRootRegistry(config, monorepoRoot) : buildWorkspaceRegistry(config);
|
|
75
|
-
const unregisteredScripts = collectUnregisteredScripts(packageScripts, activeRegistry);
|
|
76
|
-
if (Object.keys(unregisteredScripts).length > 0) {
|
|
77
|
-
lines.push('', 'Package scripts:');
|
|
78
|
-
formatRegistry(unregisteredScripts, new Set(), lines);
|
|
29
|
+
const fullActiveRegistry = shouldUseRoot ? buildRootRegistry(config, monorepoRoot) : buildWorkspaceRegistry(config);
|
|
30
|
+
const activeRegistry = filterHooks(fullActiveRegistry);
|
|
31
|
+
const markedNames = applyOverrides(activeRegistry, collectOverrides(packageScripts));
|
|
32
|
+
const commands = Object.entries(activeRegistry).map(([name, value]) => [
|
|
33
|
+
name,
|
|
34
|
+
`${describeScript(value)}${markedNames.has(name) ? ' *' : ''}`,
|
|
35
|
+
]);
|
|
36
|
+
const unregisteredScripts = collectUnregisteredScripts(packageScripts, fullActiveRegistry);
|
|
37
|
+
for (const [name, value] of Object.entries(unregisteredScripts)) {
|
|
38
|
+
commands.push([name, describeScript(value)]);
|
|
79
39
|
}
|
|
80
|
-
|
|
40
|
+
const inactiveRegistry = filterHooks(shouldUseRoot ? buildWorkspaceRegistry(config) : buildRootRegistry(config, monorepoRoot));
|
|
41
|
+
const lines = [...ENVIRONMENT_AND_FILES_LINES, '', shouldUseRoot ? 'Workspace commands:' : 'Root commands:'];
|
|
42
|
+
formatRegistry(inactiveRegistry, new Set(), lines);
|
|
43
|
+
if (markedNames.size > 0) {
|
|
81
44
|
lines.push('', '* Overridden by package.json');
|
|
82
45
|
}
|
|
83
|
-
return lines.join('\n');
|
|
46
|
+
return { commands, epilog: lines.join('\n') };
|
|
84
47
|
}
|
|
48
|
+
const ENVIRONMENT_AND_FILES_LINES = [
|
|
49
|
+
'Environment:',
|
|
50
|
+
` ${DELEGATE_ENV_VAR} Hosts on which to run each command that can be delegated,`,
|
|
51
|
+
' comma-separated; nmr runs on the first free one',
|
|
52
|
+
` ${OUTPUT_STYLE_ENV_VAR} Output style: auto (default), plain, or rich. auto prints`,
|
|
53
|
+
' plain, without emoji, when CI is set or the stream is not a terminal.',
|
|
54
|
+
'',
|
|
55
|
+
'Files:',
|
|
56
|
+
" ~/.config/nmr/config.ts User config: this machine's delegation hosts, and when to use them",
|
|
57
|
+
' ($XDG_CONFIG_HOME/nmr/config.ts when that is set)',
|
|
58
|
+
' A command that it sends runs here when no host answers',
|
|
59
|
+
];
|
|
85
60
|
function collectOverrides(packageScripts) {
|
|
86
61
|
const candidateOverrides = {};
|
|
87
62
|
for (const [name, value] of Object.entries(packageScripts)) {
|
|
@@ -2,7 +2,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
-
import {
|
|
5
|
+
import { isRecord } from '@williamthorsen/nmr-core';
|
|
6
6
|
const SELECTION_MARKER = 'nmr-selected-scope';
|
|
7
7
|
export function readFilterSelection(pattern, monorepoRoot) {
|
|
8
8
|
const listingRun = spawnSync('pnpm', ['ls', '--filter', pattern, '--depth', '-1', '--json'], {
|
|
@@ -42,7 +42,7 @@ export function interpretDelegateProbe(probe) {
|
|
|
42
42
|
return probe.stdout.includes(SELECTION_MARKER) ? 'single' : 'empty';
|
|
43
43
|
}
|
|
44
44
|
function isRootEntry(entry, monorepoRoot) {
|
|
45
|
-
if (!
|
|
45
|
+
if (!isRecord(entry) || typeof entry['path'] !== 'string') {
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
48
|
return resolveRealPath(entry['path']) === resolveRealPath(monorepoRoot);
|