@orkestrel/scaffold 0.0.77 → 0.0.79
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 +204 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +102 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +95 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +207 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +108 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +114 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +108 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +156 -0
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +196 -0
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +206 -0
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +93 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +458 -0
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +188 -0
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +300 -0
- package/dist/agents/templates/brief.md +55 -0
- package/dist/bin/main.js +58 -6
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +77 -135
- package/dist/host/agents/orchestration.md +147 -998
- package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +2 -2
- package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
- package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +6 -13
- package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/fleet.md +5 -7
- package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +11 -22
- package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +8 -16
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
- package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +13 -13
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +61 -0
- package/dist/host/agents/skills/orkestrel-dispatch/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +25 -0
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +32 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +259 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +110 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +115 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +239 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +124 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +123 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +129 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +157 -0
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +42 -193
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +38 -108
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +35 -134
- package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +10 -14
- package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/hardening.md +3 -4
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +228 -0
- package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +15 -23
- package/dist/host/agents/skills/orkestrel-journey/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/captures.md +1 -1
- package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/SKILL.md +25 -33
- package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/references/capture-harness.md +3 -3
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +33 -20
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +39 -0
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +22 -21
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +27 -14
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +220 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +114 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +629 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +242 -0
- package/dist/host/agents/skills/orkestrel-scout/SKILL.md +28 -0
- package/dist/host/agents/skills/orkestrel-scout/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +352 -0
- package/dist/host/agents/templates/brief.md +21 -142
- package/dist/host/agents/transports/claude-cli.md +21 -0
- package/dist/host/agents/transports/codex.md +38 -159
- package/dist/host/agents/transports/cursor.md +16 -65
- package/dist/host/claude/AGENTS.md +38 -0
- package/dist/host/claude/agents/analyst.md +14 -53
- package/dist/host/claude/agents/astra.md +26 -0
- package/dist/host/claude/agents/builder.md +14 -30
- package/dist/host/claude/agents/checker.md +13 -57
- package/dist/host/claude/agents/distiller.md +11 -26
- package/dist/host/claude/agents/grok.md +12 -35
- package/dist/host/claude/agents/opus.md +14 -30
- package/dist/host/claude/agents/orkestrel.md +4 -4
- package/dist/host/claude/agents/planner.md +10 -44
- package/dist/host/claude/agents/researcher.md +11 -30
- package/dist/host/claude/agents/reviewer.md +11 -95
- package/dist/host/claude/agents/scout.md +9 -23
- package/dist/host/claude/agents/verifier.md +15 -33
- package/dist/host/claude/rules/documentation.md +8 -2
- package/dist/host/claude/rules/portability.md +7 -1
- package/dist/host/claude/rules/quality.md +36 -96
- package/dist/host/claude/rules/styles.md +3 -0
- package/dist/host/claude/rules/tests.md +6 -3
- package/dist/host/claude/rules/workspace.md +21 -15
- package/dist/host/claude/rules/writing.md +57 -108
- package/dist/host/claude/settings.json +5 -3
- package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +1 -1
- package/dist/host/claude/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +2 -2
- package/dist/host/claude/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +2 -2
- package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +11 -0
- package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +2 -1
- package/dist/host/claude/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +2 -2
- package/dist/host/claude/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +2 -2
- package/dist/host/claude/skills/orkestrel-polish/SKILL.md +12 -0
- package/dist/host/claude/skills/orkestrel-scout/SKILL.md +11 -0
- package/dist/host/codex/agents/analyst.toml +14 -31
- package/dist/host/codex/agents/astra.toml +25 -0
- package/dist/host/codex/agents/builder.toml +13 -20
- package/dist/host/codex/agents/checker.toml +13 -27
- package/dist/host/codex/agents/distiller.toml +9 -22
- package/dist/host/codex/agents/grok.toml +11 -30
- package/dist/host/codex/agents/opus.toml +14 -22
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/codex/agents/planner.toml +11 -28
- package/dist/host/codex/agents/researcher.toml +10 -22
- package/dist/host/codex/agents/reviewer.toml +11 -27
- package/dist/host/codex/agents/scout.toml +11 -17
- package/dist/host/codex/agents/verifier.toml +16 -12
- package/dist/host/codex/config.toml +18 -21
- package/dist/host/cursor/mcp.json +0 -4
- package/dist/host/cursor/rules/orchestration.mdc +12 -20
- package/dist/host/dotfiles/mcp.json +0 -4
- package/dist/host/dotfiles/oxlintrc.json +7 -0
- package/dist/host/guides/probe.md +18 -14
- package/dist/host/guides/scaffold.md +147 -83
- package/dist/host/guides/test.md +442 -148
- package/dist/host/manifest.json +322 -185
- package/dist/host/scripts/codex.sh +0 -0
- package/dist/host/scripts/cursor.sh +0 -0
- package/dist/host/scripts/deps.sh +0 -0
- package/dist/host/scripts/ollama.sh +0 -0
- package/dist/host/tests/config.test.ts +86 -55
- package/dist/host/tests/policy.test.ts +1 -5
- package/dist/host/tests/setupPolicy.ts +179 -4
- package/dist/src/core/index.cjs +264 -89
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +95 -30
- package/dist/src/core/index.d.ts +95 -30
- package/dist/src/core/index.js +262 -90
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +55 -9
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +29 -4
- package/dist/src/server/index.d.ts +29 -4
- package/dist/src/server/index.js +56 -11
- package/dist/src/server/index.js.map +1 -1
- package/package.json +16 -12
- package/dist/host/CLAUDE.md +0 -61
- package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
- package/dist/host/agents/transports/claude.md +0 -49
- package/dist/host/claude/agents/application.md +0 -36
- package/dist/host/claude/agents/sol.md +0 -61
- package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
- package/dist/host/codex/agents/application.toml +0 -25
- package/dist/host/codex/agents/sol.toml +0 -19
- /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/styles.md +0 -0
|
@@ -0,0 +1,629 @@
|
|
|
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]
|
|
3
|
+
// node .agents/skills/orkestrel-publish/scripts/wave.ts --plan [--catalog PATH] [--json]
|
|
4
|
+
// --visit runs the visit `wave.md` § Visit a repository fixes, in order, stopping at the first step
|
|
5
|
+
// that fails: pin (read `scaffold audit --json`, whose drift exit is the reading the pin closes, then
|
|
6
|
+
// re-pin every @orkestrel range to the registry caret and install), commit (`git commit --only` of
|
|
7
|
+
// the manifest and lockfile as the preparation commit), overwrite (`scaffold overwrite --json`, then
|
|
8
|
+
// `scaffold audit` exiting 0; offline, an exit of 1 whose note names the catalog refusal is
|
|
9
|
+
// accepted), verify (every declared range matches the registry), install, pins (the self-pin sweep
|
|
10
|
+
// with --prior or the manifest version and every range the visit moved; hits are reported, not
|
|
11
|
+
// fatal), format, gates (format:check, lint:check, check, build, test), and compare (the rebuilt
|
|
12
|
+
// dist against the published tarball, and the final runtime dependency set against the published
|
|
13
|
+
// manifest `npm view NAME --json` serves; both readings are reported, not fatal). The summary
|
|
14
|
+
// carries each step's exit and duration, the bump ruling, and a Markdown row for
|
|
15
|
+
// `.orkestrel/release.md`. --dry-run prints the commands and runs nothing, so its ruling is
|
|
16
|
+
// unanswered. --plan reads the marker-bounded catalog table in `.claude/agents/orkestrel.md` and
|
|
17
|
+
// prints the packages by layer. Exit 0 when every step passed, 1 when a step failed, 2 when a
|
|
18
|
+
// reading failed, 64 on usage.
|
|
19
|
+
import type { SpawnSyncReturns } from 'node:child_process'
|
|
20
|
+
import { spawnSync } from 'node:child_process'
|
|
21
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
22
|
+
import { basename, dirname, extname, join } from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
import {
|
|
25
|
+
parseJsonRecord,
|
|
26
|
+
readJsonObject,
|
|
27
|
+
readMissingFlags,
|
|
28
|
+
readOption,
|
|
29
|
+
readString,
|
|
30
|
+
runNpm,
|
|
31
|
+
} from '../../orkestrel-dispatch/scripts/helpers.ts'
|
|
32
|
+
|
|
33
|
+
const STEPS: readonly string[] = [
|
|
34
|
+
'pin',
|
|
35
|
+
'commit',
|
|
36
|
+
'overwrite',
|
|
37
|
+
'verify',
|
|
38
|
+
'install',
|
|
39
|
+
'pins',
|
|
40
|
+
'format',
|
|
41
|
+
'gates',
|
|
42
|
+
'compare',
|
|
43
|
+
]
|
|
44
|
+
const GATES: readonly string[] = ['format:check', 'lint:check', 'check', 'build', 'test']
|
|
45
|
+
const RUNTIME_FIELDS: readonly string[] = ['dependencies', 'peerDependencies']
|
|
46
|
+
const RANGE_FIELDS: readonly string[] = [...RUNTIME_FIELDS, 'devDependencies']
|
|
47
|
+
const CATALOG = '.claude/agents/orkestrel.md'
|
|
48
|
+
const OFFLINE_REFUSAL = "'catalog' does not take --offline"
|
|
49
|
+
const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u
|
|
50
|
+
// The checkout runs the `.ts` sources and a built twin under `dist/agents` runs `.js` siblings.
|
|
51
|
+
const EXTENSION = extname(fileURLToPath(import.meta.url))
|
|
52
|
+
const COMPARE = fileURLToPath(new URL(`./compare${EXTENSION}`, import.meta.url))
|
|
53
|
+
const PINS = fileURLToPath(new URL(`./pins${EXTENSION}`, import.meta.url))
|
|
54
|
+
|
|
55
|
+
interface Release {
|
|
56
|
+
readonly name: string
|
|
57
|
+
readonly range: string
|
|
58
|
+
readonly latest: string | undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface StepResult {
|
|
62
|
+
readonly step: string
|
|
63
|
+
readonly command: string
|
|
64
|
+
readonly exit: number | undefined
|
|
65
|
+
readonly durationMs: number
|
|
66
|
+
readonly note: string | undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface Ruling {
|
|
70
|
+
readonly distMoved: boolean | undefined
|
|
71
|
+
readonly rangesMoved: boolean | undefined
|
|
72
|
+
readonly ranges: readonly string[] | undefined
|
|
73
|
+
readonly hits: number | undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface Visit {
|
|
77
|
+
readonly target: string
|
|
78
|
+
readonly name: string | undefined
|
|
79
|
+
readonly version: string | undefined
|
|
80
|
+
readonly steps: readonly StepResult[]
|
|
81
|
+
readonly ruling: Ruling
|
|
82
|
+
readonly row: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface Runner {
|
|
86
|
+
readonly target: string
|
|
87
|
+
readonly dry: boolean
|
|
88
|
+
readonly offline: boolean
|
|
89
|
+
readonly steps: StepResult[]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readManifest(target: string): Record<string, unknown> | undefined {
|
|
93
|
+
try {
|
|
94
|
+
return parseJsonRecord(readFileSync(join(target, 'package.json'), 'utf8'))
|
|
95
|
+
} catch {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readRanges(
|
|
101
|
+
manifest: Record<string, unknown>,
|
|
102
|
+
fields: readonly string[],
|
|
103
|
+
): ReadonlyMap<string, string> {
|
|
104
|
+
const ranges = new Map<string, string>()
|
|
105
|
+
for (const field of fields) {
|
|
106
|
+
const block = manifest[field]
|
|
107
|
+
if (typeof block !== 'object' || block === null) continue
|
|
108
|
+
for (const [name, range] of Object.entries(block)) {
|
|
109
|
+
if (typeof range === 'string') ranges.set(`${field} ${name}`, range)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return ranges
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function compareRanges(
|
|
116
|
+
published: ReadonlyMap<string, string>,
|
|
117
|
+
final: ReadonlyMap<string, string>,
|
|
118
|
+
): readonly string[] {
|
|
119
|
+
const names = [...new Set([...published.keys(), ...final.keys()])].sort()
|
|
120
|
+
return names.flatMap((name) => {
|
|
121
|
+
const before = published.get(name)
|
|
122
|
+
const after = final.get(name)
|
|
123
|
+
if (before === after) return []
|
|
124
|
+
if (before === undefined) return [`${name} ${String(after)} added`]
|
|
125
|
+
if (after === undefined) return [`${name} ${before} removed`]
|
|
126
|
+
return [`${name} ${before} -> ${after}`]
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveScaffold(target: string): readonly string[] {
|
|
131
|
+
const installed = join(target, 'node_modules', '@orkestrel', 'scaffold', 'dist', 'bin', 'main.js')
|
|
132
|
+
if (existsSync(installed)) return [process.execPath, installed]
|
|
133
|
+
const own = join(target, 'dist', 'bin', 'main.js')
|
|
134
|
+
if (existsSync(own)) return [process.execPath, own]
|
|
135
|
+
return []
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readReleases(text: string): readonly Release[] | undefined {
|
|
139
|
+
const releases = readJsonObject(text)?.releases
|
|
140
|
+
if (!Array.isArray(releases)) return undefined
|
|
141
|
+
return releases.flatMap((entry) => {
|
|
142
|
+
if (typeof entry !== 'object' || entry === null) return []
|
|
143
|
+
const record = Object.fromEntries(Object.entries(entry))
|
|
144
|
+
const name = readString(record, 'name')
|
|
145
|
+
const range = readString(record, 'range')
|
|
146
|
+
if (name === undefined || range === undefined) return []
|
|
147
|
+
return [{ name, range, latest: readString(record, 'latest') }]
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function describeCommand(file: string, args: readonly string[]): string {
|
|
152
|
+
return [file === process.execPath ? 'node' : file, ...args].join(' ')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function describeFailure(result: SpawnSyncReturns<string>): string | undefined {
|
|
156
|
+
if (result.status === 0) return undefined
|
|
157
|
+
return result.stderr
|
|
158
|
+
.trim()
|
|
159
|
+
.split(/\r\n|\n/)
|
|
160
|
+
.slice(-3)
|
|
161
|
+
.join(' | ')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function runStep(
|
|
165
|
+
runner: Runner,
|
|
166
|
+
step: string,
|
|
167
|
+
file: string,
|
|
168
|
+
args: readonly string[],
|
|
169
|
+
cwd: string,
|
|
170
|
+
): SpawnSyncReturns<string> | undefined {
|
|
171
|
+
const command = describeCommand(file, args)
|
|
172
|
+
if (runner.dry) {
|
|
173
|
+
runner.steps.push({ step, command, exit: undefined, durationMs: 0, note: 'dry run' })
|
|
174
|
+
return undefined
|
|
175
|
+
}
|
|
176
|
+
const started = Date.now()
|
|
177
|
+
const result = spawnSync(file, args, {
|
|
178
|
+
cwd,
|
|
179
|
+
encoding: 'utf8',
|
|
180
|
+
windowsHide: true,
|
|
181
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
182
|
+
})
|
|
183
|
+
runner.steps.push({
|
|
184
|
+
step,
|
|
185
|
+
command,
|
|
186
|
+
exit: result.status ?? undefined,
|
|
187
|
+
durationMs: Date.now() - started,
|
|
188
|
+
note: describeFailure(result),
|
|
189
|
+
})
|
|
190
|
+
return result
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function runNpmStep(
|
|
194
|
+
runner: Runner,
|
|
195
|
+
step: string,
|
|
196
|
+
args: readonly string[],
|
|
197
|
+
): SpawnSyncReturns<string> | undefined {
|
|
198
|
+
const command = `npm ${args.join(' ')}`
|
|
199
|
+
if (runner.dry) {
|
|
200
|
+
runner.steps.push({ step, command, exit: undefined, durationMs: 0, note: 'dry run' })
|
|
201
|
+
return undefined
|
|
202
|
+
}
|
|
203
|
+
const started = Date.now()
|
|
204
|
+
const result = runNpm(args, runner.target)
|
|
205
|
+
runner.steps.push({
|
|
206
|
+
step,
|
|
207
|
+
command,
|
|
208
|
+
exit: result.status ?? undefined,
|
|
209
|
+
durationMs: Date.now() - started,
|
|
210
|
+
note: describeFailure(result),
|
|
211
|
+
})
|
|
212
|
+
return result
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function recordStep(runner: Runner, step: string, command: string, note: string): void {
|
|
216
|
+
runner.steps.push({ step, command, exit: runner.dry ? undefined : 0, durationMs: 0, note })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function replaceLastStep(runner: Runner, exit: number | undefined, note: string | undefined): void {
|
|
220
|
+
const last = runner.steps.pop()
|
|
221
|
+
if (last !== undefined) runner.steps.push({ ...last, exit, note })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function failed(runner: Runner): boolean {
|
|
225
|
+
return runner.steps.some(
|
|
226
|
+
(entry) =>
|
|
227
|
+
entry.exit !== undefined &&
|
|
228
|
+
entry.exit !== 0 &&
|
|
229
|
+
entry.step !== 'pins' &&
|
|
230
|
+
entry.step !== 'compare',
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function writeRanges(target: string, releases: readonly Release[]): readonly string[] {
|
|
235
|
+
const manifest = readManifest(target)
|
|
236
|
+
if (manifest === undefined) return []
|
|
237
|
+
const moved: string[] = []
|
|
238
|
+
for (const field of RANGE_FIELDS) {
|
|
239
|
+
const block = manifest[field]
|
|
240
|
+
if (typeof block !== 'object' || block === null) continue
|
|
241
|
+
const ranges = Object.fromEntries(Object.entries(block))
|
|
242
|
+
for (const release of releases) {
|
|
243
|
+
if (release.latest === undefined || !(release.name in ranges)) continue
|
|
244
|
+
const caret = `^${release.latest}`
|
|
245
|
+
if (ranges[release.name] !== caret) {
|
|
246
|
+
ranges[release.name] = caret
|
|
247
|
+
moved.push(`${release.name} ${String(release.range)} -> ${caret}`)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
manifest[field] = ranges
|
|
251
|
+
}
|
|
252
|
+
if (moved.length > 0)
|
|
253
|
+
writeFileSync(join(target, 'package.json'), `${JSON.stringify(manifest, null, '\t')}\n`)
|
|
254
|
+
return moved
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function runPin(
|
|
258
|
+
runner: Runner,
|
|
259
|
+
scaffoldFile: string,
|
|
260
|
+
scaffoldPrefix: readonly string[],
|
|
261
|
+
offline: readonly string[],
|
|
262
|
+
): void {
|
|
263
|
+
const audit = runStep(
|
|
264
|
+
runner,
|
|
265
|
+
'pin',
|
|
266
|
+
scaffoldFile,
|
|
267
|
+
[...scaffoldPrefix, 'audit', '--json', ...offline],
|
|
268
|
+
runner.target,
|
|
269
|
+
)
|
|
270
|
+
if (audit !== undefined) {
|
|
271
|
+
const releases = readReleases(audit.stdout)
|
|
272
|
+
if (releases === undefined || (audit.status !== 0 && audit.status !== 1)) {
|
|
273
|
+
replaceLastStep(
|
|
274
|
+
runner,
|
|
275
|
+
audit.status === null || audit.status === 0 ? 2 : audit.status,
|
|
276
|
+
releases === undefined ? 'the audit printed no releases envelope' : describeFailure(audit),
|
|
277
|
+
)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
// The audit exits 1 on the drift this step exists to close, so its exit is a reading here.
|
|
281
|
+
replaceLastStep(runner, 0, `${releases.length} release reading(s)`)
|
|
282
|
+
const moved = writeRanges(runner.target, releases)
|
|
283
|
+
recordStep(
|
|
284
|
+
runner,
|
|
285
|
+
'pin',
|
|
286
|
+
'rewrite @orkestrel ranges to the registry caret',
|
|
287
|
+
moved.length === 0 ? 'no range moved' : moved.join(', '),
|
|
288
|
+
)
|
|
289
|
+
} else {
|
|
290
|
+
recordStep(runner, 'pin', 'rewrite @orkestrel ranges to the registry caret', 'dry run')
|
|
291
|
+
}
|
|
292
|
+
runNpmStep(runner, 'pin', ['install'])
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function runCommit(runner: Runner): void {
|
|
296
|
+
const status = runStep(
|
|
297
|
+
runner,
|
|
298
|
+
'commit',
|
|
299
|
+
'git',
|
|
300
|
+
['status', '--porcelain', '--', 'package.json', 'package-lock.json'],
|
|
301
|
+
runner.target,
|
|
302
|
+
)
|
|
303
|
+
if (status === undefined || status.stdout.trim() !== '') {
|
|
304
|
+
runStep(
|
|
305
|
+
runner,
|
|
306
|
+
'commit',
|
|
307
|
+
'git',
|
|
308
|
+
[
|
|
309
|
+
'commit',
|
|
310
|
+
'--only',
|
|
311
|
+
'-m',
|
|
312
|
+
'Re-pin the @orkestrel ranges for the release visit',
|
|
313
|
+
'--',
|
|
314
|
+
'package.json',
|
|
315
|
+
'package-lock.json',
|
|
316
|
+
],
|
|
317
|
+
runner.target,
|
|
318
|
+
)
|
|
319
|
+
} else {
|
|
320
|
+
recordStep(runner, 'commit', 'git commit', 'manifest and lockfile already committed')
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function runOverwrite(
|
|
325
|
+
runner: Runner,
|
|
326
|
+
scaffoldFile: string,
|
|
327
|
+
scaffoldPrefix: readonly string[],
|
|
328
|
+
offline: readonly string[],
|
|
329
|
+
): void {
|
|
330
|
+
const written = runStep(
|
|
331
|
+
runner,
|
|
332
|
+
'overwrite',
|
|
333
|
+
scaffoldFile,
|
|
334
|
+
[...scaffoldPrefix, 'overwrite', '--json', ...offline],
|
|
335
|
+
runner.target,
|
|
336
|
+
)
|
|
337
|
+
if (runner.offline && written !== undefined && written.status === 1) {
|
|
338
|
+
const note = readString(readJsonObject(written.stdout) ?? {}, 'note')
|
|
339
|
+
if (note !== undefined && note.includes(OFFLINE_REFUSAL)) {
|
|
340
|
+
replaceLastStep(runner, 0, 'offline overwrite skipped the catalog step by design')
|
|
341
|
+
} else {
|
|
342
|
+
replaceLastStep(runner, 1, note ?? describeFailure(written))
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (!failed(runner)) {
|
|
346
|
+
runStep(
|
|
347
|
+
runner,
|
|
348
|
+
'overwrite',
|
|
349
|
+
scaffoldFile,
|
|
350
|
+
[...scaffoldPrefix, 'audit', ...offline],
|
|
351
|
+
runner.target,
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function runVerify(
|
|
357
|
+
runner: Runner,
|
|
358
|
+
scaffoldFile: string,
|
|
359
|
+
scaffoldPrefix: readonly string[],
|
|
360
|
+
offline: readonly string[],
|
|
361
|
+
): void {
|
|
362
|
+
const audit = runStep(
|
|
363
|
+
runner,
|
|
364
|
+
'verify',
|
|
365
|
+
scaffoldFile,
|
|
366
|
+
[...scaffoldPrefix, 'audit', '--json', ...offline],
|
|
367
|
+
runner.target,
|
|
368
|
+
)
|
|
369
|
+
if (audit === undefined) return
|
|
370
|
+
const releases = readReleases(audit.stdout)
|
|
371
|
+
if (releases === undefined) {
|
|
372
|
+
replaceLastStep(
|
|
373
|
+
runner,
|
|
374
|
+
audit.status === null || audit.status === 0 ? 2 : audit.status,
|
|
375
|
+
'the audit printed no releases envelope',
|
|
376
|
+
)
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
const drift = releases.filter(
|
|
380
|
+
(release) => release.latest !== undefined && release.range !== `^${release.latest}`,
|
|
381
|
+
)
|
|
382
|
+
runner.steps.push({
|
|
383
|
+
step: 'verify',
|
|
384
|
+
command: 'compare every declared range with the registry',
|
|
385
|
+
exit: drift.length === 0 ? 0 : 1,
|
|
386
|
+
durationMs: 0,
|
|
387
|
+
note:
|
|
388
|
+
drift.length === 0
|
|
389
|
+
? undefined
|
|
390
|
+
: drift
|
|
391
|
+
.map((release) => `${release.name} ${release.range} vs ${String(release.latest)}`)
|
|
392
|
+
.join(', '),
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function runPins(
|
|
397
|
+
runner: Runner,
|
|
398
|
+
before: ReadonlyMap<string, string>,
|
|
399
|
+
version: string | undefined,
|
|
400
|
+
): number | undefined {
|
|
401
|
+
const after = readRanges(readManifest(runner.target) ?? {}, RANGE_FIELDS)
|
|
402
|
+
const prior = [...before]
|
|
403
|
+
.filter(([name, range]) => after.get(name) !== range)
|
|
404
|
+
.flatMap(([, range]) => ['--range', range])
|
|
405
|
+
if (version === undefined) {
|
|
406
|
+
recordStep(runner, 'pins', basename(PINS), 'no prior version to sweep for; pass --prior')
|
|
407
|
+
return undefined
|
|
408
|
+
}
|
|
409
|
+
const swept = runStep(
|
|
410
|
+
runner,
|
|
411
|
+
'pins',
|
|
412
|
+
process.execPath,
|
|
413
|
+
[PINS, '--version', version, ...prior],
|
|
414
|
+
runner.target,
|
|
415
|
+
)
|
|
416
|
+
return swept === undefined
|
|
417
|
+
? undefined
|
|
418
|
+
: swept.stdout.split(/\r\n|\n/).filter((line) => /^\S+:\d+: /u.test(line)).length
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function runCompare(runner: Runner, name: string | undefined): Ruling {
|
|
422
|
+
const compared = runStep(runner, 'compare', process.execPath, [COMPARE, '--json'], runner.target)
|
|
423
|
+
const distMoved =
|
|
424
|
+
compared === undefined
|
|
425
|
+
? undefined
|
|
426
|
+
: compared.status === 3
|
|
427
|
+
? true
|
|
428
|
+
: compared.status === 0
|
|
429
|
+
? false
|
|
430
|
+
: undefined
|
|
431
|
+
if (name === undefined) {
|
|
432
|
+
recordStep(runner, 'compare', 'npm view', 'the manifest names no package to read')
|
|
433
|
+
return { distMoved, rangesMoved: undefined, ranges: undefined, hits: undefined }
|
|
434
|
+
}
|
|
435
|
+
const view = runNpmStep(runner, 'compare', ['view', name, '--json'])
|
|
436
|
+
const published =
|
|
437
|
+
view === undefined || view.status !== 0 ? undefined : parseJsonRecord(view.stdout)
|
|
438
|
+
if (published === undefined) {
|
|
439
|
+
return { distMoved, rangesMoved: undefined, ranges: undefined, hits: undefined }
|
|
440
|
+
}
|
|
441
|
+
const ranges = compareRanges(
|
|
442
|
+
readRanges(published, RUNTIME_FIELDS),
|
|
443
|
+
readRanges(readManifest(runner.target) ?? {}, RUNTIME_FIELDS),
|
|
444
|
+
)
|
|
445
|
+
return { distMoved, rangesMoved: ranges.length > 0, ranges, hits: undefined }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function describeReading(moved: boolean | undefined, movedWord: string, sameWord: string): string {
|
|
449
|
+
return moved === undefined ? 'unanswered' : moved ? movedWord : sameWord
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function runVisit(argv: readonly string[]): Visit | undefined {
|
|
453
|
+
const target = readOption(argv, '--target') ?? process.cwd()
|
|
454
|
+
if (!existsSync(join(target, 'package.json'))) {
|
|
455
|
+
console.error(`wave: ${target} carries no package.json`)
|
|
456
|
+
return undefined
|
|
457
|
+
}
|
|
458
|
+
const scaffold = resolveScaffold(target)
|
|
459
|
+
if (scaffold.length === 0) {
|
|
460
|
+
console.error(`wave: no scaffold executable under ${target}; run npm install first`)
|
|
461
|
+
return undefined
|
|
462
|
+
}
|
|
463
|
+
const [scaffoldFile, ...scaffoldPrefix] = scaffold
|
|
464
|
+
if (scaffoldFile === undefined) return undefined
|
|
465
|
+
const runner: Runner = {
|
|
466
|
+
target,
|
|
467
|
+
dry: argv.includes('--dry-run'),
|
|
468
|
+
offline: argv.includes('--offline'),
|
|
469
|
+
steps: [],
|
|
470
|
+
}
|
|
471
|
+
const from = STEPS.indexOf(readOption(argv, '--from') ?? 'pin')
|
|
472
|
+
const to = STEPS.indexOf(readOption(argv, '--to') ?? 'compare')
|
|
473
|
+
const selected = new Set(STEPS.slice(from, to + 1))
|
|
474
|
+
const manifest = readManifest(target)
|
|
475
|
+
if (manifest === undefined) {
|
|
476
|
+
console.error(`wave: ${target} carries an unreadable package.json`)
|
|
477
|
+
return undefined
|
|
478
|
+
}
|
|
479
|
+
const name = readString(manifest, 'name')
|
|
480
|
+
const version = readString(manifest, 'version')
|
|
481
|
+
const before = readRanges(manifest, RANGE_FIELDS)
|
|
482
|
+
const offline = runner.offline ? ['--offline'] : []
|
|
483
|
+
let hits: number | undefined
|
|
484
|
+
let ruling: Ruling = {
|
|
485
|
+
distMoved: undefined,
|
|
486
|
+
rangesMoved: undefined,
|
|
487
|
+
ranges: undefined,
|
|
488
|
+
hits: undefined,
|
|
489
|
+
}
|
|
490
|
+
if (selected.has('pin')) runPin(runner, scaffoldFile, scaffoldPrefix, offline)
|
|
491
|
+
if (selected.has('commit') && !failed(runner)) runCommit(runner)
|
|
492
|
+
if (selected.has('overwrite') && !failed(runner)) {
|
|
493
|
+
runOverwrite(runner, scaffoldFile, scaffoldPrefix, offline)
|
|
494
|
+
}
|
|
495
|
+
if (selected.has('verify') && !failed(runner)) {
|
|
496
|
+
runVerify(runner, scaffoldFile, scaffoldPrefix, offline)
|
|
497
|
+
}
|
|
498
|
+
if (selected.has('install') && !failed(runner)) runNpmStep(runner, 'install', ['install'])
|
|
499
|
+
if (selected.has('pins') && !failed(runner)) {
|
|
500
|
+
hits = runPins(runner, before, readOption(argv, '--prior') ?? version)
|
|
501
|
+
}
|
|
502
|
+
if (selected.has('format') && !failed(runner)) runNpmStep(runner, 'format', ['run', 'format'])
|
|
503
|
+
if (selected.has('gates') && !failed(runner)) {
|
|
504
|
+
for (const gate of GATES) {
|
|
505
|
+
if (failed(runner)) break
|
|
506
|
+
runNpmStep(runner, 'gates', ['run', gate])
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (selected.has('compare') && !failed(runner)) ruling = runCompare(runner, name)
|
|
510
|
+
ruling = { ...ruling, hits }
|
|
511
|
+
const bump =
|
|
512
|
+
ruling.distMoved === true || ruling.rangesMoved === true
|
|
513
|
+
? 'bump'
|
|
514
|
+
: ruling.distMoved === undefined || ruling.rangesMoved === undefined
|
|
515
|
+
? 'unanswered'
|
|
516
|
+
: 'no bump'
|
|
517
|
+
const reading = `dist ${describeReading(ruling.distMoved, 'moved', 'same')}, ranges ${describeReading(ruling.rangesMoved, 'moved', 'same')}${hits === undefined ? '' : `, ${hits} self-pin hit(s)`}`
|
|
518
|
+
return {
|
|
519
|
+
target,
|
|
520
|
+
name,
|
|
521
|
+
version,
|
|
522
|
+
steps: runner.steps,
|
|
523
|
+
ruling,
|
|
524
|
+
row: `| ${name ?? target} | ${version ?? '?'} | ${bump} | ${reading} | round |`,
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function readLayers(catalogPath: string): ReadonlyMap<string, readonly string[]> | undefined {
|
|
529
|
+
if (!existsSync(catalogPath)) return undefined
|
|
530
|
+
const text = readFileSync(catalogPath, 'utf8')
|
|
531
|
+
const start = text.indexOf('<!-- orkestrel:catalog -->')
|
|
532
|
+
const end = text.indexOf('<!-- /orkestrel:catalog -->')
|
|
533
|
+
if (start === -1 || end === -1) return undefined
|
|
534
|
+
const layers = new Map<string, string[]>()
|
|
535
|
+
for (const line of text.slice(start, end).split(/\r\n|\n/)) {
|
|
536
|
+
const match = line.match(
|
|
537
|
+
/^\|\s*`(@orkestrel\/[a-z0-9-]+)`\s*\|\s*`([^`]+)`\s*\|\s*(L\d+)\s*\|/u,
|
|
538
|
+
)
|
|
539
|
+
if (match?.[1] === undefined || match[3] === undefined) continue
|
|
540
|
+
const members = layers.get(match[3]) ?? []
|
|
541
|
+
members.push(`${match[1]} ${match[2] ?? ''}`.trim())
|
|
542
|
+
layers.set(match[3], members)
|
|
543
|
+
}
|
|
544
|
+
return new Map(
|
|
545
|
+
[...layers.entries()].sort(([left], [right]) => Number(left.slice(1)) - Number(right.slice(1))),
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function renderVisit(visit: Visit): string {
|
|
550
|
+
const lines = visit.steps.map(
|
|
551
|
+
(entry) =>
|
|
552
|
+
`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})`}`,
|
|
553
|
+
)
|
|
554
|
+
lines.push(
|
|
555
|
+
`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)`}`,
|
|
556
|
+
)
|
|
557
|
+
for (const range of visit.ruling.ranges ?? []) lines.push(`wave: range ${range}`)
|
|
558
|
+
lines.push(visit.row)
|
|
559
|
+
return `${lines.join('\n')}\n`
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function main(argv: readonly string[]): number {
|
|
563
|
+
const missing = readMissingFlags(argv, [
|
|
564
|
+
'--target',
|
|
565
|
+
'--from',
|
|
566
|
+
'--to',
|
|
567
|
+
'--prior',
|
|
568
|
+
'--catalog',
|
|
569
|
+
'--out',
|
|
570
|
+
])
|
|
571
|
+
if (missing.length > 0) {
|
|
572
|
+
console.error(`wave: ${missing.join(', ')} given with no value`)
|
|
573
|
+
return 64
|
|
574
|
+
}
|
|
575
|
+
if (argv.includes('--plan') && argv.includes('--visit')) {
|
|
576
|
+
console.error('wave: --plan and --visit are two modes; name one')
|
|
577
|
+
return 64
|
|
578
|
+
}
|
|
579
|
+
if (argv.includes('--plan')) {
|
|
580
|
+
const layers = readLayers(readOption(argv, '--catalog') ?? CATALOG)
|
|
581
|
+
if (layers === undefined) {
|
|
582
|
+
console.error('wave: no marker-bounded catalog table; run scaffold catalog first')
|
|
583
|
+
return 2
|
|
584
|
+
}
|
|
585
|
+
if (argv.includes('--json')) console.log(JSON.stringify(Object.fromEntries(layers)))
|
|
586
|
+
else for (const [layer, members] of layers) console.log(`${layer}\t${members.join(', ')}`)
|
|
587
|
+
return 0
|
|
588
|
+
}
|
|
589
|
+
if (!argv.includes('--visit')) {
|
|
590
|
+
console.error(
|
|
591
|
+
'usage: wave.ts --visit [--target DIR] [--offline] [--from STEP] [--to STEP] [--prior VERSION] [--dry-run] [--json] [--out FILE] | --plan [--catalog PATH] [--json]',
|
|
592
|
+
)
|
|
593
|
+
return 64
|
|
594
|
+
}
|
|
595
|
+
const from = STEPS.indexOf(readOption(argv, '--from') ?? 'pin')
|
|
596
|
+
const to = STEPS.indexOf(readOption(argv, '--to') ?? 'compare')
|
|
597
|
+
if (from === -1 || to === -1 || to < from) {
|
|
598
|
+
console.error(`wave: --from and --to name steps in order from: ${STEPS.join(', ')}`)
|
|
599
|
+
return 64
|
|
600
|
+
}
|
|
601
|
+
const prior = readOption(argv, '--prior')
|
|
602
|
+
if (prior !== undefined && !VERSION.test(prior)) {
|
|
603
|
+
console.error(
|
|
604
|
+
'wave: --prior takes the version the package carried before its bump, MAJOR.MINOR.PATCH',
|
|
605
|
+
)
|
|
606
|
+
return 64
|
|
607
|
+
}
|
|
608
|
+
const visit = runVisit(argv)
|
|
609
|
+
if (visit === undefined) return 2
|
|
610
|
+
const output = argv.includes('--json') ? `${JSON.stringify(visit)}\n` : renderVisit(visit)
|
|
611
|
+
const out = readOption(argv, '--out')
|
|
612
|
+
if (out === undefined) process.stdout.write(output)
|
|
613
|
+
else {
|
|
614
|
+
mkdirSync(dirname(out), { recursive: true })
|
|
615
|
+
writeFileSync(out, output)
|
|
616
|
+
console.log(`wave: wrote ${out}`)
|
|
617
|
+
}
|
|
618
|
+
return visit.steps.some(
|
|
619
|
+
(entry) =>
|
|
620
|
+
entry.exit !== undefined &&
|
|
621
|
+
entry.exit !== 0 &&
|
|
622
|
+
entry.step !== 'pins' &&
|
|
623
|
+
entry.step !== 'compare',
|
|
624
|
+
)
|
|
625
|
+
? 1
|
|
626
|
+
: 0
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
process.exitCode = main(process.argv.slice(2))
|