rvlib-shipkit 1.1.1 → 1.3.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 +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
package/src/core/run-ci.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { passScope, runCheckPass, type CheckOpts } from './run-check.ts'
|
|
|
12
12
|
import { poolLines } from './slots.ts'
|
|
13
13
|
import { runFixPass } from './run-fixers.ts'
|
|
14
14
|
import { formatLine } from './wire.ts'
|
|
15
|
+
import { outcomeCounts } from './outcome-words.ts'
|
|
15
16
|
import { writeStatus } from './write-status.ts'
|
|
16
17
|
import { allTasks, composeChecks, envTier, FULL_TESTS_ENV, type TestTier } from '../ledger/gate-checks.ts'
|
|
17
18
|
import { CHECK_TABLE_FILE, writeCheckTable } from '../ledger/check-table.ts'
|
|
@@ -53,6 +54,8 @@ export type CiOpts = {
|
|
|
53
54
|
* own panel, and running them twice per refresh proves the same thing
|
|
54
55
|
* twice. */
|
|
55
56
|
buildOnly?: boolean
|
|
57
|
+
/** colour the wire lines (run-check.ts CheckOpts.color) */
|
|
58
|
+
color?: boolean
|
|
56
59
|
/** line sink — commands pass their context stream; defaults to console */
|
|
57
60
|
print?: (line: string) => void
|
|
58
61
|
/** per-CHECK completion tap, handed straight to the runner. The hub passes
|
|
@@ -83,6 +86,10 @@ export type CiOpts = {
|
|
|
83
86
|
tests?: TestTier
|
|
84
87
|
/** who started this run, as the run record files it */
|
|
85
88
|
invokedBy?: InvokedBy
|
|
89
|
+
/** print every line, the ones nobody acts on included: the clean fix
|
|
90
|
+
* pass, the pool, the tier, the cache declarations, the parked checks,
|
|
91
|
+
* the cache counts. STATUS.md always holds them all */
|
|
92
|
+
verbose?: boolean
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
export type { TestTier } from '../ledger/gate-checks.ts'
|
|
@@ -105,10 +112,26 @@ function ciScope(o: CiOpts): { command: string; fullGate: boolean } {
|
|
|
105
112
|
return { command: parts.length === 0 ? 'shipkit ci' : `shipkit ci ${parts.join(' ')}`, fullGate: narrowing.length === 0 }
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
/** `ok: 12 checks green`, or which ones warned. A whole gate adds its tier
|
|
116
|
+
* and where its answers came from */
|
|
117
|
+
export function verdictLine(results: CheckResult[], whole: { tier: TestTier; cache: { hit: number; miss: number; attached: number; uncacheable: number } | null } | null): string | null {
|
|
118
|
+
const ran = results.filter((r) => r.status !== 'skip').length
|
|
119
|
+
if (ran === 0) return null
|
|
120
|
+
const warned = results.filter((r) => r.status === 'warn' || (r.status === 'fail' && r.advisory)).map((r) => r.id)
|
|
121
|
+
const head = warned.length === 0 ? `${ran} check${ran === 1 ? '' : 's'} green` : `${ran} check${ran === 1 ? '' : 's'} passed, ${warned.length} with a warning: ${warned.join(', ')}`
|
|
122
|
+
if (whole == null) return `ok: ${head}`
|
|
123
|
+
const c = whole.cache
|
|
124
|
+
const cache = c == null ? '' : `; ${outcomeCounts({ cached: c.hit, ran: c.miss, attached: c.attached, neverCached: c.uncacheable })}`
|
|
125
|
+
return `ok: ${head}; ${whole.tier} tier${cache}`
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
109
129
|
const print = o.print ?? ((l: string) => console.log(l))
|
|
110
130
|
const collected: string[] = []
|
|
111
131
|
const sink = (l: string): void => { collected.push(l); print(l) }
|
|
132
|
+
// what the reader never acts on: kept for STATUS.md and --verbose, never
|
|
133
|
+
// printed on a default run, where every line costs a reader tokens
|
|
134
|
+
const detail = (l: string): void => { collected.push(l); if (o.verbose === true) print(l) }
|
|
112
135
|
let c = o.config ?? (await loadRepoConfig(root))
|
|
113
136
|
// mechanical failures are not information, so they never cost a round trip
|
|
114
137
|
let fixFailed = false
|
|
@@ -121,7 +144,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
121
144
|
// back as a test failure.
|
|
122
145
|
const narrowed = (o.onlyIds ?? []).length > 0
|
|
123
146
|
if (o.fix !== false) {
|
|
124
|
-
const pass = await runFixPass(root, c, { rules: !narrowed && o.fixRules !== false, armHooks: !narrowed && o.armHooks === true, print: sink })
|
|
147
|
+
const pass = await runFixPass(root, c, { rules: !narrowed && o.fixRules !== false, armHooks: !narrowed && o.armHooks === true, print: sink, detail })
|
|
125
148
|
fixFailed = pass.failed
|
|
126
149
|
// a fixer that rewrote the config or moved a check file leaves the
|
|
127
150
|
// config above describing a repo that is gone, so the gate reads the
|
|
@@ -132,20 +155,22 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
132
155
|
return 1
|
|
133
156
|
}
|
|
134
157
|
}
|
|
135
|
-
} else
|
|
158
|
+
} else detail('skip: fix pass (--no-fix)')
|
|
136
159
|
const tier: TestTier = o.tests ?? envTier()
|
|
137
|
-
// announced BEFORE the run
|
|
138
|
-
// tier it was asking for
|
|
139
|
-
|
|
160
|
+
// announced BEFORE the run (under --verbose, and in every release log),
|
|
161
|
+
// so a log that died mid-way still says which tier it was asking for
|
|
162
|
+
detail(tier === 'full'
|
|
140
163
|
? `ok: test tier FULL — every suite, the slow ones included (${FULL_TESTS_ENV}=1)`
|
|
141
164
|
: `skip: full test tier (quick run — \`shipkit ci --full\` or ${FULL_TESTS_ENV}=1 includes it)`)
|
|
142
|
-
for (const l of poolLines()) sink(l)
|
|
165
|
+
for (const l of poolLines()) (l.startsWith('WARN') ? sink : detail)(l)
|
|
143
166
|
// a floor slot nothing could fill is VISIBLE — a thin gate must never look
|
|
144
167
|
// like a full one (the fresh-repo floor gap)
|
|
145
168
|
const floor = await allTasks(c, root)
|
|
146
|
-
|
|
169
|
+
// a narrowed run says nothing about the slots it never asked about
|
|
170
|
+
const asked = (id: string): boolean => !narrowed || (o.onlyIds ?? []).includes(id)
|
|
171
|
+
for (const n of floor.notes ?? []) (narrowed ? detail : sink)(n)
|
|
147
172
|
for (const u of floor.unfilled) {
|
|
148
|
-
sink(`skip: ${u.id} (${u.reason})`)
|
|
173
|
+
(asked(u.id) ? sink : detail)(`skip: ${u.id} (${u.reason})`)
|
|
149
174
|
// asked for BY NAME and unfillable: this is where an agent lands after
|
|
150
175
|
// the harness refused it the raw runner, so the line says what to add
|
|
151
176
|
// rather than leaving it with `unknown check id` and no next move
|
|
@@ -187,6 +212,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
187
212
|
buildOnly: o.buildOnly,
|
|
188
213
|
bail: o.bail,
|
|
189
214
|
fresh: o.fresh,
|
|
215
|
+
color: o.color,
|
|
190
216
|
tier,
|
|
191
217
|
}
|
|
192
218
|
try {
|
|
@@ -207,7 +233,7 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
207
233
|
// visible as a shrinking undeclared one.
|
|
208
234
|
const decided = reg.checks - reg.burnDown.length - reg.declared
|
|
209
235
|
if (reg.burnDown.length > 0 || decided > 0)
|
|
210
|
-
|
|
236
|
+
detail(`skip: cache declarations — ${reg.declared}/${reg.checks} declared, ${decided} never cached on purpose, ${reg.burnDown.length} undeclared, so those run every time (shipkit keys --burn-down)`)
|
|
211
237
|
if (ledger.layer.failure != null)
|
|
212
238
|
sink(`WARN the git scan failed, so NOTHING comes from the cache and nothing is stored this run — ${ledger.layer.failure}`)
|
|
213
239
|
} catch (e) {
|
|
@@ -222,13 +248,14 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
222
248
|
onCheck: o.onCheck,
|
|
223
249
|
reportHome: false,
|
|
224
250
|
print: sink,
|
|
251
|
+
detail,
|
|
225
252
|
})
|
|
226
253
|
// what the suite ACTUALLY ran, from its own headline. A flag that was set
|
|
227
254
|
// proves the intent; the count proves the tests
|
|
228
255
|
// proves the intent; the count proves the tests. Every test check counts,
|
|
229
256
|
// one per unit
|
|
230
257
|
const counts = run.results.map((r) => /Ran (\d+) tests?/.exec(r.lastLine)?.[1]).filter((n) => n != null).map(Number)
|
|
231
|
-
if (counts.length > 0)
|
|
258
|
+
if (counts.length > 0) detail(`ok: test tier ${tier}: ${counts.reduce((a, b) => a + b, 0)} tests ran${counts.length > 1 ? ` in ${counts.length} test checks` : ''}`)
|
|
232
259
|
for (const u of run.undeclared) {
|
|
233
260
|
sink(`FAIL: ${u} does not declare whether it blocks`)
|
|
234
261
|
sink(` fix: add \`blocking: true\` (about this repo) or \`false\` (about the world) to that check`)
|
|
@@ -245,16 +272,23 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
|
|
|
245
272
|
advisoryFatal = true
|
|
246
273
|
sink(`FAIL: ${run.advisoryFailures.length} non-blocking check(s) failing and release.requireHealthy is on: ${run.advisoryFailures.join(', ')}`)
|
|
247
274
|
sink(' fix: shipkit check')
|
|
248
|
-
} else
|
|
275
|
+
} else detail(`WARN ${run.advisoryFailures.length} non-blocking check(s) failing, about the world rather than this repo: ${run.advisoryFailures.join(', ')}`)
|
|
249
276
|
}
|
|
250
277
|
// the post-run re-hash, then the store. NOTHING was written while the gate
|
|
251
278
|
// ran: a check whose inputs moved under it answered about a tree that no
|
|
252
279
|
// longer exists, and this is where that result is thrown away instead of
|
|
253
280
|
// stored under a key that describes the file as it was before the edit.
|
|
254
|
-
|
|
281
|
+
// the counts line folds into the verdict; what else it says (a store
|
|
282
|
+
// that failed, answers thrown away) is printed as it is
|
|
283
|
+
if (ledger != null) for (const [i, l] of ledger.header(ledger.commit()).entries()) (i === 0 ? detail : sink)(formatLine(l, false))
|
|
255
284
|
// one write for the whole gate, after everything has finished
|
|
256
285
|
for (const l of runLog.flush()) sink(formatLine(l, false))
|
|
257
286
|
const code = run.blocked || fixFailed || registrationFailed || advisoryFatal || run.undeclared.length > 0 || run.unknownWhen.length > 0 || run.unknownBudgets.length > 0 ? 1 : 0
|
|
287
|
+
// THE VERDICT, one line and only on a pass: a red run already said so in
|
|
288
|
+
// its FAIL lines. The whole gate adds the tier and the cache counts, which
|
|
289
|
+
// a narrowed run asked no question about
|
|
290
|
+
const verdict = code === 0 && !run.interrupted ? verdictLine(run.results, scope.fullGate ? { tier, cache: ledger?.counts ?? null } : null) : null
|
|
291
|
+
if (verdict != null) sink(verdict)
|
|
258
292
|
// motherhome row (docsBase wiki on this disk) — never affects the gate,
|
|
259
293
|
// and never fires under frozen: a bump/release gate must not re-dirty the
|
|
260
294
|
// tree it just proved clean
|
package/src/core/run-fix.ts
CHANGED
|
@@ -17,6 +17,9 @@ export type FixOpts = {
|
|
|
17
17
|
* not wire-shaped, which the frozen output contract forbids */
|
|
18
18
|
quiet?: boolean
|
|
19
19
|
print?: (line: string) => void
|
|
20
|
+
/** where a fix that changed nothing reports, since the run after it says
|
|
21
|
+
* the same about that check. Default: print */
|
|
22
|
+
detail?: (line: string) => void
|
|
20
23
|
/** the config as it stands on disk now, when the caller holds one: a
|
|
21
24
|
* second load of a rewritten repo.config.ts can hit the module cache */
|
|
22
25
|
config?: RepoConfig
|
|
@@ -75,7 +78,8 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
75
78
|
// it is an ok, not a warning. It carries a frozen wire prefix like
|
|
76
79
|
// every other line the fix pass emits: `shipkit ci` promises its whole
|
|
77
80
|
// output is parseable, and a bare line breaks that for every reader.
|
|
78
|
-
|
|
81
|
+
const say = f.changed ? print : o.detail ?? print
|
|
82
|
+
say(f.changed ? `ok: [${rule.id}] fixed: ${f.summary}` : `ok: [${rule.id}] fix changed nothing — ${f.summary}`)
|
|
79
83
|
// a fixer may rewrite repo.config.ts, and the re-run below and every
|
|
80
84
|
// fixer after it must read what is on disk now, never the config this
|
|
81
85
|
// pass loaded before the rewrite
|
|
@@ -89,7 +93,8 @@ export async function runFix(root: string, o: FixOpts = {}): Promise<number> {
|
|
|
89
93
|
await runCheckBody(rule, after)
|
|
90
94
|
// the fix already ran: advertising it again on surviving findings
|
|
91
95
|
// would send an agent in a loop
|
|
92
|
-
|
|
96
|
+
// inside the ci fix pass the run below reports this check's end state
|
|
97
|
+
for (const l of after.lines) (o.quiet === true ? o.detail ?? print : say)(formatLine({ ...l, text: l.text.replace(/ · autofix: shipkit check --fix \S+$/, '') }, color))
|
|
93
98
|
if (after.failed) exit = 1
|
|
94
99
|
}
|
|
95
100
|
const unfixable = all.filter((r) => r.fix == null).length
|
package/src/core/run-fixers.ts
CHANGED
|
@@ -140,6 +140,9 @@ export type FixPassOpts = {
|
|
|
140
140
|
* mutate the tree they are about to ship) */
|
|
141
141
|
armHooks?: boolean
|
|
142
142
|
print: (line: string) => void
|
|
143
|
+
/** where the lines nobody acts on go (a clean pass, an unfilled slot).
|
|
144
|
+
* Default: print. `shipkit ci` shows them only under --verbose */
|
|
145
|
+
detail?: (line: string) => void
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
export type FixPassResult = {
|
|
@@ -220,12 +223,13 @@ async function fixPassBody(root: string, loaded: RepoConfig, o: FixPassOpts, ver
|
|
|
220
223
|
if (o.armHooks === true && !verify) {
|
|
221
224
|
const armed = armHooks(root, config)
|
|
222
225
|
if (armed.state === 'armed') o.print(`ok: hooks armed (${armed.detail}), commit the .shipkit/hooks/ dir`)
|
|
223
|
-
else if (armed.state === 'foreign' || armed.state === 'not-root') o.print(`skip: hooks (${armed.detail})`)
|
|
226
|
+
else if (armed.state === 'foreign' || armed.state === 'not-root') (o.detail ?? o.print)(`skip: hooks (${armed.detail})`)
|
|
224
227
|
else if (armed.state === 'failed') o.print(`WARN hooks could not arm: ${armed.detail}`)
|
|
225
228
|
}
|
|
229
|
+
const detail = o.detail ?? o.print
|
|
226
230
|
const { slots, unfilled, disabled } = resolveFixers(root, config)
|
|
227
|
-
for (const u of unfilled)
|
|
228
|
-
for (const d of disabled)
|
|
231
|
+
for (const u of unfilled) detail(`skip: ${u.id} (${u.reason})`)
|
|
232
|
+
for (const d of disabled) detail(`skip: ${d.id} (${d.reason})`)
|
|
229
233
|
const before = treeState(root)
|
|
230
234
|
|
|
231
235
|
for (const s of slots) {
|
|
@@ -236,7 +240,7 @@ async function fixPassBody(root: string, loaded: RepoConfig, o: FixPassOpts, ver
|
|
|
236
240
|
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`)
|
|
237
241
|
}
|
|
238
242
|
// structural repo-config repair is never implicit, and never in verify mode
|
|
239
|
-
if (o.rules === true && !verify) await runFix(root, { quiet: true, print: o.print, config })
|
|
243
|
+
if (o.rules === true && !verify) await runFix(root, { quiet: true, print: o.print, detail: o.detail, config })
|
|
240
244
|
|
|
241
245
|
const after = treeState(root)
|
|
242
246
|
const reshaped = shapeStamp(root) !== shapeBefore
|
|
@@ -257,11 +261,11 @@ async function fixPassBody(root: string, loaded: RepoConfig, o: FixPassOpts, ver
|
|
|
257
261
|
}
|
|
258
262
|
const list = `${touched.slice(0, 8).join(' ')}${touched.length > 8 ? ` … +${touched.length - 8}` : ''}`
|
|
259
263
|
if (before == null) {
|
|
260
|
-
|
|
264
|
+
detail(`ok: fix pass ran, ${slots.length} fixer(s) (not a git repo — rewritten files cannot be listed)`)
|
|
261
265
|
return { ran: true, touched, failed: false, reshaped }
|
|
262
266
|
}
|
|
263
267
|
if (touched.length === 0) {
|
|
264
|
-
|
|
268
|
+
detail(`ok: ${verify ? 'fixers verified' : 'fix pass'} clean (${slots.length} fixer(s), nothing to rewrite)`)
|
|
265
269
|
return { ran: true, touched, failed: false, reshaped }
|
|
266
270
|
}
|
|
267
271
|
if (verify) {
|
package/src/core/slots.ts
CHANGED
|
@@ -30,6 +30,11 @@ export const poolDir = (): string => {
|
|
|
30
30
|
return env != null && env !== '' ? env : join(homedir(), '.cache', 'shipkit', 'slots')
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** the TURN the background gates take, one at a time on the whole machine:
|
|
34
|
+
* every batch the plan runs, for every repo. A gate someone asked for
|
|
35
|
+
* (a terminal, a button) never takes it, so it never waits behind one */
|
|
36
|
+
export const autoGatePool = (): SlotPool => ({ dir: `${poolDir()}-auto-gate`, size: 1 })
|
|
37
|
+
|
|
33
38
|
/** this process's pool, or null when it runs inside a check that holds one */
|
|
34
39
|
export function machinePool(): SlotPool | null {
|
|
35
40
|
return process.env[SLOT_HELD_ENV] === '1' ? null : { dir: poolDir(), size: parallelCap() }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// where a statement of a config file ends, read as text. Pure, so the page
|
|
2
|
+
// can import it too.
|
|
3
|
+
|
|
4
|
+
/** a statement that starts on `i`, ending where its brackets balance and the
|
|
5
|
+
* line is not obviously continued. Deliberately simple: anything it cannot
|
|
6
|
+
* read lands as a hand pass rather than as a half rewritten config. */
|
|
7
|
+
export function statementEnd(lines: string[], i: number): number {
|
|
8
|
+
let depth = 0
|
|
9
|
+
for (let j = i; j < lines.length; j++) {
|
|
10
|
+
for (const ch of lines[j]!) {
|
|
11
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++
|
|
12
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--
|
|
13
|
+
}
|
|
14
|
+
if (depth <= 0) return j
|
|
15
|
+
}
|
|
16
|
+
return lines.length - 1
|
|
17
|
+
}
|
package/src/core/wire.ts
CHANGED
|
@@ -49,6 +49,17 @@ export function parseDuration(d: string | number): number {
|
|
|
49
49
|
return Number(m[1]) * { ms: 1, s: 1000, m: 60_000, h: 3600_000, d: 86400_000 }[m[2] as 'ms' | 's' | 'm' | 'h' | 'd']
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** `ttl: 'forever'`: a DECIDED fact (a repo's visibility, an account that
|
|
53
|
+
* exists). It moves only when someone decides to move it, and that decision
|
|
54
|
+
* is the declared value in its key, so a clean answer never expires */
|
|
55
|
+
export const FOREVER = 'forever'
|
|
56
|
+
|
|
57
|
+
/** what a forever answer keeps for: no clock ends it, no sweep drops it */
|
|
58
|
+
export const FOREVER_MS = Number.MAX_SAFE_INTEGER
|
|
59
|
+
|
|
60
|
+
/** how long a clean answer keeps, from a declared ttl. 0 = none declared */
|
|
61
|
+
export const ttlMsOf = (ttl: string | undefined): number => (ttl == null || ttl === '' ? 0 : ttl === FOREVER ? FOREVER_MS : parseDuration(ttl))
|
|
62
|
+
|
|
52
63
|
/** absolute local stamp (2026-08-05 09:12:34), never a relative age: a
|
|
53
64
|
* relative render changes every run and turns committed STATUS.md
|
|
54
65
|
* checkpoints into noise diffs */
|
package/src/fleet/scan-fleet.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// `shipkit check`, so the fix travels through that one code path.
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
14
14
|
import { join } from 'path'
|
|
15
|
+
import { isArchived } from '../core/archived.ts'
|
|
15
16
|
import { detectPackageManager, isRefusal } from '../core/package-manager.ts'
|
|
16
17
|
import { isBreaking } from '../core/update-policy.ts'
|
|
17
18
|
|
|
@@ -84,7 +85,7 @@ export function scanFleet(root: string, o: { prefix?: string } = {}): FleetRepo[
|
|
|
84
85
|
continue
|
|
85
86
|
}
|
|
86
87
|
const pkg = readPkg(dir)
|
|
87
|
-
if (pkg == null) continue
|
|
88
|
+
if (pkg == null || isArchived(dir)) continue
|
|
88
89
|
const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }
|
|
89
90
|
const names = Object.keys(all).filter((n) => n.startsWith(prefix))
|
|
90
91
|
if (names.length === 0) continue
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// the activity page's shapes and its few pure rules, apart from the git
|
|
2
|
+
// reader so the page bundle takes them without node's modules. The words
|
|
3
|
+
// here are what the page and `shipkit cross-repo-activity` both print.
|
|
4
|
+
import type { CheckState } from '../serve/project-gutter.ts'
|
|
5
|
+
|
|
6
|
+
/** the windows the page offers, in minutes */
|
|
7
|
+
export const WINDOW_MINUTES = [10, 30, 60, 180, 720, 1440, 4320, 10080] as const
|
|
8
|
+
export const DEFAULT_MINUTES = 30
|
|
9
|
+
/** the most commits the daemon lists per repo, and the most uncommitted paths it names */
|
|
10
|
+
export const MAX_COMMITS = 100
|
|
11
|
+
export const MAX_FILES = 80
|
|
12
|
+
|
|
13
|
+
export type FileKind = 'staged' | 'modified' | 'untracked'
|
|
14
|
+
|
|
15
|
+
export type ActivityCommit = {
|
|
16
|
+
sha: string
|
|
17
|
+
/** committer time, epoch ms */
|
|
18
|
+
at: number
|
|
19
|
+
title: string
|
|
20
|
+
author: string
|
|
21
|
+
/** the branch it sits on when it is not on the checked out one, else null */
|
|
22
|
+
branch: string | null
|
|
23
|
+
/** a remote holds it; false on a branch with no upstream */
|
|
24
|
+
pushed: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ActivityRow = {
|
|
28
|
+
name: string
|
|
29
|
+
/** the checked out branch; null when detached (see `detached`) */
|
|
30
|
+
branch: string | null
|
|
31
|
+
/** the short sha HEAD sits on when detached */
|
|
32
|
+
detached: string | null
|
|
33
|
+
/** the upstream as configured, null when the branch has none */
|
|
34
|
+
upstream: string | null
|
|
35
|
+
/** the upstream is configured but no longer exists */
|
|
36
|
+
gone: boolean
|
|
37
|
+
/** commits ahead of and behind the upstream, null without one */
|
|
38
|
+
ahead: number | null
|
|
39
|
+
behind: number | null
|
|
40
|
+
counts: Record<FileKind, number>
|
|
41
|
+
/** the uncommitted paths, at most MAX_FILES; a path both staged and
|
|
42
|
+
* modified is listed once, as modified */
|
|
43
|
+
files: { kind: FileKind; path: string }[]
|
|
44
|
+
/** in the window, newest first */
|
|
45
|
+
commits: ActivityCommit[]
|
|
46
|
+
/** HEAD's latest commit when nothing was committed in the window, shown
|
|
47
|
+
* grey; null when the window holds a commit, or the repo has none */
|
|
48
|
+
last: ActivityCommit | null
|
|
49
|
+
/** its checks as its run record last saw them, only the ids it declares
|
|
50
|
+
* today when that is known; null when shipkit never ran there */
|
|
51
|
+
status: CheckState
|
|
52
|
+
/** the checks behind a red or orange status, failing first: what the
|
|
53
|
+
* status column lists on hover and on click */
|
|
54
|
+
failing?: StatusCheck[]
|
|
55
|
+
/** why git could not answer, else null */
|
|
56
|
+
error: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** one failing or warning check as the run record last saw it */
|
|
60
|
+
export type StatusCheck = {
|
|
61
|
+
id: string
|
|
62
|
+
/** its title as the repo declares it, else its id */
|
|
63
|
+
title: string
|
|
64
|
+
status: 'fail' | 'warn'
|
|
65
|
+
/** when that outcome was recorded, epoch ms */
|
|
66
|
+
at: number | null
|
|
67
|
+
/** the first line it printed about the problem, '' when it printed none */
|
|
68
|
+
line: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type ActivityAnswer = { now: number; minutes: number; rows: ActivityRow[]; ms: number }
|
|
72
|
+
|
|
73
|
+
export type ActivityRepo = { name: string; path: string }
|
|
74
|
+
|
|
75
|
+
/** the window a request names, when it is one the page offers */
|
|
76
|
+
export function windowMinutes(v: string | null): number {
|
|
77
|
+
const n = Number(v)
|
|
78
|
+
return (WINDOW_MINUTES as readonly number[]).includes(n) ? n : DEFAULT_MINUTES
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** every house repo ships from main: shown green, any other orange, a
|
|
82
|
+
* leftover master included so the repo still to rename stands out */
|
|
83
|
+
export const isMainBranch = (b: string | null): boolean => b === 'main'
|
|
84
|
+
|
|
85
|
+
/** nothing committed in the window and nothing uncommitted: it sits below the busy repos */
|
|
86
|
+
export const idleRow = (r: ActivityRow): boolean =>
|
|
87
|
+
r.error == null && r.commits.length === 0 && r.counts.staged + r.counts.modified + r.counts.untracked === 0
|
|
88
|
+
|
|
89
|
+
/** the most commits a repo shows: a number from CAP_MIN to CAP_MAX */
|
|
90
|
+
export const CAP_MIN = 1
|
|
91
|
+
export const CAP_MAX = 15
|
|
92
|
+
export const DEFAULT_CAP = 3
|
|
93
|
+
|
|
94
|
+
/** a window's label on the toggle: `10m`, `3h`, `3d` */
|
|
95
|
+
export const windowWords = (m: number): string => (m < 60 ? `${m}m` : m < 4320 ? `${m / 60}h` : `${m / 1440}d`)
|
|
96
|
+
|
|
97
|
+
/** a typed cap, as a whole number in CAP_MIN..CAP_MAX; the default when it is no number */
|
|
98
|
+
export function clampCap(v: unknown): number {
|
|
99
|
+
const n = Math.round(Number(v))
|
|
100
|
+
return Number.isFinite(n) && String(v).trim() !== '' ? Math.min(CAP_MAX, Math.max(CAP_MIN, n)) : DEFAULT_CAP
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** an age in one short unit: `now`, `4m`, `2h`, `3d` */
|
|
104
|
+
export function ageShort(at: number, now: number): string {
|
|
105
|
+
const s = Math.max(0, Math.round((now - at) / 1000))
|
|
106
|
+
if (s < 60) return 'now'
|
|
107
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`
|
|
108
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`
|
|
109
|
+
return `${Math.floor(s / 86400)}d`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** the uncommitted work in one line: `3 modified, 1 untracked`; '' when clean */
|
|
113
|
+
export function dirtWords(r: ActivityRow): string {
|
|
114
|
+
const c = r.counts
|
|
115
|
+
return [c.staged > 0 ? `${c.staged} staged` : '', c.modified > 0 ? `${c.modified} modified` : '', c.untracked > 0 ? `${c.untracked} untracked` : '']
|
|
116
|
+
.filter((x) => x !== '')
|
|
117
|
+
.join(', ')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** where the branch stands against its upstream, for the branch's hover */
|
|
121
|
+
export function upstreamWords(r: ActivityRow): string {
|
|
122
|
+
if (r.upstream == null) return 'no upstream: nothing on this branch is pushed'
|
|
123
|
+
if (r.gone) return `${r.upstream} is gone`
|
|
124
|
+
const a = r.ahead ?? 0
|
|
125
|
+
const b = r.behind ?? 0
|
|
126
|
+
if (a === 0 && b === 0) return `even with ${r.upstream}`
|
|
127
|
+
return [a > 0 ? `${a} unpushed` : '', b > 0 ? `${b} behind` : ''].filter((x) => x !== '').join(', ') + ` against ${r.upstream}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** the uncommitted paths, one per line, for the hover */
|
|
131
|
+
export const filesWords = (r: ActivityRow): string => {
|
|
132
|
+
const mark = { staged: 'S', modified: 'M', untracked: '?' } as const
|
|
133
|
+
const shown = r.files.map((f) => `${mark[f.kind]} ${f.path}`)
|
|
134
|
+
return (r.files.length >= MAX_FILES ? [...shown, '…'] : shown).join('\n')
|
|
135
|
+
}
|