@orkestrel/scaffold 0.0.107 → 0.0.109
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/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
- package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
- package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
- package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +368 -27
- package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
- package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
- package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
- package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
- package/dist/agents/templates/brief.md +1 -1
- package/dist/bin/main.js +51 -46
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +26 -20
- package/dist/host/agents/orchestration.md +98 -58
- package/dist/host/agents/platforms/posix.md +15 -0
- package/dist/host/agents/platforms/windows.md +37 -0
- package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
- package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
- package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
- package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
- package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
- package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
- package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +12 -14
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +36 -24
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +14 -11
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +428 -27
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
- package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
- package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
- package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
- package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
- package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
- package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
- package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
- package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
- package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
- package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
- package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
- package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +91 -30
- package/dist/host/agents/skills/orkestrel-parallel/agents/openai.yaml +2 -2
- package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +34 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +29 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +29 -0
- package/dist/host/agents/skills/orkestrel-parallel/references/t3.md +31 -0
- package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
- package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
- package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
- package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
- package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -10
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
- package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
- package/dist/host/agents/templates/brief.md +1 -1
- package/dist/host/agents/transports/claude-cli.md +3 -3
- package/dist/host/agents/transports/codex.md +3 -8
- package/dist/host/agents/transports/cursor.md +2 -3
- package/dist/host/claude/AGENTS.md +7 -10
- package/dist/host/claude/agents/analyst.md +3 -3
- package/dist/host/claude/agents/astra.md +3 -3
- package/dist/host/claude/agents/builder.md +1 -1
- package/dist/host/claude/agents/checker.md +1 -1
- package/dist/host/claude/agents/distiller.md +1 -1
- package/dist/host/claude/agents/grok.md +1 -1
- package/dist/host/claude/agents/opus.md +2 -2
- package/dist/host/claude/agents/orkestrel.md +57 -90
- package/dist/host/claude/agents/researcher.md +1 -1
- package/dist/host/claude/agents/reviewer.md +4 -0
- package/dist/host/claude/agents/scout.md +1 -1
- package/dist/host/claude/agents/verifier.md +1 -1
- package/dist/host/claude/rules/application.md +14 -21
- package/dist/host/claude/rules/architecture.md +34 -44
- package/dist/host/claude/rules/browser.md +3 -4
- package/dist/host/claude/rules/campaign.md +55 -0
- package/dist/host/claude/rules/documentation.md +26 -11
- package/dist/host/claude/rules/names.md +9 -4
- package/dist/host/claude/rules/patterns.md +6 -7
- package/dist/host/claude/rules/portability.md +2 -3
- package/dist/host/claude/rules/quality.md +4 -5
- package/dist/host/claude/rules/styles.md +2 -2
- package/dist/host/claude/rules/tests.md +57 -114
- package/dist/host/claude/rules/typescript.md +8 -10
- package/dist/host/claude/rules/workspace.md +21 -19
- package/dist/host/claude/rules/writing.md +3 -3
- package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-parallel/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +9 -3
- package/dist/host/codex/agents/astra.toml +3 -4
- package/dist/host/codex/agents/builder.toml +1 -1
- package/dist/host/codex/agents/opus.toml +3 -4
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/codex/agents/researcher.toml +2 -2
- package/dist/host/codex/agents/verifier.toml +1 -1
- package/dist/host/codex/config.toml +3 -3
- package/dist/host/configs/helpers.ts +21 -1
- package/dist/host/configs/policy.ts +405 -16
- package/dist/host/cursor/rules/orchestration.mdc +1 -0
- package/dist/host/dotfiles/oxlintrc.json +141 -5
- package/dist/host/guides/agent.md +111 -134
- package/dist/host/guides/capture.md +801 -0
- package/dist/host/guides/probe.md +27 -8
- package/dist/host/guides/scaffold.md +152 -19
- package/dist/host/guides/study.md +534 -440
- package/dist/host/guides/test.md +111 -44
- package/dist/host/manifest.json +199 -242
- package/dist/host/tests/config.test.ts +1558 -219
- package/dist/host/tests/policy.test.ts +127 -1
- package/dist/host/tests/setupPolicy.ts +1389 -39
- package/dist/src/core/index.cjs +239 -87
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +123 -39
- package/dist/src/core/index.d.ts +123 -39
- package/dist/src/core/index.js +234 -86
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +20 -27
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.js +21 -28
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -11
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
- package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
- package/dist/host/agents/skills/orkestrel-parallel/references/harnesses.md +0 -19
- package/dist/host/guides/supervisor.md +0 -4890
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { copyFileSync, existsSync, lstatSync, readFileSync, mkdirSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { copyFileSync, existsSync, lstatSync, readFileSync, mkdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
|
|
7
|
-
const STEPS = [
|
|
6
|
+
/** Lists the steps of a visit in the order they run. */
|
|
7
|
+
export const STEPS = [
|
|
8
8
|
'pin',
|
|
9
9
|
'commit',
|
|
10
10
|
'overwrite',
|
|
@@ -16,20 +16,268 @@ const STEPS = [
|
|
|
16
16
|
'gates',
|
|
17
17
|
'compare',
|
|
18
18
|
];
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
/** Lists the package scripts the gates step runs, in order. */
|
|
20
|
+
export const GATES = ['format:check', 'lint:check', 'check', 'build', 'test'];
|
|
21
|
+
/** Lists the manifest fields whose ranges ship to a consumer. */
|
|
22
|
+
export const RUNTIME_FIELDS = ['dependencies', 'peerDependencies'];
|
|
23
|
+
/** Lists the manifest fields whose ranges a visit re-pins. */
|
|
24
|
+
export const RANGE_FIELDS = [...RUNTIME_FIELDS, 'devDependencies'];
|
|
25
|
+
/** Names the agent file that carries the catalog table. */
|
|
26
|
+
export const CATALOG = '.claude/agents/orkestrel.md';
|
|
27
|
+
/** Holds the text an offline overwrite refusal names. */
|
|
28
|
+
export const OFFLINE_REFUSAL = "'catalog' does not take --offline";
|
|
29
|
+
/**
|
|
30
|
+
* Names the package whose checkout the vendored host is staged from.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* The vendored host is staged from this package's checkout, so overwriting it from the host deletes
|
|
34
|
+
* and replaces its own canon.
|
|
35
|
+
*/
|
|
36
|
+
export const SCAFFOLD = '@orkestrel/scaffold';
|
|
37
|
+
/** Matches a plain version, optionally with a prerelease. */
|
|
38
|
+
export const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
39
|
+
/**
|
|
40
|
+
* Names the extension of the running file.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* The checkout runs the `.ts` sources and a built twin under `dist/agents` runs `.js` siblings.
|
|
44
|
+
*/
|
|
45
|
+
export const EXTENSION = extname(fileURLToPath(import.meta.url));
|
|
46
|
+
/** Holds the path of the sibling `compare` script. */
|
|
47
|
+
export const COMPARE = fileURLToPath(new URL(`./compare${EXTENSION}`, import.meta.url));
|
|
48
|
+
/** Holds the path of the sibling `pins` script. */
|
|
49
|
+
export const PINS = fileURLToPath(new URL(`./pins${EXTENSION}`, import.meta.url));
|
|
50
|
+
/**
|
|
51
|
+
* Matches one entry of `git status --porcelain=v1 -z` output.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* A rename or copy entry carries its original path as a second NUL-terminated field. The first
|
|
55
|
+
* group holds a rename's new path, the second its original, and the third any other entry's path.
|
|
56
|
+
*/
|
|
57
|
+
export const DIRTY_ENTRY = /(?=[RC][^\0]|[^\0][RC])[^\0]{2} ([^\0]*)\0([^\0]*)(?:\0|$)|[^\0]{2} ([^\0]*)(?:\0|$)/gu;
|
|
58
|
+
/** Lists every flag the script reads, value flags and switches alike. */
|
|
59
|
+
export const FLAGS = [
|
|
60
|
+
'--visit',
|
|
61
|
+
'--plan',
|
|
62
|
+
'--target',
|
|
63
|
+
'--offline',
|
|
64
|
+
'--regenerate',
|
|
65
|
+
'--from',
|
|
66
|
+
'--to',
|
|
67
|
+
'--prior',
|
|
68
|
+
'--dry-run',
|
|
69
|
+
'--json',
|
|
70
|
+
'--out',
|
|
71
|
+
'--catalog',
|
|
72
|
+
];
|
|
73
|
+
/** Holds the usage line the script prints when no mode was given. */
|
|
74
|
+
export const USAGE = 'usage: wave.ts --visit [--target DIR] [--offline] [--regenerate] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE] | --plan [--catalog PATH] [--json]';
|
|
75
|
+
/** Names the manifest fields that hold specs, including nested overrides. */
|
|
76
|
+
export const SPEC_FIELDS = Object.freeze([
|
|
77
|
+
'dependencies',
|
|
78
|
+
'devDependencies',
|
|
79
|
+
'peerDependencies',
|
|
80
|
+
'optionalDependencies',
|
|
81
|
+
'overrides',
|
|
82
|
+
]);
|
|
83
|
+
/** Matches npm local prefixes and path starts, including drive-relative paths. */
|
|
84
|
+
export const LOCAL_SPEC_PATTERN = /^(?:file:|link:|\.|~[\\/]|[\\/]|[A-Za-z]:)/iu;
|
|
85
|
+
/** Matches archive specs with no protocol and a tgz, tar.gz, or tar extension. */
|
|
86
|
+
export const ARCHIVE_SPEC_PATTERN = /^[^:]+\.(?:tgz|tar\.gz|tar)$/iu;
|
|
87
|
+
/** Matches hosted user/repository shorthand even when its name ends in an archive extension. */
|
|
88
|
+
export const SHORTHAND_SPEC_PATTERN = /^[^:@%/\s.-][^:@%/\s]*\/[^:@\s/%]+$/u;
|
|
89
|
+
/**
|
|
90
|
+
* Checks whether a value is a non-null, non-array object.
|
|
91
|
+
*
|
|
92
|
+
* @param value - The value to inspect.
|
|
93
|
+
* @returns True if the value is an object other than null or an array; false otherwise.
|
|
94
|
+
*/
|
|
95
|
+
export function isRecord(value) {
|
|
96
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Checks whether npm resolves a spec from the local host.
|
|
100
|
+
*
|
|
101
|
+
* @param spec - The package spec, with leading whitespace ignored.
|
|
102
|
+
* @returns True if the spec names a local path or archive; false otherwise.
|
|
103
|
+
*/
|
|
104
|
+
export function matchesLocalSpec(spec) {
|
|
105
|
+
const text = spec.trimStart();
|
|
106
|
+
return (LOCAL_SPEC_PATTERN.test(text) ||
|
|
107
|
+
(ARCHIVE_SPEC_PATTERN.test(text) && !SHORTHAND_SPEC_PATTERN.test(text)));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Collects string specs with the dotted path of each nested record member.
|
|
111
|
+
*
|
|
112
|
+
* @param path - The field path that prefixes each finding.
|
|
113
|
+
* @param value - A string or finite, acyclic manifest record; other values contribute no specs.
|
|
114
|
+
* @returns The path and spec pairs in record order.
|
|
115
|
+
*/
|
|
116
|
+
export function collectSpecs(path, value) {
|
|
117
|
+
if (typeof value === 'string')
|
|
118
|
+
return [[path, value]];
|
|
119
|
+
if (!isRecord(value))
|
|
120
|
+
return [];
|
|
121
|
+
return Object.entries(value).flatMap(([key, child]) => collectSpecs(`${path}.${key}`, child));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Finds local specs in the manifest's spec fields, including nested overrides.
|
|
125
|
+
*
|
|
126
|
+
* @param manifest - A finite, acyclic package manifest record.
|
|
127
|
+
* @returns Each local spec's dotted field path and spec, in field and record order.
|
|
128
|
+
*/
|
|
129
|
+
export function scanLocalSpecs(manifest) {
|
|
130
|
+
return SPEC_FIELDS.flatMap((field) => collectSpecs(field, manifest[field]).filter(([, spec]) => matchesLocalSpec(spec)));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Removes a tree with bounded retries for transient host removal errors.
|
|
134
|
+
*
|
|
135
|
+
* @param path - The directory to remove; a missing path succeeds.
|
|
136
|
+
* @returns Nothing after the removal succeeds.
|
|
137
|
+
* @throws Thrown when removal fails after the bounded retries or the host refuses the path.
|
|
138
|
+
*/
|
|
139
|
+
export function removeTree(path) {
|
|
140
|
+
rmSync(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Normalizes a host path to forward slashes.
|
|
144
|
+
*
|
|
145
|
+
* @param path - A host path.
|
|
146
|
+
* @returns The same path with every backslash replaced by a slash.
|
|
147
|
+
*/
|
|
148
|
+
export function normalizeSlashes(path) {
|
|
149
|
+
return path.split('\\').join('/');
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Reads the value that follows a flag.
|
|
153
|
+
*
|
|
154
|
+
* @remarks
|
|
155
|
+
* Reads `--flag value` pairs: the first occurrence of a flag wins, a following value that opens
|
|
156
|
+
* with `--` is a missing value, and the `--flag=value` form is not read.
|
|
157
|
+
*
|
|
158
|
+
* @param argv - The arguments after the script path.
|
|
159
|
+
* @param name - The flag, such as `--target`.
|
|
160
|
+
* @returns The value after the flag, or undefined when the flag is absent, last, or followed by
|
|
161
|
+
* another flag.
|
|
162
|
+
*/
|
|
163
|
+
export function readOption(argv, name) {
|
|
164
|
+
const index = argv.indexOf(name);
|
|
165
|
+
const value = index === -1 ? undefined : argv[index + 1];
|
|
166
|
+
return value === undefined || value.startsWith('--') ? undefined : value;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Names the value flags a caller gave with no value.
|
|
170
|
+
*
|
|
171
|
+
* @param argv - The arguments after the script path.
|
|
172
|
+
* @param names - The flags that take a value.
|
|
173
|
+
* @returns Each named flag that appears last or is followed by another flag, in argument order.
|
|
174
|
+
*/
|
|
175
|
+
export function readMissingFlags(argv, names) {
|
|
176
|
+
return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Names the flags a caller gave that the script does not read.
|
|
180
|
+
*
|
|
181
|
+
* @remarks
|
|
182
|
+
* Reads the arguments before a bare `--`, which opens a command a script passes on. A `--flag=value`
|
|
183
|
+
* argument is unknown, because the script reads no such form.
|
|
184
|
+
*
|
|
185
|
+
* @param argv - The arguments after the script path.
|
|
186
|
+
* @param flags - Every flag the script reads, value flags and switches alike.
|
|
187
|
+
* @returns Each argument that opens with `--` and is not a read flag, in argument order.
|
|
188
|
+
*/
|
|
189
|
+
export function readUnknownFlags(argv, flags) {
|
|
190
|
+
const end = argv.indexOf('--');
|
|
191
|
+
return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Reads the last non-empty line of a text as a JSON object.
|
|
195
|
+
*
|
|
196
|
+
* @param text - Text whose last line a script printed as JSON.
|
|
197
|
+
* @returns The parsed object, or undefined when the last line is not a JSON object.
|
|
198
|
+
*/
|
|
199
|
+
export function parseJsonLine(text) {
|
|
200
|
+
const line = text
|
|
201
|
+
.split(/\r\n|\n/)
|
|
202
|
+
.filter((candidate) => candidate.trim() !== '')
|
|
203
|
+
.at(-1);
|
|
204
|
+
if (line === undefined)
|
|
205
|
+
return undefined;
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(line);
|
|
208
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
209
|
+
return Object.fromEntries(Object.entries(parsed));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Parses a whole text as one JSON object.
|
|
217
|
+
*
|
|
218
|
+
* @param text - A JSON document, formatted or not, such as a manifest or `npm view --json` output.
|
|
219
|
+
* @returns The parsed object, or undefined when the text is not a JSON object.
|
|
220
|
+
*/
|
|
221
|
+
export function parseJsonRecord(text) {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = JSON.parse(text);
|
|
224
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
225
|
+
return Object.fromEntries(Object.entries(parsed));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch { }
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Reads a string member of a record.
|
|
233
|
+
*
|
|
234
|
+
* @param record - The record, or undefined.
|
|
235
|
+
* @param key - The member name.
|
|
236
|
+
* @returns The string, or undefined when the member is absent or not a string.
|
|
237
|
+
*/
|
|
238
|
+
export function readString(record, key) {
|
|
239
|
+
const value = record?.[key];
|
|
240
|
+
return typeof value === 'string' ? value : undefined;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Resolves the npm command for this host.
|
|
244
|
+
*
|
|
245
|
+
* @remarks
|
|
246
|
+
* The `npm-cli.js` entry beside the running Node binary spawns without a shell on every host; the
|
|
247
|
+
* `npm` shim on Windows is a `.cmd` file that needs one, so that path is the fallback alone.
|
|
248
|
+
*
|
|
249
|
+
* @returns The file to spawn, the arguments that precede npm's own, and whether a shell is needed.
|
|
250
|
+
*/
|
|
251
|
+
export function resolveNpm() {
|
|
252
|
+
const cli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
253
|
+
if (existsSync(cli))
|
|
254
|
+
return { file: process.execPath, prefix: [cli], shell: false };
|
|
255
|
+
return { file: 'npm', prefix: [], shell: process.platform === 'win32' };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Runs npm with the given arguments and returns the finished child.
|
|
259
|
+
*
|
|
260
|
+
* @param args - The npm arguments, such as `['install']`.
|
|
261
|
+
* @param cwd - The working directory. Default: the current directory.
|
|
262
|
+
* @returns The spawn result with `utf8` stdout and stderr.
|
|
263
|
+
*/
|
|
264
|
+
export function runNpm(args, cwd) {
|
|
265
|
+
const npm = resolveNpm();
|
|
266
|
+
return spawnSync(npm.file, [...npm.prefix, ...args], {
|
|
267
|
+
encoding: 'utf8',
|
|
268
|
+
windowsHide: true,
|
|
269
|
+
shell: npm.shell,
|
|
270
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
271
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Reads a target's package manifest.
|
|
276
|
+
*
|
|
277
|
+
* @param target - The target directory.
|
|
278
|
+
* @returns The manifest record, or undefined when it is absent, unreadable, or not a JSON object.
|
|
279
|
+
*/
|
|
280
|
+
export function readManifest(target) {
|
|
33
281
|
try {
|
|
34
282
|
return parseJsonRecord(readFileSync(join(target, 'package.json'), 'utf8'));
|
|
35
283
|
}
|
|
@@ -37,7 +285,14 @@ function readManifest(target) {
|
|
|
37
285
|
return undefined;
|
|
38
286
|
}
|
|
39
287
|
}
|
|
40
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Reads the ranges a manifest declares in the named fields.
|
|
290
|
+
*
|
|
291
|
+
* @param manifest - The manifest record.
|
|
292
|
+
* @param fields - The fields to read.
|
|
293
|
+
* @returns Each string range keyed by `field name`.
|
|
294
|
+
*/
|
|
295
|
+
export function readRanges(manifest, fields) {
|
|
41
296
|
return new Map(fields.flatMap((field) => {
|
|
42
297
|
const block = manifest[field];
|
|
43
298
|
if (typeof block !== 'object' || block === null)
|
|
@@ -45,7 +300,14 @@ function readRanges(manifest, fields) {
|
|
|
45
300
|
return Object.entries(block).flatMap(([name, range]) => typeof range === 'string' ? [[`${field} ${name}`, range]] : []);
|
|
46
301
|
}));
|
|
47
302
|
}
|
|
48
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Lists how the final ranges differ from the published ones.
|
|
305
|
+
*
|
|
306
|
+
* @param published - The ranges the published manifest declares.
|
|
307
|
+
* @param final - The ranges the target declares now.
|
|
308
|
+
* @returns One message per range added, removed, or changed, sorted by key.
|
|
309
|
+
*/
|
|
310
|
+
export function compareRanges(published, final) {
|
|
49
311
|
const names = [...new Set([...published.keys(), ...final.keys()])].sort();
|
|
50
312
|
return names.flatMap((name) => {
|
|
51
313
|
const before = published.get(name);
|
|
@@ -59,7 +321,14 @@ function compareRanges(published, final) {
|
|
|
59
321
|
return [`${name} ${before} -> ${after}`];
|
|
60
322
|
});
|
|
61
323
|
}
|
|
62
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Resolves the scaffold executable a target runs.
|
|
326
|
+
*
|
|
327
|
+
* @param target - The target directory.
|
|
328
|
+
* @returns Node and the installed scaffold entry, else Node and the target's own built entry, else
|
|
329
|
+
* none.
|
|
330
|
+
*/
|
|
331
|
+
export function resolveScaffold(target) {
|
|
63
332
|
const installed = join(target, 'node_modules', '@orkestrel', 'scaffold', 'dist', 'bin', 'main.js');
|
|
64
333
|
if (existsSync(installed))
|
|
65
334
|
return [process.execPath, installed];
|
|
@@ -68,8 +337,15 @@ function resolveScaffold(target) {
|
|
|
68
337
|
return [process.execPath, own];
|
|
69
338
|
return [];
|
|
70
339
|
}
|
|
71
|
-
|
|
72
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Reads the release readings out of an audit's standard output.
|
|
342
|
+
*
|
|
343
|
+
* @param text - The audit's standard output, whose last line is the JSON envelope.
|
|
344
|
+
* @returns One release per reading that names a dependency and its range, or undefined when the
|
|
345
|
+
* envelope carries no `releases` list.
|
|
346
|
+
*/
|
|
347
|
+
export function readReleases(text) {
|
|
348
|
+
const releases = parseJsonLine(text)?.releases;
|
|
73
349
|
if (!Array.isArray(releases))
|
|
74
350
|
return undefined;
|
|
75
351
|
return releases.flatMap((entry) => {
|
|
@@ -83,15 +359,30 @@ function readReleases(text) {
|
|
|
83
359
|
return [{ name, range, latest: readString(record, 'latest') }];
|
|
84
360
|
});
|
|
85
361
|
}
|
|
86
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Renders a command as the line a step row carries.
|
|
364
|
+
*
|
|
365
|
+
* @param file - The executable.
|
|
366
|
+
* @param args - Its arguments.
|
|
367
|
+
* @returns The command line, naming the running Node binary as `node`.
|
|
368
|
+
*/
|
|
369
|
+
export function renderCommand(file, args) {
|
|
87
370
|
return [file === process.execPath ? 'node' : file, ...args].join(' ');
|
|
88
371
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Names why a finished command failed.
|
|
374
|
+
*
|
|
375
|
+
* @remarks
|
|
376
|
+
* A JSON verb writes its refusal envelope or its partial-run note on stdout, so the stderr tail
|
|
377
|
+
* alone reads empty for exactly the failures the operator needs named.
|
|
378
|
+
*
|
|
379
|
+
* @param result - The finished command.
|
|
380
|
+
* @returns Nothing for exit 0; else the `code: message` envelope, the `note`, or the last stderr lines.
|
|
381
|
+
*/
|
|
382
|
+
export function describeFailure(result) {
|
|
92
383
|
if (result.status === 0)
|
|
93
384
|
return undefined;
|
|
94
|
-
const value =
|
|
385
|
+
const value = parseJsonLine(result.stdout);
|
|
95
386
|
const error = value?.error;
|
|
96
387
|
if (typeof error === 'object' && error !== null && !Array.isArray(error)) {
|
|
97
388
|
const envelope = Object.fromEntries(Object.entries(error));
|
|
@@ -109,13 +400,33 @@ function describeFailure(result) {
|
|
|
109
400
|
.slice(-3)
|
|
110
401
|
.join(' | ');
|
|
111
402
|
}
|
|
112
|
-
|
|
403
|
+
/**
|
|
404
|
+
* Reads the verdict of a finished command by its exit alone.
|
|
405
|
+
*
|
|
406
|
+
* @param result - The finished command.
|
|
407
|
+
* @returns The exit and the failure note.
|
|
408
|
+
*/
|
|
409
|
+
export function readVerdict(result) {
|
|
113
410
|
return { exit: result.status ?? undefined, note: describeFailure(result) };
|
|
114
411
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
412
|
+
/**
|
|
413
|
+
* Runs a command as one step of the visit and logs its row.
|
|
414
|
+
*
|
|
415
|
+
* @remarks
|
|
416
|
+
* A judge reads the finished command and answers the exit and note its one log entry carries, so no
|
|
417
|
+
* entry is amended after it is appended.
|
|
418
|
+
*
|
|
419
|
+
* @param runner - The visit's state.
|
|
420
|
+
* @param step - The step's name.
|
|
421
|
+
* @param file - The executable.
|
|
422
|
+
* @param args - Its arguments.
|
|
423
|
+
* @param cwd - The working directory.
|
|
424
|
+
* @param judge - Reads the exit and note from the result. Default: {@link readVerdict}.
|
|
425
|
+
* @param input - Standard input for the command. Default: none.
|
|
426
|
+
* @returns The finished command, or undefined under --dry-run.
|
|
427
|
+
*/
|
|
428
|
+
export function runStep(runner, step, file, args, cwd, judge = readVerdict, input) {
|
|
429
|
+
const command = renderCommand(file, args);
|
|
119
430
|
if (runner.dry) {
|
|
120
431
|
runner.steps.push({ step, command, exit: undefined, durationMs: 0, note: 'dry run' });
|
|
121
432
|
return undefined;
|
|
@@ -133,8 +444,17 @@ function runStep(runner, step, file, args, cwd, judge = readVerdict, input) {
|
|
|
133
444
|
appendResult(runner, step, command, result, durationMs, verdict);
|
|
134
445
|
return result;
|
|
135
446
|
}
|
|
136
|
-
|
|
137
|
-
|
|
447
|
+
/**
|
|
448
|
+
* Judges the pin step's audit.
|
|
449
|
+
*
|
|
450
|
+
* @remarks
|
|
451
|
+
* The audit exits 1 on the drift the pin step exists to close, so its exit is a reading here.
|
|
452
|
+
*
|
|
453
|
+
* @param result - The audit.
|
|
454
|
+
* @returns Exit 0 with the reading count when the audit printed releases and exited 0 or 1; else a
|
|
455
|
+
* failing exit with the reason.
|
|
456
|
+
*/
|
|
457
|
+
export function judgePin(result) {
|
|
138
458
|
const releases = readReleases(result.stdout);
|
|
139
459
|
if (releases === undefined || (result.status !== 0 && result.status !== 1)) {
|
|
140
460
|
return {
|
|
@@ -144,16 +464,34 @@ function judgePin(result) {
|
|
|
144
464
|
}
|
|
145
465
|
return { exit: 0, note: `${releases.length} release reading(s)` };
|
|
146
466
|
}
|
|
147
|
-
|
|
467
|
+
/**
|
|
468
|
+
* Judges the overwrite command.
|
|
469
|
+
*
|
|
470
|
+
* @param result - The overwrite.
|
|
471
|
+
* @param offline - If `true`, an exit of 1 whose note names the catalog refusal passes; if `false`,
|
|
472
|
+
* the exit alone decides.
|
|
473
|
+
* @returns The verdict.
|
|
474
|
+
*/
|
|
475
|
+
export function judgeOverwrite(result, offline) {
|
|
148
476
|
if (!offline || result.status !== 1)
|
|
149
477
|
return readVerdict(result);
|
|
150
|
-
const note = readString(
|
|
478
|
+
const note = readString(parseJsonLine(result.stdout), 'note');
|
|
151
479
|
return note !== undefined && note.includes(OFFLINE_REFUSAL)
|
|
152
480
|
? { exit: 0, note: 'offline overwrite skipped the catalog step by design' }
|
|
153
481
|
: { exit: 1, note: describeFailure(result) };
|
|
154
482
|
}
|
|
155
|
-
|
|
156
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Judges the verify step's audit.
|
|
485
|
+
*
|
|
486
|
+
* @remarks
|
|
487
|
+
* The audit reads the canonical sources of scaffold's own checkout as foreign paths and exits 1.
|
|
488
|
+
*
|
|
489
|
+
* @param result - The audit.
|
|
490
|
+
* @param own - If `true`, the target is scaffold's own checkout and an exit of 1 passes; if `false`,
|
|
491
|
+
* the exit alone decides.
|
|
492
|
+
* @returns The verdict; an audit that printed no releases envelope fails.
|
|
493
|
+
*/
|
|
494
|
+
export function judgeVerify(result, own) {
|
|
157
495
|
if (readReleases(result.stdout) === undefined) {
|
|
158
496
|
return {
|
|
159
497
|
exit: result.status === null || result.status === 0 ? 2 : result.status,
|
|
@@ -164,7 +502,16 @@ function judgeVerify(result, own) {
|
|
|
164
502
|
? { exit: 0, note: 'foreign-path findings are expected on the scaffold checkout' }
|
|
165
503
|
: readVerdict(result);
|
|
166
504
|
}
|
|
167
|
-
|
|
505
|
+
/**
|
|
506
|
+
* Runs npm as one step of the visit and logs its row.
|
|
507
|
+
*
|
|
508
|
+
* @param runner - The visit's state.
|
|
509
|
+
* @param step - The step's name.
|
|
510
|
+
* @param args - The npm arguments.
|
|
511
|
+
* @param judge - Reads the exit and note from the result. Default: {@link readVerdict}.
|
|
512
|
+
* @returns The finished command, or undefined under --dry-run.
|
|
513
|
+
*/
|
|
514
|
+
export function runNpmStep(runner, step, args, judge = readVerdict) {
|
|
168
515
|
const command = `npm ${args.join(' ')}`;
|
|
169
516
|
if (runner.dry) {
|
|
170
517
|
runner.steps.push({
|
|
@@ -183,7 +530,19 @@ function runNpmStep(runner, step, args, judge = readVerdict) {
|
|
|
183
530
|
appendResult(runner, step, command, result, Date.now() - started, judge(result));
|
|
184
531
|
return result;
|
|
185
532
|
}
|
|
186
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Writes one step's full output to the visit's journal directory.
|
|
535
|
+
*
|
|
536
|
+
* @param runner - The visit's state.
|
|
537
|
+
* @param step - The step's name.
|
|
538
|
+
* @param command - The command line.
|
|
539
|
+
* @param exit - The exit code.
|
|
540
|
+
* @param stdout - The standard output.
|
|
541
|
+
* @param stderr - The standard error.
|
|
542
|
+
* @returns The journal path, the failure that stopped the write, or neither when the visit keeps no
|
|
543
|
+
* journal.
|
|
544
|
+
*/
|
|
545
|
+
export function writeJournal(runner, step, command, exit, stdout, stderr) {
|
|
187
546
|
if (runner.directory === undefined)
|
|
188
547
|
return {};
|
|
189
548
|
const path = join(runner.directory, `${String(runner.steps.length + 1).padStart(2, '0')}-${step}.log`);
|
|
@@ -198,7 +557,18 @@ function writeJournal(runner, step, command, exit, stdout, stderr) {
|
|
|
198
557
|
return { failure: `journal failed: ${path}: ${String(error)}` };
|
|
199
558
|
}
|
|
200
559
|
}
|
|
201
|
-
|
|
560
|
+
/**
|
|
561
|
+
* Appends a finished command's row to the visit's log.
|
|
562
|
+
*
|
|
563
|
+
* @param runner - The visit's state.
|
|
564
|
+
* @param step - The step's name.
|
|
565
|
+
* @param command - The command line.
|
|
566
|
+
* @param result - The finished command.
|
|
567
|
+
* @param durationMs - How long it ran.
|
|
568
|
+
* @param verdict - The exit and note to record; a journal failure turns the exit into 1.
|
|
569
|
+
* @returns Nothing after the row is appended.
|
|
570
|
+
*/
|
|
571
|
+
export function appendResult(runner, step, command, result, durationMs, verdict) {
|
|
202
572
|
const journal = writeJournal(runner, step, command, result.status ?? undefined, result.stdout ?? '', result.stderr ?? '');
|
|
203
573
|
runner.steps.push({
|
|
204
574
|
step,
|
|
@@ -213,7 +583,17 @@ function appendResult(runner, step, command, result, durationMs, verdict) {
|
|
|
213
583
|
}),
|
|
214
584
|
});
|
|
215
585
|
}
|
|
216
|
-
|
|
586
|
+
/**
|
|
587
|
+
* Appends a row for a step the script ran itself.
|
|
588
|
+
*
|
|
589
|
+
* @param runner - The visit's state.
|
|
590
|
+
* @param step - The step's name.
|
|
591
|
+
* @param command - The label the row carries as its command.
|
|
592
|
+
* @param note - What the step found or did.
|
|
593
|
+
* @param exit - The exit the row records. Default: 0.
|
|
594
|
+
* @returns Nothing after the row is appended.
|
|
595
|
+
*/
|
|
596
|
+
export function appendStep(runner, step, command, note, exit = 0) {
|
|
217
597
|
const journal = runner.dry ? {} : writeJournal(runner, step, command, exit, note, '');
|
|
218
598
|
runner.steps.push({
|
|
219
599
|
step,
|
|
@@ -224,14 +604,32 @@ function appendStep(runner, step, command, note, exit = 0) {
|
|
|
224
604
|
...(journal.path === undefined ? {} : { journal: journal.path }),
|
|
225
605
|
});
|
|
226
606
|
}
|
|
227
|
-
|
|
228
|
-
|
|
607
|
+
/**
|
|
608
|
+
* Checks whether a visit's log holds a failure.
|
|
609
|
+
*
|
|
610
|
+
* @remarks
|
|
611
|
+
* The one predicate the visit's steps and the script's exit code share. A superseded row, a row with
|
|
612
|
+
* no exit (a dry run), and a pins or compare row are readings, not failures.
|
|
613
|
+
*
|
|
614
|
+
* @param steps - The rows logged so far.
|
|
615
|
+
* @returns True if a row exited nonzero, was not superseded, and is not a pins or compare row; false
|
|
616
|
+
* otherwise.
|
|
617
|
+
*/
|
|
618
|
+
export function failed(steps) {
|
|
619
|
+
return steps.some((entry) => entry.superseded !== true &&
|
|
229
620
|
entry.exit !== undefined &&
|
|
230
621
|
entry.exit !== 0 &&
|
|
231
622
|
entry.step !== 'pins' &&
|
|
232
623
|
entry.step !== 'compare');
|
|
233
624
|
}
|
|
234
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Reads the resolution conflict an install reported.
|
|
627
|
+
*
|
|
628
|
+
* @param result - The finished install.
|
|
629
|
+
* @returns The conflict line npm printed, a generic reason when it printed none, or undefined when
|
|
630
|
+
* the install did not fail with ERESOLVE.
|
|
631
|
+
*/
|
|
632
|
+
export function extractConflict(result) {
|
|
235
633
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
236
634
|
if (!/\bcode ERESOLVE\b/u.test(output))
|
|
237
635
|
return undefined;
|
|
@@ -239,7 +637,14 @@ function extractConflict(result) {
|
|
|
239
637
|
return ((lines.find((line) => line.includes('Conflicting peer dependency:')) ??
|
|
240
638
|
lines.find((line) => line.includes('Could not resolve dependency:')))?.replace(/^npm (?:error|ERR!)\s*/u, '') ?? 'the install could not move a locked peer');
|
|
241
639
|
}
|
|
242
|
-
|
|
640
|
+
/**
|
|
641
|
+
* Runs the install and regenerates the lock when asked and the install cannot resolve.
|
|
642
|
+
*
|
|
643
|
+
* @param runner - The visit's state.
|
|
644
|
+
* @param step - The step the rows belong to.
|
|
645
|
+
* @returns Nothing after the rows are appended.
|
|
646
|
+
*/
|
|
647
|
+
export function runInstall(runner, step) {
|
|
243
648
|
// npm can unlink a node_modules junction before reporting its resolution failure.
|
|
244
649
|
if (!runner.dry &&
|
|
245
650
|
runner.regenerate &&
|
|
@@ -264,14 +669,28 @@ function runInstall(runner, step) {
|
|
|
264
669
|
runner.regenerate)
|
|
265
670
|
regenerateLock(runner, step, installed);
|
|
266
671
|
}
|
|
267
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Refuses a manifest that holds local dependency specs.
|
|
674
|
+
*
|
|
675
|
+
* @param runner - The visit's state.
|
|
676
|
+
* @param step - The step that refuses.
|
|
677
|
+
* @returns True if a failing row was appended; false when the manifest holds none.
|
|
678
|
+
*/
|
|
679
|
+
export function refuseLocalSpecs(runner, step) {
|
|
268
680
|
const specs = scanLocalSpecs(readManifest(runner.target) ?? {});
|
|
269
681
|
if (specs.length === 0)
|
|
270
682
|
return false;
|
|
271
683
|
appendStep(runner, step, 'scan manifest specs', `local dependency specs: ${specs.map(([path, spec]) => `${path}: ${spec}`).join(', ')}; restore the pins first`, 1);
|
|
272
684
|
return true;
|
|
273
685
|
}
|
|
274
|
-
|
|
686
|
+
/**
|
|
687
|
+
* Names why the lock must not be regenerated.
|
|
688
|
+
*
|
|
689
|
+
* @param runner - The visit's state.
|
|
690
|
+
* @returns The first refusal, or undefined when the lock is tracked and clean, the manifest holds no
|
|
691
|
+
* local spec or workspace, `tmp/` is ignored, and the target is its work tree's root.
|
|
692
|
+
*/
|
|
693
|
+
export function readRegenerationRefusal(runner) {
|
|
275
694
|
if (runStep(runner, 'regenerate', 'git', ['ls-files', '--error-unmatch', 'package-lock.json'], runner.target)?.status !== 0)
|
|
276
695
|
return 'package-lock.json is untracked';
|
|
277
696
|
const status = runStep(runner, 'regenerate', 'git', ['status', '--porcelain', '--', 'package-lock.json'], runner.target);
|
|
@@ -291,7 +710,16 @@ function readRegenerationRefusal(runner) {
|
|
|
291
710
|
return 'the target is not the root of its work tree';
|
|
292
711
|
return undefined;
|
|
293
712
|
}
|
|
294
|
-
|
|
713
|
+
/**
|
|
714
|
+
* Renames a directory, retrying while the host holds it.
|
|
715
|
+
*
|
|
716
|
+
* @param source - The directory to move.
|
|
717
|
+
* @param target - Its new path.
|
|
718
|
+
* @param attempt - The retries spent so far. Default: 0.
|
|
719
|
+
* @returns Nothing after the rename.
|
|
720
|
+
* @throws Thrown when the rename fails for another reason or after the retries.
|
|
721
|
+
*/
|
|
722
|
+
export function moveModules(source, target, attempt = 0) {
|
|
295
723
|
try {
|
|
296
724
|
renameSync(source, target);
|
|
297
725
|
}
|
|
@@ -302,7 +730,15 @@ function moveModules(source, target, attempt = 0) {
|
|
|
302
730
|
moveModules(source, target, attempt + 1);
|
|
303
731
|
}
|
|
304
732
|
}
|
|
305
|
-
|
|
733
|
+
/**
|
|
734
|
+
* Regenerates the lock and the installed tree from the manifest.
|
|
735
|
+
*
|
|
736
|
+
* @param runner - The visit's state.
|
|
737
|
+
* @param step - The step the rows belong to.
|
|
738
|
+
* @param conflict - The install that failed with ERESOLVE.
|
|
739
|
+
* @returns Nothing after the rows are appended; a failed regeneration restores the tree and lock.
|
|
740
|
+
*/
|
|
741
|
+
export function regenerateLock(runner, step, conflict) {
|
|
306
742
|
const refusal = readRegenerationRefusal(runner);
|
|
307
743
|
if (refusal !== undefined || runner.directory === undefined) {
|
|
308
744
|
appendStep(runner, step, 'regenerate lock and modules', `ERESOLVE; regeneration refused: ${refusal ?? 'tmp/ is not git-ignored'}`, 1);
|
|
@@ -348,8 +784,18 @@ function regenerateLock(runner, step, conflict) {
|
|
|
348
784
|
if (result.listed !== undefined)
|
|
349
785
|
appendResult(runner, step, 'npm ls --all --json', result.listed, 0, readVerdict(result.listed));
|
|
350
786
|
}
|
|
351
|
-
|
|
352
|
-
|
|
787
|
+
/**
|
|
788
|
+
* Installs from a manifest with no lock and proves the tree.
|
|
789
|
+
*
|
|
790
|
+
* @remarks
|
|
791
|
+
* Journal writes and backup disposal cannot enter the transaction's rollback boundary.
|
|
792
|
+
*
|
|
793
|
+
* @param target - The target directory.
|
|
794
|
+
* @param manifest - The manifest bytes before the install.
|
|
795
|
+
* @param before - The SHA-256 of the lock before.
|
|
796
|
+
* @returns Exit 0 when the install, the tree listing, and the unchanged manifest prove it; else exit 1.
|
|
797
|
+
*/
|
|
798
|
+
export function regenerateTree(target, manifest, before) {
|
|
353
799
|
const lock = join(target, 'package-lock.json');
|
|
354
800
|
try {
|
|
355
801
|
unlinkSync(lock);
|
|
@@ -374,7 +820,15 @@ function regenerateTree(target, manifest, before) {
|
|
|
374
820
|
return { exit: 1, note: `ERESOLVE; ${String(error)}` };
|
|
375
821
|
}
|
|
376
822
|
}
|
|
377
|
-
|
|
823
|
+
/**
|
|
824
|
+
* Discards the backups after a proved regeneration.
|
|
825
|
+
*
|
|
826
|
+
* @param target - The target directory.
|
|
827
|
+
* @param moved - The moved previous tree.
|
|
828
|
+
* @param backup - The saved lock.
|
|
829
|
+
* @returns Nothing on success, else the failure with the backup paths kept.
|
|
830
|
+
*/
|
|
831
|
+
export function discardBackup(target, moved, backup) {
|
|
378
832
|
try {
|
|
379
833
|
for (const path of [moved, backup, join(target, 'node_modules')])
|
|
380
834
|
validateWavePath(target, path);
|
|
@@ -385,7 +839,16 @@ function discardBackup(target, moved, backup) {
|
|
|
385
839
|
return `backup cleanup failed: ${String(error)}; proved installation retained; backups: ${backup}; ${moved}`;
|
|
386
840
|
}
|
|
387
841
|
}
|
|
388
|
-
|
|
842
|
+
/**
|
|
843
|
+
* Restores the previous tree and lock after a failed regeneration.
|
|
844
|
+
*
|
|
845
|
+
* @param target - The target directory.
|
|
846
|
+
* @param moved - The moved previous tree.
|
|
847
|
+
* @param backup - The saved lock.
|
|
848
|
+
* @param before - The SHA-256 the saved lock must still have.
|
|
849
|
+
* @returns `tree and lock restored`, else the failure with the backup paths kept.
|
|
850
|
+
*/
|
|
851
|
+
export function restoreLock(target, moved, backup, before) {
|
|
389
852
|
try {
|
|
390
853
|
for (const path of [
|
|
391
854
|
moved,
|
|
@@ -408,7 +871,16 @@ function restoreLock(target, moved, backup, before) {
|
|
|
408
871
|
return `rollback failed: ${String(error)}; backups: ${backup}; ${moved}`;
|
|
409
872
|
}
|
|
410
873
|
}
|
|
411
|
-
|
|
874
|
+
/**
|
|
875
|
+
* Checks that a destination stays inside the target and crosses no link.
|
|
876
|
+
*
|
|
877
|
+
* @param target - The target directory.
|
|
878
|
+
* @param destination - The path to write or remove.
|
|
879
|
+
* @returns Nothing when the path is safe.
|
|
880
|
+
* @throws Thrown when the path resolves outside the target, an ancestor is a link or not a
|
|
881
|
+
* directory, or an ancestor's real location leaves the target's.
|
|
882
|
+
*/
|
|
883
|
+
export function validateWavePath(target, destination) {
|
|
412
884
|
const root = resolve(target);
|
|
413
885
|
const path = resolve(destination);
|
|
414
886
|
const location = relative(root, path);
|
|
@@ -432,19 +904,26 @@ function validateWavePath(target, destination) {
|
|
|
432
904
|
throw new Error(`unsafe wave path: ${entry} resolves outside ${physical}`);
|
|
433
905
|
}
|
|
434
906
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
907
|
+
/**
|
|
908
|
+
* Reads the dirty paths out of `git status --porcelain=v1 -z` output.
|
|
909
|
+
*
|
|
910
|
+
* @remarks
|
|
911
|
+
* One pass over the entries with {@link DIRTY_ENTRY}, so the cost grows with the text and no entry
|
|
912
|
+
* recurses.
|
|
913
|
+
*
|
|
914
|
+
* @param text - The command's standard output.
|
|
915
|
+
* @returns Each entry's path; a rename or copy also lists its original path.
|
|
916
|
+
*/
|
|
917
|
+
export function readDirtyPaths(text) {
|
|
918
|
+
return [...text.matchAll(DIRTY_ENTRY)].flatMap((match) => match[1] === undefined ? [match[3] ?? ''] : [match[1], match[2] ?? '']);
|
|
446
919
|
}
|
|
447
|
-
|
|
920
|
+
/**
|
|
921
|
+
* Reads the receipt the overwrite step left.
|
|
922
|
+
*
|
|
923
|
+
* @param target - The target directory.
|
|
924
|
+
* @returns The receipt, or undefined when it is absent, unreadable, or lacks a field in its type.
|
|
925
|
+
*/
|
|
926
|
+
export function readReceipt(target) {
|
|
448
927
|
try {
|
|
449
928
|
const value = parseJsonRecord(readFileSync(join(target, 'tmp', 'units', 'wave', 'overwrite.json'), 'utf8'));
|
|
450
929
|
if (value === undefined ||
|
|
@@ -468,7 +947,15 @@ function readReceipt(target) {
|
|
|
468
947
|
return undefined;
|
|
469
948
|
}
|
|
470
949
|
}
|
|
471
|
-
|
|
950
|
+
/**
|
|
951
|
+
* Runs the record step.
|
|
952
|
+
*
|
|
953
|
+
* @param runner - The visit's state.
|
|
954
|
+
* @param name - The package name the manifest declares.
|
|
955
|
+
* @returns Nothing after the rows are appended; the overwrite's paths are committed when the receipt
|
|
956
|
+
* attributes every dirty path to them.
|
|
957
|
+
*/
|
|
958
|
+
export function runRecord(runner, name) {
|
|
472
959
|
if (name === SCAFFOLD) {
|
|
473
960
|
appendStep(runner, 'record', 'git commit', `skipped: ${SCAFFOLD}`);
|
|
474
961
|
return;
|
|
@@ -509,17 +996,26 @@ function runRecord(runner, name) {
|
|
|
509
996
|
return;
|
|
510
997
|
}
|
|
511
998
|
runStep(runner, 'record', 'git', ['--literal-pathspecs', 'add', '-A', '--pathspec-from-file=-', '--pathspec-file-nul'], runner.target, readVerdict, `${dirty.join('\0')}\0`);
|
|
512
|
-
if (failed(runner))
|
|
999
|
+
if (failed(runner.steps))
|
|
513
1000
|
return;
|
|
514
1001
|
runStep(runner, 'record', 'git', ['commit', '-m', `Overwrite with scaffold ${receipt.version}`], runner.target);
|
|
515
|
-
if (failed(runner))
|
|
1002
|
+
if (failed(runner.steps))
|
|
516
1003
|
return;
|
|
517
1004
|
runStep(runner, 'record', 'git', ['status', '--porcelain'], runner.target, (result) => result.status === 0 && result.stdout.trim() !== ''
|
|
518
1005
|
? { exit: 1, note: `tree remains dirty: ${result.stdout}` }
|
|
519
1006
|
: readVerdict(result));
|
|
520
1007
|
}
|
|
521
|
-
|
|
522
|
-
|
|
1008
|
+
/**
|
|
1009
|
+
* Moves each named dependency to the registry caret of its latest release.
|
|
1010
|
+
*
|
|
1011
|
+
* @remarks
|
|
1012
|
+
* A later release of the same name reads the range the earlier reading wrote.
|
|
1013
|
+
*
|
|
1014
|
+
* @param ranges - The block's ranges by name.
|
|
1015
|
+
* @param releases - The audit's release readings.
|
|
1016
|
+
* @returns The ranges after the moves and one message per move.
|
|
1017
|
+
*/
|
|
1018
|
+
export function computeRanges(ranges, releases) {
|
|
523
1019
|
return releases.reduce((state, release) => {
|
|
524
1020
|
if (release.latest === undefined || !(release.name in state.ranges))
|
|
525
1021
|
return state;
|
|
@@ -532,7 +1028,14 @@ function computeRanges(ranges, releases) {
|
|
|
532
1028
|
};
|
|
533
1029
|
}, { ranges, moved: [] });
|
|
534
1030
|
}
|
|
535
|
-
|
|
1031
|
+
/**
|
|
1032
|
+
* Writes the re-pinned ranges into the target's manifest.
|
|
1033
|
+
*
|
|
1034
|
+
* @param target - The target directory.
|
|
1035
|
+
* @param releases - The audit's release readings.
|
|
1036
|
+
* @returns One message per range moved; the manifest is rewritten only when a range moved.
|
|
1037
|
+
*/
|
|
1038
|
+
export function writeRanges(target, releases) {
|
|
536
1039
|
const manifest = readManifest(target);
|
|
537
1040
|
if (manifest === undefined)
|
|
538
1041
|
return [];
|
|
@@ -552,7 +1055,16 @@ function writeRanges(target, releases) {
|
|
|
552
1055
|
}
|
|
553
1056
|
return moved;
|
|
554
1057
|
}
|
|
555
|
-
|
|
1058
|
+
/**
|
|
1059
|
+
* Runs the pin step.
|
|
1060
|
+
*
|
|
1061
|
+
* @param runner - The visit's state.
|
|
1062
|
+
* @param scaffoldFile - The scaffold executable.
|
|
1063
|
+
* @param scaffoldPrefix - The arguments that precede scaffold's own.
|
|
1064
|
+
* @param offline - The `--offline` argument, or none.
|
|
1065
|
+
* @returns Nothing after the rows are appended.
|
|
1066
|
+
*/
|
|
1067
|
+
export function runPin(runner, scaffoldFile, scaffoldPrefix, offline) {
|
|
556
1068
|
const audit = runStep(runner, 'pin', scaffoldFile, [...scaffoldPrefix, 'audit', '--json', ...offline], runner.target, judgePin);
|
|
557
1069
|
if (audit !== undefined) {
|
|
558
1070
|
const releases = readReleases(audit.stdout);
|
|
@@ -566,7 +1078,13 @@ function runPin(runner, scaffoldFile, scaffoldPrefix, offline) {
|
|
|
566
1078
|
}
|
|
567
1079
|
runInstall(runner, 'pin');
|
|
568
1080
|
}
|
|
569
|
-
|
|
1081
|
+
/**
|
|
1082
|
+
* Runs the commit step.
|
|
1083
|
+
*
|
|
1084
|
+
* @param runner - The visit's state.
|
|
1085
|
+
* @returns Nothing after the rows are appended.
|
|
1086
|
+
*/
|
|
1087
|
+
export function runCommit(runner) {
|
|
570
1088
|
if (refuseLocalSpecs(runner, 'commit'))
|
|
571
1089
|
return;
|
|
572
1090
|
const status = runStep(runner, 'commit', 'git', ['status', '--porcelain', '--', 'package.json', 'package-lock.json'], runner.target);
|
|
@@ -585,10 +1103,19 @@ function runCommit(runner) {
|
|
|
585
1103
|
appendStep(runner, 'commit', 'git commit', 'manifest and lockfile already committed');
|
|
586
1104
|
}
|
|
587
1105
|
}
|
|
588
|
-
|
|
1106
|
+
/**
|
|
1107
|
+
* Runs the overwrite step.
|
|
1108
|
+
*
|
|
1109
|
+
* @param runner - The visit's state.
|
|
1110
|
+
* @param scaffoldFile - The scaffold executable.
|
|
1111
|
+
* @param scaffoldPrefix - The arguments that precede scaffold's own.
|
|
1112
|
+
* @param offline - The `--offline` argument, or none.
|
|
1113
|
+
* @returns Nothing after the rows are appended and the receipt is written.
|
|
1114
|
+
*/
|
|
1115
|
+
export function runOverwrite(runner, scaffoldFile, scaffoldPrefix, offline) {
|
|
589
1116
|
const overwrite = runStep(runner, 'overwrite', scaffoldFile, [...scaffoldPrefix, 'overwrite', '--json', ...offline], runner.target, (result) => judgeOverwrite(result, runner.offline));
|
|
590
|
-
if (overwrite !== undefined && !failed(runner) && runner.directory !== undefined) {
|
|
591
|
-
const receipt =
|
|
1117
|
+
if (overwrite !== undefined && !failed(runner.steps) && runner.directory !== undefined) {
|
|
1118
|
+
const receipt = parseJsonLine(overwrite.stdout);
|
|
592
1119
|
const head = runStep(runner, 'overwrite', 'git', ['rev-parse', 'HEAD'], runner.target);
|
|
593
1120
|
const installed = readManifest(join(runner.target, 'node_modules', '@orkestrel', 'scaffold'));
|
|
594
1121
|
const version = readString(installed, 'version');
|
|
@@ -610,11 +1137,21 @@ function runOverwrite(runner, scaffoldFile, scaffoldPrefix, offline) {
|
|
|
610
1137
|
}
|
|
611
1138
|
}
|
|
612
1139
|
}
|
|
613
|
-
if (!failed(runner)) {
|
|
1140
|
+
if (!failed(runner.steps)) {
|
|
614
1141
|
runStep(runner, 'overwrite', scaffoldFile, [...scaffoldPrefix, 'audit', ...offline], runner.target);
|
|
615
1142
|
}
|
|
616
1143
|
}
|
|
617
|
-
|
|
1144
|
+
/**
|
|
1145
|
+
* Runs the verify step.
|
|
1146
|
+
*
|
|
1147
|
+
* @param runner - The visit's state.
|
|
1148
|
+
* @param scaffoldFile - The scaffold executable.
|
|
1149
|
+
* @param scaffoldPrefix - The arguments that precede scaffold's own.
|
|
1150
|
+
* @param offline - The `--offline` argument, or none.
|
|
1151
|
+
* @param own - If `true`, the target is scaffold's own checkout; if `false`, it is another package.
|
|
1152
|
+
* @returns Nothing after the rows are appended.
|
|
1153
|
+
*/
|
|
1154
|
+
export function runVerify(runner, scaffoldFile, scaffoldPrefix, offline, own) {
|
|
618
1155
|
const audit = runStep(runner, 'verify', scaffoldFile, [...scaffoldPrefix, 'audit', '--json', ...offline], runner.target, (result) => judgeVerify(result, own));
|
|
619
1156
|
if (audit === undefined)
|
|
620
1157
|
return;
|
|
@@ -628,7 +1165,15 @@ function runVerify(runner, scaffoldFile, scaffoldPrefix, offline, own) {
|
|
|
628
1165
|
.map((release) => `${release.name} ${release.range} vs ${String(release.latest)}`)
|
|
629
1166
|
.join(', '), drift.length === 0 ? 0 : 1);
|
|
630
1167
|
}
|
|
631
|
-
|
|
1168
|
+
/**
|
|
1169
|
+
* Runs the pins step.
|
|
1170
|
+
*
|
|
1171
|
+
* @param runner - The visit's state.
|
|
1172
|
+
* @param before - The ranges the manifest declared before the visit.
|
|
1173
|
+
* @param version - The version to sweep for, or undefined when none is known.
|
|
1174
|
+
* @returns The number of hit lines, or undefined when the sweep did not run.
|
|
1175
|
+
*/
|
|
1176
|
+
export function runPins(runner, before, version) {
|
|
632
1177
|
const after = readRanges(readManifest(runner.target) ?? {}, RANGE_FIELDS);
|
|
633
1178
|
const prior = [...before]
|
|
634
1179
|
.filter(([name, range]) => after.get(name) !== range)
|
|
@@ -642,7 +1187,14 @@ function runPins(runner, before, version) {
|
|
|
642
1187
|
? undefined
|
|
643
1188
|
: swept.stdout.split(/\r\n|\n/).filter((line) => /^\S+:\d+: /u.test(line)).length;
|
|
644
1189
|
}
|
|
645
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* Runs the compare step.
|
|
1192
|
+
*
|
|
1193
|
+
* @param runner - The visit's state.
|
|
1194
|
+
* @param name - The package name the manifest declares.
|
|
1195
|
+
* @returns The bump ruling the two readings reach.
|
|
1196
|
+
*/
|
|
1197
|
+
export function runCompare(runner, name) {
|
|
646
1198
|
const compared = runStep(runner, 'compare', process.execPath, [COMPARE, '--json'], runner.target);
|
|
647
1199
|
const distMoved = compared === undefined
|
|
648
1200
|
? undefined
|
|
@@ -663,10 +1215,25 @@ function runCompare(runner, name) {
|
|
|
663
1215
|
const ranges = compareRanges(readRanges(published, RUNTIME_FIELDS), readRanges(readManifest(runner.target) ?? {}, RUNTIME_FIELDS));
|
|
664
1216
|
return { distMoved, rangesMoved: ranges.length > 0, ranges, hits: undefined };
|
|
665
1217
|
}
|
|
666
|
-
|
|
1218
|
+
/**
|
|
1219
|
+
* Words one reading of the ruling.
|
|
1220
|
+
*
|
|
1221
|
+
* @param moved - The reading, or undefined when unanswered.
|
|
1222
|
+
* @param movedWord - The word for a moved reading.
|
|
1223
|
+
* @param sameWord - The word for an unmoved reading.
|
|
1224
|
+
* @returns `unanswered`, the moved word, or the same word.
|
|
1225
|
+
*/
|
|
1226
|
+
export function describeChange(moved, movedWord, sameWord) {
|
|
667
1227
|
return moved === undefined ? 'unanswered' : moved ? movedWord : sameWord;
|
|
668
1228
|
}
|
|
669
|
-
|
|
1229
|
+
/**
|
|
1230
|
+
* Runs one repository's visit.
|
|
1231
|
+
*
|
|
1232
|
+
* @param argv - The arguments after the script path.
|
|
1233
|
+
* @returns The finished visit, or undefined when the target carries no readable manifest or no
|
|
1234
|
+
* scaffold executable.
|
|
1235
|
+
*/
|
|
1236
|
+
export function runVisit(argv) {
|
|
670
1237
|
const target = readOption(argv, '--target') ?? process.cwd();
|
|
671
1238
|
if (!existsSync(join(target, 'package.json'))) {
|
|
672
1239
|
console.error(`wave: ${target} carries no package.json`);
|
|
@@ -711,37 +1278,37 @@ function runVisit(argv) {
|
|
|
711
1278
|
}
|
|
712
1279
|
const before = readRanges(manifest, RANGE_FIELDS);
|
|
713
1280
|
const offline = runner.offline ? ['--offline'] : [];
|
|
714
|
-
if (selected.has('pin') && !failed(runner))
|
|
1281
|
+
if (selected.has('pin') && !failed(runner.steps))
|
|
715
1282
|
runPin(runner, scaffoldFile, scaffoldPrefix, offline);
|
|
716
|
-
if (selected.has('commit') && !failed(runner))
|
|
1283
|
+
if (selected.has('commit') && !failed(runner.steps))
|
|
717
1284
|
runCommit(runner);
|
|
718
|
-
if (selected.has('overwrite') && !failed(runner)) {
|
|
1285
|
+
if (selected.has('overwrite') && !failed(runner.steps)) {
|
|
719
1286
|
if (name === SCAFFOLD) {
|
|
720
|
-
appendStep(runner, 'overwrite',
|
|
1287
|
+
appendStep(runner, 'overwrite', renderCommand(scaffoldFile, [...scaffoldPrefix, 'overwrite', '--json', ...offline]), `skipped with its audit: ${SCAFFOLD} is the package whose checkout the vendored host is staged from`);
|
|
721
1288
|
}
|
|
722
1289
|
else
|
|
723
1290
|
runOverwrite(runner, scaffoldFile, scaffoldPrefix, offline);
|
|
724
1291
|
}
|
|
725
|
-
if (selected.has('verify') && !failed(runner)) {
|
|
1292
|
+
if (selected.has('verify') && !failed(runner.steps)) {
|
|
726
1293
|
runVerify(runner, scaffoldFile, scaffoldPrefix, offline, name === SCAFFOLD);
|
|
727
1294
|
}
|
|
728
|
-
if (selected.has('install') && !failed(runner))
|
|
1295
|
+
if (selected.has('install') && !failed(runner.steps))
|
|
729
1296
|
runInstall(runner, 'install');
|
|
730
|
-
const hits = selected.has('pins') && !failed(runner)
|
|
1297
|
+
const hits = selected.has('pins') && !failed(runner.steps)
|
|
731
1298
|
? runPins(runner, before, readOption(argv, '--prior') ?? version)
|
|
732
1299
|
: undefined;
|
|
733
|
-
if (selected.has('format') && !failed(runner))
|
|
1300
|
+
if (selected.has('format') && !failed(runner.steps))
|
|
734
1301
|
runNpmStep(runner, 'format', ['run', 'format']);
|
|
735
|
-
if (selected.has('record') && !failed(runner))
|
|
1302
|
+
if (selected.has('record') && !failed(runner.steps))
|
|
736
1303
|
runRecord(runner, name);
|
|
737
|
-
if (selected.has('gates') && !failed(runner)) {
|
|
1304
|
+
if (selected.has('gates') && !failed(runner.steps)) {
|
|
738
1305
|
for (const gate of GATES) {
|
|
739
|
-
if (failed(runner))
|
|
1306
|
+
if (failed(runner.steps))
|
|
740
1307
|
break;
|
|
741
1308
|
runNpmStep(runner, 'gates', ['run', gate]);
|
|
742
1309
|
}
|
|
743
1310
|
}
|
|
744
|
-
const compared = selected.has('compare') && !failed(runner)
|
|
1311
|
+
const compared = selected.has('compare') && !failed(runner.steps)
|
|
745
1312
|
? runCompare(runner, name)
|
|
746
1313
|
: { distMoved: undefined, rangesMoved: undefined, ranges: undefined, hits: undefined };
|
|
747
1314
|
const ruling = { ...compared, hits };
|
|
@@ -750,17 +1317,24 @@ function runVisit(argv) {
|
|
|
750
1317
|
: ruling.distMoved === undefined || ruling.rangesMoved === undefined
|
|
751
1318
|
? 'unanswered'
|
|
752
1319
|
: 'no bump';
|
|
753
|
-
const
|
|
1320
|
+
const changes = `dist ${describeChange(ruling.distMoved, 'moved', 'same')}, ranges ${describeChange(ruling.rangesMoved, 'moved', 'same')}${hits === undefined ? '' : `, ${hits} self-pin hit(s)`}`;
|
|
754
1321
|
return {
|
|
755
1322
|
target,
|
|
756
1323
|
name,
|
|
757
1324
|
version,
|
|
758
1325
|
steps: runner.steps,
|
|
759
1326
|
ruling,
|
|
760
|
-
row: `| ${name ?? target} | ${version ?? '?'} | ${bump} | ${
|
|
1327
|
+
row: `| ${name ?? target} | ${version ?? '?'} | ${bump} | ${changes} | round |`,
|
|
761
1328
|
};
|
|
762
1329
|
}
|
|
763
|
-
|
|
1330
|
+
/**
|
|
1331
|
+
* Chooses the directory a visit journals its steps in.
|
|
1332
|
+
*
|
|
1333
|
+
* @param target - The target directory.
|
|
1334
|
+
* @returns A new stamped directory under the target's `tmp/units/wave/` when the target is inside a
|
|
1335
|
+
* work tree that ignores `tmp/`, else undefined.
|
|
1336
|
+
*/
|
|
1337
|
+
export function readJournalDirectory(target) {
|
|
764
1338
|
const inside = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
765
1339
|
cwd: target,
|
|
766
1340
|
encoding: 'utf8',
|
|
@@ -777,7 +1351,14 @@ function readJournalDirectory(target) {
|
|
|
777
1351
|
? join(target, 'tmp', 'units', 'wave', new Date().toISOString().replace(/[:.]/gu, '-'))
|
|
778
1352
|
: undefined;
|
|
779
1353
|
}
|
|
780
|
-
|
|
1354
|
+
/**
|
|
1355
|
+
* Reads the packages by layer out of the catalog table.
|
|
1356
|
+
*
|
|
1357
|
+
* @param catalogPath - The agent file that carries the marker-bounded table.
|
|
1358
|
+
* @returns Each layer's `name range` members, layers in numeric order, or undefined when the file or
|
|
1359
|
+
* its markers are absent.
|
|
1360
|
+
*/
|
|
1361
|
+
export function readLayers(catalogPath) {
|
|
781
1362
|
if (!existsSync(catalogPath))
|
|
782
1363
|
return undefined;
|
|
783
1364
|
const text = readFileSync(catalogPath, 'utf8');
|
|
@@ -798,16 +1379,34 @@ function readLayers(catalogPath) {
|
|
|
798
1379
|
.map(([layer, members]) => [layer, members.map(({ member }) => member)])
|
|
799
1380
|
.sort(([left], [right]) => Number(left.slice(1)) - Number(right.slice(1))));
|
|
800
1381
|
}
|
|
801
|
-
|
|
1382
|
+
/**
|
|
1383
|
+
* Renders a visit as text lines.
|
|
1384
|
+
*
|
|
1385
|
+
* @param visit - The finished visit.
|
|
1386
|
+
* @returns One line per step, the ruling, each moved range, and the release row.
|
|
1387
|
+
*/
|
|
1388
|
+
export function renderVisit(visit) {
|
|
802
1389
|
const lines = [
|
|
803
1390
|
...visit.steps.map((entry) => `wave: ${entry.step.padEnd(9)} exit=${entry.exit === undefined ? 'dry' : String(entry.exit)} ${String(entry.durationMs).padStart(7)} ms ${entry.command}${entry.note === undefined ? '' : ` (${entry.note})`}${entry.journal === undefined ? '' : ` journal=${entry.journal}`}`),
|
|
804
|
-
`wave: ruling dist ${
|
|
1391
|
+
`wave: ruling dist ${describeChange(visit.ruling.distMoved, 'moved', 'same')}, ranges ${describeChange(visit.ruling.rangesMoved, 'moved', 'same')}${visit.ruling.hits === undefined ? '' : `, ${visit.ruling.hits} self-pin hit(s)`}`,
|
|
805
1392
|
...(visit.ruling.ranges ?? []).map((range) => `wave: range ${range}`),
|
|
806
1393
|
visit.row,
|
|
807
1394
|
];
|
|
808
1395
|
return `${lines.join('\n')}\n`;
|
|
809
1396
|
}
|
|
810
|
-
|
|
1397
|
+
/**
|
|
1398
|
+
* Runs the mode the arguments name.
|
|
1399
|
+
*
|
|
1400
|
+
* @param argv - The arguments after the script path.
|
|
1401
|
+
* @returns The exit code: 0 when every step passed, 1 when one failed, 2 when a reading failed, 64
|
|
1402
|
+
* on usage.
|
|
1403
|
+
*/
|
|
1404
|
+
export function main(argv) {
|
|
1405
|
+
const unknown = readUnknownFlags(argv, FLAGS);
|
|
1406
|
+
if (unknown.length > 0) {
|
|
1407
|
+
console.error(`wave: unknown flag ${unknown.join(', ')}; give each flag and its value as separate arguments\n${USAGE}`);
|
|
1408
|
+
return 64;
|
|
1409
|
+
}
|
|
811
1410
|
const missing = readMissingFlags(argv, [
|
|
812
1411
|
'--target',
|
|
813
1412
|
'--from',
|
|
@@ -842,7 +1441,7 @@ function main(argv) {
|
|
|
842
1441
|
return 0;
|
|
843
1442
|
}
|
|
844
1443
|
if (!argv.includes('--visit')) {
|
|
845
|
-
console.error(
|
|
1444
|
+
console.error(USAGE);
|
|
846
1445
|
return 64;
|
|
847
1446
|
}
|
|
848
1447
|
const from = STEPS.indexOf(readOption(argv, '--from') ?? 'pin');
|
|
@@ -868,12 +1467,8 @@ function main(argv) {
|
|
|
868
1467
|
writeFileSync(out, output);
|
|
869
1468
|
console.log(`wave: wrote ${out}`);
|
|
870
1469
|
}
|
|
871
|
-
return visit.steps
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
entry.step !== 'compare')
|
|
876
|
-
? 1
|
|
877
|
-
: 0;
|
|
1470
|
+
return failed(visit.steps) ? 1 : 0;
|
|
1471
|
+
}
|
|
1472
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
1473
|
+
process.exitCode = main(process.argv.slice(2));
|
|
878
1474
|
}
|
|
879
|
-
process.exitCode = main(process.argv.slice(2));
|