@orkestrel/scaffold 0.0.103 → 0.0.104
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/helpers.js +84 -1
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +21 -6
- package/dist/agents/skills/orkestrel-harden/scripts/titles.js +338 -0
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +3 -39
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +433 -47
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +2 -1
- package/dist/bin/main.js +16 -9
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/orchestration.md +3 -3
- package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +2 -2
- package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
- package/dist/host/agents/skills/orkestrel-build/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +100 -1
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +27 -6
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +4 -4
- package/dist/host/agents/skills/orkestrel-falsify/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +8 -7
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +6 -6
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +6 -1
- package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +1700 -0
- package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -2
- package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +48 -0
- package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +4 -2
- package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +425 -0
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +9 -8
- package/dist/host/agents/skills/orkestrel-polish/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +1 -0
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +56 -13
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +6 -6
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +5 -49
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +646 -49
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +2 -1
- package/dist/host/agents/transports/codex.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +54 -53
- package/dist/host/claude/agents/reviewer.md +4 -4
- package/dist/host/claude/rules/documentation.md +2 -1
- package/dist/host/claude/rules/patterns.md +1 -1
- package/dist/host/claude/rules/quality.md +2 -2
- package/dist/host/claude/rules/styles.md +1 -1
- package/dist/host/claude/rules/tests.md +34 -8
- package/dist/host/claude/rules/typescript.md +29 -0
- package/dist/host/claude/rules/workspace.md +32 -22
- package/dist/host/claude/rules/writing.md +34 -26
- package/dist/host/claude/settings.json +2 -0
- package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +3 -3
- package/dist/host/codex/agents/reviewer.toml +4 -4
- package/dist/host/configs/policy.ts +13 -2
- package/dist/host/dotfiles/editorconfig +0 -1
- package/dist/host/dotfiles/gitattributes +0 -1
- package/dist/host/dotfiles/gitignore +0 -1
- package/dist/host/dotfiles/oxlintrc.json +24 -0
- package/dist/host/guides/agent.md +1638 -598
- package/dist/host/guides/budget.md +89 -29
- package/dist/host/guides/form.md +477 -91
- package/dist/host/guides/mcp.md +14 -2
- package/dist/host/guides/ollama.md +97 -73
- package/dist/host/guides/probe.md +1 -1
- package/dist/host/guides/scaffold.md +158 -101
- package/dist/host/guides/study.md +2635 -0
- package/dist/host/guides/table.md +225 -15
- package/dist/host/guides/terminal.md +22 -20
- package/dist/host/guides/test.md +689 -255
- package/dist/host/manifest.json +87 -63
- package/dist/host/tests/config.test.ts +136 -82
- package/dist/host/tests/policy.test.ts +39 -1
- package/dist/host/tests/setupPolicy.ts +86 -28
- package/dist/src/core/index.cjs +1387 -611
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +35 -20
- package/dist/src/core/index.d.ts +35 -20
- package/dist/src/core/index.js +1385 -612
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +12 -12
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +12 -12
- package/dist/src/server/index.d.ts +12 -12
- package/dist/src/server/index.js +12 -12
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Run one repository's release visit, or print the fleet's layer order. Run from the checkout root:
|
|
2
|
-
// node .agents/skills/orkestrel-publish/scripts/wave.ts --visit [--target DIR] [--offline] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE]
|
|
2
|
+
// node .agents/skills/orkestrel-publish/scripts/wave.ts --visit [--target DIR] [--offline] [--regenerate] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE]
|
|
3
3
|
// node .agents/skills/orkestrel-publish/scripts/wave.ts --plan [--catalog PATH] [--json]
|
|
4
4
|
// --visit runs the visit `wave.md` § Visit a repository fixes, in order, stopping at the first step
|
|
5
5
|
// that fails: pin (read `scaffold audit --json`, whose drift exit is the reading the pin closes, then
|
|
6
|
-
// re-pin every
|
|
6
|
+
// re-pin every range the audit release readings name to the registry caret and install), commit (`git commit --only` of
|
|
7
7
|
// the manifest and lockfile as the preparation commit), overwrite (`scaffold overwrite --json`, then
|
|
8
8
|
// `scaffold audit` exiting 0; offline, an exit of 1 whose note names the catalog refusal is
|
|
9
9
|
// accepted; on @orkestrel/scaffold itself both are skipped with a note, because the vendored host
|
|
10
10
|
// is staged from that checkout), verify (`scaffold audit --json` prints the releases envelope and
|
|
11
|
-
//
|
|
11
|
+
// the audit's writable dependency population matches its lookup bounds; on @orkestrel/scaffold itself an audit exit of 1 is
|
|
12
12
|
// read as the expected foreign-path findings of its own canonical sources), install,
|
|
13
13
|
// pins (the self-pin sweep with --prior or the manifest version and every range the visit moved; hits are reported, not
|
|
14
|
-
// fatal), format, gates (format:check, lint:check, check, build, test), and compare (the rebuilt
|
|
14
|
+
// fatal), format, record (commit the overwrite paths), gates (format:check, lint:check, check, build, test), and compare (the rebuilt
|
|
15
15
|
// dist against the published tarball, and the final runtime dependency set against the published
|
|
16
16
|
// manifest `npm view NAME --json` serves; both readings are reported, not fatal). The summary
|
|
17
17
|
// carries each step's exit and duration, a failed step's refusal (the `code: message` envelope or
|
|
@@ -19,14 +19,29 @@
|
|
|
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
21
|
// `.claude/agents/orkestrel.md` and prints the packages by layer. Exit 0 when every step passed, 1
|
|
22
|
-
// when a step failed, 2 when a reading failed, 64 on usage.
|
|
22
|
+
// when a step failed, 2 when a reading failed, 64 on usage, including --plan --regenerate.
|
|
23
23
|
import type { SpawnSyncReturns } from 'node:child_process'
|
|
24
24
|
import { spawnSync } from 'node:child_process'
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { createHash } from 'node:crypto'
|
|
26
|
+
import {
|
|
27
|
+
copyFileSync,
|
|
28
|
+
existsSync,
|
|
29
|
+
lstatSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
mkdirSync,
|
|
32
|
+
realpathSync,
|
|
33
|
+
renameSync,
|
|
34
|
+
unlinkSync,
|
|
35
|
+
writeFileSync,
|
|
36
|
+
} from 'node:fs'
|
|
37
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
27
38
|
import { fileURLToPath } from 'node:url'
|
|
28
39
|
import {
|
|
29
40
|
parseJsonRecord,
|
|
41
|
+
isRecord,
|
|
42
|
+
normalizeSlashes,
|
|
43
|
+
removeTree,
|
|
44
|
+
scanLocalSpecs,
|
|
30
45
|
readJsonObject,
|
|
31
46
|
readMissingFlags,
|
|
32
47
|
readOption,
|
|
@@ -42,6 +57,7 @@ const STEPS: readonly string[] = [
|
|
|
42
57
|
'install',
|
|
43
58
|
'pins',
|
|
44
59
|
'format',
|
|
60
|
+
'record',
|
|
45
61
|
'gates',
|
|
46
62
|
'compare',
|
|
47
63
|
]
|
|
@@ -71,11 +87,14 @@ interface StepResult {
|
|
|
71
87
|
readonly exit: number | undefined
|
|
72
88
|
readonly durationMs: number
|
|
73
89
|
readonly note: string | undefined
|
|
90
|
+
readonly journal?: string
|
|
91
|
+
readonly superseded?: true
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
interface Verdict {
|
|
77
95
|
readonly exit: number | undefined
|
|
78
96
|
readonly note: string | undefined
|
|
97
|
+
readonly superseded?: true
|
|
79
98
|
}
|
|
80
99
|
|
|
81
100
|
interface RangeMove {
|
|
@@ -103,9 +122,32 @@ interface Runner {
|
|
|
103
122
|
readonly target: string
|
|
104
123
|
readonly dry: boolean
|
|
105
124
|
readonly offline: boolean
|
|
125
|
+
readonly regenerate: boolean
|
|
126
|
+
readonly directory: string | undefined
|
|
106
127
|
readonly steps: StepResult[]
|
|
107
128
|
}
|
|
108
129
|
|
|
130
|
+
interface OverwriteReceipt {
|
|
131
|
+
readonly head: string
|
|
132
|
+
readonly version: string
|
|
133
|
+
readonly offline: boolean
|
|
134
|
+
readonly written: readonly string[]
|
|
135
|
+
readonly removed: readonly string[]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface Journal {
|
|
139
|
+
readonly path?: string
|
|
140
|
+
readonly failure?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface Regeneration {
|
|
144
|
+
readonly exit: number
|
|
145
|
+
readonly note: string
|
|
146
|
+
readonly installed?: SpawnSyncReturns<string>
|
|
147
|
+
readonly listed?: SpawnSyncReturns<string>
|
|
148
|
+
readonly duration?: number
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
function readManifest(target: string): Record<string, unknown> | undefined {
|
|
110
152
|
try {
|
|
111
153
|
return parseJsonRecord(readFileSync(join(target, 'package.json'), 'utf8'))
|
|
@@ -184,8 +226,8 @@ function describeFailure(result: SpawnSyncReturns<string>): string | undefined {
|
|
|
184
226
|
const note = readString(value, 'note')
|
|
185
227
|
if (note !== undefined) return note
|
|
186
228
|
return result.stderr
|
|
187
|
-
.trim()
|
|
188
229
|
.split(/\r\n|\n/)
|
|
230
|
+
.filter((line) => line.trim() !== '')
|
|
189
231
|
.slice(-3)
|
|
190
232
|
.join(' | ')
|
|
191
233
|
}
|
|
@@ -203,6 +245,7 @@ function runStep(
|
|
|
203
245
|
args: readonly string[],
|
|
204
246
|
cwd: string,
|
|
205
247
|
judge: (result: SpawnSyncReturns<string>) => Verdict = readVerdict,
|
|
248
|
+
input?: string,
|
|
206
249
|
): SpawnSyncReturns<string> | undefined {
|
|
207
250
|
const command = describeCommand(file, args)
|
|
208
251
|
if (runner.dry) {
|
|
@@ -215,10 +258,11 @@ function runStep(
|
|
|
215
258
|
encoding: 'utf8',
|
|
216
259
|
windowsHide: true,
|
|
217
260
|
maxBuffer: 64 * 1024 * 1024,
|
|
261
|
+
...(input === undefined ? {} : { input }),
|
|
218
262
|
})
|
|
219
263
|
const durationMs = Date.now() - started
|
|
220
264
|
const verdict = judge(result)
|
|
221
|
-
runner
|
|
265
|
+
appendResult(runner, step, command, result, durationMs, verdict)
|
|
222
266
|
return result
|
|
223
267
|
}
|
|
224
268
|
|
|
@@ -260,31 +304,101 @@ function runNpmStep(
|
|
|
260
304
|
runner: Runner,
|
|
261
305
|
step: string,
|
|
262
306
|
args: readonly string[],
|
|
307
|
+
judge: (result: SpawnSyncReturns<string>) => Verdict = readVerdict,
|
|
263
308
|
): SpawnSyncReturns<string> | undefined {
|
|
264
309
|
const command = `npm ${args.join(' ')}`
|
|
265
310
|
if (runner.dry) {
|
|
266
|
-
runner.steps.push({
|
|
311
|
+
runner.steps.push({
|
|
312
|
+
step,
|
|
313
|
+
command,
|
|
314
|
+
exit: undefined,
|
|
315
|
+
durationMs: 0,
|
|
316
|
+
note:
|
|
317
|
+
runner.regenerate && args[0] === 'install'
|
|
318
|
+
? 'dry run (regenerates on ERESOLVE)'
|
|
319
|
+
: 'dry run',
|
|
320
|
+
})
|
|
267
321
|
return undefined
|
|
268
322
|
}
|
|
269
323
|
const started = Date.now()
|
|
270
324
|
const result = runNpm(args, runner.target)
|
|
325
|
+
appendResult(runner, step, command, result, Date.now() - started, judge(result))
|
|
326
|
+
return result
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function writeJournal(
|
|
330
|
+
runner: Runner,
|
|
331
|
+
step: string,
|
|
332
|
+
command: string,
|
|
333
|
+
exit: number | undefined,
|
|
334
|
+
stdout: string,
|
|
335
|
+
stderr: string,
|
|
336
|
+
): Journal {
|
|
337
|
+
if (runner.directory === undefined) return {}
|
|
338
|
+
const path = join(
|
|
339
|
+
runner.directory,
|
|
340
|
+
`${String(runner.steps.length + 1).padStart(2, '0')}-${step}.log`,
|
|
341
|
+
)
|
|
342
|
+
try {
|
|
343
|
+
validateWavePath(runner.target, path)
|
|
344
|
+
mkdirSync(runner.directory, { recursive: true })
|
|
345
|
+
validateWavePath(runner.target, path)
|
|
346
|
+
writeFileSync(
|
|
347
|
+
path,
|
|
348
|
+
`command: ${command}\nexit: ${String(exit)}\nstdout:\n${stdout}\nstderr:\n${stderr}\n`,
|
|
349
|
+
)
|
|
350
|
+
return { path }
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return { failure: `journal failed: ${path}: ${String(error)}` }
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function appendResult(
|
|
357
|
+
runner: Runner,
|
|
358
|
+
step: string,
|
|
359
|
+
command: string,
|
|
360
|
+
result: SpawnSyncReturns<string>,
|
|
361
|
+
durationMs: number,
|
|
362
|
+
verdict: Verdict,
|
|
363
|
+
): void {
|
|
364
|
+
const journal = writeJournal(
|
|
365
|
+
runner,
|
|
366
|
+
step,
|
|
367
|
+
command,
|
|
368
|
+
result.status ?? undefined,
|
|
369
|
+
result.stdout ?? '',
|
|
370
|
+
result.stderr ?? '',
|
|
371
|
+
)
|
|
271
372
|
runner.steps.push({
|
|
272
373
|
step,
|
|
273
374
|
command,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
375
|
+
durationMs,
|
|
376
|
+
...(journal.path === undefined ? {} : { journal: journal.path }),
|
|
377
|
+
...(journal.failure === undefined
|
|
378
|
+
? verdict
|
|
379
|
+
: {
|
|
380
|
+
exit: 1,
|
|
381
|
+
note: [verdict.note, journal.failure].filter((value) => value !== undefined).join('; '),
|
|
382
|
+
}),
|
|
277
383
|
})
|
|
278
|
-
return result
|
|
279
384
|
}
|
|
280
385
|
|
|
281
|
-
function
|
|
282
|
-
|
|
386
|
+
function appendStep(runner: Runner, step: string, command: string, note: string, exit = 0): void {
|
|
387
|
+
const journal = runner.dry ? {} : writeJournal(runner, step, command, exit, note, '')
|
|
388
|
+
runner.steps.push({
|
|
389
|
+
step,
|
|
390
|
+
command,
|
|
391
|
+
exit: runner.dry ? undefined : journal.failure === undefined ? exit : 1,
|
|
392
|
+
durationMs: 0,
|
|
393
|
+
note: journal.failure === undefined ? note : `${note}; ${journal.failure}`,
|
|
394
|
+
...(journal.path === undefined ? {} : { journal: journal.path }),
|
|
395
|
+
})
|
|
283
396
|
}
|
|
284
397
|
|
|
285
398
|
function failed(runner: Runner): boolean {
|
|
286
399
|
return runner.steps.some(
|
|
287
400
|
(entry) =>
|
|
401
|
+
entry.superseded !== true &&
|
|
288
402
|
entry.exit !== undefined &&
|
|
289
403
|
entry.exit !== 0 &&
|
|
290
404
|
entry.step !== 'pins' &&
|
|
@@ -292,8 +406,428 @@ function failed(runner: Runner): boolean {
|
|
|
292
406
|
)
|
|
293
407
|
}
|
|
294
408
|
|
|
295
|
-
|
|
296
|
-
|
|
409
|
+
function extractConflict(result: SpawnSyncReturns<string>): string | undefined {
|
|
410
|
+
const output = `${result.stdout}\n${result.stderr}`
|
|
411
|
+
if (!/\bcode ERESOLVE\b/u.test(output)) return undefined
|
|
412
|
+
const lines = output.split(/\r\n|\n/)
|
|
413
|
+
return (
|
|
414
|
+
(
|
|
415
|
+
lines.find((line) => line.includes('Conflicting peer dependency:')) ??
|
|
416
|
+
lines.find((line) => line.includes('Could not resolve dependency:'))
|
|
417
|
+
)?.replace(/^npm (?:error|ERR!)\s*/u, '') ?? 'the install could not move a locked peer'
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function runInstall(runner: Runner, step: string): void {
|
|
422
|
+
// npm can unlink a node_modules junction before reporting its resolution failure.
|
|
423
|
+
if (
|
|
424
|
+
!runner.dry &&
|
|
425
|
+
runner.regenerate &&
|
|
426
|
+
existsSync(join(runner.target, 'node_modules')) &&
|
|
427
|
+
lstatSync(join(runner.target, 'node_modules')).isSymbolicLink()
|
|
428
|
+
) {
|
|
429
|
+
appendStep(
|
|
430
|
+
runner,
|
|
431
|
+
step,
|
|
432
|
+
'regenerate lock and modules',
|
|
433
|
+
'ERESOLVE; regeneration refused: node_modules is a link',
|
|
434
|
+
1,
|
|
435
|
+
)
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
const installed = runNpmStep(runner, step, ['install'], (result) => {
|
|
439
|
+
const conflict = extractConflict(result)
|
|
440
|
+
return conflict === undefined || result.status === 0
|
|
441
|
+
? readVerdict(result)
|
|
442
|
+
: {
|
|
443
|
+
exit: result.status ?? 1,
|
|
444
|
+
note: `ERESOLVE (${conflict}): pass --regenerate and resume with --from ${step} to replace package-lock.json and node_modules from the manifest`,
|
|
445
|
+
...(runner.regenerate ? { superseded: true } : {}),
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
if (
|
|
449
|
+
installed !== undefined &&
|
|
450
|
+
installed.status !== 0 &&
|
|
451
|
+
extractConflict(installed) !== undefined &&
|
|
452
|
+
runner.regenerate
|
|
453
|
+
)
|
|
454
|
+
regenerateLock(runner, step, installed)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function refuseLocalSpecs(runner: Runner, step: string): boolean {
|
|
458
|
+
const specs = scanLocalSpecs(readManifest(runner.target) ?? {})
|
|
459
|
+
if (specs.length === 0) return false
|
|
460
|
+
appendStep(
|
|
461
|
+
runner,
|
|
462
|
+
step,
|
|
463
|
+
'scan manifest specs',
|
|
464
|
+
`local dependency specs: ${specs.map(([path, spec]) => `${path}: ${spec}`).join(', ')}; restore the pins first`,
|
|
465
|
+
1,
|
|
466
|
+
)
|
|
467
|
+
return true
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function readRegenerationRefusal(runner: Runner): string | undefined {
|
|
471
|
+
if (
|
|
472
|
+
runStep(
|
|
473
|
+
runner,
|
|
474
|
+
'regenerate',
|
|
475
|
+
'git',
|
|
476
|
+
['ls-files', '--error-unmatch', 'package-lock.json'],
|
|
477
|
+
runner.target,
|
|
478
|
+
)?.status !== 0
|
|
479
|
+
)
|
|
480
|
+
return 'package-lock.json is untracked'
|
|
481
|
+
const status = runStep(
|
|
482
|
+
runner,
|
|
483
|
+
'regenerate',
|
|
484
|
+
'git',
|
|
485
|
+
['status', '--porcelain', '--', 'package-lock.json'],
|
|
486
|
+
runner.target,
|
|
487
|
+
)
|
|
488
|
+
if (status?.status !== 0 || status.stdout.trim() !== '')
|
|
489
|
+
return 'package-lock.json carries uncommitted changes'
|
|
490
|
+
const manifest = readManifest(runner.target)
|
|
491
|
+
const specs = scanLocalSpecs(manifest ?? {})
|
|
492
|
+
if (specs.length > 0)
|
|
493
|
+
return `local dependency specs: ${specs.map(([path, spec]) => `${path}: ${spec}`).join(', ')}`
|
|
494
|
+
if (manifest !== undefined && 'workspaces' in manifest) return 'the manifest declares workspaces'
|
|
495
|
+
if (
|
|
496
|
+
runStep(
|
|
497
|
+
runner,
|
|
498
|
+
'regenerate',
|
|
499
|
+
'git',
|
|
500
|
+
['check-ignore', '-q', 'tmp/units/wave/probe'],
|
|
501
|
+
runner.target,
|
|
502
|
+
)?.status !== 0
|
|
503
|
+
)
|
|
504
|
+
return 'tmp/ is not git-ignored'
|
|
505
|
+
const root = runStep(runner, 'regenerate', 'git', ['rev-parse', '--show-toplevel'], runner.target)
|
|
506
|
+
if (
|
|
507
|
+
root?.status !== 0 ||
|
|
508
|
+
normalizeSlashes(resolve(root.stdout.trim())) !== normalizeSlashes(resolve(runner.target))
|
|
509
|
+
)
|
|
510
|
+
return 'the target is not the root of its work tree'
|
|
511
|
+
return undefined
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function moveModules(source: string, target: string, attempt = 0): void {
|
|
515
|
+
try {
|
|
516
|
+
renameSync(source, target)
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (!isRecord(error) || (error.code !== 'EBUSY' && error.code !== 'EPERM') || attempt >= 9)
|
|
519
|
+
throw error
|
|
520
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100)
|
|
521
|
+
moveModules(source, target, attempt + 1)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function regenerateLock(runner: Runner, step: string, conflict: SpawnSyncReturns<string>): void {
|
|
526
|
+
const refusal = readRegenerationRefusal(runner)
|
|
527
|
+
if (refusal !== undefined || runner.directory === undefined) {
|
|
528
|
+
appendStep(
|
|
529
|
+
runner,
|
|
530
|
+
step,
|
|
531
|
+
'regenerate lock and modules',
|
|
532
|
+
`ERESOLVE; regeneration refused: ${refusal ?? 'tmp/ is not git-ignored'}`,
|
|
533
|
+
1,
|
|
534
|
+
)
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
const lock = join(runner.target, 'package-lock.json')
|
|
538
|
+
const modules = join(runner.target, 'node_modules')
|
|
539
|
+
const backup = join(runner.directory, 'package-lock.before.json')
|
|
540
|
+
const moved = join(runner.directory, 'node_modules.before')
|
|
541
|
+
const manifest = readFileSync(join(runner.target, 'package.json'))
|
|
542
|
+
const before = createHash('sha256').update(readFileSync(lock)).digest('hex')
|
|
543
|
+
try {
|
|
544
|
+
for (const path of [backup, moved, lock, modules]) validateWavePath(runner.target, path)
|
|
545
|
+
mkdirSync(runner.directory, { recursive: true })
|
|
546
|
+
for (const path of [backup, moved, lock, modules]) validateWavePath(runner.target, path)
|
|
547
|
+
copyFileSync(lock, backup)
|
|
548
|
+
moveModules(modules, moved)
|
|
549
|
+
} catch (error) {
|
|
550
|
+
appendStep(
|
|
551
|
+
runner,
|
|
552
|
+
step,
|
|
553
|
+
'regenerate lock and modules',
|
|
554
|
+
`ERESOLVE; regeneration refused: ${isRecord(error) ? String(error.code) : ''} in use; stop the processes that hold it; ${String(error)}`,
|
|
555
|
+
1,
|
|
556
|
+
)
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
const result = regenerateTree(runner.target, manifest, before)
|
|
560
|
+
const restored = result.exit === 0 ? undefined : restoreLock(runner.target, moved, backup, before)
|
|
561
|
+
const cleanup = result.exit === 0 ? discardBackup(runner.target, moved, backup) : undefined
|
|
562
|
+
const names = [
|
|
563
|
+
...new Set(
|
|
564
|
+
[
|
|
565
|
+
...`${conflict.stdout}\n${conflict.stderr}`.matchAll(
|
|
566
|
+
/(?:node_modules\/)((?:@[^/\s]+\/)?[^/\s]+)/gu,
|
|
567
|
+
),
|
|
568
|
+
].flatMap((match) => (match[1] === undefined ? [] : [match[1]])),
|
|
569
|
+
),
|
|
570
|
+
]
|
|
571
|
+
const versions =
|
|
572
|
+
result.exit === 0
|
|
573
|
+
? names
|
|
574
|
+
.map(
|
|
575
|
+
(name) =>
|
|
576
|
+
`${name}@${readString(readManifest(join(modules, name)), 'version') ?? 'absent'}`,
|
|
577
|
+
)
|
|
578
|
+
.join(', ')
|
|
579
|
+
: undefined
|
|
580
|
+
appendStep(
|
|
581
|
+
runner,
|
|
582
|
+
step,
|
|
583
|
+
'regenerate lock and modules',
|
|
584
|
+
[result.note, restored, versions === undefined ? undefined : `installed ${versions}`, cleanup]
|
|
585
|
+
.filter((value) => value !== undefined)
|
|
586
|
+
.join('; '),
|
|
587
|
+
cleanup === undefined ? result.exit : 1,
|
|
588
|
+
)
|
|
589
|
+
if (result.installed !== undefined)
|
|
590
|
+
appendResult(
|
|
591
|
+
runner,
|
|
592
|
+
step,
|
|
593
|
+
'npm install (regenerated)',
|
|
594
|
+
result.installed,
|
|
595
|
+
result.duration ?? 0,
|
|
596
|
+
readVerdict(result.installed),
|
|
597
|
+
)
|
|
598
|
+
if (result.listed !== undefined)
|
|
599
|
+
appendResult(runner, step, 'npm ls --all --json', result.listed, 0, readVerdict(result.listed))
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Journal writes and backup disposal cannot enter the transaction's rollback boundary.
|
|
603
|
+
function regenerateTree(target: string, manifest: Buffer, before: string): Regeneration {
|
|
604
|
+
const lock = join(target, 'package-lock.json')
|
|
605
|
+
try {
|
|
606
|
+
unlinkSync(lock)
|
|
607
|
+
const started = Date.now()
|
|
608
|
+
const installed = runNpm(['install'], target)
|
|
609
|
+
const duration = Date.now() - started
|
|
610
|
+
const listed = installed.status === 0 ? runNpm(['ls', '--all', '--json'], target) : undefined
|
|
611
|
+
const unchanged = manifest.equals(readFileSync(join(target, 'package.json')))
|
|
612
|
+
const proved = installed.status === 0 && existsSync(lock) && listed?.status === 0 && unchanged
|
|
613
|
+
const after = existsSync(lock)
|
|
614
|
+
? createHash('sha256').update(readFileSync(lock)).digest('hex')
|
|
615
|
+
: 'absent'
|
|
616
|
+
return {
|
|
617
|
+
exit: proved ? 0 : 1,
|
|
618
|
+
note: `${proved ? '' : 'ERESOLVE; '}lock SHA-256 ${before} -> ${after}${unchanged ? '' : '; manifest bytes changed'}`,
|
|
619
|
+
installed,
|
|
620
|
+
...(listed === undefined ? {} : { listed }),
|
|
621
|
+
duration,
|
|
622
|
+
}
|
|
623
|
+
} catch (error) {
|
|
624
|
+
return { exit: 1, note: `ERESOLVE; ${String(error)}` }
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function discardBackup(target: string, moved: string, backup: string): string | undefined {
|
|
629
|
+
try {
|
|
630
|
+
for (const path of [moved, backup, join(target, 'node_modules')]) validateWavePath(target, path)
|
|
631
|
+
removeTree(moved)
|
|
632
|
+
return undefined
|
|
633
|
+
} catch (error) {
|
|
634
|
+
return `backup cleanup failed: ${String(error)}; proved installation retained; backups: ${backup}; ${moved}`
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function restoreLock(target: string, moved: string, backup: string, before: string): string {
|
|
639
|
+
try {
|
|
640
|
+
for (const path of [
|
|
641
|
+
moved,
|
|
642
|
+
backup,
|
|
643
|
+
join(target, 'node_modules'),
|
|
644
|
+
join(target, 'package-lock.json'),
|
|
645
|
+
])
|
|
646
|
+
validateWavePath(target, path)
|
|
647
|
+
if (!lstatSync(moved).isDirectory() || !lstatSync(backup).isFile())
|
|
648
|
+
throw new Error('no recoverable backup')
|
|
649
|
+
const saved = readFileSync(backup)
|
|
650
|
+
if (createHash('sha256').update(saved).digest('hex') !== before)
|
|
651
|
+
throw new Error('backup lock digest changed')
|
|
652
|
+
removeTree(join(target, 'node_modules'))
|
|
653
|
+
moveModules(moved, join(target, 'node_modules'))
|
|
654
|
+
writeFileSync(join(target, 'package-lock.json'), saved)
|
|
655
|
+
return 'tree and lock restored'
|
|
656
|
+
} catch (error) {
|
|
657
|
+
return `rollback failed: ${String(error)}; backups: ${backup}; ${moved}`
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function validateWavePath(target: string, destination: string): void {
|
|
662
|
+
const root = resolve(target)
|
|
663
|
+
const path = resolve(destination)
|
|
664
|
+
const location = relative(root, path)
|
|
665
|
+
if (location === '..' || location.startsWith(`..${sep}`) || isAbsolute(location))
|
|
666
|
+
throw new Error(`unsafe wave path: ${path} resolves outside ${root}`)
|
|
667
|
+
const physical = realpathSync(root)
|
|
668
|
+
const segments = location === '' ? [] : location.split(sep)
|
|
669
|
+
for (const entry of [
|
|
670
|
+
root,
|
|
671
|
+
...segments.map((_, index) => join(root, ...segments.slice(0, index + 1))),
|
|
672
|
+
]) {
|
|
673
|
+
const status = lstatSync(entry, { throwIfNoEntry: false })
|
|
674
|
+
if (status === undefined) continue
|
|
675
|
+
if (status.isSymbolicLink()) throw new Error(`unsafe wave path: ${entry} is a link`)
|
|
676
|
+
if (entry !== path && !status.isDirectory())
|
|
677
|
+
throw new Error(`unsafe wave path: ${entry} is not a directory`)
|
|
678
|
+
const actual = relative(physical, realpathSync(entry))
|
|
679
|
+
if (actual === '..' || actual.startsWith(`..${sep}`) || isAbsolute(actual))
|
|
680
|
+
throw new Error(`unsafe wave path: ${entry} resolves outside ${physical}`)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function readDirtyPaths(text: string): readonly string[] {
|
|
685
|
+
const [entry, ...rest] = text.split('\0')
|
|
686
|
+
if (entry === undefined || entry === '') return []
|
|
687
|
+
const renamed = /[RC]/u.test(entry.slice(0, 2))
|
|
688
|
+
const original = renamed ? rest[0] : undefined
|
|
689
|
+
return [
|
|
690
|
+
entry.slice(3),
|
|
691
|
+
...(original === undefined ? [] : [original]),
|
|
692
|
+
...readDirtyPaths((renamed ? rest.slice(1) : rest).join('\0')),
|
|
693
|
+
]
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function readReceipt(target: string): OverwriteReceipt | undefined {
|
|
697
|
+
try {
|
|
698
|
+
const value = parseJsonRecord(
|
|
699
|
+
readFileSync(join(target, 'tmp', 'units', 'wave', 'overwrite.json'), 'utf8'),
|
|
700
|
+
)
|
|
701
|
+
if (
|
|
702
|
+
value === undefined ||
|
|
703
|
+
typeof value.head !== 'string' ||
|
|
704
|
+
typeof value.version !== 'string' ||
|
|
705
|
+
typeof value.offline !== 'boolean' ||
|
|
706
|
+
!Array.isArray(value.written) ||
|
|
707
|
+
!value.written.every((path) => typeof path === 'string') ||
|
|
708
|
+
!Array.isArray(value.removed) ||
|
|
709
|
+
!value.removed.every((path) => typeof path === 'string')
|
|
710
|
+
)
|
|
711
|
+
return undefined
|
|
712
|
+
return {
|
|
713
|
+
head: value.head,
|
|
714
|
+
version: value.version,
|
|
715
|
+
offline: value.offline,
|
|
716
|
+
written: value.written,
|
|
717
|
+
removed: value.removed,
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
return undefined
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function runRecord(runner: Runner, name: string | undefined): void {
|
|
725
|
+
if (name === SCAFFOLD) {
|
|
726
|
+
appendStep(runner, 'record', 'git commit', `skipped: ${SCAFFOLD}`)
|
|
727
|
+
return
|
|
728
|
+
}
|
|
729
|
+
if (refuseLocalSpecs(runner, 'record')) return
|
|
730
|
+
if (runner.dry) {
|
|
731
|
+
runStep(
|
|
732
|
+
runner,
|
|
733
|
+
'record',
|
|
734
|
+
'git',
|
|
735
|
+
['add', '-A', '--pathspec-from-file=-', '--pathspec-file-nul'],
|
|
736
|
+
runner.target,
|
|
737
|
+
)
|
|
738
|
+
runStep(
|
|
739
|
+
runner,
|
|
740
|
+
'record',
|
|
741
|
+
'git',
|
|
742
|
+
['commit', '-m', 'Overwrite with scaffold <version>'],
|
|
743
|
+
runner.target,
|
|
744
|
+
)
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
const status = runStep(
|
|
748
|
+
runner,
|
|
749
|
+
'record',
|
|
750
|
+
'git',
|
|
751
|
+
['status', '--porcelain=v1', '-z', '--untracked-files=all'],
|
|
752
|
+
runner.target,
|
|
753
|
+
)
|
|
754
|
+
if (status?.status !== 0) return
|
|
755
|
+
const dirty = [...new Set(readDirtyPaths(status.stdout))]
|
|
756
|
+
if (dirty.length === 0) {
|
|
757
|
+
appendStep(runner, 'record', 'git commit', 'nothing to commit')
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
const receipt = readReceipt(runner.target)
|
|
761
|
+
const head = runStep(runner, 'record', 'git', ['rev-parse', 'HEAD'], runner.target)
|
|
762
|
+
if (receipt === undefined || head?.status !== 0 || receipt.head !== head.stdout.trim()) {
|
|
763
|
+
appendStep(
|
|
764
|
+
runner,
|
|
765
|
+
'record',
|
|
766
|
+
'attribute overwrite paths',
|
|
767
|
+
`uncommitted changes the visit cannot attribute to an overwrite at HEAD: ${dirty.join(', ')}`,
|
|
768
|
+
1,
|
|
769
|
+
)
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
if (receipt.offline) {
|
|
773
|
+
appendStep(
|
|
774
|
+
runner,
|
|
775
|
+
'record',
|
|
776
|
+
'git commit',
|
|
777
|
+
'an offline overwrite stays uncommitted until scaffold publishes',
|
|
778
|
+
)
|
|
779
|
+
return
|
|
780
|
+
}
|
|
781
|
+
const allowed = [...receipt.written, ...receipt.removed, 'package.json', 'package-lock.json'].map(
|
|
782
|
+
(path) => normalizeSlashes(relative(runner.target, resolve(runner.target, path))),
|
|
783
|
+
)
|
|
784
|
+
const outside = dirty.filter(
|
|
785
|
+
(path) =>
|
|
786
|
+
!allowed.some(
|
|
787
|
+
(entry) =>
|
|
788
|
+
entry !== '' &&
|
|
789
|
+
entry !== '..' &&
|
|
790
|
+
!entry.startsWith('../') &&
|
|
791
|
+
!isAbsolute(entry) &&
|
|
792
|
+
path === entry,
|
|
793
|
+
),
|
|
794
|
+
)
|
|
795
|
+
if (outside.length > 0) {
|
|
796
|
+
appendStep(
|
|
797
|
+
runner,
|
|
798
|
+
'record',
|
|
799
|
+
'contain overwrite paths',
|
|
800
|
+
`dirty paths outside the overwrite: ${outside.join(', ')}`,
|
|
801
|
+
1,
|
|
802
|
+
)
|
|
803
|
+
return
|
|
804
|
+
}
|
|
805
|
+
runStep(
|
|
806
|
+
runner,
|
|
807
|
+
'record',
|
|
808
|
+
'git',
|
|
809
|
+
['--literal-pathspecs', 'add', '-A', '--pathspec-from-file=-', '--pathspec-file-nul'],
|
|
810
|
+
runner.target,
|
|
811
|
+
readVerdict,
|
|
812
|
+
`${dirty.join('\0')}\0`,
|
|
813
|
+
)
|
|
814
|
+
if (failed(runner)) return
|
|
815
|
+
runStep(
|
|
816
|
+
runner,
|
|
817
|
+
'record',
|
|
818
|
+
'git',
|
|
819
|
+
['commit', '-m', `Overwrite with scaffold ${receipt.version}`],
|
|
820
|
+
runner.target,
|
|
821
|
+
)
|
|
822
|
+
if (failed(runner)) return
|
|
823
|
+
runStep(runner, 'record', 'git', ['status', '--porcelain'], runner.target, (result) =>
|
|
824
|
+
result.status === 0 && result.stdout.trim() !== ''
|
|
825
|
+
? { exit: 1, note: `tree remains dirty: ${result.stdout}` }
|
|
826
|
+
: readVerdict(result),
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// A later release of the same name reads the range the earlier reading wrote.
|
|
297
831
|
function computeRanges(ranges: Record<string, unknown>, releases: readonly Release[]): RangeMove {
|
|
298
832
|
return releases.reduce<RangeMove>(
|
|
299
833
|
(state, release) => {
|
|
@@ -347,19 +881,20 @@ function runPin(
|
|
|
347
881
|
const releases = readReleases(audit.stdout)
|
|
348
882
|
if (releases === undefined || (audit.status !== 0 && audit.status !== 1)) return
|
|
349
883
|
const moved = writeRanges(runner.target, releases)
|
|
350
|
-
|
|
884
|
+
appendStep(
|
|
351
885
|
runner,
|
|
352
886
|
'pin',
|
|
353
|
-
'rewrite
|
|
887
|
+
'rewrite audit release ranges to the registry caret',
|
|
354
888
|
moved.length === 0 ? 'no range moved' : moved.join(', '),
|
|
355
889
|
)
|
|
356
890
|
} else {
|
|
357
|
-
|
|
891
|
+
appendStep(runner, 'pin', 'rewrite audit release ranges to the registry caret', 'dry run')
|
|
358
892
|
}
|
|
359
|
-
|
|
893
|
+
runInstall(runner, 'pin')
|
|
360
894
|
}
|
|
361
895
|
|
|
362
896
|
function runCommit(runner: Runner): void {
|
|
897
|
+
if (refuseLocalSpecs(runner, 'commit')) return
|
|
363
898
|
const status = runStep(
|
|
364
899
|
runner,
|
|
365
900
|
'commit',
|
|
@@ -384,7 +919,7 @@ function runCommit(runner: Runner): void {
|
|
|
384
919
|
runner.target,
|
|
385
920
|
)
|
|
386
921
|
} else {
|
|
387
|
-
|
|
922
|
+
appendStep(runner, 'commit', 'git commit', 'manifest and lockfile already committed')
|
|
388
923
|
}
|
|
389
924
|
}
|
|
390
925
|
|
|
@@ -394,7 +929,7 @@ function runOverwrite(
|
|
|
394
929
|
scaffoldPrefix: readonly string[],
|
|
395
930
|
offline: readonly string[],
|
|
396
931
|
): void {
|
|
397
|
-
runStep(
|
|
932
|
+
const overwrite = runStep(
|
|
398
933
|
runner,
|
|
399
934
|
'overwrite',
|
|
400
935
|
scaffoldFile,
|
|
@@ -402,6 +937,33 @@ function runOverwrite(
|
|
|
402
937
|
runner.target,
|
|
403
938
|
(result) => judgeOverwrite(result, runner.offline),
|
|
404
939
|
)
|
|
940
|
+
if (overwrite !== undefined && !failed(runner) && runner.directory !== undefined) {
|
|
941
|
+
const receipt = readJsonObject(overwrite.stdout)
|
|
942
|
+
const head = runStep(runner, 'overwrite', 'git', ['rev-parse', 'HEAD'], runner.target)
|
|
943
|
+
const installed = readManifest(join(runner.target, 'node_modules', '@orkestrel', 'scaffold'))
|
|
944
|
+
const version = readString(installed, 'version')
|
|
945
|
+
if (
|
|
946
|
+
head?.status === 0 &&
|
|
947
|
+
version !== undefined &&
|
|
948
|
+
Array.isArray(receipt?.written) &&
|
|
949
|
+
receipt.written.every((path) => typeof path === 'string') &&
|
|
950
|
+
Array.isArray(receipt.removed) &&
|
|
951
|
+
receipt.removed.every((path) => typeof path === 'string')
|
|
952
|
+
) {
|
|
953
|
+
const path = join(runner.target, 'tmp', 'units', 'wave', 'overwrite.json')
|
|
954
|
+
try {
|
|
955
|
+
validateWavePath(runner.target, path)
|
|
956
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
957
|
+
validateWavePath(runner.target, path)
|
|
958
|
+
writeFileSync(
|
|
959
|
+
path,
|
|
960
|
+
`${JSON.stringify({ head: head.stdout.trim(), version, offline: runner.offline, written: receipt.written, removed: receipt.removed })}\n`,
|
|
961
|
+
)
|
|
962
|
+
} catch (error) {
|
|
963
|
+
appendStep(runner, 'overwrite', 'write overwrite receipt', String(error), 1)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
405
967
|
if (!failed(runner)) {
|
|
406
968
|
runStep(
|
|
407
969
|
runner,
|
|
@@ -434,18 +996,17 @@ function runVerify(
|
|
|
434
996
|
const drift = releases.filter(
|
|
435
997
|
(release) => release.latest !== undefined && release.range !== `^${release.latest}`,
|
|
436
998
|
)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
drift
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
})
|
|
999
|
+
appendStep(
|
|
1000
|
+
runner,
|
|
1001
|
+
'verify',
|
|
1002
|
+
'compare every declared range with the registry',
|
|
1003
|
+
drift.length === 0
|
|
1004
|
+
? 'all declared ranges match'
|
|
1005
|
+
: drift
|
|
1006
|
+
.map((release) => `${release.name} ${release.range} vs ${String(release.latest)}`)
|
|
1007
|
+
.join(', '),
|
|
1008
|
+
drift.length === 0 ? 0 : 1,
|
|
1009
|
+
)
|
|
449
1010
|
}
|
|
450
1011
|
|
|
451
1012
|
function runPins(
|
|
@@ -458,7 +1019,7 @@ function runPins(
|
|
|
458
1019
|
.filter(([name, range]) => after.get(name) !== range)
|
|
459
1020
|
.flatMap(([, range]) => ['--range', range])
|
|
460
1021
|
if (version === undefined) {
|
|
461
|
-
|
|
1022
|
+
appendStep(runner, 'pins', basename(PINS), 'no prior version to sweep for; pass --prior')
|
|
462
1023
|
return undefined
|
|
463
1024
|
}
|
|
464
1025
|
const swept = runStep(
|
|
@@ -484,7 +1045,7 @@ function runCompare(runner: Runner, name: string | undefined): Ruling {
|
|
|
484
1045
|
? false
|
|
485
1046
|
: undefined
|
|
486
1047
|
if (name === undefined) {
|
|
487
|
-
|
|
1048
|
+
appendStep(runner, 'compare', 'npm view', 'the manifest names no package to read')
|
|
488
1049
|
return { distMoved, rangesMoved: undefined, ranges: undefined, hits: undefined }
|
|
489
1050
|
}
|
|
490
1051
|
const view = runNpmStep(runner, 'compare', ['view', name, '--json'])
|
|
@@ -517,12 +1078,6 @@ function runVisit(argv: readonly string[]): Visit | undefined {
|
|
|
517
1078
|
}
|
|
518
1079
|
const [scaffoldFile, ...scaffoldPrefix] = scaffold
|
|
519
1080
|
if (scaffoldFile === undefined) return undefined
|
|
520
|
-
const runner: Runner = {
|
|
521
|
-
target,
|
|
522
|
-
dry: argv.includes('--dry-run'),
|
|
523
|
-
offline: argv.includes('--offline'),
|
|
524
|
-
steps: [],
|
|
525
|
-
}
|
|
526
1081
|
const from = STEPS.indexOf(readOption(argv, '--from') ?? 'pin')
|
|
527
1082
|
const to = STEPS.indexOf(readOption(argv, '--to') ?? 'compare')
|
|
528
1083
|
const selected = new Set(STEPS.slice(from, to + 1))
|
|
@@ -533,13 +1088,32 @@ function runVisit(argv: readonly string[]): Visit | undefined {
|
|
|
533
1088
|
}
|
|
534
1089
|
const name = readString(manifest, 'name')
|
|
535
1090
|
const version = readString(manifest, 'version')
|
|
1091
|
+
const runner: Runner = {
|
|
1092
|
+
target: resolve(target),
|
|
1093
|
+
dry: argv.includes('--dry-run'),
|
|
1094
|
+
offline: argv.includes('--offline'),
|
|
1095
|
+
regenerate: argv.includes('--regenerate'),
|
|
1096
|
+
directory:
|
|
1097
|
+
argv.includes('--dry-run') ||
|
|
1098
|
+
(name === SCAFFOLD && from === STEPS.indexOf('record') && to === from)
|
|
1099
|
+
? undefined
|
|
1100
|
+
: readJournalDirectory(resolve(target)),
|
|
1101
|
+
steps: [],
|
|
1102
|
+
}
|
|
1103
|
+
if (runner.directory !== undefined) {
|
|
1104
|
+
try {
|
|
1105
|
+
validateWavePath(runner.target, runner.directory)
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
appendStep(runner, 'journal', 'validate wave destination', String(error), 1)
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
536
1110
|
const before = readRanges(manifest, RANGE_FIELDS)
|
|
537
1111
|
const offline = runner.offline ? ['--offline'] : []
|
|
538
|
-
if (selected.has('pin')) runPin(runner, scaffoldFile, scaffoldPrefix, offline)
|
|
1112
|
+
if (selected.has('pin') && !failed(runner)) runPin(runner, scaffoldFile, scaffoldPrefix, offline)
|
|
539
1113
|
if (selected.has('commit') && !failed(runner)) runCommit(runner)
|
|
540
1114
|
if (selected.has('overwrite') && !failed(runner)) {
|
|
541
1115
|
if (name === SCAFFOLD) {
|
|
542
|
-
|
|
1116
|
+
appendStep(
|
|
543
1117
|
runner,
|
|
544
1118
|
'overwrite',
|
|
545
1119
|
describeCommand(scaffoldFile, [...scaffoldPrefix, 'overwrite', '--json', ...offline]),
|
|
@@ -550,12 +1124,13 @@ function runVisit(argv: readonly string[]): Visit | undefined {
|
|
|
550
1124
|
if (selected.has('verify') && !failed(runner)) {
|
|
551
1125
|
runVerify(runner, scaffoldFile, scaffoldPrefix, offline, name === SCAFFOLD)
|
|
552
1126
|
}
|
|
553
|
-
if (selected.has('install') && !failed(runner))
|
|
1127
|
+
if (selected.has('install') && !failed(runner)) runInstall(runner, 'install')
|
|
554
1128
|
const hits =
|
|
555
1129
|
selected.has('pins') && !failed(runner)
|
|
556
1130
|
? runPins(runner, before, readOption(argv, '--prior') ?? version)
|
|
557
1131
|
: undefined
|
|
558
1132
|
if (selected.has('format') && !failed(runner)) runNpmStep(runner, 'format', ['run', 'format'])
|
|
1133
|
+
if (selected.has('record') && !failed(runner)) runRecord(runner, name)
|
|
559
1134
|
if (selected.has('gates') && !failed(runner)) {
|
|
560
1135
|
for (const gate of GATES) {
|
|
561
1136
|
if (failed(runner)) break
|
|
@@ -584,6 +1159,23 @@ function runVisit(argv: readonly string[]): Visit | undefined {
|
|
|
584
1159
|
}
|
|
585
1160
|
}
|
|
586
1161
|
|
|
1162
|
+
function readJournalDirectory(target: string): string | undefined {
|
|
1163
|
+
const inside = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
1164
|
+
cwd: target,
|
|
1165
|
+
encoding: 'utf8',
|
|
1166
|
+
windowsHide: true,
|
|
1167
|
+
})
|
|
1168
|
+
if (inside.status !== 0 || inside.stdout.trim() !== 'true') return undefined
|
|
1169
|
+
const ignored = spawnSync('git', ['check-ignore', '-q', 'tmp/units/wave/probe'], {
|
|
1170
|
+
cwd: target,
|
|
1171
|
+
encoding: 'utf8',
|
|
1172
|
+
windowsHide: true,
|
|
1173
|
+
})
|
|
1174
|
+
return ignored.status === 0
|
|
1175
|
+
? join(target, 'tmp', 'units', 'wave', new Date().toISOString().replace(/[:.]/gu, '-'))
|
|
1176
|
+
: undefined
|
|
1177
|
+
}
|
|
1178
|
+
|
|
587
1179
|
function readLayers(catalogPath: string): ReadonlyMap<string, readonly string[]> | undefined {
|
|
588
1180
|
if (!existsSync(catalogPath)) return undefined
|
|
589
1181
|
const text = readFileSync(catalogPath, 'utf8')
|
|
@@ -612,7 +1204,7 @@ function renderVisit(visit: Visit): string {
|
|
|
612
1204
|
const lines = [
|
|
613
1205
|
...visit.steps.map(
|
|
614
1206
|
(entry) =>
|
|
615
|
-
`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})`}`,
|
|
1207
|
+
`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}`}`,
|
|
616
1208
|
),
|
|
617
1209
|
`wave: ruling dist ${describeReading(visit.ruling.distMoved, 'moved', 'same')}, ranges ${describeReading(visit.ruling.rangesMoved, 'moved', 'same')}${visit.ruling.hits === undefined ? '' : `, ${visit.ruling.hits} self-pin hit(s)`}`,
|
|
618
1210
|
...(visit.ruling.ranges ?? []).map((range) => `wave: range ${range}`),
|
|
@@ -639,6 +1231,10 @@ function main(argv: readonly string[]): number {
|
|
|
639
1231
|
return 64
|
|
640
1232
|
}
|
|
641
1233
|
if (argv.includes('--plan')) {
|
|
1234
|
+
if (argv.includes('--regenerate')) {
|
|
1235
|
+
console.error('wave: --regenerate requires --visit, not --plan')
|
|
1236
|
+
return 64
|
|
1237
|
+
}
|
|
642
1238
|
const layers = readLayers(readOption(argv, '--catalog') ?? CATALOG)
|
|
643
1239
|
if (layers === undefined) {
|
|
644
1240
|
console.error('wave: no marker-bounded catalog table; run scaffold catalog first')
|
|
@@ -650,7 +1246,7 @@ function main(argv: readonly string[]): number {
|
|
|
650
1246
|
}
|
|
651
1247
|
if (!argv.includes('--visit')) {
|
|
652
1248
|
console.error(
|
|
653
|
-
'usage: wave.ts --visit [--target DIR] [--offline] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE] | --plan [--catalog PATH] [--json]',
|
|
1249
|
+
'usage: wave.ts --visit [--target DIR] [--offline] [--regenerate] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE] | --plan [--catalog PATH] [--json]',
|
|
654
1250
|
)
|
|
655
1251
|
return 64
|
|
656
1252
|
}
|
|
@@ -679,6 +1275,7 @@ function main(argv: readonly string[]): number {
|
|
|
679
1275
|
}
|
|
680
1276
|
return visit.steps.some(
|
|
681
1277
|
(entry) =>
|
|
1278
|
+
entry.superseded !== true &&
|
|
682
1279
|
entry.exit !== undefined &&
|
|
683
1280
|
entry.exit !== 0 &&
|
|
684
1281
|
entry.step !== 'pins' &&
|