@williamthorsen/nmr 0.48.0 → 0.49.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 +158 -0
- package/.readyup/kits/default.js +1527 -1460
- package/.readyup/manifest.json +96 -11
- package/CHANGELOG.md +41 -0
- package/README.md +36 -8
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/check-cache-build-output.d.ts +24 -0
- package/dist/esm/check-cache-build-output.js +49 -0
- package/dist/esm/check-cache-delegation.d.ts +27 -0
- package/dist/esm/check-cache-delegation.js +92 -0
- package/dist/esm/check-cache-entries.d.ts +109 -0
- package/dist/esm/check-cache-entries.js +87 -0
- package/dist/esm/check-cache-gate.d.ts +64 -0
- package/dist/esm/check-cache-gate.js +226 -0
- package/dist/esm/check-cache-key.d.ts +47 -0
- package/dist/esm/check-cache-key.js +77 -0
- package/dist/esm/check-cache-record.d.ts +69 -0
- package/dist/esm/check-cache-record.js +167 -0
- package/dist/esm/check-cache.d.ts +8 -176
- package/dist/esm/check-cache.js +9 -214
- package/dist/esm/child-env.d.ts +57 -0
- package/dist/esm/child-env.js +59 -0
- package/dist/esm/child-process.d.ts +55 -0
- package/dist/esm/child-process.js +112 -0
- package/dist/esm/commands/build.d.ts +0 -16
- package/dist/esm/commands/build.js +9 -214
- package/dist/esm/commands/clean.js +3 -2
- package/dist/esm/commands/compiler.d.ts +18 -0
- package/dist/esm/commands/compiler.js +49 -0
- package/dist/esm/commands/fmt.d.ts +0 -39
- package/dist/esm/commands/fmt.js +4 -81
- package/dist/esm/commands/format-targets.d.ts +39 -0
- package/dist/esm/commands/format-targets.js +83 -0
- package/dist/esm/commands/rewrite-specifiers.d.ts +17 -0
- package/dist/esm/commands/rewrite-specifiers.js +122 -0
- package/dist/esm/commands/tsconfig-chain.d.ts +8 -0
- package/dist/esm/commands/tsconfig-chain.js +63 -0
- package/dist/esm/config-check-cache.d.ts +17 -0
- package/dist/esm/config-check-cache.js +63 -0
- package/dist/esm/config-validation.d.ts +20 -0
- package/dist/esm/config-validation.js +236 -0
- package/dist/esm/config.js +4 -286
- package/dist/esm/crossing-diagnostics.d.ts +39 -0
- package/dist/esm/crossing-diagnostics.js +132 -0
- package/dist/esm/default-scripts.js +1 -1
- package/dist/esm/delegate-host.d.ts +43 -0
- package/dist/esm/delegate-host.js +178 -0
- package/dist/esm/delegation-target.d.ts +52 -0
- package/dist/esm/delegation-target.js +71 -0
- package/dist/esm/fan-out.d.ts +57 -0
- package/dist/esm/fan-out.js +146 -0
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help.js +9 -3
- package/dist/esm/helpers/deriveExcerpt.d.ts +9 -0
- package/dist/esm/helpers/deriveExcerpt.js +1 -1
- package/dist/esm/helpers/renderQuotedList.d.ts +2 -0
- package/dist/esm/helpers/renderQuotedList.js +9 -0
- package/dist/esm/hooks.d.ts +17 -0
- package/dist/esm/hooks.js +23 -0
- package/dist/esm/invocation-args.d.ts +40 -0
- package/dist/esm/invocation-args.js +178 -0
- package/dist/esm/invocation.d.ts +67 -0
- package/dist/esm/invocation.js +3 -0
- package/dist/esm/machine-lock.d.ts +2 -0
- package/dist/esm/machine-lock.js +1 -1
- package/dist/esm/output-channel.d.ts +23 -0
- package/dist/esm/output-channel.js +14 -0
- package/dist/esm/pass-manifest.d.ts +53 -0
- package/dist/esm/pass-manifest.js +89 -0
- package/dist/esm/presentation.d.ts +47 -0
- package/dist/esm/presentation.js +32 -0
- package/dist/esm/recording.d.ts +1 -1
- package/dist/esm/recording.js +2 -2
- package/dist/esm/remote-host-session.d.ts +62 -0
- package/dist/esm/remote-host-session.js +277 -0
- package/dist/esm/remote-probe-host.d.ts +22 -0
- package/dist/esm/remote-probe-host.js +50 -0
- package/dist/esm/remote-probe.d.ts +31 -0
- package/dist/esm/remote-probe.js +78 -0
- package/dist/esm/remote-run.d.ts +28 -73
- package/dist/esm/remote-run.js +103 -165
- package/dist/esm/remote-session.d.ts +40 -0
- package/dist/esm/remote-session.js +151 -0
- package/dist/esm/remote-snapshot.d.ts +21 -0
- package/dist/esm/remote-snapshot.js +80 -0
- package/dist/esm/replay-assembly.d.ts +2 -2
- package/dist/esm/replay-assembly.js +1 -1
- package/dist/esm/run-chain.d.ts +21 -0
- package/dist/esm/run-chain.js +108 -0
- package/dist/esm/runCli.d.ts +1 -9
- package/dist/esm/runCli.js +39 -1002
- package/dist/esm/runner.d.ts +1 -54
- package/dist/esm/runner.js +2 -125
- package/dist/esm/shell-syntax.d.ts +30 -0
- package/dist/esm/shell-syntax.js +120 -0
- package/dist/esm/steps.d.ts +0 -2
- package/dist/esm/steps.js +3 -119
- package/dist/esm/types.d.ts +5 -0
- package/dist/esm/user-config.d.ts +34 -0
- package/dist/esm/user-config.js +93 -0
- package/dist/esm/verbosity.d.ts +6 -0
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +1 -1
- package/dist/esm/verdict.js +1 -1
- package/docs/check-cache.md +28 -12
- package/docs/delegate.md +96 -16
- package/docs/reporting.md +6 -1
- package/docs/scripts.md +2 -1
- package/package.json +3 -3
- /package/dist/esm/{prettier-default-config.d.ts → prettier-default.config.d.ts} +0 -0
- /package/dist/esm/{prettier-default-config.js → prettier-default.config.js} +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Writable } from 'node:stream';
|
|
2
|
+
import { quoteForShell } from "./helpers/quoteForShell.js";
|
|
3
|
+
import { isObject } from "./helpers/type-guards.js";
|
|
4
|
+
import { runHostSession, } from "./remote-host-session.js";
|
|
5
|
+
export function composeSessionScript(options) {
|
|
6
|
+
const { checkoutDir, params } = options;
|
|
7
|
+
const source = `(${runHostSession.toString()})(JSON.parse(process.argv[1]));`;
|
|
8
|
+
return `cd ${checkoutDir} && exec node -e ${quoteForShell(source)} ${quoteForShell(JSON.stringify(params))}`;
|
|
9
|
+
}
|
|
10
|
+
export function createSessionDemultiplexer(options) {
|
|
11
|
+
const { marker, onMalformedRecord, onRecord, stderr } = options;
|
|
12
|
+
const prefix = `${marker} `;
|
|
13
|
+
const prefixBytes = Buffer.from(prefix);
|
|
14
|
+
let pending = Buffer.alloc(0);
|
|
15
|
+
let output = '';
|
|
16
|
+
let hasStarted = false;
|
|
17
|
+
const stream = new Writable({
|
|
18
|
+
write(chunk, _encoding, callback) {
|
|
19
|
+
pending = pending.length === 0 ? chunk : Buffer.concat([pending, chunk]);
|
|
20
|
+
readSessionLines();
|
|
21
|
+
if (hasStarted) {
|
|
22
|
+
forwardRunOutput();
|
|
23
|
+
}
|
|
24
|
+
callback();
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
flush() {
|
|
29
|
+
if (!hasStarted || pending.length === 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
stderr.write(pending);
|
|
33
|
+
pending = Buffer.alloc(0);
|
|
34
|
+
},
|
|
35
|
+
hasStarted: () => hasStarted,
|
|
36
|
+
readOutput: () => (hasStarted ? output : output + pending.toString('utf8')),
|
|
37
|
+
stream,
|
|
38
|
+
};
|
|
39
|
+
function readSessionLines() {
|
|
40
|
+
let newlineIndex = pending.indexOf(NEWLINE);
|
|
41
|
+
while (!hasStarted && newlineIndex !== -1) {
|
|
42
|
+
const line = pending.subarray(0, newlineIndex).toString('utf8');
|
|
43
|
+
pending = pending.subarray(newlineIndex + 1);
|
|
44
|
+
const record = line.startsWith(prefix) ? parseRecord(line.slice(prefix.length)) : undefined;
|
|
45
|
+
if (record === undefined) {
|
|
46
|
+
output += `${line}\n`;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
onRecord(record);
|
|
50
|
+
hasStarted = record.kind === 'run';
|
|
51
|
+
}
|
|
52
|
+
newlineIndex = pending.indexOf(NEWLINE);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function forwardRunOutput() {
|
|
56
|
+
let segmentStart = 0;
|
|
57
|
+
let searchFrom = 0;
|
|
58
|
+
let markerIndex = pending.indexOf(prefixBytes, searchFrom);
|
|
59
|
+
while (markerIndex !== -1) {
|
|
60
|
+
const newlineIndex = pending.indexOf(NEWLINE, markerIndex);
|
|
61
|
+
if (newlineIndex === -1) {
|
|
62
|
+
writeRange(segmentStart, markerIndex);
|
|
63
|
+
pending = pending.subarray(markerIndex);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const kind = readPassesRecord(pending.subarray(markerIndex + prefixBytes.length, newlineIndex).toString('utf8'));
|
|
67
|
+
if (kind !== 'other') {
|
|
68
|
+
writeRange(segmentStart, markerIndex);
|
|
69
|
+
segmentStart = newlineIndex + 1;
|
|
70
|
+
}
|
|
71
|
+
searchFrom = newlineIndex + 1;
|
|
72
|
+
markerIndex = pending.indexOf(prefixBytes, searchFrom);
|
|
73
|
+
}
|
|
74
|
+
const holdFrom = findPartialPrefix(searchFrom);
|
|
75
|
+
writeRange(segmentStart, holdFrom);
|
|
76
|
+
pending = pending.subarray(holdFrom);
|
|
77
|
+
}
|
|
78
|
+
function findPartialPrefix(from) {
|
|
79
|
+
for (let index = Math.max(from, pending.length - prefixBytes.length + 1); index < pending.length; index++) {
|
|
80
|
+
if (prefixBytes.subarray(0, pending.length - index).equals(pending.subarray(index))) {
|
|
81
|
+
return index;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return pending.length;
|
|
85
|
+
}
|
|
86
|
+
function readPassesRecord(json) {
|
|
87
|
+
let value;
|
|
88
|
+
try {
|
|
89
|
+
value = JSON.parse(json);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return 'other';
|
|
93
|
+
}
|
|
94
|
+
if (!isObject(value) || value['kind'] !== 'passes') {
|
|
95
|
+
return 'other';
|
|
96
|
+
}
|
|
97
|
+
const entries = value['entries'];
|
|
98
|
+
if (!Array.isArray(entries)) {
|
|
99
|
+
onMalformedRecord?.();
|
|
100
|
+
return 'malformed';
|
|
101
|
+
}
|
|
102
|
+
onRecord({ kind: 'passes', entries });
|
|
103
|
+
return 'passes';
|
|
104
|
+
}
|
|
105
|
+
function writeRange(start, end) {
|
|
106
|
+
if (end > start) {
|
|
107
|
+
stderr.write(pending.subarray(start, end));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function formatDelegationWaitingLine(holder, host) {
|
|
112
|
+
if (holder === undefined) {
|
|
113
|
+
return `nmr: waiting for another delegation's checkout on ${host}`;
|
|
114
|
+
}
|
|
115
|
+
return `nmr: waiting for ${holder.command} in ${holder.dir} (pid ${holder.pid}) to finish on ${host}`;
|
|
116
|
+
}
|
|
117
|
+
const NEWLINE = 0x0a;
|
|
118
|
+
function isDelegationHolder(value) {
|
|
119
|
+
return (isObject(value) &&
|
|
120
|
+
typeof value['command'] === 'string' &&
|
|
121
|
+
typeof value['dir'] === 'string' &&
|
|
122
|
+
typeof value['pid'] === 'number');
|
|
123
|
+
}
|
|
124
|
+
function isSessionRecord(value) {
|
|
125
|
+
if (!isObject(value))
|
|
126
|
+
return false;
|
|
127
|
+
switch (value['kind']) {
|
|
128
|
+
case 'failed':
|
|
129
|
+
return (typeof value['exitCode'] === 'number' &&
|
|
130
|
+
typeof value['output'] === 'string' &&
|
|
131
|
+
(value['phase'] === 'check out' || value['phase'] === 'install'));
|
|
132
|
+
case 'locked':
|
|
133
|
+
case 'run':
|
|
134
|
+
return true;
|
|
135
|
+
case 'passes':
|
|
136
|
+
return Array.isArray(value['entries']);
|
|
137
|
+
case 'waiting':
|
|
138
|
+
return value['holder'] === undefined || isDelegationHolder(value['holder']);
|
|
139
|
+
default:
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function parseRecord(json) {
|
|
144
|
+
try {
|
|
145
|
+
const value = JSON.parse(json);
|
|
146
|
+
return isSessionRecord(value) ? value : undefined;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A commit that records the working tree, or why one could not be made. */
|
|
2
|
+
export type SnapshotResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
sha: string;
|
|
5
|
+
toplevel: string;
|
|
6
|
+
treeId: string;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
isUnsupported: boolean;
|
|
10
|
+
reason: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Builds a commit whose tree is the working tree as it is on disk: tracked changes, staged or not, and untracked
|
|
14
|
+
* files that git does not ignore. The commit is built in a temporary index, so the repository's index, refs, and
|
|
15
|
+
* stash are untouched; the only write is to the object database.
|
|
16
|
+
*
|
|
17
|
+
* The identity and date are fixed, so that the same tree on the same `HEAD` always yields the same commit and a
|
|
18
|
+
* repeated push transfers nothing. A submodule, or a nested repository that `git add` would record as one, is
|
|
19
|
+
* refused: Its content is in a repository that the snapshot does not include.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createSnapshot(cwd: string, env: NodeJS.ProcessEnv): SnapshotResult;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { GIT_OUTPUT_LIMIT } from '@williamthorsen/nmr-core';
|
|
6
|
+
export function createSnapshot(cwd, env) {
|
|
7
|
+
const toplevelResult = runGit(['rev-parse', '--show-toplevel'], cwd, env);
|
|
8
|
+
if (!toplevelResult.ok) {
|
|
9
|
+
return { ok: false, isUnsupported: true, reason: `not a git repository: ${toplevelResult.error}` };
|
|
10
|
+
}
|
|
11
|
+
const toplevel = toplevelResult.stdout.trim();
|
|
12
|
+
if (fs.existsSync(path.join(toplevel, '.gitmodules'))) {
|
|
13
|
+
return { ok: false, isUnsupported: true, reason: 'the repository declares submodules' };
|
|
14
|
+
}
|
|
15
|
+
const indexPathResult = runGit(['rev-parse', '--git-path', 'index'], toplevel, env);
|
|
16
|
+
if (!indexPathResult.ok) {
|
|
17
|
+
return { ok: false, isUnsupported: false, reason: indexPathResult.error };
|
|
18
|
+
}
|
|
19
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nmr-snapshot-'));
|
|
20
|
+
try {
|
|
21
|
+
const tempIndex = path.join(tempDir, 'index');
|
|
22
|
+
const indexPath = path.resolve(toplevel, indexPathResult.stdout.trim());
|
|
23
|
+
if (fs.existsSync(indexPath)) {
|
|
24
|
+
copyIndex(indexPath, tempIndex);
|
|
25
|
+
}
|
|
26
|
+
const indexEnv = { ...env, GIT_INDEX_FILE: tempIndex };
|
|
27
|
+
const addResult = runGit(['add', '--all'], toplevel, indexEnv);
|
|
28
|
+
if (!addResult.ok) {
|
|
29
|
+
return { ok: false, isUnsupported: false, reason: addResult.error };
|
|
30
|
+
}
|
|
31
|
+
const stageResult = runGit(['ls-files', '--stage', '-z'], toplevel, indexEnv);
|
|
32
|
+
if (!stageResult.ok) {
|
|
33
|
+
return { ok: false, isUnsupported: false, reason: stageResult.error };
|
|
34
|
+
}
|
|
35
|
+
if (stageResult.stdout.split('\0').some((entry) => entry.startsWith('160000 '))) {
|
|
36
|
+
return { ok: false, isUnsupported: true, reason: 'the working tree contains a submodule or a nested repository' };
|
|
37
|
+
}
|
|
38
|
+
const treeResult = runGit(['write-tree'], toplevel, indexEnv);
|
|
39
|
+
if (!treeResult.ok) {
|
|
40
|
+
return { ok: false, isUnsupported: false, reason: treeResult.error };
|
|
41
|
+
}
|
|
42
|
+
const headResult = runGit(['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'], toplevel, env);
|
|
43
|
+
const parentArgs = headResult.ok ? ['-p', headResult.stdout.trim()] : [];
|
|
44
|
+
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
|
+
});
|
|
49
|
+
if (!commitResult.ok) {
|
|
50
|
+
return { ok: false, isUnsupported: false, reason: commitResult.error };
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, sha: commitResult.stdout.trim(), toplevel, treeId };
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
fs.rmSync(tempDir, { force: true, recursive: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const SNAPSHOT_IDENTITY_ENV = {
|
|
59
|
+
GIT_AUTHOR_DATE: '@0 +0000',
|
|
60
|
+
GIT_AUTHOR_EMAIL: 'nmr@localhost',
|
|
61
|
+
GIT_AUTHOR_NAME: 'nmr delegate',
|
|
62
|
+
GIT_COMMITTER_DATE: '@0 +0000',
|
|
63
|
+
GIT_COMMITTER_EMAIL: 'nmr@localhost',
|
|
64
|
+
GIT_COMMITTER_NAME: 'nmr delegate',
|
|
65
|
+
};
|
|
66
|
+
function copyIndex(source, destination) {
|
|
67
|
+
fs.copyFileSync(source, destination);
|
|
68
|
+
const { atime, mtime } = fs.statSync(source);
|
|
69
|
+
fs.utimesSync(destination, atime, mtime);
|
|
70
|
+
}
|
|
71
|
+
function runGit(args, cwd, env) {
|
|
72
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', env, maxBuffer: GIT_OUTPUT_LIMIT });
|
|
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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ReplayLine } from './check-cache.js';
|
|
1
|
+
import { type ReplayLine } from './check-cache-entries.js';
|
|
2
2
|
import { type Step } from './steps.js';
|
|
3
3
|
/**
|
|
4
4
|
* Assembles what a composite's skip replays: the excerpts recorded by its constituents, in the order in which
|
|
@@ -14,7 +14,7 @@ import { type Step } from './steps.js';
|
|
|
14
14
|
*
|
|
15
15
|
* An entry is admissible when the run that certified it is this one and the tree that it describes is this one.
|
|
16
16
|
* The witness is what a run stamps on an excerpt that it records and on one that it recalls and replays; the
|
|
17
|
-
* tree
|
|
17
|
+
* tree id bounds an identity carried by a process out of the run that issued it.
|
|
18
18
|
*/
|
|
19
19
|
export declare function assembleReplay(options: {
|
|
20
20
|
anchorDir: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readCheckCacheEntry } from "./check-cache.js";
|
|
1
|
+
import { readCheckCacheEntry } from "./check-cache-entries.js";
|
|
2
2
|
import { readNmrStep } from "./steps.js";
|
|
3
3
|
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
4
4
|
export async function assembleReplay(options) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
|
+
import type { TreeSnapshot } from './check-cache.js';
|
|
3
|
+
import type { Invocation, PhaseResult, RunPlan } from './invocation.js';
|
|
4
|
+
import type { ScriptRegistry } from './resolve-scripts.js';
|
|
5
|
+
import { type ResolvedScript } from './resolver.js';
|
|
6
|
+
/**
|
|
7
|
+
* Assembles the chain that a resolved script runs at this scope, the hooks wrapped around it included, and the key
|
|
8
|
+
* under which a pass by that chain is recorded. Settles a no-op override and an argument that no step can take.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assembleChain(options: {
|
|
11
|
+
invocation: Invocation;
|
|
12
|
+
registry: ScriptRegistry;
|
|
13
|
+
resolvedScript: ResolvedScript;
|
|
14
|
+
snapshot: TreeSnapshot | undefined;
|
|
15
|
+
}): PhaseResult<Pick<RunPlan, 'commandString' | 'key' | 'ownSteps' | 'steps'>>;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the line announcing that a `package.json` script is standing in for a built-in, or `undefined` under
|
|
18
|
+
* `quiet` or when none is due. Only a script replacing a name that the registry already defines is worth
|
|
19
|
+
* announcing; an ordinary tier-3 entry that happens to resolve is not standing in for anything.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatOverrideNotice(resolvedScript: ResolvedScript, registry: ScriptRegistry, command: string, anchorDir: string, quiet: boolean, style: OutputStyle): string | undefined;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { formatGlyphLine, reportError } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { resolveCacheKey } from "./check-cache-gate.js";
|
|
4
|
+
import { reportNmrCrossing } from "./crossing-diagnostics.js";
|
|
5
|
+
import { NMR_GLYPHS } from "./glyphs.js";
|
|
6
|
+
import { isHookName } from "./helpers/hook-name.js";
|
|
7
|
+
import { quoteForShell } from "./helpers/quoteForShell.js";
|
|
8
|
+
import { wrapWithHooks } from "./hooks.js";
|
|
9
|
+
import { reportVerdict } from "./presentation.js";
|
|
10
|
+
import { applyDevBinToSteps } from "./resolver.js";
|
|
11
|
+
import { dropRecursiveSteps, renderChain } from "./steps.js";
|
|
12
|
+
export function assembleChain(options) {
|
|
13
|
+
const { invocation, registry, resolvedScript, snapshot } = options;
|
|
14
|
+
const { anchorDir, command, context, env, parsedArgs, stderr, stdout, styles } = invocation;
|
|
15
|
+
const { isEmptiedByWorkspace, steps: runnableSteps } = readRunnableSteps(resolvedScript, context.workspacePackageDirs);
|
|
16
|
+
const resolvedCommand = renderChain(runnableSteps);
|
|
17
|
+
const noOpReason = findNoOpReason(resolvedCommand, isEmptiedByWorkspace);
|
|
18
|
+
if (noOpReason !== undefined && !parsedArgs.shouldPrintLog) {
|
|
19
|
+
reportVerdict({ command, scope: invocation.scope, outcome: 'no-op', reason: noOpReason }, stdout, invocation.format, styles.stdout);
|
|
20
|
+
return { settled: true, result: { exitCode: 0 } };
|
|
21
|
+
}
|
|
22
|
+
const substitutedSteps = applyDevBinToSteps(runnableSteps, context.config.devBin, context.monorepoRoot);
|
|
23
|
+
const substitutedCommand = renderChain(substitutedSteps);
|
|
24
|
+
const bindResult = bindPassthrough(substitutedSteps, parsedArgs.passthrough, command);
|
|
25
|
+
if (!bindResult.ok) {
|
|
26
|
+
reportError(bindResult.error, stderr);
|
|
27
|
+
return { settled: true, result: { exitCode: 1 } };
|
|
28
|
+
}
|
|
29
|
+
const mainSteps = bindResult.steps;
|
|
30
|
+
const isHookInvocation = isHookName(command);
|
|
31
|
+
const fullSteps = isHookInvocation
|
|
32
|
+
? mainSteps
|
|
33
|
+
: wrapWithHooks(command, mainSteps, registry, anchorDir, parsedArgs.isWorkspaceRoot);
|
|
34
|
+
const fullCommand = renderChain(fullSteps);
|
|
35
|
+
reportNmrCrossing({
|
|
36
|
+
config: context.config,
|
|
37
|
+
isReading: parsedArgs.shouldPrintLog,
|
|
38
|
+
isWorkspaceRoot: parsedArgs.isWorkspaceRoot,
|
|
39
|
+
monorepoRoot: context.monorepoRoot,
|
|
40
|
+
registry,
|
|
41
|
+
resolvedScript,
|
|
42
|
+
shouldUseRoot: invocation.shouldUseRoot,
|
|
43
|
+
stderr,
|
|
44
|
+
style: styles.stderr,
|
|
45
|
+
});
|
|
46
|
+
const key = resolveCacheKey({
|
|
47
|
+
anchorDir,
|
|
48
|
+
command,
|
|
49
|
+
commandString: fullCommand,
|
|
50
|
+
env,
|
|
51
|
+
monorepoRoot: context.monorepoRoot,
|
|
52
|
+
snapshot,
|
|
53
|
+
stderr,
|
|
54
|
+
substitution: substitutedCommand === resolvedCommand ? undefined : substitutedCommand,
|
|
55
|
+
});
|
|
56
|
+
return { settled: false, value: { commandString: fullCommand, key, ownSteps: mainSteps, steps: fullSteps } };
|
|
57
|
+
}
|
|
58
|
+
export function formatOverrideNotice(resolvedScript, registry, command, anchorDir, quiet, style) {
|
|
59
|
+
const registryEntry = Object.hasOwn(registry, command) ? registry[command] : undefined;
|
|
60
|
+
if (quiet || resolvedScript.origin.tier !== 'package' || registryEntry === undefined) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const notice = `${path.basename(anchorDir)}: Using override script: ${renderChain(resolvedScript.steps)}`;
|
|
64
|
+
return `${formatGlyphLine(NMR_GLYPHS, style, 'package', notice)}\n`;
|
|
65
|
+
}
|
|
66
|
+
function acceptsArguments(step) {
|
|
67
|
+
return step.kind === 'opaque' || step.shouldDeclineArguments !== true;
|
|
68
|
+
}
|
|
69
|
+
function bindPassthrough(steps, passthrough, command) {
|
|
70
|
+
if (passthrough.length === 0) {
|
|
71
|
+
return { ok: true, steps };
|
|
72
|
+
}
|
|
73
|
+
if (!steps.some(acceptsArguments)) {
|
|
74
|
+
return { ok: false, error: formatUnroutableArgumentsError(command) };
|
|
75
|
+
}
|
|
76
|
+
const boundSteps = steps.map((step) => {
|
|
77
|
+
if (!acceptsArguments(step)) {
|
|
78
|
+
return step;
|
|
79
|
+
}
|
|
80
|
+
return step.kind === 'structural'
|
|
81
|
+
? { ...step, argv: [...step.argv, ...passthrough] }
|
|
82
|
+
: { kind: 'opaque', command: `${step.command} ${passthrough.map(quoteForShell).join(' ')}` };
|
|
83
|
+
});
|
|
84
|
+
return { ok: true, steps: boundSteps };
|
|
85
|
+
}
|
|
86
|
+
function findNoOpReason(resolvedCommand, isEmptiedByWorkspace) {
|
|
87
|
+
if (isEmptiedByWorkspace) {
|
|
88
|
+
return 'empty-workspace';
|
|
89
|
+
}
|
|
90
|
+
if (resolvedCommand === '') {
|
|
91
|
+
return 'empty-override';
|
|
92
|
+
}
|
|
93
|
+
if (resolvedCommand === ':') {
|
|
94
|
+
return 'noop-override';
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
function formatUnroutableArgumentsError(command) {
|
|
99
|
+
return (`\`${command}\` does not take trailing arguments: every step of its chain declines them. ` +
|
|
100
|
+
'Running it unnarrowed is not what the arguments asked for, so nothing ran.');
|
|
101
|
+
}
|
|
102
|
+
function readRunnableSteps(resolvedScript, workspacePackageDirs) {
|
|
103
|
+
if (workspacePackageDirs.length > 0) {
|
|
104
|
+
return { isEmptiedByWorkspace: false, steps: resolvedScript.steps };
|
|
105
|
+
}
|
|
106
|
+
const steps = dropRecursiveSteps(resolvedScript.steps);
|
|
107
|
+
return { isEmptiedByWorkspace: steps.length === 0 && resolvedScript.steps.length > 0, steps };
|
|
108
|
+
}
|
package/dist/esm/runCli.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
|
-
|
|
3
|
-
* Marks a run made on a delegating caller's behalf, in which a command that the registry does not define, and a
|
|
4
|
-
* `--log` with nothing to show, exit 0 rather than failing.
|
|
5
|
-
*/
|
|
6
|
-
export declare const RUN_IF_PRESENT_ENV_VAR = "NMR_RUN_IF_PRESENT";
|
|
2
|
+
import { type RunCliResult } from './invocation.js';
|
|
7
3
|
/** @internal */
|
|
8
4
|
export interface RunCliOptions {
|
|
9
5
|
/** Post-slice CLI arguments (equivalent to `process.argv.slice(2)`). */
|
|
@@ -17,10 +13,6 @@ export interface RunCliOptions {
|
|
|
17
13
|
/** Stream for errors, warnings, and debug notes. */
|
|
18
14
|
stderr: Writable;
|
|
19
15
|
}
|
|
20
|
-
/** @internal */
|
|
21
|
-
export interface RunCliResult {
|
|
22
|
-
exitCode: number;
|
|
23
|
-
}
|
|
24
16
|
/**
|
|
25
17
|
* Executes the nmr CLI flow in-process and returns the resulting exit code.
|
|
26
18
|
* Does not hold global state, does not read `process.*` globals, and never calls `process.exit`.
|