rvlib-shipkit 0.38.0 → 1.0.0
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 +371 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- 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} +46 -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 +6 -2
- 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 +258 -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 +557 -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 +86 -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 +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -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/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- 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 +155 -85
- 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 +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +216 -18
- 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/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
|
@@ -13,11 +13,15 @@ import { join } from 'path'
|
|
|
13
13
|
import { fileURLToPath } from 'url'
|
|
14
14
|
import { armHooks } from '../hooks/run-hooks.ts'
|
|
15
15
|
import type { RepoConfig } from '../repo-config.ts'
|
|
16
|
-
import { runFix } from '
|
|
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 { 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,46 +173,52 @@ 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
183
|
async function fixPassBody(root: string, config: RepoConfig, o: FixPassOpts, verify: boolean): Promise<FixPassResult> {
|
|
184
|
+
// the old root layout moves under .shipkit/ before anything else reads a
|
|
185
|
+
// path: a move is not a decision, so it lands as one machine commit and
|
|
186
|
+
// only what a machine cannot decide reaches a human. Runs before the tree
|
|
187
|
+
// snapshot, and before hooks arm, because the hooks folder may be what moves
|
|
188
|
+
if (!verify) {
|
|
189
|
+
const m = migrateLayout(root, { config, renderGitignore: (t, legacy) => renderPolicy(t, config, legacy) })
|
|
190
|
+
if (m.moved.length > 0 || m.removed.length > 0) {
|
|
191
|
+
const what = m.moved.filter((x) => x.kind !== 'cache').map((x) => `${x.from}/`).join(' ')
|
|
192
|
+
if (m.uncommitted === true) {
|
|
193
|
+
o.print(`WARN moved ${what} under .shipkit/, and the commit did not land`)
|
|
194
|
+
const p = m.pending ?? []
|
|
195
|
+
o.print(` fix: commit exactly these paths, never a bare commit: git commit -m 'chore: move the shipkit folders under .shipkit/' -- ${p.slice(0, 12).join(' ')}${p.length > 12 ? ` (+${p.length - 12} more, listed by git status)` : ''}`)
|
|
196
|
+
} else if (what !== '') o.print(`ok: moved ${what} under .shipkit/${m.commit != null ? `, committed as ${m.commit}` : ''}`)
|
|
197
|
+
}
|
|
198
|
+
if (m.deferred != null) {
|
|
199
|
+
o.print(`WARN the move under .shipkit/ is deferred: ${m.deferred}`)
|
|
200
|
+
o.print(' fix: commit or discard that edit, then re-run shipkit ci')
|
|
201
|
+
}
|
|
202
|
+
for (const r of m.refusals) {
|
|
203
|
+
o.print(`WARN ${r.from}/ cannot move to ${r.to}/: ${r.reason}`)
|
|
204
|
+
o.print(` fix: move the files into ${r.to}/ yourself, then delete ${r.from}/`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
168
207
|
// commit gates arm themselves on the interactive gate: a repo.config with
|
|
169
208
|
// dark hooks is the half-wired state that let a whole drill session
|
|
170
|
-
// commit ungated (2026-08-04).
|
|
171
|
-
//
|
|
209
|
+
// commit ungated (2026-08-04). Arming is its own reported concern, never
|
|
210
|
+
// counted as a fixer rewrite.
|
|
172
211
|
if (o.armHooks === true && !verify) {
|
|
173
212
|
const armed = armHooks(root, config)
|
|
174
|
-
if (armed.state === 'armed') o.print(`ok: hooks armed (${armed.detail}), commit the hooks/ dir`)
|
|
213
|
+
if (armed.state === 'armed') o.print(`ok: hooks armed (${armed.detail}), commit the .shipkit/hooks/ dir`)
|
|
175
214
|
else if (armed.state === 'foreign' || armed.state === 'not-root') o.print(`skip: hooks (${armed.detail})`)
|
|
176
215
|
else if (armed.state === 'failed') o.print(`WARN hooks could not arm: ${armed.detail}`)
|
|
177
216
|
}
|
|
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
217
|
const { slots, unfilled, disabled } = resolveFixers(root, config)
|
|
197
218
|
for (const u of unfilled) o.print(`skip: ${u.id} (${u.reason})`)
|
|
198
219
|
for (const d of disabled) o.print(`skip: ${d.id} (${d.reason})`)
|
|
199
220
|
const before = treeState(root)
|
|
221
|
+
const shapeBefore = shapeStamp(root)
|
|
200
222
|
|
|
201
223
|
for (const s of slots) {
|
|
202
224
|
const r = await spawnWithTimeout(s.cmd, { cwd: root, timeoutMs: FIXER_TIMEOUT_MS })
|
|
@@ -209,6 +231,7 @@ async function fixPassBody(root: string, config: RepoConfig, o: FixPassOpts, ver
|
|
|
209
231
|
if (o.rules === true && !verify) await runFix(root, { quiet: true, print: o.print })
|
|
210
232
|
|
|
211
233
|
const after = treeState(root)
|
|
234
|
+
const reshaped = shapeStamp(root) !== shapeBefore
|
|
212
235
|
const touched: string[] = []
|
|
213
236
|
if (before != null && after != null) {
|
|
214
237
|
for (const [path, hash] of after) if (before.get(path) !== hash) touched.push(path)
|
|
@@ -227,17 +250,17 @@ async function fixPassBody(root: string, config: RepoConfig, o: FixPassOpts, ver
|
|
|
227
250
|
const list = `${touched.slice(0, 8).join(' ')}${touched.length > 8 ? ` … +${touched.length - 8}` : ''}`
|
|
228
251
|
if (before == null) {
|
|
229
252
|
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 }
|
|
253
|
+
return { ran: true, touched, failed: false, reshaped }
|
|
231
254
|
}
|
|
232
255
|
if (touched.length === 0) {
|
|
233
256
|
o.print(`ok: ${verify ? 'fixers verified' : 'fix pass'} clean (${slots.length} fixer(s), nothing to rewrite)`)
|
|
234
|
-
return { ran: true, touched, failed: false }
|
|
257
|
+
return { ran: true, touched, failed: false, reshaped }
|
|
235
258
|
}
|
|
236
259
|
if (verify) {
|
|
237
260
|
o.print(`FAIL: ${touched.length} file(s) are not fixer-clean: ${list}`)
|
|
238
261
|
o.print(` fix: run \`shipkit ci\` locally (the fix pass rewrites them) and commit the result`)
|
|
239
|
-
return { ran: true, touched, failed: true }
|
|
262
|
+
return { ran: true, touched, failed: true, reshaped }
|
|
240
263
|
}
|
|
241
264
|
o.print(`ok: fix pass rewrote ${touched.length} file(s): ${list}`)
|
|
242
|
-
return { ran: true, touched, failed: false }
|
|
265
|
+
return { ran: true, touched, failed: false, reshaped }
|
|
243
266
|
}
|
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 } 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,9 @@ 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 cmd = detectPackageManager(root).update([PKG])
|
|
108
|
+
console.error(`ok: rvlib-shipkit ${plan.installed} → ${plan.latest}, self-updating (${cmd.join(' ')})`)
|
|
109
|
+
const up = Bun.spawnSync(cmd, { cwd: root, stdout: 'inherit', stderr: 'inherit' })
|
|
104
110
|
if (up.exitCode !== 0) { console.error(`WARN self-update failed (exit ${up.exitCode}), continuing on ${plan.installed}`) ; return }
|
|
105
111
|
console.error(`ok: restarting on ${plan.latest} — commit package.json + lockfile with your work`)
|
|
106
112
|
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 }
|