@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
package/dist/esm/remote-probe.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { isRecord } from '@williamthorsen/nmr-core';
|
|
2
3
|
import { quoteForShell } from "./helpers/quoteForShell.js";
|
|
3
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
4
4
|
import { LOCK_DIR_ENV_VAR, LOCK_FILE, STALE_AFTER_MS } from "./machine-lock.js";
|
|
5
5
|
import { CHECKOUT_LOCK_PATH } from "./remote-host-session.js";
|
|
6
6
|
import { probeHostLocks } from "./remote-probe-host.js";
|
|
@@ -80,7 +80,7 @@ export function selectHost(hosts, results) {
|
|
|
80
80
|
function readBusyField(line) {
|
|
81
81
|
try {
|
|
82
82
|
const value = JSON.parse(line);
|
|
83
|
-
return
|
|
83
|
+
return isRecord(value) && typeof value['busy'] === 'boolean' ? value['busy'] : undefined;
|
|
84
84
|
}
|
|
85
85
|
catch {
|
|
86
86
|
return undefined;
|
package/dist/esm/remote-run.d.ts
CHANGED
|
@@ -3,13 +3,13 @@ import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
|
3
3
|
/** Names the hosts to which nmr delegates a command when the invocation does not say otherwise. */
|
|
4
4
|
export declare const DELEGATE_ENV_VAR = "NMR_DELEGATE";
|
|
5
5
|
/** The flag that runs the command on a host that `NMR_DELEGATE` or the user config names. */
|
|
6
|
-
export declare const DELEGATE_FLAG
|
|
6
|
+
export declare const DELEGATE_FLAG: string;
|
|
7
7
|
/** The flag that names the hosts on which to run the command. */
|
|
8
|
-
export declare const DELEGATE_TO_FLAG
|
|
8
|
+
export declare const DELEGATE_TO_FLAG: string;
|
|
9
9
|
/** The exit status of a delegation that failed outside its command, which matches `ssh`'s own. */
|
|
10
10
|
export declare const DELEGATION_FAILURE_EXIT_CODE = 255;
|
|
11
11
|
/** The flag that runs the command here, whatever `NMR_DELEGATE` or the user config says. */
|
|
12
|
-
export declare const NO_DELEGATE_FLAG
|
|
12
|
+
export declare const NO_DELEGATE_FLAG: string;
|
|
13
13
|
/** Names the ssh command that the environment supplies, which git and nmr run in place of plain `ssh`. */
|
|
14
14
|
export declare const SSH_COMMAND_ENV_VAR = "GIT_SSH_COMMAND";
|
|
15
15
|
/** Keeps `ssh` from prompting or waiting on a terminal, and bounds the wait for a host that does not answer. */
|
package/dist/esm/remote-run.js
CHANGED
|
@@ -3,18 +3,19 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import process from 'node:process';
|
|
6
|
-
import { startTimer, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
|
|
6
|
+
import { GIT_OUTPUT_LIMIT_BYTES, startTimer, STATUS_GLYPHS } from '@williamthorsen/nmr-core';
|
|
7
7
|
import { PASS_MANIFEST_DIR_ENV_VAR, writeDebugNote } from "./check-cache.js";
|
|
8
8
|
import { createBoundedBuffer } from "./helpers/createBoundedBuffer.js";
|
|
9
9
|
import { quoteForShell } from "./helpers/quoteForShell.js";
|
|
10
10
|
import { STALE_AFTER_MS } from "./machine-lock.js";
|
|
11
|
+
import { formatFlagSpelling } from "./nmr-flags.js";
|
|
11
12
|
import { CHECKOUT_LOCK_PATH } from "./remote-host-session.js";
|
|
12
13
|
import { composeSessionScript, createSessionDemultiplexer, formatDelegationWaitingLine } from "./remote-session.js";
|
|
13
14
|
export const DELEGATE_ENV_VAR = 'NMR_DELEGATE';
|
|
14
|
-
export const DELEGATE_FLAG = '
|
|
15
|
-
export const DELEGATE_TO_FLAG = '
|
|
15
|
+
export const DELEGATE_FLAG = formatFlagSpelling('delegate');
|
|
16
|
+
export const DELEGATE_TO_FLAG = formatFlagSpelling('delegateTo');
|
|
16
17
|
export const DELEGATION_FAILURE_EXIT_CODE = 255;
|
|
17
|
-
export const NO_DELEGATE_FLAG = '
|
|
18
|
+
export const NO_DELEGATE_FLAG = formatFlagSpelling('noDelegate');
|
|
18
19
|
export const SSH_COMMAND_ENV_VAR = 'GIT_SSH_COMMAND';
|
|
19
20
|
export const SSH_OPTIONS = ['-T', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10'];
|
|
20
21
|
export function composeGitSshCommand(env) {
|
|
@@ -102,6 +103,7 @@ export async function runRemotely(options) {
|
|
|
102
103
|
const script = composeSessionScript({
|
|
103
104
|
checkoutDir,
|
|
104
105
|
params: {
|
|
106
|
+
gitOutputLimitBytes: GIT_OUTPUT_LIMIT_BYTES,
|
|
105
107
|
holder: { command, dir: cwd, pid: process.pid },
|
|
106
108
|
lockPath: CHECKOUT_LOCK_PATH,
|
|
107
109
|
marker,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Writable } from 'node:stream';
|
|
2
|
+
import { isRecord } from '@williamthorsen/nmr-core';
|
|
2
3
|
import { quoteForShell } from "./helpers/quoteForShell.js";
|
|
3
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
4
4
|
import { runHostSession, } from "./remote-host-session.js";
|
|
5
5
|
export function composeSessionScript(options) {
|
|
6
6
|
const { checkoutDir, params } = options;
|
|
@@ -91,7 +91,7 @@ export function createSessionDemultiplexer(options) {
|
|
|
91
91
|
catch {
|
|
92
92
|
return 'other';
|
|
93
93
|
}
|
|
94
|
-
if (!
|
|
94
|
+
if (!isRecord(value) || value['kind'] !== 'passes') {
|
|
95
95
|
return 'other';
|
|
96
96
|
}
|
|
97
97
|
const entries = value['entries'];
|
|
@@ -116,13 +116,13 @@ export function formatDelegationWaitingLine(holder, host) {
|
|
|
116
116
|
}
|
|
117
117
|
const NEWLINE = 0x0a;
|
|
118
118
|
function isDelegationHolder(value) {
|
|
119
|
-
return (
|
|
119
|
+
return (isRecord(value) &&
|
|
120
120
|
typeof value['command'] === 'string' &&
|
|
121
121
|
typeof value['dir'] === 'string' &&
|
|
122
122
|
typeof value['pid'] === 'number');
|
|
123
123
|
}
|
|
124
124
|
function isSessionRecord(value) {
|
|
125
|
-
if (!
|
|
125
|
+
if (!isRecord(value))
|
|
126
126
|
return false;
|
|
127
127
|
switch (value['kind']) {
|
|
128
128
|
case 'failed':
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
1
|
import fs from 'node:fs';
|
|
3
2
|
import os from 'node:os';
|
|
4
3
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
4
|
+
import { runGit } from '@williamthorsen/nmr-core';
|
|
6
5
|
export function createSnapshot(cwd, env) {
|
|
7
|
-
const toplevelResult = runGit(['rev-parse', '--show-toplevel'], cwd, env);
|
|
6
|
+
const toplevelResult = runGit(['rev-parse', '--show-toplevel'], cwd, undefined, env);
|
|
8
7
|
if (!toplevelResult.ok) {
|
|
9
8
|
return { ok: false, isUnsupported: true, reason: `not a git repository: ${toplevelResult.error}` };
|
|
10
9
|
}
|
|
@@ -12,7 +11,7 @@ export function createSnapshot(cwd, env) {
|
|
|
12
11
|
if (fs.existsSync(path.join(toplevel, '.gitmodules'))) {
|
|
13
12
|
return { ok: false, isUnsupported: true, reason: 'the repository declares submodules' };
|
|
14
13
|
}
|
|
15
|
-
const indexPathResult = runGit(['rev-parse', '--git-path', 'index'], toplevel, env);
|
|
14
|
+
const indexPathResult = runGit(['rev-parse', '--git-path', 'index'], toplevel, undefined, env);
|
|
16
15
|
if (!indexPathResult.ok) {
|
|
17
16
|
return { ok: false, isUnsupported: false, reason: indexPathResult.error };
|
|
18
17
|
}
|
|
@@ -24,28 +23,25 @@ export function createSnapshot(cwd, env) {
|
|
|
24
23
|
copyIndex(indexPath, tempIndex);
|
|
25
24
|
}
|
|
26
25
|
const indexEnv = { ...env, GIT_INDEX_FILE: tempIndex };
|
|
27
|
-
const addResult = runGit(['add', '--all'], toplevel, indexEnv);
|
|
26
|
+
const addResult = runGit(['add', '--all'], toplevel, undefined, indexEnv);
|
|
28
27
|
if (!addResult.ok) {
|
|
29
28
|
return { ok: false, isUnsupported: false, reason: addResult.error };
|
|
30
29
|
}
|
|
31
|
-
const stageResult = runGit(['ls-files', '--stage', '-z'], toplevel, indexEnv);
|
|
30
|
+
const stageResult = runGit(['ls-files', '--stage', '-z'], toplevel, undefined, indexEnv);
|
|
32
31
|
if (!stageResult.ok) {
|
|
33
32
|
return { ok: false, isUnsupported: false, reason: stageResult.error };
|
|
34
33
|
}
|
|
35
34
|
if (stageResult.stdout.split('\0').some((entry) => entry.startsWith('160000 '))) {
|
|
36
35
|
return { ok: false, isUnsupported: true, reason: 'the working tree contains a submodule or a nested repository' };
|
|
37
36
|
}
|
|
38
|
-
const treeResult = runGit(['write-tree'], toplevel, indexEnv);
|
|
37
|
+
const treeResult = runGit(['write-tree'], toplevel, undefined, indexEnv);
|
|
39
38
|
if (!treeResult.ok) {
|
|
40
39
|
return { ok: false, isUnsupported: false, reason: treeResult.error };
|
|
41
40
|
}
|
|
42
|
-
const headResult = runGit(['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'], toplevel, env);
|
|
41
|
+
const headResult = runGit(['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'], toplevel, undefined, env);
|
|
43
42
|
const parentArgs = headResult.ok ? ['-p', headResult.stdout.trim()] : [];
|
|
44
43
|
const treeId = treeResult.stdout.trim();
|
|
45
|
-
const commitResult = runGit(['commit-tree', treeId, ...parentArgs, '-m', 'nmr delegate snapshot'], toplevel, {
|
|
46
|
-
...env,
|
|
47
|
-
...SNAPSHOT_IDENTITY_ENV,
|
|
48
|
-
});
|
|
44
|
+
const commitResult = runGit(['commit-tree', treeId, ...parentArgs, '-m', 'nmr delegate snapshot'], toplevel, undefined, { ...env, ...SNAPSHOT_IDENTITY_ENV });
|
|
49
45
|
if (!commitResult.ok) {
|
|
50
46
|
return { ok: false, isUnsupported: false, reason: commitResult.error };
|
|
51
47
|
}
|
|
@@ -68,13 +64,3 @@ function copyIndex(source, destination) {
|
|
|
68
64
|
const { atime, mtime } = fs.statSync(source);
|
|
69
65
|
fs.utimesSync(destination, atime, mtime);
|
|
70
66
|
}
|
|
71
|
-
function runGit(args, cwd, env) {
|
|
72
|
-
const result = spawnSync('git', args, { cwd, encoding: 'utf8', env, maxBuffer: GIT_OUTPUT_LIMIT_BYTES });
|
|
73
|
-
if (result.error !== undefined) {
|
|
74
|
-
return { ok: false, error: result.error.message };
|
|
75
|
-
}
|
|
76
|
-
if (result.status !== 0) {
|
|
77
|
-
return { ok: false, error: result.stderr.trim() || `git ${args[0] ?? ''} exited ${result.status ?? 'abnormally'}` };
|
|
78
|
-
}
|
|
79
|
-
return { ok: true, stdout: result.stdout };
|
|
80
|
-
}
|
package/dist/esm/resolver.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { hasErrnoCode } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { hasErrnoCode, isRecord } from '@williamthorsen/nmr-core';
|
|
4
4
|
import { parsePackageJson, readScriptRecord, resolvePackageJsonPath } from "./helpers/package-json.js";
|
|
5
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
6
5
|
import { getDefaultRootScripts, getDefaultWorkspaceScripts, } from "./resolve-scripts.js";
|
|
7
6
|
import { composeNmrStep, readSelfReference } from "./steps.js";
|
|
8
7
|
import { isMonorepoRoot } from "./workspace.js";
|
|
@@ -77,10 +76,10 @@ export function readPackageJsonScripts(packageDir) {
|
|
|
77
76
|
throw error;
|
|
78
77
|
}
|
|
79
78
|
const parsedManifest = parsePackageJson(rawText, file);
|
|
80
|
-
if (!
|
|
79
|
+
if (!isRecord(parsedManifest))
|
|
81
80
|
return undefined;
|
|
82
81
|
const scripts = parsedManifest['scripts'];
|
|
83
|
-
if (!
|
|
82
|
+
if (!isRecord(scripts))
|
|
84
83
|
return undefined;
|
|
85
84
|
return readScriptRecord(packageDir, scripts);
|
|
86
85
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import { type CommandNode } from '@williamthorsen/nmr-core/cli';
|
|
3
|
+
import type { ResolvedContext } from './context.js';
|
|
4
|
+
import { type Invocation } from './invocation.js';
|
|
5
|
+
import { type ParsedArgs } from './nmr-flags.js';
|
|
6
|
+
/** What the command tree of one invocation is built from. */
|
|
7
|
+
export interface RouteInput {
|
|
8
|
+
context: ResolvedContext;
|
|
9
|
+
cwd: string;
|
|
10
|
+
env: NodeJS.ProcessEnv;
|
|
11
|
+
/** The invocation's flags as the first pass read them, or `undefined` when they do not parse. */
|
|
12
|
+
firstPass: ParsedArgs | undefined;
|
|
13
|
+
/** Runs an invocation that reached a command and resolves to its exit code. */
|
|
14
|
+
runInvocation: (invocation: Invocation) => Promise<number>;
|
|
15
|
+
stderr: Writable;
|
|
16
|
+
stdout: Writable;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Builds the command tree of one invocation: nmr's flags, and a passthrough command for each script that help lists
|
|
20
|
+
* for the scope.
|
|
21
|
+
*
|
|
22
|
+
* The command that the invocation names is added when any scope defines it, or when nmr answers for it on its own
|
|
23
|
+
* (a run on a delegating caller's behalf, or a host name after a bare `--delegate`), so that nmr rather than the
|
|
24
|
+
* router decides what an invocation that a scope cannot run reports. A request for help adds nothing, so that help
|
|
25
|
+
* lists the scope alone.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildRoot(input: RouteInput): CommandNode<undefined>;
|
|
28
|
+
/** Builds a root that does not have any commands, which answers `--version` without loading the config. */
|
|
29
|
+
export declare function buildVersionRoot(): CommandNode<undefined>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createCli, UsageError } from '@williamthorsen/nmr-core/cli';
|
|
3
|
+
import { listDefinedCommands } from "./fan-out.js";
|
|
4
|
+
import { describeHelp } from "./help.js";
|
|
5
|
+
import { RUN_IF_PRESENT_ENV_VAR } from "./invocation.js";
|
|
6
|
+
import { NMR_FLAGS, toParsedArgs } from "./nmr-flags.js";
|
|
7
|
+
import { isTerminalStream } from "./output-style.js";
|
|
8
|
+
import { readPresentation, resolveReportingVerbosity } from "./presentation.js";
|
|
9
|
+
import { resolveReportFormat } from "./report-format.js";
|
|
10
|
+
const { defineGroup } = createCli();
|
|
11
|
+
const { defineCommand } = createCli();
|
|
12
|
+
export function buildRoot(input) {
|
|
13
|
+
const { context, env, firstPass } = input;
|
|
14
|
+
const shouldUseRoot = (firstPass?.isWorkspaceRoot ?? false) || context.isRoot;
|
|
15
|
+
const anchorDir = resolveAnchorDir(context, shouldUseRoot);
|
|
16
|
+
const listing = describeHelp(context.config, context.monorepoRoot, anchorDir, shouldUseRoot);
|
|
17
|
+
const commands = {};
|
|
18
|
+
for (const [name, summary] of listing.commands) {
|
|
19
|
+
commands[name] = defineScriptCommand(name, summary, input);
|
|
20
|
+
}
|
|
21
|
+
const command = firstPass?.command;
|
|
22
|
+
if (command !== undefined && !firstPass?.shouldShowHelp && !Object.hasOwn(commands, command)) {
|
|
23
|
+
const isAnsweredByNmr = env[RUN_IF_PRESENT_ENV_VAR] === '1' ||
|
|
24
|
+
(firstPass?.shouldDelegate === true && firstPass.delegateHosts === undefined) ||
|
|
25
|
+
listDefinedCommands(context).has(command);
|
|
26
|
+
if (isAnsweredByNmr) {
|
|
27
|
+
commands[command] = defineScriptCommand(command, 'A script that another scope defines', input);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return defineGroup({
|
|
31
|
+
summary: 'Runs the scripts of a pnpm monorepo in the scope of the current directory',
|
|
32
|
+
flags: NMR_FLAGS,
|
|
33
|
+
deriveContext: (flags) => deriveInvocation(flags, input),
|
|
34
|
+
commands,
|
|
35
|
+
epilog: listing.epilog,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function buildVersionRoot() {
|
|
39
|
+
return defineGroup({ summary: 'nmr', flags: NMR_FLAGS, commands: {} });
|
|
40
|
+
}
|
|
41
|
+
function defineScriptCommand(name, summary, input) {
|
|
42
|
+
return defineCommand({
|
|
43
|
+
summary,
|
|
44
|
+
passthrough: true,
|
|
45
|
+
run: ({ args, context }) => input.runInvocation({
|
|
46
|
+
...context,
|
|
47
|
+
command: name,
|
|
48
|
+
parsedArgs: { ...context.parsedArgs, command: name, passthrough: args },
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function deriveInvocation(flags, input) {
|
|
53
|
+
const { context, cwd, env, firstPass, stderr, stdout } = input;
|
|
54
|
+
const parsedArgs = toParsedArgs(flags, []);
|
|
55
|
+
const presentationRead = readPresentation({
|
|
56
|
+
env,
|
|
57
|
+
flagValue: parsedArgs.outputStyle,
|
|
58
|
+
stderrIsTty: isTerminalStream(stderr),
|
|
59
|
+
stdoutIsTty: isTerminalStream(stdout),
|
|
60
|
+
});
|
|
61
|
+
if (!presentationRead.ok) {
|
|
62
|
+
throw new UsageError(presentationRead.error);
|
|
63
|
+
}
|
|
64
|
+
if (parsedArgs.shouldPrintLog && firstPass?.command === undefined) {
|
|
65
|
+
throw new UsageError('--log requires a command name: `nmr --log <command>`');
|
|
66
|
+
}
|
|
67
|
+
const format = resolveReportFormat({ envFormat: presentationRead.format, hasJsonFlag: parsedArgs.shouldEmitJson });
|
|
68
|
+
const verbosity = resolveReportingVerbosity({
|
|
69
|
+
env,
|
|
70
|
+
envVerbosity: presentationRead.verbosity,
|
|
71
|
+
format,
|
|
72
|
+
output: context.config.output,
|
|
73
|
+
hasQuietFlag: parsedArgs.quiet,
|
|
74
|
+
});
|
|
75
|
+
const shouldUseRoot = parsedArgs.isWorkspaceRoot || context.isRoot;
|
|
76
|
+
const anchorDir = resolveAnchorDir(context, shouldUseRoot);
|
|
77
|
+
return {
|
|
78
|
+
anchorDir,
|
|
79
|
+
context,
|
|
80
|
+
cwd,
|
|
81
|
+
env,
|
|
82
|
+
format,
|
|
83
|
+
parsedArgs,
|
|
84
|
+
scope: path.basename(anchorDir),
|
|
85
|
+
shouldUseRoot,
|
|
86
|
+
stderr,
|
|
87
|
+
stdout,
|
|
88
|
+
styles: presentationRead.styles,
|
|
89
|
+
verbosity,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function resolveAnchorDir(context, shouldUseRoot) {
|
|
93
|
+
return shouldUseRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
|
|
94
|
+
}
|
package/dist/esm/run-chain.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
2
|
import type { TreeSnapshot } from './check-cache.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Invocation, type PhaseResult, type RunPlan } from './invocation.js';
|
|
4
4
|
import type { ScriptRegistry } from './resolve-scripts.js';
|
|
5
5
|
import { type ResolvedScript } from './resolver.js';
|
|
6
6
|
/**
|
package/dist/esm/run-chain.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { formatGlyphLine, reportError } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { NMR_BIN } from "./bins.js";
|
|
3
4
|
import { resolveCacheKey } from "./check-cache-gate.js";
|
|
4
5
|
import { reportNmrCrossing } from "./crossing-diagnostics.js";
|
|
5
6
|
import { NMR_GLYPHS } from "./glyphs.js";
|
|
6
7
|
import { isHookName } from "./helpers/hook-name.js";
|
|
7
8
|
import { quoteForShell } from "./helpers/quoteForShell.js";
|
|
8
9
|
import { wrapWithHooks } from "./hooks.js";
|
|
10
|
+
import { USAGE_EXIT_CODE } from "./invocation.js";
|
|
9
11
|
import { reportVerdict } from "./presentation.js";
|
|
10
12
|
import { applyDevBinToSteps } from "./resolver.js";
|
|
11
13
|
import { dropRecursiveSteps, renderChain } from "./steps.js";
|
|
@@ -23,8 +25,8 @@ export function assembleChain(options) {
|
|
|
23
25
|
const substitutedCommand = renderChain(substitutedSteps);
|
|
24
26
|
const bindResult = bindPassthrough(substitutedSteps, parsedArgs.passthrough, command);
|
|
25
27
|
if (!bindResult.ok) {
|
|
26
|
-
reportError(bindResult.error, stderr);
|
|
27
|
-
return { settled: true, result: { exitCode:
|
|
28
|
+
reportError(NMR_BIN, bindResult.error, stderr);
|
|
29
|
+
return { settled: true, result: { exitCode: USAGE_EXIT_CODE } };
|
|
28
30
|
}
|
|
29
31
|
const mainSteps = bindResult.steps;
|
|
30
32
|
const isHookInvocation = isHookName(command);
|
package/dist/esm/runCli.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface RunCliOptions {
|
|
|
17
17
|
* Executes the nmr CLI flow in-process and returns the resulting exit code.
|
|
18
18
|
* Does not hold global state, does not read `process.*` globals, and never calls `process.exit`.
|
|
19
19
|
*
|
|
20
|
+
* nmr's flags decide which commands the tree holds, so a first pass reads them before the router parses them again.
|
|
21
|
+
*
|
|
20
22
|
* @internal
|
|
21
23
|
*/
|
|
22
24
|
export declare function runCli(options: RunCliOptions): Promise<RunCliResult>;
|
package/dist/esm/runCli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
1
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
2
|
+
import { runCli as runRouter } from '@williamthorsen/nmr-core/cli';
|
|
3
3
|
import { prependBinDirs } from "./bin-path.js";
|
|
4
|
+
import { NMR_BIN } from "./bins.js";
|
|
4
5
|
import { NO_CACHE_ENV_VAR, resolveRunId, RUN_ID_ENV_VAR } from "./check-cache.js";
|
|
5
6
|
import { openGate, runGated } from "./check-cache-gate.js";
|
|
6
7
|
import { reportRecording } from "./check-cache-record.js";
|
|
@@ -9,32 +10,48 @@ import { resolveContext } from "./context.js";
|
|
|
9
10
|
import { assertNoSelfReference } from "./crossing-diagnostics.js";
|
|
10
11
|
import { resolveExecutionTarget, runInvocationOnHost } from "./delegate-host.js";
|
|
11
12
|
import { enterChain } from "./entry-chain.js";
|
|
12
|
-
import { composeDelegation,
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { readPresentation, reportVerdict, resolveReportingVerbosity } from "./presentation.js";
|
|
13
|
+
import { composeDelegation, runDelegation } from "./fan-out.js";
|
|
14
|
+
import { RUN_IF_PRESENT_ENV_VAR, USAGE_EXIT_CODE, VERSION, } from "./invocation.js";
|
|
15
|
+
import { formatMisplacedFlagHint } from "./invocation-args.js";
|
|
16
|
+
import { readInvocationFlags } from "./nmr-flags.js";
|
|
17
|
+
import { reportVerdict } from "./presentation.js";
|
|
18
18
|
import { DELEGATE_TO_FLAG } from "./remote-run.js";
|
|
19
|
-
import { resolveReportFormat } from "./report-format.js";
|
|
20
19
|
import { buildRootRegistry, buildWorkspaceRegistry, resolveScript } from "./resolver.js";
|
|
20
|
+
import { buildRoot, buildVersionRoot } from "./route.js";
|
|
21
21
|
import { assembleChain, formatOverrideNotice } from "./run-chain.js";
|
|
22
22
|
export async function runCli(options) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const { args, cwd, env, stdout, stderr } = options;
|
|
24
|
+
const reading = readInvocationFlags(args);
|
|
25
|
+
const firstPass = reading.ok ? reading.parsedArgs : undefined;
|
|
26
|
+
const routerOptions = { name: NMR_BIN, context: undefined, stdout, stderr, baseDir: cwd, version: () => VERSION };
|
|
27
|
+
if (firstPass?.shouldShowVersion === true && !firstPass.shouldShowHelp) {
|
|
28
|
+
return { exitCode: await runRouter(args, buildVersionRoot(), routerOptions) };
|
|
26
29
|
}
|
|
27
|
-
const
|
|
30
|
+
const context = await resolveContext(cwd);
|
|
31
|
+
const root = buildRoot({
|
|
32
|
+
context,
|
|
33
|
+
cwd,
|
|
34
|
+
env,
|
|
35
|
+
firstPass,
|
|
36
|
+
runInvocation: (invocation) => runInvocation(invocation, options),
|
|
37
|
+
stderr,
|
|
38
|
+
stdout,
|
|
39
|
+
});
|
|
40
|
+
const isBare = firstPass !== undefined && firstPass.command === undefined && !firstPass.shouldPrintLog && !args.includes('--');
|
|
41
|
+
const routedArgs = isBare ? [...args, '--help'] : args;
|
|
42
|
+
return { exitCode: await runRouter(routedArgs, root, routerOptions) };
|
|
43
|
+
}
|
|
44
|
+
async function runInvocation(invocation, options) {
|
|
28
45
|
const resolvedTarget = await resolveExecutionTarget(invocation);
|
|
29
46
|
if (resolvedTarget.settled) {
|
|
30
|
-
return
|
|
47
|
+
return resolvedTarget.result.exitCode;
|
|
31
48
|
}
|
|
32
49
|
let target = resolvedTarget.value;
|
|
33
50
|
const { filter, recursive } = invocation.parsedArgs;
|
|
34
51
|
if (target.kind === 'remote' && (filter !== undefined || recursive)) {
|
|
35
52
|
const delegated = await runInvocationOnHost(invocation, target);
|
|
36
53
|
if (delegated !== undefined) {
|
|
37
|
-
return delegated;
|
|
54
|
+
return delegated.exitCode;
|
|
38
55
|
}
|
|
39
56
|
target = { kind: 'local' };
|
|
40
57
|
}
|
|
@@ -46,64 +63,7 @@ export async function runCli(options) {
|
|
|
46
63
|
options.stderr.write(`${hint}\n`);
|
|
47
64
|
}
|
|
48
65
|
}
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
async function establishInvocation(options) {
|
|
52
|
-
const { args, cwd, env, stdout, stderr } = options;
|
|
53
|
-
const parseResult = parseArgs(args);
|
|
54
|
-
if (!parseResult.ok) {
|
|
55
|
-
reportError(parseResult.error, stderr);
|
|
56
|
-
return { settled: true, result: { exitCode: 1 } };
|
|
57
|
-
}
|
|
58
|
-
const { parsedArgs } = parseResult;
|
|
59
|
-
const presentationRead = readPresentation({
|
|
60
|
-
env,
|
|
61
|
-
flagValue: parsedArgs.outputStyle,
|
|
62
|
-
stderrIsTty: isTerminalStream(stderr),
|
|
63
|
-
stdoutIsTty: isTerminalStream(stdout),
|
|
64
|
-
});
|
|
65
|
-
if (!presentationRead.ok) {
|
|
66
|
-
reportError(presentationRead.error, stderr);
|
|
67
|
-
return { settled: true, result: { exitCode: 1 } };
|
|
68
|
-
}
|
|
69
|
-
const { styles } = presentationRead;
|
|
70
|
-
if (parsedArgs.shouldShowVersion) {
|
|
71
|
-
stdout.write(`${VERSION}\n`);
|
|
72
|
-
return { settled: true, result: { exitCode: 0 } };
|
|
73
|
-
}
|
|
74
|
-
const context = await resolveContext(cwd);
|
|
75
|
-
const format = resolveReportFormat({ envFormat: presentationRead.format, hasJsonFlag: parsedArgs.shouldEmitJson });
|
|
76
|
-
const verbosity = resolveReportingVerbosity({
|
|
77
|
-
env,
|
|
78
|
-
envVerbosity: presentationRead.verbosity,
|
|
79
|
-
format,
|
|
80
|
-
output: context.config.output,
|
|
81
|
-
hasQuietFlag: parsedArgs.quiet,
|
|
82
|
-
});
|
|
83
|
-
const shouldUseRoot = parsedArgs.isWorkspaceRoot || context.isRoot;
|
|
84
|
-
const anchorDir = shouldUseRoot ? context.monorepoRoot : (context.packageDir ?? context.monorepoRoot);
|
|
85
|
-
if (parsedArgs.shouldShowHelp || !parsedArgs.command) {
|
|
86
|
-
stdout.write(`${generateHelp(context.config, context.monorepoRoot, anchorDir, shouldUseRoot)}\n`);
|
|
87
|
-
return { settled: true, result: { exitCode: 0 } };
|
|
88
|
-
}
|
|
89
|
-
return {
|
|
90
|
-
settled: false,
|
|
91
|
-
value: {
|
|
92
|
-
anchorDir,
|
|
93
|
-
command: parsedArgs.command,
|
|
94
|
-
context,
|
|
95
|
-
cwd,
|
|
96
|
-
env,
|
|
97
|
-
format,
|
|
98
|
-
parsedArgs,
|
|
99
|
-
scope: path.basename(anchorDir),
|
|
100
|
-
shouldUseRoot,
|
|
101
|
-
stderr,
|
|
102
|
-
stdout,
|
|
103
|
-
styles,
|
|
104
|
-
verbosity,
|
|
105
|
-
},
|
|
106
|
-
};
|
|
66
|
+
return exitCode;
|
|
107
67
|
}
|
|
108
68
|
async function planRun(invocation, target) {
|
|
109
69
|
const { anchorDir, command, context, env, parsedArgs, stderr, stdout, styles } = invocation;
|
|
@@ -131,6 +91,7 @@ async function planRun(invocation, target) {
|
|
|
131
91
|
verbosity: invocation.verbosity,
|
|
132
92
|
});
|
|
133
93
|
const runOptions = {
|
|
94
|
+
bin: NMR_BIN,
|
|
134
95
|
quiet,
|
|
135
96
|
stdout,
|
|
136
97
|
stderr,
|
|
@@ -147,10 +108,7 @@ async function planRun(invocation, target) {
|
|
|
147
108
|
finally {
|
|
148
109
|
await lock?.release();
|
|
149
110
|
}
|
|
150
|
-
return {
|
|
151
|
-
settled: true,
|
|
152
|
-
result: { ...result, isUnknownCommand: result.exitCode !== 0 && !isDefinedInAnyScope(command, context) },
|
|
153
|
-
};
|
|
111
|
+
return { settled: true, result };
|
|
154
112
|
}
|
|
155
113
|
const registry = invocation.shouldUseRoot
|
|
156
114
|
? buildRootRegistry(context.config, context.monorepoRoot)
|
|
@@ -171,8 +129,8 @@ async function planRun(invocation, target) {
|
|
|
171
129
|
const delegateToHint = parsedArgs.shouldDelegate && parsedArgs.delegateHosts === undefined
|
|
172
130
|
? `. \`${command}\` is not a command; name a host with ${DELEGATE_TO_FLAG} ${command}`
|
|
173
131
|
: '';
|
|
174
|
-
reportError(`Unknown command: ${command}${delegateToHint}`, stderr);
|
|
175
|
-
return { settled: true, result: { exitCode:
|
|
132
|
+
reportError(NMR_BIN, `Unknown command: ${command}${delegateToHint}`, stderr);
|
|
133
|
+
return { settled: true, result: { exitCode: USAGE_EXIT_CODE, isUnknownCommand: true } };
|
|
176
134
|
}
|
|
177
135
|
const entryRunOptions = parsedArgs.shouldPrintLog
|
|
178
136
|
? runOptions
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import process from 'node:process';
|
|
3
|
+
import { isRecord } from '@williamthorsen/nmr-core';
|
|
3
4
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
4
|
-
import { isObject } from "./helpers/type-guards.js";
|
|
5
5
|
export const OUTCOME_FILE_ENV_VAR = 'NMR_OUTCOME_FILE';
|
|
6
6
|
export function omitOutcomeFile(env) {
|
|
7
7
|
return Object.fromEntries(Object.entries(env).filter(([name]) => name !== OUTCOME_FILE_ENV_VAR));
|
|
@@ -49,10 +49,10 @@ export async function writeStatedOutcome(env, outcome) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
function isMissingFileError(error) {
|
|
52
|
-
return
|
|
52
|
+
return isRecord(error) && error['code'] === 'ENOENT';
|
|
53
53
|
}
|
|
54
54
|
function isStatedOutcome(value) {
|
|
55
|
-
if (!
|
|
55
|
+
if (!isRecord(value) || value['kind'] !== 'build-unchanged' || !Number.isSafeInteger(value['parentPid'])) {
|
|
56
56
|
return false;
|
|
57
57
|
}
|
|
58
58
|
const savedMs = value['savedMs'];
|
package/dist/esm/steps.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readInvocationFlags } from "./nmr-flags.js";
|
|
1
2
|
import { dropLeadingAssignments, isShellSafeToken, quoteToken, splitSegments, tokenize, tokenizeSegment, } from "./shell-syntax.js";
|
|
2
3
|
const LAUNCHERS = new Map([
|
|
3
4
|
['bun', new Set(['x'])],
|
|
@@ -141,37 +142,12 @@ function readNmrTail(segment) {
|
|
|
141
142
|
: undefined;
|
|
142
143
|
}
|
|
143
144
|
function readNmrTarget(tokens) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
let index = 0;
|
|
148
|
-
while (index < tokens.length) {
|
|
149
|
-
const token = tokens[index] ?? '';
|
|
150
|
-
switch (token) {
|
|
151
|
-
case '-F':
|
|
152
|
-
case '--filter':
|
|
153
|
-
isDelegate = true;
|
|
154
|
-
index += 2;
|
|
155
|
-
break;
|
|
156
|
-
case '-R':
|
|
157
|
-
case '--recursive':
|
|
158
|
-
isDelegate = true;
|
|
159
|
-
isRecursive = true;
|
|
160
|
-
index += 1;
|
|
161
|
-
break;
|
|
162
|
-
case '-w':
|
|
163
|
-
case '--workspace-root':
|
|
164
|
-
isWorkspaceRoot = true;
|
|
165
|
-
index += 1;
|
|
166
|
-
break;
|
|
167
|
-
default:
|
|
168
|
-
if (!token.startsWith('-')) {
|
|
169
|
-
return { command: token, isDelegate, isRecursive, isWorkspaceRoot };
|
|
170
|
-
}
|
|
171
|
-
index += 1;
|
|
172
|
-
}
|
|
145
|
+
const reading = readInvocationFlags(tokens);
|
|
146
|
+
if (!reading.ok || reading.parsedArgs.command === undefined) {
|
|
147
|
+
return undefined;
|
|
173
148
|
}
|
|
174
|
-
|
|
149
|
+
const { command, filter, isWorkspaceRoot, recursive } = reading.parsedArgs;
|
|
150
|
+
return { command, isDelegate: filter !== undefined || recursive, isRecursive: recursive, isWorkspaceRoot };
|
|
175
151
|
}
|
|
176
152
|
function renderStep(step) {
|
|
177
153
|
return step.kind === 'opaque' ? step.command : step.argv.map(quoteToken).join(' ');
|
package/dist/esm/user-config.js
CHANGED
|
@@ -9,9 +9,9 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
|
-
import { UserError } from '@williamthorsen/nmr-core';
|
|
12
|
+
import { isRecord, UserError } from '@williamthorsen/nmr-core';
|
|
13
13
|
import { assertRecognizedKeys } from "./config-validation.js";
|
|
14
|
-
import {
|
|
14
|
+
import { resolveXdgBaseDir } from "./helpers/resolveXdgBaseDir.js";
|
|
15
15
|
export const DELEGATE_WHEN_VALUES = ['agent', 'always'];
|
|
16
16
|
export function formatDisplayPath(filePath, env) {
|
|
17
17
|
const home = env['HOME'];
|
|
@@ -30,19 +30,12 @@ export async function loadUserConfig(env) {
|
|
|
30
30
|
return undefined;
|
|
31
31
|
}
|
|
32
32
|
const importedModule = await import(__rewriteRelativeImportExtension(pathToFileURL(configPath).href));
|
|
33
|
-
const loadedConfig =
|
|
33
|
+
const loadedConfig = isRecord(importedModule) ? importedModule['default'] : undefined;
|
|
34
34
|
return { config: validateUserConfig(loadedConfig, configPath), path: configPath };
|
|
35
35
|
}
|
|
36
36
|
export function resolveUserConfigPath(env) {
|
|
37
|
-
const configHome = env
|
|
38
|
-
|
|
39
|
-
return path.join(configHome, USER_CONFIG_RELATIVE_PATH);
|
|
40
|
-
}
|
|
41
|
-
const home = env['HOME'];
|
|
42
|
-
if (home !== undefined && path.isAbsolute(home)) {
|
|
43
|
-
return path.join(home, '.config', USER_CONFIG_RELATIVE_PATH);
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
37
|
+
const configHome = resolveXdgBaseDir(env, 'XDG_CONFIG_HOME', '.config');
|
|
38
|
+
return configHome === undefined ? undefined : path.join(configHome, USER_CONFIG_RELATIVE_PATH);
|
|
46
39
|
}
|
|
47
40
|
const USER_CONFIG_RELATIVE_PATH = path.join('nmr', 'config.ts');
|
|
48
41
|
const RECOGNIZED_KEYS = ['delegate'];
|
|
@@ -56,7 +49,7 @@ function validateDelegateField(value, configPath) {
|
|
|
56
49
|
if (delegate === undefined) {
|
|
57
50
|
return undefined;
|
|
58
51
|
}
|
|
59
|
-
if (!
|
|
52
|
+
if (!isRecord(delegate)) {
|
|
60
53
|
throw new UserError(`Invalid nmr config at ${configPath}: \`delegate\` must be an object`);
|
|
61
54
|
}
|
|
62
55
|
assertRecognizedKeys(delegate, RECOGNIZED_DELEGATE_KEYS, configPath, 'delegate.');
|
|
@@ -84,7 +77,7 @@ function validateHosts(value, configPath) {
|
|
|
84
77
|
return [first, ...rest];
|
|
85
78
|
}
|
|
86
79
|
function validateUserConfig(value, configPath) {
|
|
87
|
-
if (!
|
|
80
|
+
if (!isRecord(value)) {
|
|
88
81
|
throw new UserError(`Invalid nmr config at ${configPath}: expected an object, got ${typeof value}`);
|
|
89
82
|
}
|
|
90
83
|
assertRecognizedKeys(value, RECOGNIZED_KEYS, configPath);
|