rvlib-shipkit 0.38.1 → 1.0.1
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/CHANGELOG.md +379 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/derived.ts +1 -1
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +49 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +16 -3
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +257 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +616 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +88 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +152 -189
- package/src/core/run-fix.ts +99 -0
- package/src/core/run-fixers.ts +64 -33
- package/src/core/self-update.ts +11 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/fleet/scan-fleet.ts +16 -0
- package/src/hooks/leak-guard.ts +20 -9
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +38 -26
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +157 -88
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +9 -4
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +209 -15
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/scan-md.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
package/src/core/run-fixers.ts
CHANGED
|
@@ -12,12 +12,16 @@ import { existsSync, readFileSync } from 'fs'
|
|
|
12
12
|
import { join } from 'path'
|
|
13
13
|
import { fileURLToPath } from 'url'
|
|
14
14
|
import { armHooks } from '../hooks/run-hooks.ts'
|
|
15
|
-
import type
|
|
16
|
-
import { runFix } from '
|
|
15
|
+
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
16
|
+
import { runFix } from './run-fix.ts'
|
|
17
17
|
import { routesToShipkit } from './canonical-scripts.ts'
|
|
18
|
+
import { discoverLegacyChecks } from './discover-legacy-checks.ts'
|
|
19
|
+
import { repoCheckFiles } from './discover-checks.ts'
|
|
18
20
|
import { mutateLease, withMutateLease } from './locked-write.ts'
|
|
19
|
-
import {
|
|
21
|
+
import { layoutSummary, migrateLayout } from './migrate-layout.ts'
|
|
22
|
+
import { renderPolicy } from '../checks/SK004-gitignore-policy.check.ts'
|
|
20
23
|
import { spawnWithTimeout } from './spawn-timeout.ts'
|
|
24
|
+
import { gitBin } from './git-bin.ts'
|
|
21
25
|
|
|
22
26
|
const FIXER_TIMEOUT_MS = 120_000
|
|
23
27
|
|
|
@@ -103,7 +107,7 @@ export function resolveFixers(root: string, config?: RepoConfig): FixerResolutio
|
|
|
103
107
|
* actually stops another agent's work being reported here is the lease plus
|
|
104
108
|
* the byte comparison, not a path list. */
|
|
105
109
|
function treeState(root: string): Map<string, string> | null {
|
|
106
|
-
const p = Bun.spawnSync([
|
|
110
|
+
const p = Bun.spawnSync([gitBin(), 'status', '--porcelain'], { cwd: root })
|
|
107
111
|
if (p.exitCode !== 0) return null
|
|
108
112
|
const m = new Map<string, string>()
|
|
109
113
|
for (const l of p.stdout.toString().split('\n')) {
|
|
@@ -119,6 +123,15 @@ function treeState(root: string): Map<string, string> | null {
|
|
|
119
123
|
return m
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
/** the bytes of repo.config.ts and the set of check and check files, as one
|
|
127
|
+
* hash. Read without git, so a repo with no history is covered too */
|
|
128
|
+
function shapeStamp(root: string): string {
|
|
129
|
+
let config = ''
|
|
130
|
+
try { config = readFileSync(join(root, 'repo.config.ts'), 'utf8') } catch {}
|
|
131
|
+
const files = [...repoCheckFiles(root), ...discoverLegacyChecks(root).map((d) => d.path)].sort()
|
|
132
|
+
return String(Bun.hash(`${config}\0${files.join('\0')}`))
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
export type FixPassOpts = {
|
|
123
136
|
/** also apply the structural SK rule fixers (default false) */
|
|
124
137
|
rules?: boolean
|
|
@@ -135,6 +148,9 @@ export type FixPassResult = {
|
|
|
135
148
|
touched: string[]
|
|
136
149
|
/** verify mode only: a fixer had to rewrite something, so the commit is not clean */
|
|
137
150
|
failed: boolean
|
|
151
|
+
/** repo.config.ts or a check file moved under the pass, so a config or an
|
|
152
|
+
* inventory read before it describes a repo that is gone */
|
|
153
|
+
reshaped: boolean
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
export async function runFixPass(root: string, config: RepoConfig, o: FixPassOpts): Promise<FixPassResult> {
|
|
@@ -157,42 +173,56 @@ export async function runFixPass(root: string, config: RepoConfig, o: FixPassOpt
|
|
|
157
173
|
if (verify) {
|
|
158
174
|
o.print(`FAIL: fixers never ran — ${lease.warn}`)
|
|
159
175
|
o.print(' fix: nothing was verified. re-run once that process finishes, or pass --no-fix to state that on purpose')
|
|
160
|
-
return { ran: false, touched: [], failed: true }
|
|
176
|
+
return { ran: false, touched: [], failed: true, reshaped: false }
|
|
161
177
|
}
|
|
162
178
|
o.print(`WARN fix pass skipped — ${lease.warn}`)
|
|
163
179
|
o.print(' fix: nothing was repaired this run; the gate below reports the tree as it stands')
|
|
164
|
-
return { ran: false, touched: [], failed: false }
|
|
180
|
+
return { ran: false, touched: [], failed: false, reshaped: false }
|
|
165
181
|
}
|
|
166
182
|
|
|
167
|
-
async function fixPassBody(root: string,
|
|
183
|
+
async function fixPassBody(root: string, loaded: RepoConfig, o: FixPassOpts, verify: boolean): Promise<FixPassResult> {
|
|
184
|
+
let config = loaded
|
|
185
|
+
const shapeBefore = shapeStamp(root)
|
|
186
|
+
// the old root layout moves under .shipkit/ before anything else reads a
|
|
187
|
+
// path: a move is not a decision, so it lands as one machine commit and
|
|
188
|
+
// only what a machine cannot decide reaches a human. Runs before the tree
|
|
189
|
+
// snapshot, and before hooks arm, because the hooks folder may be what moves
|
|
190
|
+
if (!verify) {
|
|
191
|
+
const m = migrateLayout(root, { config, renderGitignore: (t, legacy, cfg) => renderPolicy(t, cfg ?? config, legacy) })
|
|
192
|
+
// the move rewrote repo.config.ts, so the config loaded before it names
|
|
193
|
+
// folders that are gone, and every fixer below must read the new one
|
|
194
|
+
if (m.swept.includes('repo.config.ts')) {
|
|
195
|
+
try { config = await loadRepoConfig(root, { fresh: true }) } catch (e) {
|
|
196
|
+
o.print(`WARN repo.config.ts does not load after the move rewrote it: ${String((e as Error)?.message ?? e).slice(0, 160)}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const what = layoutSummary(m)
|
|
200
|
+
if (what !== '') {
|
|
201
|
+
if (m.uncommitted === true) {
|
|
202
|
+
o.print(`WARN layout: ${what}. The commit did not land`)
|
|
203
|
+
const p = m.pending ?? []
|
|
204
|
+
o.print(` fix: commit exactly these paths, never a bare commit: git commit -m 'chore: ${what}' -- ${p.slice(0, 12).join(' ')}${p.length > 12 ? ` (+${p.length - 12} more, listed by git status)` : ''}`)
|
|
205
|
+
} else o.print(`ok: layout: ${what}${m.commit != null ? `, committed as ${m.commit}` : ''}`)
|
|
206
|
+
}
|
|
207
|
+
if (m.deferred != null) {
|
|
208
|
+
o.print(`WARN the move under .shipkit/ is deferred: ${m.deferred}`)
|
|
209
|
+
o.print(' fix: commit or discard that edit, then re-run shipkit ci')
|
|
210
|
+
}
|
|
211
|
+
for (const r of m.refusals) {
|
|
212
|
+
o.print(`WARN ${r.from}/ cannot move to ${r.to}/: ${r.reason}`)
|
|
213
|
+
o.print(` fix: move the files into ${r.to}/ yourself, then delete ${r.from}/`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
168
216
|
// commit gates arm themselves on the interactive gate: a repo.config with
|
|
169
217
|
// dark hooks is the half-wired state that let a whole drill session
|
|
170
|
-
// commit ungated (2026-08-04).
|
|
171
|
-
//
|
|
218
|
+
// commit ungated (2026-08-04). Arming is its own reported concern, never
|
|
219
|
+
// counted as a fixer rewrite.
|
|
172
220
|
if (o.armHooks === true && !verify) {
|
|
173
221
|
const armed = armHooks(root, config)
|
|
174
|
-
if (armed.state === 'armed') o.print(`ok: hooks armed (${armed.detail}), commit the hooks/ dir`)
|
|
222
|
+
if (armed.state === 'armed') o.print(`ok: hooks armed (${armed.detail}), commit the .shipkit/hooks/ dir`)
|
|
175
223
|
else if (armed.state === 'foreign' || armed.state === 'not-root') o.print(`skip: hooks (${armed.detail})`)
|
|
176
224
|
else if (armed.state === 'failed') o.print(`WARN hooks could not arm: ${armed.detail}`)
|
|
177
225
|
}
|
|
178
|
-
// a contract folder typed without its dot is a typo a machine can fix with
|
|
179
|
-
// certainty, so it is repaired and committed rather than reported. Only the
|
|
180
|
-
// ambiguous case reaches a human. Runs before the tree snapshot: the rename
|
|
181
|
-
// lands in its own commit and is never counted as a fixer rewrite
|
|
182
|
-
if (!verify) {
|
|
183
|
-
const repaired = repairContractDirs(root)
|
|
184
|
-
for (const r of repaired.refusals) {
|
|
185
|
-
o.print(`WARN ${r.from}/ and ${r.to}/ both exist — ${r.reason}`)
|
|
186
|
-
o.print(` fix: move the files into ${r.to}/ yourself, then delete ${r.from}/`)
|
|
187
|
-
}
|
|
188
|
-
// silence is for a repair that COMPLETED. one that renamed and could
|
|
189
|
-
// not land the commit leaves dirt nobody can trace to a run they did
|
|
190
|
-
// not make, which is the failure the silence was meant to prevent
|
|
191
|
-
for (const r of repaired.uncommitted) {
|
|
192
|
-
o.print(`WARN renamed ${r.from}/ to ${r.to}/, and the commit did not land`)
|
|
193
|
-
o.print(` fix: commit the removal of ${r.from}/ yourself — the files are in ${r.to}/`)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
226
|
const { slots, unfilled, disabled } = resolveFixers(root, config)
|
|
197
227
|
for (const u of unfilled) o.print(`skip: ${u.id} (${u.reason})`)
|
|
198
228
|
for (const d of disabled) o.print(`skip: ${d.id} (${d.reason})`)
|
|
@@ -206,9 +236,10 @@ async function fixPassBody(root: string, config: RepoConfig, o: FixPassOpts, ver
|
|
|
206
236
|
o.print(`WARN fix ${s.id} failed (exit ${r.code}) — ${r.out.trim().split('\n').at(-1)?.slice(0, 160) ?? ''}; the gate below still ran`)
|
|
207
237
|
}
|
|
208
238
|
// structural repo-config repair is never implicit, and never in verify mode
|
|
209
|
-
if (o.rules === true && !verify) await runFix(root, { quiet: true, print: o.print })
|
|
239
|
+
if (o.rules === true && !verify) await runFix(root, { quiet: true, print: o.print, config })
|
|
210
240
|
|
|
211
241
|
const after = treeState(root)
|
|
242
|
+
const reshaped = shapeStamp(root) !== shapeBefore
|
|
212
243
|
const touched: string[] = []
|
|
213
244
|
if (before != null && after != null) {
|
|
214
245
|
for (const [path, hash] of after) if (before.get(path) !== hash) touched.push(path)
|
|
@@ -227,17 +258,17 @@ async function fixPassBody(root: string, config: RepoConfig, o: FixPassOpts, ver
|
|
|
227
258
|
const list = `${touched.slice(0, 8).join(' ')}${touched.length > 8 ? ` … +${touched.length - 8}` : ''}`
|
|
228
259
|
if (before == null) {
|
|
229
260
|
o.print(`ok: fix pass ran, ${slots.length} fixer(s) (not a git repo — rewritten files cannot be listed)`)
|
|
230
|
-
return { ran: true, touched, failed: false }
|
|
261
|
+
return { ran: true, touched, failed: false, reshaped }
|
|
231
262
|
}
|
|
232
263
|
if (touched.length === 0) {
|
|
233
264
|
o.print(`ok: ${verify ? 'fixers verified' : 'fix pass'} clean (${slots.length} fixer(s), nothing to rewrite)`)
|
|
234
|
-
return { ran: true, touched, failed: false }
|
|
265
|
+
return { ran: true, touched, failed: false, reshaped }
|
|
235
266
|
}
|
|
236
267
|
if (verify) {
|
|
237
268
|
o.print(`FAIL: ${touched.length} file(s) are not fixer-clean: ${list}`)
|
|
238
269
|
o.print(` fix: run \`shipkit ci\` locally (the fix pass rewrites them) and commit the result`)
|
|
239
|
-
return { ran: true, touched, failed: true }
|
|
270
|
+
return { ran: true, touched, failed: true, reshaped }
|
|
240
271
|
}
|
|
241
272
|
o.print(`ok: fix pass rewrote ${touched.length} file(s): ${list}`)
|
|
242
|
-
return { ran: true, touched, failed: false }
|
|
273
|
+
return { ran: true, touched, failed: false, reshaped }
|
|
243
274
|
}
|
package/src/core/self-update.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// checks the registry (24h cache on disk), updates rvlib-shipkit when it is
|
|
3
3
|
// behind, and RE-EXECS the same invocation so the run continues on the new
|
|
4
4
|
// version. a stale toolkit must never wait for a human to police versions.
|
|
5
|
+
// the update runs through the manager the repo's own lock files name: on an
|
|
6
|
+
// npm consumer, bun would move package.json and leave package-lock.json on
|
|
7
|
+
// the old version, which is a pair `npm ci` refuses at deploy time.
|
|
5
8
|
//
|
|
6
9
|
// never fires: running from source (not under node_modules), CI env, frozen,
|
|
7
10
|
// re-exec guard set, dirty package.json/lockfile (a tool must not mix its
|
|
@@ -11,6 +14,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
|
11
14
|
import { homedir } from 'os'
|
|
12
15
|
import { dirname, join } from 'path'
|
|
13
16
|
import { fileURLToPath } from 'url'
|
|
17
|
+
import { detectPackageManager, isRefusal } from './package-manager.ts'
|
|
18
|
+
import { gitBin } from './git-bin.ts'
|
|
14
19
|
|
|
15
20
|
const PKG = 'rvlib-shipkit'
|
|
16
21
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
|
@@ -77,7 +82,7 @@ async function latestCached(): Promise<string | null> {
|
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
const manifestDirty = (root: string): boolean => {
|
|
80
|
-
const p = Bun.spawnSync([
|
|
85
|
+
const p = Bun.spawnSync([gitBin(), 'status', '--porcelain', '--', 'package.json', 'bun.lock', 'bun.lockb', 'package-lock.json'], { cwd: root })
|
|
81
86
|
if (p.exitCode !== 0) return true // no git = no safe write
|
|
82
87
|
return p.stdout.toString().trim() !== ''
|
|
83
88
|
}
|
|
@@ -99,8 +104,11 @@ export async function maybeSelfUpdateAndRestart(root: string): Promise<void> {
|
|
|
99
104
|
const plan = planSelfUpdate({ ...base, latest: await latestCached(), manifestDirty: manifestDirty(root) })
|
|
100
105
|
if (plan.action === 'warn') { console.error(`WARN self-update: ${plan.reason}`); return }
|
|
101
106
|
if (plan.action !== 'update') return
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
const pm = detectPackageManager(root)
|
|
108
|
+
if (isRefusal(pm)) { console.error(`WARN self-update: rvlib-shipkit ${plan.installed} is behind ${plan.latest}, not updating: ${pm.refused}`); return }
|
|
109
|
+
const cmd = pm.update([PKG])
|
|
110
|
+
console.error(`ok: rvlib-shipkit ${plan.installed} → ${plan.latest}, self-updating (${cmd.join(' ')})`)
|
|
111
|
+
const up = Bun.spawnSync(cmd, { cwd: root, stdout: 'inherit', stderr: 'inherit' })
|
|
104
112
|
if (up.exitCode !== 0) { console.error(`WARN self-update failed (exit ${up.exitCode}), continuing on ${plan.installed}`) ; return }
|
|
105
113
|
console.error(`ok: restarting on ${plan.latest} — commit package.json + lockfile with your work`)
|
|
106
114
|
const child = Bun.spawnSync([process.execPath, ...process.argv.slice(1)], {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// THE MACHINE POOL: every shipkit process on this machine (every repo, every
|
|
2
|
+
// agent, the hub) measures at most parallelCap() checks at once. One lock file
|
|
3
|
+
// per slot, `slot-<n>.lock` under ~/.cache/shipkit/slots/, taken through the
|
|
4
|
+
// mutate lease's non blocking form (locked-write.ts tryLease): a slot whose
|
|
5
|
+
// holder died is free at once, and a live holder heartbeats it.
|
|
6
|
+
//
|
|
7
|
+
// The one constraint a reader breaks first: a process started BY a check
|
|
8
|
+
// (SHIPKIT_SLOT_HELD=1 in its environment) takes no slot. It already runs on
|
|
9
|
+
// its parent's, and a parent waiting on a child that waits for a slot the
|
|
10
|
+
// parent holds is a deadlock the moment every slot is such a parent.
|
|
11
|
+
import { mkdirSync, readdirSync } from 'fs'
|
|
12
|
+
import { homedir } from 'os'
|
|
13
|
+
import { join } from 'path'
|
|
14
|
+
import { leaseHolder, tryLease } from './locked-write.ts'
|
|
15
|
+
import { PARALLEL_ENV, parallelCap, parallelCapInfo } from './parallel.ts'
|
|
16
|
+
|
|
17
|
+
/** where the slots live, for a test that must not touch the machine's pool */
|
|
18
|
+
export const SLOTS_DIR_ENV = 'SHIPKIT_SLOTS_DIR'
|
|
19
|
+
/** set for every process a check spawns: it runs on its parent's slot */
|
|
20
|
+
export const SLOT_HELD_ENV = 'SHIPKIT_SLOT_HELD'
|
|
21
|
+
|
|
22
|
+
export type SlotPool = { dir: string; size: number }
|
|
23
|
+
|
|
24
|
+
export type SlotHolder = { slot: number; pid: number; repo: string; check: string; since: number }
|
|
25
|
+
|
|
26
|
+
const SLOT_POLL_MS = 25
|
|
27
|
+
|
|
28
|
+
export const poolDir = (): string => {
|
|
29
|
+
const env = process.env[SLOTS_DIR_ENV]
|
|
30
|
+
return env != null && env !== '' ? env : join(homedir(), '.cache', 'shipkit', 'slots')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** this process's pool, or null when it runs inside a check that holds one */
|
|
34
|
+
export function machinePool(): SlotPool | null {
|
|
35
|
+
return process.env[SLOT_HELD_ENV] === '1' ? null : { dir: poolDir(), size: parallelCap() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** who holds which slot right now, dead holders left out. Every slot file in
|
|
39
|
+
* the dir is read, so a slot taken under a larger size still shows. */
|
|
40
|
+
export function poolOccupancy(pool: SlotPool = { dir: poolDir(), size: parallelCap() }): { size: number; dir: string; held: SlotHolder[] } {
|
|
41
|
+
const held: SlotHolder[] = []
|
|
42
|
+
let names: string[] = []
|
|
43
|
+
try { names = readdirSync(pool.dir) } catch { /* no pool yet: nobody holds anything */ }
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
const m = /^slot-(\d+)\.lock$/.exec(name)
|
|
46
|
+
if (m == null) continue
|
|
47
|
+
const h = leaseHolder(join(pool.dir, `slot-${m[1]}`))
|
|
48
|
+
if (h != null) held.push({ slot: Number(m[1]), pid: h.pid, repo: h.meta.repo ?? '?', check: h.meta.check ?? h.label, since: h.at })
|
|
49
|
+
}
|
|
50
|
+
return { size: pool.size, dir: pool.dir, held: held.sort((a, b) => a.slot - b.slot) }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** what `shipkit ci` prints about the pool, one line (two with a bad env) */
|
|
54
|
+
export function poolLines(): string[] {
|
|
55
|
+
const info = parallelCapInfo()
|
|
56
|
+
const pool = machinePool()
|
|
57
|
+
const why = info.source === 'env' ? `${PARALLEL_ENV} sets it` : `this machine's cores, 2 to 8; ${PARALLEL_ENV} sets it`
|
|
58
|
+
return [
|
|
59
|
+
...(info.problem == null ? [] : [`WARN ${info.problem}`]),
|
|
60
|
+
pool == null
|
|
61
|
+
? `ok: parallel: inside a check that holds a machine slot, so this run takes none (up to ${info.cap} at once in this process)`
|
|
62
|
+
: `ok: parallel: one machine pool of ${info.cap} slot(s) shared by every shipkit process, ${poolOccupancy(pool).held.length} held now (${why})`,
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** wait for a free slot and hold it until release(). null = abort said stop. */
|
|
67
|
+
export async function takeSlot(
|
|
68
|
+
pool: SlotPool,
|
|
69
|
+
who: { repo: string; check: string },
|
|
70
|
+
o: { abort?: () => boolean; pollMs?: number; onWait?: (held: SlotHolder[]) => void } = {},
|
|
71
|
+
): Promise<{ slot: number; release: () => void } | null> {
|
|
72
|
+
mkdirSync(pool.dir, { recursive: true })
|
|
73
|
+
let heard = false
|
|
74
|
+
for (;;) {
|
|
75
|
+
for (let slot = 0; slot < pool.size; slot++) {
|
|
76
|
+
const lease = tryLease(join(pool.dir, `slot-${slot}`), who.check, { repo: who.repo, check: who.check })
|
|
77
|
+
if (lease != null) return { slot, release: lease.release }
|
|
78
|
+
}
|
|
79
|
+
if (o.abort?.() === true) return null
|
|
80
|
+
if (!heard) { heard = true; o.onWait?.(poolOccupancy(pool).held) }
|
|
81
|
+
await Bun.sleep(o.pollMs ?? SLOT_POLL_MS)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
// Resolves the binary first (Bun.spawn throws on a missing one), streams
|
|
4
4
|
// stdout+stderr, SIGTERM at timeoutMs, SIGKILL after a grace period so a
|
|
5
5
|
// SIGTERM-ignoring child can never hang the caller.
|
|
6
|
+
//
|
|
7
|
+
// The kill walks the whole process TREE, because the cmd is usually a wrapper
|
|
8
|
+
// (`bun scripts/test.ts` spawning `bun test`) and Bun.spawn offers no process
|
|
9
|
+
// group to signal: signalling the wrapper alone leaves the process doing the
|
|
10
|
+
// work orphaned and spinning, with nothing left holding a handle on it.
|
|
6
11
|
|
|
7
12
|
import { existsSync } from 'fs'
|
|
13
|
+
import { gitBin } from './git-bin.ts'
|
|
14
|
+
import { trackChild, untrackChild } from './instance.ts'
|
|
15
|
+
import { descendantPids } from './process-tree.ts'
|
|
8
16
|
|
|
9
17
|
export type SpawnOutcome = {
|
|
10
18
|
code: number
|
|
@@ -26,12 +34,28 @@ export type SpawnTimeoutOpts = {
|
|
|
26
34
|
onSpawn?: (proc: ReturnType<typeof Bun.spawn>) => void
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
/** signal the descendants, then the child itself through its own handle (the
|
|
38
|
+
* handle knows the child exited, a bare pid does not). A pid that already
|
|
39
|
+
* went is the normal case here, never an error. Returns what it signalled, so
|
|
40
|
+
* a later pass can reach a survivor that reparenting has hidden. A remembered
|
|
41
|
+
* pid could be recycled inside that pass's grace window, which is the price
|
|
42
|
+
* of a runtime with no process group to signal. */
|
|
43
|
+
export function killTree(proc: { pid: number; kill: (s?: NodeJS.Signals) => void }, signal: NodeJS.Signals, known?: number[]): number[] {
|
|
44
|
+
const pids = [...new Set([...(known ?? []), ...descendantPids(proc.pid)])]
|
|
45
|
+
for (const pid of pids) {
|
|
46
|
+
try { process.kill(pid, signal) } catch {}
|
|
47
|
+
}
|
|
48
|
+
try { proc.kill(signal) } catch {}
|
|
49
|
+
return pids
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
export async function spawnWithTimeout(cmd: string[], opts: SpawnTimeoutOpts): Promise<SpawnOutcome> {
|
|
30
53
|
const head = cmd[0]
|
|
31
54
|
if (head == null) return { code: -1, out: 'empty command', timedOut: false, spawnFailed: true }
|
|
32
55
|
// path-style heads verified too — Bun.spawn on a missing path throws,
|
|
33
56
|
// which must surface as a result, never a crash
|
|
34
|
-
const
|
|
57
|
+
const name = head === 'git' ? gitBin() : head
|
|
58
|
+
const bin = name.includes('/') ? name : Bun.which(name)
|
|
35
59
|
if (bin == null || !existsSync(bin)) return { code: -1, out: `${head}: not found`, timedOut: false, spawnFailed: true }
|
|
36
60
|
let proc: ReturnType<typeof Bun.spawn>
|
|
37
61
|
try {
|
|
@@ -44,10 +68,16 @@ export async function spawnWithTimeout(cmd: string[], opts: SpawnTimeoutOpts): P
|
|
|
44
68
|
} catch (e) {
|
|
45
69
|
return { code: -1, out: `spawn error: ${String(e).slice(0, 200)}`, timedOut: false, spawnFailed: true }
|
|
46
70
|
}
|
|
71
|
+
// listed while it runs, so a later image of this pid can reap it
|
|
72
|
+
trackChild(proc.pid)
|
|
47
73
|
opts.onSpawn?.(proc)
|
|
48
74
|
let timedOut = false
|
|
49
|
-
|
|
50
|
-
|
|
75
|
+
// what the tree looked like at SIGTERM time: a survivor is reparented the
|
|
76
|
+
// moment its parent dies, so the SIGKILL pass can no longer walk down to
|
|
77
|
+
// it from the child
|
|
78
|
+
let tree: number[] = []
|
|
79
|
+
const timer = setTimeout(() => { timedOut = true; tree = killTree(proc, 'SIGTERM') }, opts.timeoutMs)
|
|
80
|
+
const killer = setTimeout(() => { killTree(proc, 'SIGKILL', tree) }, opts.timeoutMs + (opts.killGraceMs ?? 2000))
|
|
51
81
|
let out = ''
|
|
52
82
|
const eat = async (stream: ReadableStream<Uint8Array>): Promise<void> => {
|
|
53
83
|
const dec = new TextDecoder()
|
|
@@ -59,6 +89,7 @@ export async function spawnWithTimeout(cmd: string[], opts: SpawnTimeoutOpts): P
|
|
|
59
89
|
}
|
|
60
90
|
await Promise.all([eat(proc.stdout as ReadableStream<Uint8Array>), eat(proc.stderr as ReadableStream<Uint8Array>)])
|
|
61
91
|
const code = await proc.exited
|
|
92
|
+
untrackChild(proc.pid)
|
|
62
93
|
clearTimeout(timer)
|
|
63
94
|
clearTimeout(killer)
|
|
64
95
|
return { code, out, timedOut, spawnFailed: false }
|