@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
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
// the `note` a JSON verb prints on stdout, else the last stderr lines), the bump ruling, and a
|
|
19
19
|
// Markdown row for `.orkestrel/release.md`. --dry-run prints the commands and runs nothing, so its
|
|
20
20
|
// ruling is unanswered. --plan reads the marker-bounded catalog table in
|
|
21
|
-
// `.claude/agents/orkestrel.md` and prints the packages by layer.
|
|
22
|
-
//
|
|
21
|
+
// `.claude/agents/orkestrel.md` and prints the packages by layer. The pins and compare steps run
|
|
22
|
+
// `pins.ts` and `compare.ts` as child commands. Exit 0 when every step passed, 1 when a step
|
|
23
|
+
// failed, 2 when a reading failed, 64 on usage, including --plan --regenerate, a flag the script
|
|
24
|
+
// does not read, and the `--flag=value` form.
|
|
23
25
|
import type { SpawnSyncReturns } from 'node:child_process'
|
|
24
26
|
import { spawnSync } from 'node:child_process'
|
|
25
27
|
import { createHash } from 'node:crypto'
|
|
@@ -31,25 +33,15 @@ import {
|
|
|
31
33
|
mkdirSync,
|
|
32
34
|
realpathSync,
|
|
33
35
|
renameSync,
|
|
36
|
+
rmSync,
|
|
34
37
|
unlinkSync,
|
|
35
38
|
writeFileSync,
|
|
36
39
|
} from 'node:fs'
|
|
37
40
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
38
41
|
import { fileURLToPath } from 'node:url'
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
normalizeSlashes,
|
|
43
|
-
removeTree,
|
|
44
|
-
scanLocalSpecs,
|
|
45
|
-
readJsonObject,
|
|
46
|
-
readMissingFlags,
|
|
47
|
-
readOption,
|
|
48
|
-
readString,
|
|
49
|
-
runNpm,
|
|
50
|
-
} from '../../orkestrel-dispatch/scripts/helpers.ts'
|
|
51
|
-
|
|
52
|
-
const STEPS: readonly string[] = [
|
|
42
|
+
|
|
43
|
+
/** Lists the steps of a visit in the order they run. */
|
|
44
|
+
export const STEPS: readonly string[] = [
|
|
53
45
|
'pin',
|
|
54
46
|
'commit',
|
|
55
47
|
'overwrite',
|
|
@@ -61,27 +53,105 @@ const STEPS: readonly string[] = [
|
|
|
61
53
|
'gates',
|
|
62
54
|
'compare',
|
|
63
55
|
]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
|
|
57
|
+
/** Lists the package scripts the gates step runs, in order. */
|
|
58
|
+
export const GATES: readonly string[] = ['format:check', 'lint:check', 'check', 'build', 'test']
|
|
59
|
+
|
|
60
|
+
/** Lists the manifest fields whose ranges ship to a consumer. */
|
|
61
|
+
export const RUNTIME_FIELDS: readonly string[] = ['dependencies', 'peerDependencies']
|
|
62
|
+
|
|
63
|
+
/** Lists the manifest fields whose ranges a visit re-pins. */
|
|
64
|
+
export const RANGE_FIELDS: readonly string[] = [...RUNTIME_FIELDS, 'devDependencies']
|
|
65
|
+
|
|
66
|
+
/** Names the agent file that carries the catalog table. */
|
|
67
|
+
export const CATALOG = '.claude/agents/orkestrel.md'
|
|
68
|
+
|
|
69
|
+
/** Holds the text an offline overwrite refusal names. */
|
|
70
|
+
export const OFFLINE_REFUSAL = "'catalog' does not take --offline"
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Names the package whose checkout the vendored host is staged from.
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* The vendored host is staged from this package's checkout, so overwriting it from the host deletes
|
|
77
|
+
* and replaces its own canon.
|
|
78
|
+
*/
|
|
79
|
+
export const SCAFFOLD = '@orkestrel/scaffold'
|
|
80
|
+
|
|
81
|
+
/** Matches a plain version, optionally with a prerelease. */
|
|
82
|
+
export const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Names the extension of the running file.
|
|
86
|
+
*
|
|
87
|
+
* @remarks
|
|
88
|
+
* The checkout runs the `.ts` sources and a built twin under `dist/agents` runs `.js` siblings.
|
|
89
|
+
*/
|
|
90
|
+
export const EXTENSION = extname(fileURLToPath(import.meta.url))
|
|
91
|
+
|
|
92
|
+
/** Holds the path of the sibling `compare` script. */
|
|
93
|
+
export const COMPARE = fileURLToPath(new URL(`./compare${EXTENSION}`, import.meta.url))
|
|
94
|
+
|
|
95
|
+
/** Holds the path of the sibling `pins` script. */
|
|
96
|
+
export const PINS = fileURLToPath(new URL(`./pins${EXTENSION}`, import.meta.url))
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Matches one entry of `git status --porcelain=v1 -z` output.
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* A rename or copy entry carries its original path as a second NUL-terminated field. The first
|
|
103
|
+
* group holds a rename's new path, the second its original, and the third any other entry's path.
|
|
104
|
+
*/
|
|
105
|
+
export const DIRTY_ENTRY =
|
|
106
|
+
/(?=[RC][^\0]|[^\0][RC])[^\0]{2} ([^\0]*)\0([^\0]*)(?:\0|$)|[^\0]{2} ([^\0]*)(?:\0|$)/gu
|
|
107
|
+
|
|
108
|
+
/** Lists every flag the script reads, value flags and switches alike. */
|
|
109
|
+
export const FLAGS: readonly string[] = [
|
|
110
|
+
'--visit',
|
|
111
|
+
'--plan',
|
|
112
|
+
'--target',
|
|
113
|
+
'--offline',
|
|
114
|
+
'--regenerate',
|
|
115
|
+
'--from',
|
|
116
|
+
'--to',
|
|
117
|
+
'--prior',
|
|
118
|
+
'--dry-run',
|
|
119
|
+
'--json',
|
|
120
|
+
'--out',
|
|
121
|
+
'--catalog',
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
/** Holds the usage line the script prints when no mode was given. */
|
|
125
|
+
export const USAGE =
|
|
126
|
+
'usage: wave.ts --visit [--target DIR] [--offline] [--regenerate] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE] | --plan [--catalog PATH] [--json]'
|
|
127
|
+
|
|
128
|
+
/** Names the manifest fields that hold specs, including nested overrides. */
|
|
129
|
+
export const SPEC_FIELDS: readonly string[] = Object.freeze([
|
|
130
|
+
'dependencies',
|
|
131
|
+
'devDependencies',
|
|
132
|
+
'peerDependencies',
|
|
133
|
+
'optionalDependencies',
|
|
134
|
+
'overrides',
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
/** Matches npm local prefixes and path starts, including drive-relative paths. */
|
|
138
|
+
export const LOCAL_SPEC_PATTERN = /^(?:file:|link:|\.|~[\\/]|[\\/]|[A-Za-z]:)/iu
|
|
139
|
+
|
|
140
|
+
/** Matches archive specs with no protocol and a tgz, tar.gz, or tar extension. */
|
|
141
|
+
export const ARCHIVE_SPEC_PATTERN = /^[^:]+\.(?:tgz|tar\.gz|tar)$/iu
|
|
142
|
+
|
|
143
|
+
/** Matches hosted user/repository shorthand even when its name ends in an archive extension. */
|
|
144
|
+
export const SHORTHAND_SPEC_PATTERN = /^[^:@%/\s.-][^:@%/\s]*\/[^:@\s/%]+$/u
|
|
145
|
+
|
|
146
|
+
/** Describes one dependency the audit read a release for. */
|
|
147
|
+
export interface Release {
|
|
79
148
|
readonly name: string
|
|
80
149
|
readonly range: string
|
|
81
150
|
readonly latest: string | undefined
|
|
82
151
|
}
|
|
83
152
|
|
|
84
|
-
|
|
153
|
+
/** Describes one row of a visit's log. */
|
|
154
|
+
export interface StepResult {
|
|
85
155
|
readonly step: string
|
|
86
156
|
readonly command: string
|
|
87
157
|
readonly exit: number | undefined
|
|
@@ -91,25 +161,29 @@ interface StepResult {
|
|
|
91
161
|
readonly superseded?: true
|
|
92
162
|
}
|
|
93
163
|
|
|
94
|
-
|
|
164
|
+
/** Describes the exit and note a judge reads from a finished command. */
|
|
165
|
+
export interface Verdict {
|
|
95
166
|
readonly exit: number | undefined
|
|
96
167
|
readonly note: string | undefined
|
|
97
168
|
readonly superseded?: true
|
|
98
169
|
}
|
|
99
170
|
|
|
100
|
-
|
|
171
|
+
/** Describes the ranges after a re-pin and the moves that made them. */
|
|
172
|
+
export interface RangeMove {
|
|
101
173
|
readonly ranges: Record<string, unknown>
|
|
102
174
|
readonly moved: readonly string[]
|
|
103
175
|
}
|
|
104
176
|
|
|
105
|
-
|
|
177
|
+
/** Describes the bump ruling a visit reaches. */
|
|
178
|
+
export interface Ruling {
|
|
106
179
|
readonly distMoved: boolean | undefined
|
|
107
180
|
readonly rangesMoved: boolean | undefined
|
|
108
181
|
readonly ranges: readonly string[] | undefined
|
|
109
182
|
readonly hits: number | undefined
|
|
110
183
|
}
|
|
111
184
|
|
|
112
|
-
|
|
185
|
+
/** Describes a finished visit. */
|
|
186
|
+
export interface Visit {
|
|
113
187
|
readonly target: string
|
|
114
188
|
readonly name: string | undefined
|
|
115
189
|
readonly version: string | undefined
|
|
@@ -118,7 +192,8 @@ interface Visit {
|
|
|
118
192
|
readonly row: string
|
|
119
193
|
}
|
|
120
194
|
|
|
121
|
-
|
|
195
|
+
/** Describes the state one visit runs with. */
|
|
196
|
+
export interface Runner {
|
|
122
197
|
readonly target: string
|
|
123
198
|
readonly dry: boolean
|
|
124
199
|
readonly offline: boolean
|
|
@@ -127,7 +202,8 @@ interface Runner {
|
|
|
127
202
|
readonly steps: StepResult[]
|
|
128
203
|
}
|
|
129
204
|
|
|
130
|
-
|
|
205
|
+
/** Describes the receipt the overwrite step leaves for the record step. */
|
|
206
|
+
export interface OverwriteReceipt {
|
|
131
207
|
readonly head: string
|
|
132
208
|
readonly version: string
|
|
133
209
|
readonly offline: boolean
|
|
@@ -135,12 +211,14 @@ interface OverwriteReceipt {
|
|
|
135
211
|
readonly removed: readonly string[]
|
|
136
212
|
}
|
|
137
213
|
|
|
138
|
-
|
|
214
|
+
/** Describes the outcome of writing a journal file. */
|
|
215
|
+
export interface Journal {
|
|
139
216
|
readonly path?: string
|
|
140
217
|
readonly failure?: string
|
|
141
218
|
}
|
|
142
219
|
|
|
143
|
-
|
|
220
|
+
/** Describes the outcome of regenerating a lock and its tree. */
|
|
221
|
+
export interface Regeneration {
|
|
144
222
|
readonly exit: number
|
|
145
223
|
readonly note: string
|
|
146
224
|
readonly installed?: SpawnSyncReturns<string>
|
|
@@ -148,7 +226,233 @@ interface Regeneration {
|
|
|
148
226
|
readonly duration?: number
|
|
149
227
|
}
|
|
150
228
|
|
|
151
|
-
|
|
229
|
+
/** Describes how to spawn npm on this host without a shell where the CLI entry is present. */
|
|
230
|
+
export interface NpmCommand {
|
|
231
|
+
readonly file: string
|
|
232
|
+
readonly prefix: readonly string[]
|
|
233
|
+
readonly shell: boolean
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Checks whether a value is a non-null, non-array object.
|
|
238
|
+
*
|
|
239
|
+
* @param value - The value to inspect.
|
|
240
|
+
* @returns True if the value is an object other than null or an array; false otherwise.
|
|
241
|
+
*/
|
|
242
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
243
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Checks whether npm resolves a spec from the local host.
|
|
248
|
+
*
|
|
249
|
+
* @param spec - The package spec, with leading whitespace ignored.
|
|
250
|
+
* @returns True if the spec names a local path or archive; false otherwise.
|
|
251
|
+
*/
|
|
252
|
+
export function matchesLocalSpec(spec: string): boolean {
|
|
253
|
+
const text = spec.trimStart()
|
|
254
|
+
return (
|
|
255
|
+
LOCAL_SPEC_PATTERN.test(text) ||
|
|
256
|
+
(ARCHIVE_SPEC_PATTERN.test(text) && !SHORTHAND_SPEC_PATTERN.test(text))
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Collects string specs with the dotted path of each nested record member.
|
|
262
|
+
*
|
|
263
|
+
* @param path - The field path that prefixes each finding.
|
|
264
|
+
* @param value - A string or finite, acyclic manifest record; other values contribute no specs.
|
|
265
|
+
* @returns The path and spec pairs in record order.
|
|
266
|
+
*/
|
|
267
|
+
export function collectSpecs(
|
|
268
|
+
path: string,
|
|
269
|
+
value: unknown,
|
|
270
|
+
): ReadonlyArray<readonly [string, string]> {
|
|
271
|
+
if (typeof value === 'string') return [[path, value]]
|
|
272
|
+
if (!isRecord(value)) return []
|
|
273
|
+
return Object.entries(value).flatMap(([key, child]) => collectSpecs(`${path}.${key}`, child))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Finds local specs in the manifest's spec fields, including nested overrides.
|
|
278
|
+
*
|
|
279
|
+
* @param manifest - A finite, acyclic package manifest record.
|
|
280
|
+
* @returns Each local spec's dotted field path and spec, in field and record order.
|
|
281
|
+
*/
|
|
282
|
+
export function scanLocalSpecs(
|
|
283
|
+
manifest: Readonly<Record<string, unknown>>,
|
|
284
|
+
): ReadonlyArray<readonly [string, string]> {
|
|
285
|
+
return SPEC_FIELDS.flatMap((field) =>
|
|
286
|
+
collectSpecs(field, manifest[field]).filter(([, spec]) => matchesLocalSpec(spec)),
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Removes a tree with bounded retries for transient host removal errors.
|
|
292
|
+
*
|
|
293
|
+
* @param path - The directory to remove; a missing path succeeds.
|
|
294
|
+
* @returns Nothing after the removal succeeds.
|
|
295
|
+
* @throws Thrown when removal fails after the bounded retries or the host refuses the path.
|
|
296
|
+
*/
|
|
297
|
+
export function removeTree(path: string): void {
|
|
298
|
+
rmSync(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Normalizes a host path to forward slashes.
|
|
303
|
+
*
|
|
304
|
+
* @param path - A host path.
|
|
305
|
+
* @returns The same path with every backslash replaced by a slash.
|
|
306
|
+
*/
|
|
307
|
+
export function normalizeSlashes(path: string): string {
|
|
308
|
+
return path.split('\\').join('/')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Reads the value that follows a flag.
|
|
313
|
+
*
|
|
314
|
+
* @remarks
|
|
315
|
+
* Reads `--flag value` pairs: the first occurrence of a flag wins, a following value that opens
|
|
316
|
+
* with `--` is a missing value, and the `--flag=value` form is not read.
|
|
317
|
+
*
|
|
318
|
+
* @param argv - The arguments after the script path.
|
|
319
|
+
* @param name - The flag, such as `--target`.
|
|
320
|
+
* @returns The value after the flag, or undefined when the flag is absent, last, or followed by
|
|
321
|
+
* another flag.
|
|
322
|
+
*/
|
|
323
|
+
export function readOption(argv: readonly string[], name: string): string | undefined {
|
|
324
|
+
const index = argv.indexOf(name)
|
|
325
|
+
const value = index === -1 ? undefined : argv[index + 1]
|
|
326
|
+
return value === undefined || value.startsWith('--') ? undefined : value
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Names the value flags a caller gave with no value.
|
|
331
|
+
*
|
|
332
|
+
* @param argv - The arguments after the script path.
|
|
333
|
+
* @param names - The flags that take a value.
|
|
334
|
+
* @returns Each named flag that appears last or is followed by another flag, in argument order.
|
|
335
|
+
*/
|
|
336
|
+
export function readMissingFlags(
|
|
337
|
+
argv: readonly string[],
|
|
338
|
+
names: readonly string[],
|
|
339
|
+
): readonly string[] {
|
|
340
|
+
return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Names the flags a caller gave that the script does not read.
|
|
345
|
+
*
|
|
346
|
+
* @remarks
|
|
347
|
+
* Reads the arguments before a bare `--`, which opens a command a script passes on. A `--flag=value`
|
|
348
|
+
* argument is unknown, because the script reads no such form.
|
|
349
|
+
*
|
|
350
|
+
* @param argv - The arguments after the script path.
|
|
351
|
+
* @param flags - Every flag the script reads, value flags and switches alike.
|
|
352
|
+
* @returns Each argument that opens with `--` and is not a read flag, in argument order.
|
|
353
|
+
*/
|
|
354
|
+
export function readUnknownFlags(
|
|
355
|
+
argv: readonly string[],
|
|
356
|
+
flags: readonly string[],
|
|
357
|
+
): readonly string[] {
|
|
358
|
+
const end = argv.indexOf('--')
|
|
359
|
+
return (end === -1 ? argv : argv.slice(0, end)).filter(
|
|
360
|
+
(argument) => argument.startsWith('--') && !flags.includes(argument),
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Reads the last non-empty line of a text as a JSON object.
|
|
366
|
+
*
|
|
367
|
+
* @param text - Text whose last line a script printed as JSON.
|
|
368
|
+
* @returns The parsed object, or undefined when the last line is not a JSON object.
|
|
369
|
+
*/
|
|
370
|
+
export function parseJsonLine(text: string): Record<string, unknown> | undefined {
|
|
371
|
+
const line = text
|
|
372
|
+
.split(/\r\n|\n/)
|
|
373
|
+
.filter((candidate) => candidate.trim() !== '')
|
|
374
|
+
.at(-1)
|
|
375
|
+
if (line === undefined) return undefined
|
|
376
|
+
try {
|
|
377
|
+
const parsed: unknown = JSON.parse(line)
|
|
378
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
379
|
+
return Object.fromEntries(Object.entries(parsed))
|
|
380
|
+
}
|
|
381
|
+
} catch {}
|
|
382
|
+
return undefined
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Parses a whole text as one JSON object.
|
|
387
|
+
*
|
|
388
|
+
* @param text - A JSON document, formatted or not, such as a manifest or `npm view --json` output.
|
|
389
|
+
* @returns The parsed object, or undefined when the text is not a JSON object.
|
|
390
|
+
*/
|
|
391
|
+
export function parseJsonRecord(text: string): Record<string, unknown> | undefined {
|
|
392
|
+
try {
|
|
393
|
+
const parsed: unknown = JSON.parse(text)
|
|
394
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
395
|
+
return Object.fromEntries(Object.entries(parsed))
|
|
396
|
+
}
|
|
397
|
+
} catch {}
|
|
398
|
+
return undefined
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Reads a string member of a record.
|
|
403
|
+
*
|
|
404
|
+
* @param record - The record, or undefined.
|
|
405
|
+
* @param key - The member name.
|
|
406
|
+
* @returns The string, or undefined when the member is absent or not a string.
|
|
407
|
+
*/
|
|
408
|
+
export function readString(
|
|
409
|
+
record: Record<string, unknown> | undefined,
|
|
410
|
+
key: string,
|
|
411
|
+
): string | undefined {
|
|
412
|
+
const value = record?.[key]
|
|
413
|
+
return typeof value === 'string' ? value : undefined
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Resolves the npm command for this host.
|
|
418
|
+
*
|
|
419
|
+
* @remarks
|
|
420
|
+
* The `npm-cli.js` entry beside the running Node binary spawns without a shell on every host; the
|
|
421
|
+
* `npm` shim on Windows is a `.cmd` file that needs one, so that path is the fallback alone.
|
|
422
|
+
*
|
|
423
|
+
* @returns The file to spawn, the arguments that precede npm's own, and whether a shell is needed.
|
|
424
|
+
*/
|
|
425
|
+
export function resolveNpm(): NpmCommand {
|
|
426
|
+
const cli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js')
|
|
427
|
+
if (existsSync(cli)) return { file: process.execPath, prefix: [cli], shell: false }
|
|
428
|
+
return { file: 'npm', prefix: [], shell: process.platform === 'win32' }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Runs npm with the given arguments and returns the finished child.
|
|
433
|
+
*
|
|
434
|
+
* @param args - The npm arguments, such as `['install']`.
|
|
435
|
+
* @param cwd - The working directory. Default: the current directory.
|
|
436
|
+
* @returns The spawn result with `utf8` stdout and stderr.
|
|
437
|
+
*/
|
|
438
|
+
export function runNpm(args: readonly string[], cwd?: string): SpawnSyncReturns<string> {
|
|
439
|
+
const npm = resolveNpm()
|
|
440
|
+
return spawnSync(npm.file, [...npm.prefix, ...args], {
|
|
441
|
+
encoding: 'utf8',
|
|
442
|
+
windowsHide: true,
|
|
443
|
+
shell: npm.shell,
|
|
444
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
445
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Reads a target's package manifest.
|
|
451
|
+
*
|
|
452
|
+
* @param target - The target directory.
|
|
453
|
+
* @returns The manifest record, or undefined when it is absent, unreadable, or not a JSON object.
|
|
454
|
+
*/
|
|
455
|
+
export function readManifest(target: string): Record<string, unknown> | undefined {
|
|
152
456
|
try {
|
|
153
457
|
return parseJsonRecord(readFileSync(join(target, 'package.json'), 'utf8'))
|
|
154
458
|
} catch {
|
|
@@ -156,7 +460,14 @@ function readManifest(target: string): Record<string, unknown> | undefined {
|
|
|
156
460
|
}
|
|
157
461
|
}
|
|
158
462
|
|
|
159
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Reads the ranges a manifest declares in the named fields.
|
|
465
|
+
*
|
|
466
|
+
* @param manifest - The manifest record.
|
|
467
|
+
* @param fields - The fields to read.
|
|
468
|
+
* @returns Each string range keyed by `field name`.
|
|
469
|
+
*/
|
|
470
|
+
export function readRanges(
|
|
160
471
|
manifest: Record<string, unknown>,
|
|
161
472
|
fields: readonly string[],
|
|
162
473
|
): ReadonlyMap<string, string> {
|
|
@@ -171,7 +482,14 @@ function readRanges(
|
|
|
171
482
|
)
|
|
172
483
|
}
|
|
173
484
|
|
|
174
|
-
|
|
485
|
+
/**
|
|
486
|
+
* Lists how the final ranges differ from the published ones.
|
|
487
|
+
*
|
|
488
|
+
* @param published - The ranges the published manifest declares.
|
|
489
|
+
* @param final - The ranges the target declares now.
|
|
490
|
+
* @returns One message per range added, removed, or changed, sorted by key.
|
|
491
|
+
*/
|
|
492
|
+
export function compareRanges(
|
|
175
493
|
published: ReadonlyMap<string, string>,
|
|
176
494
|
final: ReadonlyMap<string, string>,
|
|
177
495
|
): readonly string[] {
|
|
@@ -186,7 +504,14 @@ function compareRanges(
|
|
|
186
504
|
})
|
|
187
505
|
}
|
|
188
506
|
|
|
189
|
-
|
|
507
|
+
/**
|
|
508
|
+
* Resolves the scaffold executable a target runs.
|
|
509
|
+
*
|
|
510
|
+
* @param target - The target directory.
|
|
511
|
+
* @returns Node and the installed scaffold entry, else Node and the target's own built entry, else
|
|
512
|
+
* none.
|
|
513
|
+
*/
|
|
514
|
+
export function resolveScaffold(target: string): readonly string[] {
|
|
190
515
|
const installed = join(target, 'node_modules', '@orkestrel', 'scaffold', 'dist', 'bin', 'main.js')
|
|
191
516
|
if (existsSync(installed)) return [process.execPath, installed]
|
|
192
517
|
const own = join(target, 'dist', 'bin', 'main.js')
|
|
@@ -194,8 +519,15 @@ function resolveScaffold(target: string): readonly string[] {
|
|
|
194
519
|
return []
|
|
195
520
|
}
|
|
196
521
|
|
|
197
|
-
|
|
198
|
-
|
|
522
|
+
/**
|
|
523
|
+
* Reads the release readings out of an audit's standard output.
|
|
524
|
+
*
|
|
525
|
+
* @param text - The audit's standard output, whose last line is the JSON envelope.
|
|
526
|
+
* @returns One release per reading that names a dependency and its range, or undefined when the
|
|
527
|
+
* envelope carries no `releases` list.
|
|
528
|
+
*/
|
|
529
|
+
export function readReleases(text: string): readonly Release[] | undefined {
|
|
530
|
+
const releases = parseJsonLine(text)?.releases
|
|
199
531
|
if (!Array.isArray(releases)) return undefined
|
|
200
532
|
return releases.flatMap((entry) => {
|
|
201
533
|
if (typeof entry !== 'object' || entry === null) return []
|
|
@@ -207,15 +539,30 @@ function readReleases(text: string): readonly Release[] | undefined {
|
|
|
207
539
|
})
|
|
208
540
|
}
|
|
209
541
|
|
|
210
|
-
|
|
542
|
+
/**
|
|
543
|
+
* Renders a command as the line a step row carries.
|
|
544
|
+
*
|
|
545
|
+
* @param file - The executable.
|
|
546
|
+
* @param args - Its arguments.
|
|
547
|
+
* @returns The command line, naming the running Node binary as `node`.
|
|
548
|
+
*/
|
|
549
|
+
export function renderCommand(file: string, args: readonly string[]): string {
|
|
211
550
|
return [file === process.execPath ? 'node' : file, ...args].join(' ')
|
|
212
551
|
}
|
|
213
552
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
553
|
+
/**
|
|
554
|
+
* Names why a finished command failed.
|
|
555
|
+
*
|
|
556
|
+
* @remarks
|
|
557
|
+
* A JSON verb writes its refusal envelope or its partial-run note on stdout, so the stderr tail
|
|
558
|
+
* alone reads empty for exactly the failures the operator needs named.
|
|
559
|
+
*
|
|
560
|
+
* @param result - The finished command.
|
|
561
|
+
* @returns Nothing for exit 0; else the `code: message` envelope, the `note`, or the last stderr lines.
|
|
562
|
+
*/
|
|
563
|
+
export function describeFailure(result: SpawnSyncReturns<string>): string | undefined {
|
|
217
564
|
if (result.status === 0) return undefined
|
|
218
|
-
const value =
|
|
565
|
+
const value = parseJsonLine(result.stdout)
|
|
219
566
|
const error = value?.error
|
|
220
567
|
if (typeof error === 'object' && error !== null && !Array.isArray(error)) {
|
|
221
568
|
const envelope = Object.fromEntries(Object.entries(error))
|
|
@@ -232,13 +579,33 @@ function describeFailure(result: SpawnSyncReturns<string>): string | undefined {
|
|
|
232
579
|
.join(' | ')
|
|
233
580
|
}
|
|
234
581
|
|
|
235
|
-
|
|
582
|
+
/**
|
|
583
|
+
* Reads the verdict of a finished command by its exit alone.
|
|
584
|
+
*
|
|
585
|
+
* @param result - The finished command.
|
|
586
|
+
* @returns The exit and the failure note.
|
|
587
|
+
*/
|
|
588
|
+
export function readVerdict(result: SpawnSyncReturns<string>): Verdict {
|
|
236
589
|
return { exit: result.status ?? undefined, note: describeFailure(result) }
|
|
237
590
|
}
|
|
238
591
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
592
|
+
/**
|
|
593
|
+
* Runs a command as one step of the visit and logs its row.
|
|
594
|
+
*
|
|
595
|
+
* @remarks
|
|
596
|
+
* A judge reads the finished command and answers the exit and note its one log entry carries, so no
|
|
597
|
+
* entry is amended after it is appended.
|
|
598
|
+
*
|
|
599
|
+
* @param runner - The visit's state.
|
|
600
|
+
* @param step - The step's name.
|
|
601
|
+
* @param file - The executable.
|
|
602
|
+
* @param args - Its arguments.
|
|
603
|
+
* @param cwd - The working directory.
|
|
604
|
+
* @param judge - Reads the exit and note from the result. Default: {@link readVerdict}.
|
|
605
|
+
* @param input - Standard input for the command. Default: none.
|
|
606
|
+
* @returns The finished command, or undefined under --dry-run.
|
|
607
|
+
*/
|
|
608
|
+
export function runStep(
|
|
242
609
|
runner: Runner,
|
|
243
610
|
step: string,
|
|
244
611
|
file: string,
|
|
@@ -247,7 +614,7 @@ function runStep(
|
|
|
247
614
|
judge: (result: SpawnSyncReturns<string>) => Verdict = readVerdict,
|
|
248
615
|
input?: string,
|
|
249
616
|
): SpawnSyncReturns<string> | undefined {
|
|
250
|
-
const command =
|
|
617
|
+
const command = renderCommand(file, args)
|
|
251
618
|
if (runner.dry) {
|
|
252
619
|
runner.steps.push({ step, command, exit: undefined, durationMs: 0, note: 'dry run' })
|
|
253
620
|
return undefined
|
|
@@ -266,8 +633,17 @@ function runStep(
|
|
|
266
633
|
return result
|
|
267
634
|
}
|
|
268
635
|
|
|
269
|
-
|
|
270
|
-
|
|
636
|
+
/**
|
|
637
|
+
* Judges the pin step's audit.
|
|
638
|
+
*
|
|
639
|
+
* @remarks
|
|
640
|
+
* The audit exits 1 on the drift the pin step exists to close, so its exit is a reading here.
|
|
641
|
+
*
|
|
642
|
+
* @param result - The audit.
|
|
643
|
+
* @returns Exit 0 with the reading count when the audit printed releases and exited 0 or 1; else a
|
|
644
|
+
* failing exit with the reason.
|
|
645
|
+
*/
|
|
646
|
+
export function judgePin(result: SpawnSyncReturns<string>): Verdict {
|
|
271
647
|
const releases = readReleases(result.stdout)
|
|
272
648
|
if (releases === undefined || (result.status !== 0 && result.status !== 1)) {
|
|
273
649
|
return {
|
|
@@ -279,16 +655,34 @@ function judgePin(result: SpawnSyncReturns<string>): Verdict {
|
|
|
279
655
|
return { exit: 0, note: `${releases.length} release reading(s)` }
|
|
280
656
|
}
|
|
281
657
|
|
|
282
|
-
|
|
658
|
+
/**
|
|
659
|
+
* Judges the overwrite command.
|
|
660
|
+
*
|
|
661
|
+
* @param result - The overwrite.
|
|
662
|
+
* @param offline - If `true`, an exit of 1 whose note names the catalog refusal passes; if `false`,
|
|
663
|
+
* the exit alone decides.
|
|
664
|
+
* @returns The verdict.
|
|
665
|
+
*/
|
|
666
|
+
export function judgeOverwrite(result: SpawnSyncReturns<string>, offline: boolean): Verdict {
|
|
283
667
|
if (!offline || result.status !== 1) return readVerdict(result)
|
|
284
|
-
const note = readString(
|
|
668
|
+
const note = readString(parseJsonLine(result.stdout), 'note')
|
|
285
669
|
return note !== undefined && note.includes(OFFLINE_REFUSAL)
|
|
286
670
|
? { exit: 0, note: 'offline overwrite skipped the catalog step by design' }
|
|
287
671
|
: { exit: 1, note: describeFailure(result) }
|
|
288
672
|
}
|
|
289
673
|
|
|
290
|
-
|
|
291
|
-
|
|
674
|
+
/**
|
|
675
|
+
* Judges the verify step's audit.
|
|
676
|
+
*
|
|
677
|
+
* @remarks
|
|
678
|
+
* The audit reads the canonical sources of scaffold's own checkout as foreign paths and exits 1.
|
|
679
|
+
*
|
|
680
|
+
* @param result - The audit.
|
|
681
|
+
* @param own - If `true`, the target is scaffold's own checkout and an exit of 1 passes; if `false`,
|
|
682
|
+
* the exit alone decides.
|
|
683
|
+
* @returns The verdict; an audit that printed no releases envelope fails.
|
|
684
|
+
*/
|
|
685
|
+
export function judgeVerify(result: SpawnSyncReturns<string>, own: boolean): Verdict {
|
|
292
686
|
if (readReleases(result.stdout) === undefined) {
|
|
293
687
|
return {
|
|
294
688
|
exit: result.status === null || result.status === 0 ? 2 : result.status,
|
|
@@ -300,7 +694,16 @@ function judgeVerify(result: SpawnSyncReturns<string>, own: boolean): Verdict {
|
|
|
300
694
|
: readVerdict(result)
|
|
301
695
|
}
|
|
302
696
|
|
|
303
|
-
|
|
697
|
+
/**
|
|
698
|
+
* Runs npm as one step of the visit and logs its row.
|
|
699
|
+
*
|
|
700
|
+
* @param runner - The visit's state.
|
|
701
|
+
* @param step - The step's name.
|
|
702
|
+
* @param args - The npm arguments.
|
|
703
|
+
* @param judge - Reads the exit and note from the result. Default: {@link readVerdict}.
|
|
704
|
+
* @returns The finished command, or undefined under --dry-run.
|
|
705
|
+
*/
|
|
706
|
+
export function runNpmStep(
|
|
304
707
|
runner: Runner,
|
|
305
708
|
step: string,
|
|
306
709
|
args: readonly string[],
|
|
@@ -326,7 +729,19 @@ function runNpmStep(
|
|
|
326
729
|
return result
|
|
327
730
|
}
|
|
328
731
|
|
|
329
|
-
|
|
732
|
+
/**
|
|
733
|
+
* Writes one step's full output to the visit's journal directory.
|
|
734
|
+
*
|
|
735
|
+
* @param runner - The visit's state.
|
|
736
|
+
* @param step - The step's name.
|
|
737
|
+
* @param command - The command line.
|
|
738
|
+
* @param exit - The exit code.
|
|
739
|
+
* @param stdout - The standard output.
|
|
740
|
+
* @param stderr - The standard error.
|
|
741
|
+
* @returns The journal path, the failure that stopped the write, or neither when the visit keeps no
|
|
742
|
+
* journal.
|
|
743
|
+
*/
|
|
744
|
+
export function writeJournal(
|
|
330
745
|
runner: Runner,
|
|
331
746
|
step: string,
|
|
332
747
|
command: string,
|
|
@@ -353,7 +768,18 @@ function writeJournal(
|
|
|
353
768
|
}
|
|
354
769
|
}
|
|
355
770
|
|
|
356
|
-
|
|
771
|
+
/**
|
|
772
|
+
* Appends a finished command's row to the visit's log.
|
|
773
|
+
*
|
|
774
|
+
* @param runner - The visit's state.
|
|
775
|
+
* @param step - The step's name.
|
|
776
|
+
* @param command - The command line.
|
|
777
|
+
* @param result - The finished command.
|
|
778
|
+
* @param durationMs - How long it ran.
|
|
779
|
+
* @param verdict - The exit and note to record; a journal failure turns the exit into 1.
|
|
780
|
+
* @returns Nothing after the row is appended.
|
|
781
|
+
*/
|
|
782
|
+
export function appendResult(
|
|
357
783
|
runner: Runner,
|
|
358
784
|
step: string,
|
|
359
785
|
command: string,
|
|
@@ -383,7 +809,23 @@ function appendResult(
|
|
|
383
809
|
})
|
|
384
810
|
}
|
|
385
811
|
|
|
386
|
-
|
|
812
|
+
/**
|
|
813
|
+
* Appends a row for a step the script ran itself.
|
|
814
|
+
*
|
|
815
|
+
* @param runner - The visit's state.
|
|
816
|
+
* @param step - The step's name.
|
|
817
|
+
* @param command - The label the row carries as its command.
|
|
818
|
+
* @param note - What the step found or did.
|
|
819
|
+
* @param exit - The exit the row records. Default: 0.
|
|
820
|
+
* @returns Nothing after the row is appended.
|
|
821
|
+
*/
|
|
822
|
+
export function appendStep(
|
|
823
|
+
runner: Runner,
|
|
824
|
+
step: string,
|
|
825
|
+
command: string,
|
|
826
|
+
note: string,
|
|
827
|
+
exit = 0,
|
|
828
|
+
): void {
|
|
387
829
|
const journal = runner.dry ? {} : writeJournal(runner, step, command, exit, note, '')
|
|
388
830
|
runner.steps.push({
|
|
389
831
|
step,
|
|
@@ -395,8 +837,19 @@ function appendStep(runner: Runner, step: string, command: string, note: string,
|
|
|
395
837
|
})
|
|
396
838
|
}
|
|
397
839
|
|
|
398
|
-
|
|
399
|
-
|
|
840
|
+
/**
|
|
841
|
+
* Checks whether a visit's log holds a failure.
|
|
842
|
+
*
|
|
843
|
+
* @remarks
|
|
844
|
+
* The one predicate the visit's steps and the script's exit code share. A superseded row, a row with
|
|
845
|
+
* no exit (a dry run), and a pins or compare row are readings, not failures.
|
|
846
|
+
*
|
|
847
|
+
* @param steps - The rows logged so far.
|
|
848
|
+
* @returns True if a row exited nonzero, was not superseded, and is not a pins or compare row; false
|
|
849
|
+
* otherwise.
|
|
850
|
+
*/
|
|
851
|
+
export function failed(steps: readonly StepResult[]): boolean {
|
|
852
|
+
return steps.some(
|
|
400
853
|
(entry) =>
|
|
401
854
|
entry.superseded !== true &&
|
|
402
855
|
entry.exit !== undefined &&
|
|
@@ -406,7 +859,14 @@ function failed(runner: Runner): boolean {
|
|
|
406
859
|
)
|
|
407
860
|
}
|
|
408
861
|
|
|
409
|
-
|
|
862
|
+
/**
|
|
863
|
+
* Reads the resolution conflict an install reported.
|
|
864
|
+
*
|
|
865
|
+
* @param result - The finished install.
|
|
866
|
+
* @returns The conflict line npm printed, a generic reason when it printed none, or undefined when
|
|
867
|
+
* the install did not fail with ERESOLVE.
|
|
868
|
+
*/
|
|
869
|
+
export function extractConflict(result: SpawnSyncReturns<string>): string | undefined {
|
|
410
870
|
const output = `${result.stdout}\n${result.stderr}`
|
|
411
871
|
if (!/\bcode ERESOLVE\b/u.test(output)) return undefined
|
|
412
872
|
const lines = output.split(/\r\n|\n/)
|
|
@@ -418,7 +878,14 @@ function extractConflict(result: SpawnSyncReturns<string>): string | undefined {
|
|
|
418
878
|
)
|
|
419
879
|
}
|
|
420
880
|
|
|
421
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Runs the install and regenerates the lock when asked and the install cannot resolve.
|
|
883
|
+
*
|
|
884
|
+
* @param runner - The visit's state.
|
|
885
|
+
* @param step - The step the rows belong to.
|
|
886
|
+
* @returns Nothing after the rows are appended.
|
|
887
|
+
*/
|
|
888
|
+
export function runInstall(runner: Runner, step: string): void {
|
|
422
889
|
// npm can unlink a node_modules junction before reporting its resolution failure.
|
|
423
890
|
if (
|
|
424
891
|
!runner.dry &&
|
|
@@ -454,7 +921,14 @@ function runInstall(runner: Runner, step: string): void {
|
|
|
454
921
|
regenerateLock(runner, step, installed)
|
|
455
922
|
}
|
|
456
923
|
|
|
457
|
-
|
|
924
|
+
/**
|
|
925
|
+
* Refuses a manifest that holds local dependency specs.
|
|
926
|
+
*
|
|
927
|
+
* @param runner - The visit's state.
|
|
928
|
+
* @param step - The step that refuses.
|
|
929
|
+
* @returns True if a failing row was appended; false when the manifest holds none.
|
|
930
|
+
*/
|
|
931
|
+
export function refuseLocalSpecs(runner: Runner, step: string): boolean {
|
|
458
932
|
const specs = scanLocalSpecs(readManifest(runner.target) ?? {})
|
|
459
933
|
if (specs.length === 0) return false
|
|
460
934
|
appendStep(
|
|
@@ -467,7 +941,14 @@ function refuseLocalSpecs(runner: Runner, step: string): boolean {
|
|
|
467
941
|
return true
|
|
468
942
|
}
|
|
469
943
|
|
|
470
|
-
|
|
944
|
+
/**
|
|
945
|
+
* Names why the lock must not be regenerated.
|
|
946
|
+
*
|
|
947
|
+
* @param runner - The visit's state.
|
|
948
|
+
* @returns The first refusal, or undefined when the lock is tracked and clean, the manifest holds no
|
|
949
|
+
* local spec or workspace, `tmp/` is ignored, and the target is its work tree's root.
|
|
950
|
+
*/
|
|
951
|
+
export function readRegenerationRefusal(runner: Runner): string | undefined {
|
|
471
952
|
if (
|
|
472
953
|
runStep(
|
|
473
954
|
runner,
|
|
@@ -511,7 +992,16 @@ function readRegenerationRefusal(runner: Runner): string | undefined {
|
|
|
511
992
|
return undefined
|
|
512
993
|
}
|
|
513
994
|
|
|
514
|
-
|
|
995
|
+
/**
|
|
996
|
+
* Renames a directory, retrying while the host holds it.
|
|
997
|
+
*
|
|
998
|
+
* @param source - The directory to move.
|
|
999
|
+
* @param target - Its new path.
|
|
1000
|
+
* @param attempt - The retries spent so far. Default: 0.
|
|
1001
|
+
* @returns Nothing after the rename.
|
|
1002
|
+
* @throws Thrown when the rename fails for another reason or after the retries.
|
|
1003
|
+
*/
|
|
1004
|
+
export function moveModules(source: string, target: string, attempt = 0): void {
|
|
515
1005
|
try {
|
|
516
1006
|
renameSync(source, target)
|
|
517
1007
|
} catch (error) {
|
|
@@ -522,7 +1012,19 @@ function moveModules(source: string, target: string, attempt = 0): void {
|
|
|
522
1012
|
}
|
|
523
1013
|
}
|
|
524
1014
|
|
|
525
|
-
|
|
1015
|
+
/**
|
|
1016
|
+
* Regenerates the lock and the installed tree from the manifest.
|
|
1017
|
+
*
|
|
1018
|
+
* @param runner - The visit's state.
|
|
1019
|
+
* @param step - The step the rows belong to.
|
|
1020
|
+
* @param conflict - The install that failed with ERESOLVE.
|
|
1021
|
+
* @returns Nothing after the rows are appended; a failed regeneration restores the tree and lock.
|
|
1022
|
+
*/
|
|
1023
|
+
export function regenerateLock(
|
|
1024
|
+
runner: Runner,
|
|
1025
|
+
step: string,
|
|
1026
|
+
conflict: SpawnSyncReturns<string>,
|
|
1027
|
+
): void {
|
|
526
1028
|
const refusal = readRegenerationRefusal(runner)
|
|
527
1029
|
if (refusal !== undefined || runner.directory === undefined) {
|
|
528
1030
|
appendStep(
|
|
@@ -599,8 +1101,18 @@ function regenerateLock(runner: Runner, step: string, conflict: SpawnSyncReturns
|
|
|
599
1101
|
appendResult(runner, step, 'npm ls --all --json', result.listed, 0, readVerdict(result.listed))
|
|
600
1102
|
}
|
|
601
1103
|
|
|
602
|
-
|
|
603
|
-
|
|
1104
|
+
/**
|
|
1105
|
+
* Installs from a manifest with no lock and proves the tree.
|
|
1106
|
+
*
|
|
1107
|
+
* @remarks
|
|
1108
|
+
* Journal writes and backup disposal cannot enter the transaction's rollback boundary.
|
|
1109
|
+
*
|
|
1110
|
+
* @param target - The target directory.
|
|
1111
|
+
* @param manifest - The manifest bytes before the install.
|
|
1112
|
+
* @param before - The SHA-256 of the lock before.
|
|
1113
|
+
* @returns Exit 0 when the install, the tree listing, and the unchanged manifest prove it; else exit 1.
|
|
1114
|
+
*/
|
|
1115
|
+
export function regenerateTree(target: string, manifest: Buffer, before: string): Regeneration {
|
|
604
1116
|
const lock = join(target, 'package-lock.json')
|
|
605
1117
|
try {
|
|
606
1118
|
unlinkSync(lock)
|
|
@@ -625,7 +1137,15 @@ function regenerateTree(target: string, manifest: Buffer, before: string): Regen
|
|
|
625
1137
|
}
|
|
626
1138
|
}
|
|
627
1139
|
|
|
628
|
-
|
|
1140
|
+
/**
|
|
1141
|
+
* Discards the backups after a proved regeneration.
|
|
1142
|
+
*
|
|
1143
|
+
* @param target - The target directory.
|
|
1144
|
+
* @param moved - The moved previous tree.
|
|
1145
|
+
* @param backup - The saved lock.
|
|
1146
|
+
* @returns Nothing on success, else the failure with the backup paths kept.
|
|
1147
|
+
*/
|
|
1148
|
+
export function discardBackup(target: string, moved: string, backup: string): string | undefined {
|
|
629
1149
|
try {
|
|
630
1150
|
for (const path of [moved, backup, join(target, 'node_modules')]) validateWavePath(target, path)
|
|
631
1151
|
removeTree(moved)
|
|
@@ -635,7 +1155,16 @@ function discardBackup(target: string, moved: string, backup: string): string |
|
|
|
635
1155
|
}
|
|
636
1156
|
}
|
|
637
1157
|
|
|
638
|
-
|
|
1158
|
+
/**
|
|
1159
|
+
* Restores the previous tree and lock after a failed regeneration.
|
|
1160
|
+
*
|
|
1161
|
+
* @param target - The target directory.
|
|
1162
|
+
* @param moved - The moved previous tree.
|
|
1163
|
+
* @param backup - The saved lock.
|
|
1164
|
+
* @param before - The SHA-256 the saved lock must still have.
|
|
1165
|
+
* @returns `tree and lock restored`, else the failure with the backup paths kept.
|
|
1166
|
+
*/
|
|
1167
|
+
export function restoreLock(target: string, moved: string, backup: string, before: string): string {
|
|
639
1168
|
try {
|
|
640
1169
|
for (const path of [
|
|
641
1170
|
moved,
|
|
@@ -658,7 +1187,16 @@ function restoreLock(target: string, moved: string, backup: string, before: stri
|
|
|
658
1187
|
}
|
|
659
1188
|
}
|
|
660
1189
|
|
|
661
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* Checks that a destination stays inside the target and crosses no link.
|
|
1192
|
+
*
|
|
1193
|
+
* @param target - The target directory.
|
|
1194
|
+
* @param destination - The path to write or remove.
|
|
1195
|
+
* @returns Nothing when the path is safe.
|
|
1196
|
+
* @throws Thrown when the path resolves outside the target, an ancestor is a link or not a
|
|
1197
|
+
* directory, or an ancestor's real location leaves the target's.
|
|
1198
|
+
*/
|
|
1199
|
+
export function validateWavePath(target: string, destination: string): void {
|
|
662
1200
|
const root = resolve(target)
|
|
663
1201
|
const path = resolve(destination)
|
|
664
1202
|
const location = relative(root, path)
|
|
@@ -681,19 +1219,29 @@ function validateWavePath(target: string, destination: string): void {
|
|
|
681
1219
|
}
|
|
682
1220
|
}
|
|
683
1221
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1222
|
+
/**
|
|
1223
|
+
* Reads the dirty paths out of `git status --porcelain=v1 -z` output.
|
|
1224
|
+
*
|
|
1225
|
+
* @remarks
|
|
1226
|
+
* One pass over the entries with {@link DIRTY_ENTRY}, so the cost grows with the text and no entry
|
|
1227
|
+
* recurses.
|
|
1228
|
+
*
|
|
1229
|
+
* @param text - The command's standard output.
|
|
1230
|
+
* @returns Each entry's path; a rename or copy also lists its original path.
|
|
1231
|
+
*/
|
|
1232
|
+
export function readDirtyPaths(text: string): readonly string[] {
|
|
1233
|
+
return [...text.matchAll(DIRTY_ENTRY)].flatMap((match) =>
|
|
1234
|
+
match[1] === undefined ? [match[3] ?? ''] : [match[1], match[2] ?? ''],
|
|
1235
|
+
)
|
|
694
1236
|
}
|
|
695
1237
|
|
|
696
|
-
|
|
1238
|
+
/**
|
|
1239
|
+
* Reads the receipt the overwrite step left.
|
|
1240
|
+
*
|
|
1241
|
+
* @param target - The target directory.
|
|
1242
|
+
* @returns The receipt, or undefined when it is absent, unreadable, or lacks a field in its type.
|
|
1243
|
+
*/
|
|
1244
|
+
export function readReceipt(target: string): OverwriteReceipt | undefined {
|
|
697
1245
|
try {
|
|
698
1246
|
const value = parseJsonRecord(
|
|
699
1247
|
readFileSync(join(target, 'tmp', 'units', 'wave', 'overwrite.json'), 'utf8'),
|
|
@@ -721,7 +1269,15 @@ function readReceipt(target: string): OverwriteReceipt | undefined {
|
|
|
721
1269
|
}
|
|
722
1270
|
}
|
|
723
1271
|
|
|
724
|
-
|
|
1272
|
+
/**
|
|
1273
|
+
* Runs the record step.
|
|
1274
|
+
*
|
|
1275
|
+
* @param runner - The visit's state.
|
|
1276
|
+
* @param name - The package name the manifest declares.
|
|
1277
|
+
* @returns Nothing after the rows are appended; the overwrite's paths are committed when the receipt
|
|
1278
|
+
* attributes every dirty path to them.
|
|
1279
|
+
*/
|
|
1280
|
+
export function runRecord(runner: Runner, name: string | undefined): void {
|
|
725
1281
|
if (name === SCAFFOLD) {
|
|
726
1282
|
appendStep(runner, 'record', 'git commit', `skipped: ${SCAFFOLD}`)
|
|
727
1283
|
return
|
|
@@ -811,7 +1367,7 @@ function runRecord(runner: Runner, name: string | undefined): void {
|
|
|
811
1367
|
readVerdict,
|
|
812
1368
|
`${dirty.join('\0')}\0`,
|
|
813
1369
|
)
|
|
814
|
-
if (failed(runner)) return
|
|
1370
|
+
if (failed(runner.steps)) return
|
|
815
1371
|
runStep(
|
|
816
1372
|
runner,
|
|
817
1373
|
'record',
|
|
@@ -819,7 +1375,7 @@ function runRecord(runner: Runner, name: string | undefined): void {
|
|
|
819
1375
|
['commit', '-m', `Overwrite with scaffold ${receipt.version}`],
|
|
820
1376
|
runner.target,
|
|
821
1377
|
)
|
|
822
|
-
if (failed(runner)) return
|
|
1378
|
+
if (failed(runner.steps)) return
|
|
823
1379
|
runStep(runner, 'record', 'git', ['status', '--porcelain'], runner.target, (result) =>
|
|
824
1380
|
result.status === 0 && result.stdout.trim() !== ''
|
|
825
1381
|
? { exit: 1, note: `tree remains dirty: ${result.stdout}` }
|
|
@@ -827,8 +1383,20 @@ function runRecord(runner: Runner, name: string | undefined): void {
|
|
|
827
1383
|
)
|
|
828
1384
|
}
|
|
829
1385
|
|
|
830
|
-
|
|
831
|
-
|
|
1386
|
+
/**
|
|
1387
|
+
* Moves each named dependency to the registry caret of its latest release.
|
|
1388
|
+
*
|
|
1389
|
+
* @remarks
|
|
1390
|
+
* A later release of the same name reads the range the earlier reading wrote.
|
|
1391
|
+
*
|
|
1392
|
+
* @param ranges - The block's ranges by name.
|
|
1393
|
+
* @param releases - The audit's release readings.
|
|
1394
|
+
* @returns The ranges after the moves and one message per move.
|
|
1395
|
+
*/
|
|
1396
|
+
export function computeRanges(
|
|
1397
|
+
ranges: Record<string, unknown>,
|
|
1398
|
+
releases: readonly Release[],
|
|
1399
|
+
): RangeMove {
|
|
832
1400
|
return releases.reduce<RangeMove>(
|
|
833
1401
|
(state, release) => {
|
|
834
1402
|
if (release.latest === undefined || !(release.name in state.ranges)) return state
|
|
@@ -844,7 +1412,14 @@ function computeRanges(ranges: Record<string, unknown>, releases: readonly Relea
|
|
|
844
1412
|
)
|
|
845
1413
|
}
|
|
846
1414
|
|
|
847
|
-
|
|
1415
|
+
/**
|
|
1416
|
+
* Writes the re-pinned ranges into the target's manifest.
|
|
1417
|
+
*
|
|
1418
|
+
* @param target - The target directory.
|
|
1419
|
+
* @param releases - The audit's release readings.
|
|
1420
|
+
* @returns One message per range moved; the manifest is rewritten only when a range moved.
|
|
1421
|
+
*/
|
|
1422
|
+
export function writeRanges(target: string, releases: readonly Release[]): readonly string[] {
|
|
848
1423
|
const manifest = readManifest(target)
|
|
849
1424
|
if (manifest === undefined) return []
|
|
850
1425
|
const fields = RANGE_FIELDS.flatMap((field) => {
|
|
@@ -863,7 +1438,16 @@ function writeRanges(target: string, releases: readonly Release[]): readonly str
|
|
|
863
1438
|
return moved
|
|
864
1439
|
}
|
|
865
1440
|
|
|
866
|
-
|
|
1441
|
+
/**
|
|
1442
|
+
* Runs the pin step.
|
|
1443
|
+
*
|
|
1444
|
+
* @param runner - The visit's state.
|
|
1445
|
+
* @param scaffoldFile - The scaffold executable.
|
|
1446
|
+
* @param scaffoldPrefix - The arguments that precede scaffold's own.
|
|
1447
|
+
* @param offline - The `--offline` argument, or none.
|
|
1448
|
+
* @returns Nothing after the rows are appended.
|
|
1449
|
+
*/
|
|
1450
|
+
export function runPin(
|
|
867
1451
|
runner: Runner,
|
|
868
1452
|
scaffoldFile: string,
|
|
869
1453
|
scaffoldPrefix: readonly string[],
|
|
@@ -893,7 +1477,13 @@ function runPin(
|
|
|
893
1477
|
runInstall(runner, 'pin')
|
|
894
1478
|
}
|
|
895
1479
|
|
|
896
|
-
|
|
1480
|
+
/**
|
|
1481
|
+
* Runs the commit step.
|
|
1482
|
+
*
|
|
1483
|
+
* @param runner - The visit's state.
|
|
1484
|
+
* @returns Nothing after the rows are appended.
|
|
1485
|
+
*/
|
|
1486
|
+
export function runCommit(runner: Runner): void {
|
|
897
1487
|
if (refuseLocalSpecs(runner, 'commit')) return
|
|
898
1488
|
const status = runStep(
|
|
899
1489
|
runner,
|
|
@@ -923,7 +1513,16 @@ function runCommit(runner: Runner): void {
|
|
|
923
1513
|
}
|
|
924
1514
|
}
|
|
925
1515
|
|
|
926
|
-
|
|
1516
|
+
/**
|
|
1517
|
+
* Runs the overwrite step.
|
|
1518
|
+
*
|
|
1519
|
+
* @param runner - The visit's state.
|
|
1520
|
+
* @param scaffoldFile - The scaffold executable.
|
|
1521
|
+
* @param scaffoldPrefix - The arguments that precede scaffold's own.
|
|
1522
|
+
* @param offline - The `--offline` argument, or none.
|
|
1523
|
+
* @returns Nothing after the rows are appended and the receipt is written.
|
|
1524
|
+
*/
|
|
1525
|
+
export function runOverwrite(
|
|
927
1526
|
runner: Runner,
|
|
928
1527
|
scaffoldFile: string,
|
|
929
1528
|
scaffoldPrefix: readonly string[],
|
|
@@ -937,8 +1536,8 @@ function runOverwrite(
|
|
|
937
1536
|
runner.target,
|
|
938
1537
|
(result) => judgeOverwrite(result, runner.offline),
|
|
939
1538
|
)
|
|
940
|
-
if (overwrite !== undefined && !failed(runner) && runner.directory !== undefined) {
|
|
941
|
-
const receipt =
|
|
1539
|
+
if (overwrite !== undefined && !failed(runner.steps) && runner.directory !== undefined) {
|
|
1540
|
+
const receipt = parseJsonLine(overwrite.stdout)
|
|
942
1541
|
const head = runStep(runner, 'overwrite', 'git', ['rev-parse', 'HEAD'], runner.target)
|
|
943
1542
|
const installed = readManifest(join(runner.target, 'node_modules', '@orkestrel', 'scaffold'))
|
|
944
1543
|
const version = readString(installed, 'version')
|
|
@@ -964,7 +1563,7 @@ function runOverwrite(
|
|
|
964
1563
|
}
|
|
965
1564
|
}
|
|
966
1565
|
}
|
|
967
|
-
if (!failed(runner)) {
|
|
1566
|
+
if (!failed(runner.steps)) {
|
|
968
1567
|
runStep(
|
|
969
1568
|
runner,
|
|
970
1569
|
'overwrite',
|
|
@@ -975,7 +1574,17 @@ function runOverwrite(
|
|
|
975
1574
|
}
|
|
976
1575
|
}
|
|
977
1576
|
|
|
978
|
-
|
|
1577
|
+
/**
|
|
1578
|
+
* Runs the verify step.
|
|
1579
|
+
*
|
|
1580
|
+
* @param runner - The visit's state.
|
|
1581
|
+
* @param scaffoldFile - The scaffold executable.
|
|
1582
|
+
* @param scaffoldPrefix - The arguments that precede scaffold's own.
|
|
1583
|
+
* @param offline - The `--offline` argument, or none.
|
|
1584
|
+
* @param own - If `true`, the target is scaffold's own checkout; if `false`, it is another package.
|
|
1585
|
+
* @returns Nothing after the rows are appended.
|
|
1586
|
+
*/
|
|
1587
|
+
export function runVerify(
|
|
979
1588
|
runner: Runner,
|
|
980
1589
|
scaffoldFile: string,
|
|
981
1590
|
scaffoldPrefix: readonly string[],
|
|
@@ -1009,7 +1618,15 @@ function runVerify(
|
|
|
1009
1618
|
)
|
|
1010
1619
|
}
|
|
1011
1620
|
|
|
1012
|
-
|
|
1621
|
+
/**
|
|
1622
|
+
* Runs the pins step.
|
|
1623
|
+
*
|
|
1624
|
+
* @param runner - The visit's state.
|
|
1625
|
+
* @param before - The ranges the manifest declared before the visit.
|
|
1626
|
+
* @param version - The version to sweep for, or undefined when none is known.
|
|
1627
|
+
* @returns The number of hit lines, or undefined when the sweep did not run.
|
|
1628
|
+
*/
|
|
1629
|
+
export function runPins(
|
|
1013
1630
|
runner: Runner,
|
|
1014
1631
|
before: ReadonlyMap<string, string>,
|
|
1015
1632
|
version: string | undefined,
|
|
@@ -1034,7 +1651,14 @@ function runPins(
|
|
|
1034
1651
|
: swept.stdout.split(/\r\n|\n/).filter((line) => /^\S+:\d+: /u.test(line)).length
|
|
1035
1652
|
}
|
|
1036
1653
|
|
|
1037
|
-
|
|
1654
|
+
/**
|
|
1655
|
+
* Runs the compare step.
|
|
1656
|
+
*
|
|
1657
|
+
* @param runner - The visit's state.
|
|
1658
|
+
* @param name - The package name the manifest declares.
|
|
1659
|
+
* @returns The bump ruling the two readings reach.
|
|
1660
|
+
*/
|
|
1661
|
+
export function runCompare(runner: Runner, name: string | undefined): Ruling {
|
|
1038
1662
|
const compared = runStep(runner, 'compare', process.execPath, [COMPARE, '--json'], runner.target)
|
|
1039
1663
|
const distMoved =
|
|
1040
1664
|
compared === undefined
|
|
@@ -1061,11 +1685,30 @@ function runCompare(runner: Runner, name: string | undefined): Ruling {
|
|
|
1061
1685
|
return { distMoved, rangesMoved: ranges.length > 0, ranges, hits: undefined }
|
|
1062
1686
|
}
|
|
1063
1687
|
|
|
1064
|
-
|
|
1688
|
+
/**
|
|
1689
|
+
* Words one reading of the ruling.
|
|
1690
|
+
*
|
|
1691
|
+
* @param moved - The reading, or undefined when unanswered.
|
|
1692
|
+
* @param movedWord - The word for a moved reading.
|
|
1693
|
+
* @param sameWord - The word for an unmoved reading.
|
|
1694
|
+
* @returns `unanswered`, the moved word, or the same word.
|
|
1695
|
+
*/
|
|
1696
|
+
export function describeChange(
|
|
1697
|
+
moved: boolean | undefined,
|
|
1698
|
+
movedWord: string,
|
|
1699
|
+
sameWord: string,
|
|
1700
|
+
): string {
|
|
1065
1701
|
return moved === undefined ? 'unanswered' : moved ? movedWord : sameWord
|
|
1066
1702
|
}
|
|
1067
1703
|
|
|
1068
|
-
|
|
1704
|
+
/**
|
|
1705
|
+
* Runs one repository's visit.
|
|
1706
|
+
*
|
|
1707
|
+
* @param argv - The arguments after the script path.
|
|
1708
|
+
* @returns The finished visit, or undefined when the target carries no readable manifest or no
|
|
1709
|
+
* scaffold executable.
|
|
1710
|
+
*/
|
|
1711
|
+
export function runVisit(argv: readonly string[]): Visit | undefined {
|
|
1069
1712
|
const target = readOption(argv, '--target') ?? process.cwd()
|
|
1070
1713
|
if (!existsSync(join(target, 'package.json'))) {
|
|
1071
1714
|
console.error(`wave: ${target} carries no package.json`)
|
|
@@ -1109,36 +1752,38 @@ function runVisit(argv: readonly string[]): Visit | undefined {
|
|
|
1109
1752
|
}
|
|
1110
1753
|
const before = readRanges(manifest, RANGE_FIELDS)
|
|
1111
1754
|
const offline = runner.offline ? ['--offline'] : []
|
|
1112
|
-
if (selected.has('pin') && !failed(runner))
|
|
1113
|
-
|
|
1114
|
-
if (selected.has('
|
|
1755
|
+
if (selected.has('pin') && !failed(runner.steps))
|
|
1756
|
+
runPin(runner, scaffoldFile, scaffoldPrefix, offline)
|
|
1757
|
+
if (selected.has('commit') && !failed(runner.steps)) runCommit(runner)
|
|
1758
|
+
if (selected.has('overwrite') && !failed(runner.steps)) {
|
|
1115
1759
|
if (name === SCAFFOLD) {
|
|
1116
1760
|
appendStep(
|
|
1117
1761
|
runner,
|
|
1118
1762
|
'overwrite',
|
|
1119
|
-
|
|
1763
|
+
renderCommand(scaffoldFile, [...scaffoldPrefix, 'overwrite', '--json', ...offline]),
|
|
1120
1764
|
`skipped with its audit: ${SCAFFOLD} is the package whose checkout the vendored host is staged from`,
|
|
1121
1765
|
)
|
|
1122
1766
|
} else runOverwrite(runner, scaffoldFile, scaffoldPrefix, offline)
|
|
1123
1767
|
}
|
|
1124
|
-
if (selected.has('verify') && !failed(runner)) {
|
|
1768
|
+
if (selected.has('verify') && !failed(runner.steps)) {
|
|
1125
1769
|
runVerify(runner, scaffoldFile, scaffoldPrefix, offline, name === SCAFFOLD)
|
|
1126
1770
|
}
|
|
1127
|
-
if (selected.has('install') && !failed(runner)) runInstall(runner, 'install')
|
|
1771
|
+
if (selected.has('install') && !failed(runner.steps)) runInstall(runner, 'install')
|
|
1128
1772
|
const hits =
|
|
1129
|
-
selected.has('pins') && !failed(runner)
|
|
1773
|
+
selected.has('pins') && !failed(runner.steps)
|
|
1130
1774
|
? runPins(runner, before, readOption(argv, '--prior') ?? version)
|
|
1131
1775
|
: undefined
|
|
1132
|
-
if (selected.has('format') && !failed(runner))
|
|
1133
|
-
|
|
1134
|
-
if (selected.has('
|
|
1776
|
+
if (selected.has('format') && !failed(runner.steps))
|
|
1777
|
+
runNpmStep(runner, 'format', ['run', 'format'])
|
|
1778
|
+
if (selected.has('record') && !failed(runner.steps)) runRecord(runner, name)
|
|
1779
|
+
if (selected.has('gates') && !failed(runner.steps)) {
|
|
1135
1780
|
for (const gate of GATES) {
|
|
1136
|
-
if (failed(runner)) break
|
|
1781
|
+
if (failed(runner.steps)) break
|
|
1137
1782
|
runNpmStep(runner, 'gates', ['run', gate])
|
|
1138
1783
|
}
|
|
1139
1784
|
}
|
|
1140
1785
|
const compared: Ruling =
|
|
1141
|
-
selected.has('compare') && !failed(runner)
|
|
1786
|
+
selected.has('compare') && !failed(runner.steps)
|
|
1142
1787
|
? runCompare(runner, name)
|
|
1143
1788
|
: { distMoved: undefined, rangesMoved: undefined, ranges: undefined, hits: undefined }
|
|
1144
1789
|
const ruling: Ruling = { ...compared, hits }
|
|
@@ -1148,18 +1793,25 @@ function runVisit(argv: readonly string[]): Visit | undefined {
|
|
|
1148
1793
|
: ruling.distMoved === undefined || ruling.rangesMoved === undefined
|
|
1149
1794
|
? 'unanswered'
|
|
1150
1795
|
: 'no bump'
|
|
1151
|
-
const
|
|
1796
|
+
const changes = `dist ${describeChange(ruling.distMoved, 'moved', 'same')}, ranges ${describeChange(ruling.rangesMoved, 'moved', 'same')}${hits === undefined ? '' : `, ${hits} self-pin hit(s)`}`
|
|
1152
1797
|
return {
|
|
1153
1798
|
target,
|
|
1154
1799
|
name,
|
|
1155
1800
|
version,
|
|
1156
1801
|
steps: runner.steps,
|
|
1157
1802
|
ruling,
|
|
1158
|
-
row: `| ${name ?? target} | ${version ?? '?'} | ${bump} | ${
|
|
1803
|
+
row: `| ${name ?? target} | ${version ?? '?'} | ${bump} | ${changes} | round |`,
|
|
1159
1804
|
}
|
|
1160
1805
|
}
|
|
1161
1806
|
|
|
1162
|
-
|
|
1807
|
+
/**
|
|
1808
|
+
* Chooses the directory a visit journals its steps in.
|
|
1809
|
+
*
|
|
1810
|
+
* @param target - The target directory.
|
|
1811
|
+
* @returns A new stamped directory under the target's `tmp/units/wave/` when the target is inside a
|
|
1812
|
+
* work tree that ignores `tmp/`, else undefined.
|
|
1813
|
+
*/
|
|
1814
|
+
export function readJournalDirectory(target: string): string | undefined {
|
|
1163
1815
|
const inside = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
1164
1816
|
cwd: target,
|
|
1165
1817
|
encoding: 'utf8',
|
|
@@ -1176,7 +1828,16 @@ function readJournalDirectory(target: string): string | undefined {
|
|
|
1176
1828
|
: undefined
|
|
1177
1829
|
}
|
|
1178
1830
|
|
|
1179
|
-
|
|
1831
|
+
/**
|
|
1832
|
+
* Reads the packages by layer out of the catalog table.
|
|
1833
|
+
*
|
|
1834
|
+
* @param catalogPath - The agent file that carries the marker-bounded table.
|
|
1835
|
+
* @returns Each layer's `name range` members, layers in numeric order, or undefined when the file or
|
|
1836
|
+
* its markers are absent.
|
|
1837
|
+
*/
|
|
1838
|
+
export function readLayers(
|
|
1839
|
+
catalogPath: string,
|
|
1840
|
+
): ReadonlyMap<string, readonly string[]> | undefined {
|
|
1180
1841
|
if (!existsSync(catalogPath)) return undefined
|
|
1181
1842
|
const text = readFileSync(catalogPath, 'utf8')
|
|
1182
1843
|
const start = text.indexOf('<!-- orkestrel:catalog -->')
|
|
@@ -1200,20 +1861,40 @@ function readLayers(catalogPath: string): ReadonlyMap<string, readonly string[]>
|
|
|
1200
1861
|
)
|
|
1201
1862
|
}
|
|
1202
1863
|
|
|
1203
|
-
|
|
1864
|
+
/**
|
|
1865
|
+
* Renders a visit as text lines.
|
|
1866
|
+
*
|
|
1867
|
+
* @param visit - The finished visit.
|
|
1868
|
+
* @returns One line per step, the ruling, each moved range, and the release row.
|
|
1869
|
+
*/
|
|
1870
|
+
export function renderVisit(visit: Visit): string {
|
|
1204
1871
|
const lines = [
|
|
1205
1872
|
...visit.steps.map(
|
|
1206
1873
|
(entry) =>
|
|
1207
1874
|
`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}`}`,
|
|
1208
1875
|
),
|
|
1209
|
-
`wave: ruling dist ${
|
|
1876
|
+
`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)`}`,
|
|
1210
1877
|
...(visit.ruling.ranges ?? []).map((range) => `wave: range ${range}`),
|
|
1211
1878
|
visit.row,
|
|
1212
1879
|
]
|
|
1213
1880
|
return `${lines.join('\n')}\n`
|
|
1214
1881
|
}
|
|
1215
1882
|
|
|
1216
|
-
|
|
1883
|
+
/**
|
|
1884
|
+
* Runs the mode the arguments name.
|
|
1885
|
+
*
|
|
1886
|
+
* @param argv - The arguments after the script path.
|
|
1887
|
+
* @returns The exit code: 0 when every step passed, 1 when one failed, 2 when a reading failed, 64
|
|
1888
|
+
* on usage.
|
|
1889
|
+
*/
|
|
1890
|
+
export function main(argv: readonly string[]): number {
|
|
1891
|
+
const unknown = readUnknownFlags(argv, FLAGS)
|
|
1892
|
+
if (unknown.length > 0) {
|
|
1893
|
+
console.error(
|
|
1894
|
+
`wave: unknown flag ${unknown.join(', ')}; give each flag and its value as separate arguments\n${USAGE}`,
|
|
1895
|
+
)
|
|
1896
|
+
return 64
|
|
1897
|
+
}
|
|
1217
1898
|
const missing = readMissingFlags(argv, [
|
|
1218
1899
|
'--target',
|
|
1219
1900
|
'--from',
|
|
@@ -1245,9 +1926,7 @@ function main(argv: readonly string[]): number {
|
|
|
1245
1926
|
return 0
|
|
1246
1927
|
}
|
|
1247
1928
|
if (!argv.includes('--visit')) {
|
|
1248
|
-
console.error(
|
|
1249
|
-
'usage: wave.ts --visit [--target DIR] [--offline] [--regenerate] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE] | --plan [--catalog PATH] [--json]',
|
|
1250
|
-
)
|
|
1929
|
+
console.error(USAGE)
|
|
1251
1930
|
return 64
|
|
1252
1931
|
}
|
|
1253
1932
|
const from = STEPS.indexOf(readOption(argv, '--from') ?? 'pin')
|
|
@@ -1273,16 +1952,9 @@ function main(argv: readonly string[]): number {
|
|
|
1273
1952
|
writeFileSync(out, output)
|
|
1274
1953
|
console.log(`wave: wrote ${out}`)
|
|
1275
1954
|
}
|
|
1276
|
-
return visit.steps
|
|
1277
|
-
(entry) =>
|
|
1278
|
-
entry.superseded !== true &&
|
|
1279
|
-
entry.exit !== undefined &&
|
|
1280
|
-
entry.exit !== 0 &&
|
|
1281
|
-
entry.step !== 'pins' &&
|
|
1282
|
-
entry.step !== 'compare',
|
|
1283
|
-
)
|
|
1284
|
-
? 1
|
|
1285
|
-
: 0
|
|
1955
|
+
return failed(visit.steps) ? 1 : 0
|
|
1286
1956
|
}
|
|
1287
1957
|
|
|
1288
|
-
process.
|
|
1958
|
+
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
|
|
1959
|
+
process.exitCode = main(process.argv.slice(2))
|
|
1960
|
+
}
|