rvlib-shipkit 1.4.0 → 1.5.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 +68 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/check.ts +68 -95
- package/src/checks/SK001-package-json-shape.check.ts +6 -2
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +4 -0
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
- package/src/checks/SK028-readme.check.ts +5 -1
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +6 -5
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +9 -4
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +37 -20
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +8 -4
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +10 -14
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/hubs.ts +5 -5
- package/src/home/pm.ts +8 -0
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +113 -59
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +30 -55
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +14 -35
- package/src/serve/hub-app/col-tables-view.tsx +13 -6
- package/src/serve/hub-app/cols-svc.ts +5 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +6 -0
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +14 -2
- package/src/serve/hub-app/home-strip.css +43 -8
- package/src/serve/hub-app/home-strip.tsx +111 -47
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +14 -4
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +65 -43
- package/src/serve/hub-app/icons.tsx +11 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +5 -2
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +18 -22
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +46 -9
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
package/src/core/when-block.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// 1. the file must not have moved since the caller read it (`expect`)
|
|
13
13
|
// 2. the block must still MATCH what the config loads, or a hand edit the
|
|
14
14
|
// writer cannot account for would be silently overwritten
|
|
15
|
-
// 3.
|
|
15
|
+
// 3. an `off` or `tests` declared outside the block is refused, because a
|
|
16
16
|
// second declaration is a duplicate key rather than a merge
|
|
17
17
|
// 4. the value is validated the way `defineRepo` validates it, before it
|
|
18
18
|
// reaches the file
|
|
@@ -28,7 +28,6 @@ import { existsSync, readFileSync } from 'fs'
|
|
|
28
28
|
import { join } from 'path'
|
|
29
29
|
import { atomicWrite } from './locked-write.ts'
|
|
30
30
|
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
31
|
-
import { parseWhen } from './when.ts'
|
|
32
31
|
import type { SlowSuite, TestsConfig } from '../suite/judge-suites.ts'
|
|
33
32
|
import { normWord, proseWordsProblems, type ProseAllowWord, type ProseBanWord, type ProseWords } from '../prose/ste.ts'
|
|
34
33
|
|
|
@@ -41,7 +40,7 @@ export const WORDS_END = '// <<< shipkit prose words'
|
|
|
41
40
|
|
|
42
41
|
/** every key this block owns. A repo declaring one of them outside the
|
|
43
42
|
* markers keeps it, and the block refuses to write rather than duplicate it */
|
|
44
|
-
const OWNED = ['
|
|
43
|
+
const OWNED = ['off', 'tests'] as const
|
|
45
44
|
|
|
46
45
|
export type BlockState = 'missing-file' | 'missing-block' | 'stale' | 'current'
|
|
47
46
|
|
|
@@ -51,7 +50,8 @@ export type GateBlock = {
|
|
|
51
50
|
* the write, and a mismatch means somebody else edited it meanwhile */
|
|
52
51
|
hash: string
|
|
53
52
|
state: BlockState
|
|
54
|
-
|
|
53
|
+
/** check id → why this repo turns it off */
|
|
54
|
+
off: Record<string, string>
|
|
55
55
|
tests: TestsConfig
|
|
56
56
|
/** true when a key this block owns is declared outside the markers */
|
|
57
57
|
declaredOutside: boolean
|
|
@@ -65,8 +65,8 @@ export type GateBlock = {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export type GateEdit = {
|
|
68
|
-
/** check id →
|
|
69
|
-
|
|
68
|
+
/** check id → why it is off, or null to turn it back on */
|
|
69
|
+
off?: Record<string, string | null>
|
|
70
70
|
/** suites to declare slow, each with the case `defineRepo` demands */
|
|
71
71
|
slow?: SlowSuite[]
|
|
72
72
|
/** suite files to drop back to the quick tier */
|
|
@@ -109,12 +109,12 @@ const q = (s: string): string => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'
|
|
|
109
109
|
const k = (s: string): string => (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) ? s : q(s))
|
|
110
110
|
|
|
111
111
|
/** the bytes between the markers, markers included */
|
|
112
|
-
export function renderGateBlock(
|
|
112
|
+
export function renderGateBlock(off: Record<string, string>, tests: TestsConfig, pad: string): string[] {
|
|
113
113
|
const out = [`${pad}${GATE_BEGIN}`]
|
|
114
|
-
const ids = Object.keys(
|
|
114
|
+
const ids = Object.keys(off).sort()
|
|
115
115
|
if (ids.length > 0) {
|
|
116
|
-
out.push(`${pad}
|
|
117
|
-
for (const id of ids) out.push(`${pad}${pad}${k(id)}: ${q(
|
|
116
|
+
out.push(`${pad}off: {`)
|
|
117
|
+
for (const id of ids) out.push(`${pad}${pad}${k(id)}: ${q(off[id]!)},`)
|
|
118
118
|
out.push(`${pad}},`)
|
|
119
119
|
}
|
|
120
120
|
const slow = tests.slow ?? []
|
|
@@ -219,13 +219,13 @@ export function configDirty(root: string): boolean {
|
|
|
219
219
|
/** what the block holds today, and whether it still describes the config */
|
|
220
220
|
export async function readGateBlock(root: string): Promise<GateBlock> {
|
|
221
221
|
const path = configPath(root)
|
|
222
|
-
if (!existsSync(path)) return { path, hash: '', state: 'missing-file',
|
|
222
|
+
if (!existsSync(path)) return { path, hash: '', state: 'missing-file', off: {}, tests: {}, declaredOutside: false, words: {}, wordsState: 'no-prose', wordsOutside: false }
|
|
223
223
|
const text = readFileSync(path, 'utf8')
|
|
224
224
|
const lines = text.split('\n')
|
|
225
225
|
const b = lines.findIndex(isBegin)
|
|
226
226
|
const e = lines.findIndex(isEnd)
|
|
227
227
|
const c = await loadRepoConfig(root, { fresh: true })
|
|
228
|
-
const
|
|
228
|
+
const off = c.off ?? {}
|
|
229
229
|
const tests = c.tests ?? {}
|
|
230
230
|
const hash = sha(text)
|
|
231
231
|
const w = locateWords(lines, c)
|
|
@@ -234,7 +234,7 @@ export async function readGateBlock(root: string): Promise<GateBlock> {
|
|
|
234
234
|
// no block: the keys it owns must be unclaimed, or a write would be a
|
|
235
235
|
// second declaration of the same key rather than an edit of the first
|
|
236
236
|
const declaredOutside = OWNED.some((key) => c[key] != null)
|
|
237
|
-
return { path, hash, state: 'missing-block',
|
|
237
|
+
return { path, hash, state: 'missing-block', off, tests, declaredOutside, ...wordsPart }
|
|
238
238
|
}
|
|
239
239
|
// the block DESCRIBES the config when re-rendering the loaded values
|
|
240
240
|
// reproduces it byte for byte. Anything else (a hand edit, a second
|
|
@@ -242,25 +242,25 @@ export async function readGateBlock(root: string): Promise<GateBlock> {
|
|
|
242
242
|
// stale refuses instead of overwriting what somebody wrote
|
|
243
243
|
const pad = indentOf(lines.slice(b + 1, e))
|
|
244
244
|
const current = lines.slice(b, e + 1).join('\n')
|
|
245
|
-
const expected = renderGateBlock(
|
|
246
|
-
return { path, hash, state: current === expected ? 'current' : 'stale',
|
|
245
|
+
const expected = renderGateBlock(off, tests, pad === '' ? indentOf(lines) : pad).join('\n')
|
|
246
|
+
return { path, hash, state: current === expected ? 'current' : 'stale', off, tests, declaredOutside: false, ...wordsPart }
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
/** does what the loader read back say what was written? BOTH keys the block
|
|
250
|
-
* owns, not just `
|
|
250
|
+
* owns, not just `off`: a `tests` entry that loads and parses differently
|
|
251
251
|
* (a renderer that drops a field, a loader that reshapes one) is exactly
|
|
252
252
|
* what reading the result back exists to catch, and comparing half of the
|
|
253
253
|
* block would let that half through. Key and array ORDER are the renderer's,
|
|
254
254
|
* so the comparison is by content. */
|
|
255
|
-
export function roundTripProblem(wrote: {
|
|
256
|
-
const
|
|
255
|
+
export function roundTripProblem(wrote: { off: Record<string, string>; tests: TestsConfig; words?: ProseWords }, loaded: RepoConfig): string | null {
|
|
256
|
+
const normOff = (m: Record<string, string> | undefined): string => JSON.stringify(Object.entries(m ?? {}).sort())
|
|
257
257
|
const normTests = (t: TestsConfig | undefined): string =>
|
|
258
258
|
JSON.stringify({
|
|
259
259
|
budgetMs: t?.budgetMs ?? null,
|
|
260
260
|
slow: (t?.slow ?? []).map((s) => [s.file, s.catches, s.whyNotFast]).sort(),
|
|
261
261
|
serial: (t?.serial ?? []).map((s) => [s.file, s.why]).sort(),
|
|
262
262
|
})
|
|
263
|
-
if (
|
|
263
|
+
if (normOff(loaded.off) !== normOff(wrote.off)) return `the loader read off ${JSON.stringify(loaded.off ?? {})}`
|
|
264
264
|
if (normTests(loaded.tests) !== normTests(wrote.tests)) return `the loader read tests ${JSON.stringify(loaded.tests ?? {})}`
|
|
265
265
|
if (wrote.words != null) {
|
|
266
266
|
const normWords = (x: ProseWords | undefined): string => {
|
|
@@ -315,13 +315,13 @@ export async function writeGateBlock(root: string, edit: GateEdit, o: { expect?:
|
|
|
315
315
|
if (before.state === 'missing-file') throw new Error(`no repo.config.ts in ${root} — run: shipkit init`)
|
|
316
316
|
if (o.expect != null && o.expect !== before.hash)
|
|
317
317
|
throw new Error(`repo.config.ts changed since you read it, reload the page and set it again, so this edit cannot overwrite one made elsewhere`)
|
|
318
|
-
const touchGate = edit.
|
|
318
|
+
const touchGate = edit.off != null || edit.slow != null || edit.slowRemove != null
|
|
319
319
|
const touchWords = edit.words != null
|
|
320
|
-
if (!touchGate && !touchWords) throw new Error(`nothing to write: the edit names no
|
|
320
|
+
if (!touchGate && !touchWords) throw new Error(`nothing to write: the edit names no off, no slow suite and no prose word`)
|
|
321
321
|
if (touchGate && before.state === 'stale')
|
|
322
322
|
throw new Error(`the shipkit gate block in repo.config.ts no longer matches what the config loads, so it was hand edited or something else declares these keys — reconcile it by hand, then use the switch again`)
|
|
323
323
|
if (touchGate && before.state === 'missing-block' && before.declaredOutside)
|
|
324
|
-
throw new Error(`repo.config.ts declares \`
|
|
324
|
+
throw new Error(`repo.config.ts declares \`off\` or \`tests\` outside the managed block — move that declaration between the markers first, or the switch would write a duplicate key`)
|
|
325
325
|
if (touchWords && before.wordsState === 'no-prose')
|
|
326
326
|
throw new Error(`repo.config.ts has no \`prose: {\` line to write the word lists under — declare \`prose: { include: [...] }\` first, the lists only matter to a repo rule SK014 checks`)
|
|
327
327
|
if (touchWords && before.wordsState === 'stale')
|
|
@@ -331,12 +331,11 @@ export async function writeGateBlock(root: string, edit: GateEdit, o: { expect?:
|
|
|
331
331
|
|
|
332
332
|
// the next values, validated the way defineRepo validates them, BEFORE
|
|
333
333
|
// anything reaches the file
|
|
334
|
-
const
|
|
335
|
-
for (const [id,
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
when[id] = raw
|
|
334
|
+
const off: Record<string, string> = { ...before.off }
|
|
335
|
+
for (const [id, why] of Object.entries(edit.off ?? {})) {
|
|
336
|
+
if (why == null) { delete off[id]; continue }
|
|
337
|
+
if (typeof why !== 'string' || why.trim() === '') throw new Error(`off.${id} needs a reason: a check nobody can explain is a silenced gate`)
|
|
338
|
+
off[id] = why.trim()
|
|
340
339
|
}
|
|
341
340
|
const slow = [...(before.tests.slow ?? [])]
|
|
342
341
|
for (const s of edit.slow ?? []) {
|
|
@@ -360,7 +359,7 @@ export async function writeGateBlock(root: string, edit: GateEdit, o: { expect?:
|
|
|
360
359
|
const b = lines.findIndex(isBegin)
|
|
361
360
|
const e = lines.findIndex(isEnd)
|
|
362
361
|
const pad = b === -1 ? indentOf(lines) : indentOf(lines.slice(b + 1, e)) || indentOf(lines)
|
|
363
|
-
const block = renderGateBlock(
|
|
362
|
+
const block = renderGateBlock(off, tests, pad)
|
|
364
363
|
if (b !== -1 && e !== -1) out = [...lines.slice(0, b), ...block, ...lines.slice(e + 1)]
|
|
365
364
|
else {
|
|
366
365
|
// the anchor is the object literal `defineRepo({` opens. Without it
|
|
@@ -391,7 +390,7 @@ export async function writeGateBlock(root: string, edit: GateEdit, o: { expect?:
|
|
|
391
390
|
// round trip is a broken config, so the previous bytes go back
|
|
392
391
|
try {
|
|
393
392
|
const after = await loadRepoConfig(root, { fresh: true })
|
|
394
|
-
const problem = roundTripProblem({
|
|
393
|
+
const problem = roundTripProblem({ off, tests, words }, after)
|
|
395
394
|
if (problem != null) throw new Error(`the block did not round trip: ${problem}`)
|
|
396
395
|
} catch (err) {
|
|
397
396
|
atomicWrite(before.path, text)
|
package/src/core/when.ts
CHANGED
|
@@ -1,42 +1,34 @@
|
|
|
1
|
-
//
|
|
1
|
+
// THE CHECKS A REPO TURNS OFF: `off: { <id>: '<why>' }` in repo.config.ts,
|
|
2
|
+
// the switch the hub writes inside its managed block. Where a check runs is
|
|
3
|
+
// its own declaration (core/runs.ts); the one thing a repo says about a check
|
|
4
|
+
// it does not own is that it does not run here, and why. An off row runs in
|
|
5
|
+
// no context, and every surface shows it with its reason.
|
|
2
6
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
// Four answers, and they are the words on screen:
|
|
10
|
-
// always every gate and every health pass
|
|
11
|
-
// release out of the quick gate, in at `--full` and on the release path
|
|
12
|
-
// health the health pass (`shipkit check`) only, never a gate
|
|
13
|
-
// off:<why> not run at all, and the reason is mandatory
|
|
14
|
-
//
|
|
15
|
-
// The third word says what it MEANS, never which command runs it: a value
|
|
16
|
-
// nobody can read is a switch nobody moves.
|
|
17
|
-
import type { TestTier, CheckRow } from '../ledger/gate-checks.ts'
|
|
7
|
+
// Two retired spellings still read as off while SK029 ports them: a `when`
|
|
8
|
+
// entry `'off:<why>'` (defineRepo refuses the other words) and `ruleExempt`.
|
|
9
|
+
// They die once `shipkit fleet` shows no repo carrying them.
|
|
10
|
+
import type { CheckRow } from '../ledger/gate-checks.ts'
|
|
11
|
+
import type { ResolvedRuns } from './runs.ts'
|
|
18
12
|
import type { RepoConfig } from '../repo-config.ts'
|
|
19
13
|
|
|
14
|
+
/** the coarse word a resolved row reads as on the check page */
|
|
20
15
|
export type WhenAt = 'always' | 'release' | 'health' | 'off'
|
|
21
16
|
|
|
22
|
-
/** which declaration is answering: the `
|
|
23
|
-
* the check file
|
|
24
|
-
|
|
25
|
-
export type WhenSource = 'when' | 'check-file' | 'ruleExempt' | 'optIn' | 'gate-export' | 'defaults' | 'default'
|
|
17
|
+
/** which declaration is answering: the repo's `off`, one of its two retired
|
|
18
|
+
* spellings, the check file, the `defaults` block, or a derived row */
|
|
19
|
+
export type WhenSource = 'off' | 'when' | 'ruleExempt' | 'check-file' | 'defaults' | 'default'
|
|
26
20
|
|
|
27
21
|
export type WhenDecl = { at: WhenAt; reason: string | null; source: WhenSource }
|
|
28
22
|
|
|
29
23
|
export const WHEN_AT: readonly WhenAt[] = ['always', 'release', 'health', 'off']
|
|
30
24
|
|
|
31
|
-
/** the words a
|
|
25
|
+
/** the words a retired `when` value used */
|
|
32
26
|
const WORDS: Record<string, WhenAt> = { always: 'always', release: 'release', health: 'health' }
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/** one config value, or the problem with it. A string back is the message
|
|
37
|
-
* `defineRepo` throws, so a bad value dies at config load and never mid gate */
|
|
28
|
+
/** one retired `when` value, or the problem with it. defineRepo refuses every
|
|
29
|
+
* word but `off:<why>`, which reads as `off` */
|
|
38
30
|
export function parseWhen(raw: unknown): WhenDecl | string {
|
|
39
|
-
if (typeof raw !== 'string') return `must be a string:
|
|
31
|
+
if (typeof raw !== 'string') return `must be a string: off:<reason>`
|
|
40
32
|
const v = raw.trim()
|
|
41
33
|
if (v === 'off' || v.startsWith('off:')) {
|
|
42
34
|
const reason = v.slice(4).trim()
|
|
@@ -44,92 +36,38 @@ export function parseWhen(raw: unknown): WhenDecl | string {
|
|
|
44
36
|
return { at: 'off', reason, source: 'when' }
|
|
45
37
|
}
|
|
46
38
|
const at = WORDS[v]
|
|
47
|
-
if (at == null) return `'${v}' is not a when value (
|
|
39
|
+
if (at == null) return `'${v}' is not a when value (off:<reason>)`
|
|
48
40
|
return { at, reason: null, source: 'when' }
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
/** the
|
|
52
|
-
|
|
53
|
-
* means the config was never loaded through it */
|
|
54
|
-
export function declaredWhen(c: RepoConfig, id: string): WhenDecl | null {
|
|
55
|
-
const raw = c.when?.[id]
|
|
56
|
-
if (raw == null) return null
|
|
57
|
-
const w = parseWhen(raw)
|
|
58
|
-
return typeof w === 'string' ? null : w
|
|
59
|
-
}
|
|
43
|
+
/** the repo turned this check off, with why and where it said so, or null */
|
|
44
|
+
export type RepoOff = { reason: string; source: 'off' | 'when' | 'ruleExempt' }
|
|
60
45
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
46
|
+
export function repoOff(c: RepoConfig, id: string): RepoOff | null {
|
|
47
|
+
const own = c.off?.[id]
|
|
48
|
+
if (own != null) return { reason: own, source: 'off' }
|
|
49
|
+
const w = c.when?.[id] == null ? null : parseWhen(c.when[id])
|
|
50
|
+
if (w != null && typeof w !== 'string' && w.at === 'off') return { reason: w.reason ?? '', source: 'when' }
|
|
65
51
|
const exempt = c.ruleExempt?.[id]
|
|
66
|
-
|
|
67
|
-
return DEFAULT
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** a task: `when` wins, then `optIn`, which means off the default gate and
|
|
71
|
-
* reachable by name (`--with <id>`), which is what `health` means for a task */
|
|
72
|
-
export function whenOfTask(c: RepoConfig, t: { id: string; optIn?: boolean }): WhenDecl {
|
|
73
|
-
const w = declaredWhen(c, t.id)
|
|
74
|
-
if (w != null) return w
|
|
75
|
-
if (t.optIn === true) return { at: 'health', reason: null, source: 'optIn' }
|
|
76
|
-
return DEFAULT
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** a floor slot the repo turned off with `defaults: { <id>: false }`. That
|
|
80
|
-
* declaration REMOVES the task, so it wins over any `when`: a row claiming
|
|
81
|
-
* always over a slot that resolves to nothing is a check the gate never
|
|
82
|
-
* runs and nobody can see. The source is the `defaults` block itself, and
|
|
83
|
-
* the reason names the line, so a page can say which declaration answered */
|
|
84
|
-
export function whenOfDisabledSlot(id: string): WhenDecl {
|
|
85
|
-
return { at: 'off', reason: `defaults.${id}: false`, source: 'defaults' }
|
|
52
|
+
return exempt == null ? null : { reason: exempt, source: 'ruleExempt' }
|
|
86
53
|
}
|
|
87
54
|
|
|
88
|
-
/** a
|
|
89
|
-
|
|
90
|
-
export function whenOfCheck(c: RepoConfig, u: { id: string; def: { when?: string } }): WhenDecl {
|
|
91
|
-
const w = declaredWhen(c, u.id)
|
|
92
|
-
if (w != null) return w
|
|
93
|
-
// the legacy spelling still moves a check the repo does not own, and a
|
|
94
|
-
// repo statement wins over the file's own default
|
|
95
|
-
const exempt = c.ruleExempt?.[u.id]
|
|
96
|
-
if (exempt != null) return { at: 'off', reason: exempt, source: 'ruleExempt' }
|
|
97
|
-
if (u.def.when == null) return DEFAULT
|
|
98
|
-
const own = parseWhen(u.def.when)
|
|
99
|
-
// `check()` already refused anything unparseable, so reaching this with a
|
|
100
|
-
// string means the object never went through it
|
|
101
|
-
return typeof own === 'string' ? DEFAULT : { ...own, source: 'check-file' }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** a discovered check: `when` wins, then the file's own `export const gate` */
|
|
105
|
-
export function whenOfLegacyCheck(c: RepoConfig, d: { name: string; gate: boolean }): WhenDecl {
|
|
106
|
-
const w = declaredWhen(c, d.name)
|
|
107
|
-
if (w != null) return w
|
|
108
|
-
if (!d.gate) return { at: 'health', reason: null, source: 'gate-export' }
|
|
109
|
-
return DEFAULT
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** does the GATE run it at this tier? `shipkit check` asks `runsInCheck` */
|
|
113
|
-
export function runsInGate(w: WhenDecl, tier: TestTier): boolean {
|
|
114
|
-
if (w.at === 'off' || w.at === 'health') return false
|
|
115
|
-
return w.at === 'release' ? tier === 'full' : true
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** does the health pass run it? everything but off: `shipkit check` is where
|
|
119
|
-
* a proactive probe belongs, and only off means never */
|
|
120
|
-
export function runsInCheck(w: WhenDecl): boolean {
|
|
121
|
-
return w.at !== 'off'
|
|
122
|
-
}
|
|
55
|
+
/** a resolved row the repo turned off: it runs in no context */
|
|
56
|
+
export const withRepoOff = (base: ResolvedRuns, off: RepoOff | null): ResolvedRuns => (off == null ? base : { ...base, off: off.reason })
|
|
123
57
|
|
|
124
|
-
/** the
|
|
125
|
-
|
|
126
|
-
|
|
58
|
+
/** the coarse word a resolved row reads as, for the check page. Derived,
|
|
59
|
+
* never declared */
|
|
60
|
+
export function whenSummary(r: ResolvedRuns, off: RepoOff | { reason: string; source: 'defaults' } | null, fromFile: boolean): WhenDecl {
|
|
61
|
+
const source: WhenSource = off?.source ?? (fromFile ? 'check-file' : 'default')
|
|
62
|
+
if (r.off != null) return { at: 'off', reason: r.off, source }
|
|
63
|
+
const at: WhenAt = r.onCi ? 'always' : r.onFull || r.onRelease ? 'release' : r.onHealth || r.onSave !== false || r.onClock !== false ? 'health' : 'off'
|
|
64
|
+
return { at, reason: at === 'off' ? 'no context runs it' : null, source }
|
|
127
65
|
}
|
|
128
66
|
|
|
129
|
-
/** ids
|
|
130
|
-
* the rule namespace: a task is resolved from the floor and a
|
|
131
|
-
* discovered on disk, so this is where the rest is caught, loud */
|
|
132
|
-
export function
|
|
67
|
+
/** ids the repo turns off that name no check of this repo. `defineRepo` can
|
|
68
|
+
* only check the rule namespace: a task is resolved from the floor and a
|
|
69
|
+
* check is discovered on disk, so this is where the rest is caught, loud */
|
|
70
|
+
export function unknownOffIds(rows: CheckRow[], c: RepoConfig): string[] {
|
|
133
71
|
const known = new Set(rows.map((r) => r.decl.id))
|
|
134
|
-
return Object.keys(c.when ?? {}).filter((id) => !known.has(id))
|
|
72
|
+
return [...new Set([...Object.keys(c.off ?? {}), ...Object.keys(c.when ?? {})])].filter((id) => !known.has(id))
|
|
135
73
|
}
|
package/src/core/wire.ts
CHANGED
|
@@ -3,31 +3,27 @@
|
|
|
3
3
|
// exit 0/1. FROZEN: consumers grep startsWith('FAIL'/'WARN'/'skip:') — a
|
|
4
4
|
// prefix change is a breaking change of the whole toolkit.
|
|
5
5
|
|
|
6
|
-
import { CACHED, RAN } from './outcome-words.ts'
|
|
6
|
+
import { CACHED, costShort, RAN, spanShort } from './outcome-words.ts'
|
|
7
|
+
|
|
8
|
+
export { spanShort }
|
|
7
9
|
|
|
8
10
|
/** when a remembered world answer was measured, and until when it keeps
|
|
9
11
|
* (null: until its declaration changes). Kept as data, never as text, and
|
|
10
12
|
* rendered against the reader's clock: a time frozen into the line reads
|
|
11
13
|
* as a future expiry long after it passed */
|
|
12
|
-
export type Stamp = { at: number; until: number | null; cached?: boolean }
|
|
14
|
+
export type Stamp = { at: number; until: number | null; cached?: boolean; ranMs?: number }
|
|
13
15
|
|
|
14
16
|
export type Line = { kind: 'ok' | 'warn' | 'fail' | 'note' | 'fix-warn' | 'fix-fail' | 'skip'; text: string; stamp?: Stamp }
|
|
15
17
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (s < 60) return `${s}s`
|
|
20
|
-
if (s < 3600) return `${Math.floor(s / 60)}m`
|
|
21
|
-
if (s < 86400) return `${Math.floor(s / 3600)}h`
|
|
22
|
-
return `${Math.floor(s / 86400)}d`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** `cached, ran 3h ago, kept 57m more`, `ran 1d ago, expired 1d ago` */
|
|
18
|
+
/** `cached, 3h old, kept 57m more` for a replay, `ran 0.4s, kept 1h more`
|
|
19
|
+
* for an answer this run measured, `ran 1d ago, expired 1d ago` for one
|
|
20
|
+
* read back later */
|
|
26
21
|
export function stampWords(s: Stamp, now: number): string {
|
|
27
22
|
const age = now - s.at
|
|
28
|
-
const ran = age < 1000 ? `${RAN} just now` : `${RAN} ${spanShort(age)} ago`
|
|
29
23
|
const keeps = s.until == null ? 'kept until its declaration changes' : s.until > now ? `kept ${spanShort(s.until - now)} more` : `expired ${spanShort(now - s.until)} ago`
|
|
30
|
-
|
|
24
|
+
if (s.cached === true) return `${CACHED}, ${spanShort(age)} old, ${keeps}`
|
|
25
|
+
if (s.ranMs != null) return `${RAN} ${costShort(s.ranMs)}, ${keeps}`
|
|
26
|
+
return `${age < 1000 ? `${RAN} just now` : `${RAN} ${spanShort(age)} ago`}, ${keeps}`
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
/** a line's text as a reader sees it now, its stamp rendered */
|
package/src/fleet/scan-fleet.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
|
14
14
|
import { join } from 'path'
|
|
15
15
|
import { isArchived } from '../core/archived.ts'
|
|
16
16
|
import { detectPackageManager, isRefusal } from '../core/package-manager.ts'
|
|
17
|
-
import { isBreaking } from '../core/update-policy.ts'
|
|
17
|
+
import { isBreaking, isLinkedInstall, pinFloor } from '../core/update-policy.ts'
|
|
18
18
|
|
|
19
19
|
export type Pkg = { name?: string; version?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
20
20
|
|
|
@@ -24,6 +24,11 @@ export type FleetDep = {
|
|
|
24
24
|
pin: string
|
|
25
25
|
/** what node_modules actually carries, null when nothing is installed */
|
|
26
26
|
installed: string | null
|
|
27
|
+
/** node_modules/<name> is a `bun link` symlink to a source tree */
|
|
28
|
+
linkedInstall: boolean
|
|
29
|
+
/** the version freshness is judged by: the pin's floor under a link
|
|
30
|
+
* (a fresh clone installs the pin, never the source), else `installed` */
|
|
31
|
+
at: string | null
|
|
27
32
|
/** npm latest, null when the registry was not consulted or not reachable */
|
|
28
33
|
latest: string | null
|
|
29
34
|
}
|
|
@@ -54,6 +59,13 @@ const readPkg = (dir: string): Pkg | null => {
|
|
|
54
59
|
|
|
55
60
|
export const installedVersion = (dir: string, name: string): string | null => readPkg(join(dir, 'node_modules', name))?.version ?? null
|
|
56
61
|
|
|
62
|
+
/** what `FleetDep.at` reads now, for a verdict after an update ran */
|
|
63
|
+
export const judgedVersion = (dir: string, name: string): string | null => {
|
|
64
|
+
if (!isLinkedInstall(dir, name)) return installedVersion(dir, name)
|
|
65
|
+
const pkg = readPkg(dir)
|
|
66
|
+
return pinFloor(pkg?.dependencies?.[name] ?? pkg?.devDependencies?.[name] ?? '')
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
/** semver-ish compare, enough to answer "is this one behind that one". A
|
|
58
70
|
* prerelease tag sorts before its release, which is the safe direction: it
|
|
59
71
|
* reports behind rather than pretending parity. */
|
|
@@ -89,7 +101,11 @@ export function scanFleet(root: string, o: { prefix?: string } = {}): FleetRepo[
|
|
|
89
101
|
const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }
|
|
90
102
|
const names = Object.keys(all).filter((n) => n.startsWith(prefix))
|
|
91
103
|
if (names.length === 0) continue
|
|
92
|
-
const deps: FleetDep[] = names.sort().map((name) =>
|
|
104
|
+
const deps: FleetDep[] = names.sort().map((name) => {
|
|
105
|
+
const installed = installedVersion(dir, name)
|
|
106
|
+
const linkedInstall = isLinkedInstall(dir, name)
|
|
107
|
+
return { name, pin: all[name]!, installed, linkedInstall, at: linkedInstall ? pinFloor(all[name]!) : installed, latest: null }
|
|
108
|
+
})
|
|
93
109
|
out.push({
|
|
94
110
|
name: e,
|
|
95
111
|
dir,
|
|
@@ -120,8 +136,8 @@ export async function resolveLatest(repos: FleetRepo[], fetchLatest: (name: stri
|
|
|
120
136
|
)
|
|
121
137
|
for (const r of repos) {
|
|
122
138
|
for (const d of r.deps) d.latest = latest.get(d.name) ?? null
|
|
123
|
-
r.behind = r.deps.filter((d) => d.
|
|
124
|
-
r.breaking = r.behind.filter((d) => isBreaking(d.
|
|
139
|
+
r.behind = r.deps.filter((d) => d.at != null && d.latest != null && isBehind(d.at, d.latest))
|
|
140
|
+
r.breaking = r.behind.filter((d) => isBreaking(d.at!, d.latest!))
|
|
125
141
|
}
|
|
126
142
|
return latest
|
|
127
143
|
}
|
|
@@ -7,7 +7,7 @@ import { join } from 'path'
|
|
|
7
7
|
import { spawnWithTimeout } from '../core/spawn-timeout.ts'
|
|
8
8
|
import { detectPackageManager, isRefusal } from '../core/package-manager.ts'
|
|
9
9
|
import { ALLOW_MAJOR_ENV, breakingAdvice } from '../core/update-policy.ts'
|
|
10
|
-
import {
|
|
10
|
+
import { isBehind, judgedVersion, updateRefusal, type FleetRepo } from './scan-fleet.ts'
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
export const short = (name: string): string => name.replace(/^rvlib-/, '')
|
|
@@ -64,15 +64,15 @@ export async function updateFleet(behind: FleetRepo[], o: FleetUpdateOpts): Prom
|
|
|
64
64
|
const env: Record<string, string> = Object.fromEntries(Object.entries(process.env).filter((e): e is [string, string] => e[1] != null && e[0] !== 'SHIPKIT_SELF_UPDATED' && e[0] !== ALLOW_MAJOR_ENV))
|
|
65
65
|
if (o.major) env[ALLOW_MAJOR_ENV] = '1'
|
|
66
66
|
const res = await spawnWithTimeout(['bun', cli, 'check', '--fresh'], { cwd: r.dir, env, timeoutMs: o.timeoutMs ?? 600_000 })
|
|
67
|
-
// judge by what node_modules carries now
|
|
68
|
-
// printed line: the code covers every rule
|
|
69
|
-
// format belongs to whatever version that repo runs
|
|
67
|
+
// judge by what node_modules carries now (the pin, for a linked dep),
|
|
68
|
+
// never by the exit code or a printed line: the code covers every rule
|
|
69
|
+
// in that repo, and the line format belongs to whatever version that repo runs
|
|
70
70
|
const stuck = wanted.filter((d) => {
|
|
71
|
-
const now =
|
|
71
|
+
const now = judgedVersion(r.dir, d.name)
|
|
72
72
|
return now == null || isBehind(now, d.latest!)
|
|
73
73
|
})
|
|
74
74
|
if (stuck.length === 0) {
|
|
75
|
-
w(`ok: ${r.name} — ${wanted.map((d) => `${short(d.name)} ${d.
|
|
75
|
+
w(`ok: ${r.name} — ${wanted.map((d) => `${short(d.name)}${d.linkedInstall ? ' pin' : ''} ${d.at} → ${judgedVersion(r.dir, d.name)}`).join(', ')}`)
|
|
76
76
|
continue
|
|
77
77
|
}
|
|
78
78
|
failed++
|
package/src/home/home-launchd.ts
CHANGED
|
@@ -22,7 +22,10 @@ const esc = (s: string): string => s.replace(/&/g, '&').replace(/</g, '<'
|
|
|
22
22
|
* every command, so any edit would restart pm. pm,
|
|
23
23
|
* the daemon and the web each watch their own code and restart in place on
|
|
24
24
|
* it (lib-watch.ts). PATH is the installing shell's, since launchd starts
|
|
25
|
-
* with almost none and the daemon needs bun, git, ps, lsof and ssh
|
|
25
|
+
* with almost none and the daemon needs bun, git, ps, lsof and ssh.
|
|
26
|
+
* ProcessType Interactive: with none, launchd throttles the agent's disk
|
|
27
|
+
* reads under load, every child inherits it, and a busy disk left the
|
|
28
|
+
* daemon's boot reads minutes long while its port was already open */
|
|
26
29
|
export function renderPlist(o: { bun: string; cli: string; root: string; path: string; log: string }): string {
|
|
27
30
|
const args = [o.bun, o.cli, 'pm'].map((a) => `\t\t<string>${esc(a)}</string>`).join('\n')
|
|
28
31
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -42,6 +45,8 @@ ${args}
|
|
|
42
45
|
\t\t<key>PATH</key>
|
|
43
46
|
\t\t<string>${esc(o.path)}</string>
|
|
44
47
|
\t</dict>
|
|
48
|
+
\t<key>ProcessType</key>
|
|
49
|
+
\t<string>Interactive</string>
|
|
45
50
|
\t<key>RunAtLoad</key>
|
|
46
51
|
\t<true/>
|
|
47
52
|
\t<key>KeepAlive</key>
|
|
@@ -73,7 +78,13 @@ const launchctl: Launchctl = async (args) => {
|
|
|
73
78
|
|
|
74
79
|
const domain = (): string => `gui/${process.getuid?.() ?? 501}`
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
/** `bootout` returns before launchd lets go of the label, and a bootstrap in
|
|
82
|
+
* that window fails (exit 5, EIO) with the old agent already gone: retried
|
|
83
|
+
* this many times, BOOTSTRAP_STEP_MS apart, before the install fails */
|
|
84
|
+
export const BOOTSTRAP_TRIES = 20
|
|
85
|
+
const BOOTSTRAP_STEP_MS = 250
|
|
86
|
+
|
|
87
|
+
export async function installHome(o: { root: string; cli: string; print: (l: string) => void; run?: Launchctl; sleep?: (ms: number) => Promise<void> }): Promise<number> {
|
|
77
88
|
if (process.platform !== 'darwin') {
|
|
78
89
|
o.print(`FAIL: the pm LaunchAgent is macOS only. Run \`shipkit pm\` under your own supervisor on ${process.platform}`)
|
|
79
90
|
return 1
|
|
@@ -89,9 +100,14 @@ export async function installHome(o: { root: string; cli: string; print: (l: str
|
|
|
89
100
|
await run(['bootout', `${domain()}/${LABEL}`])
|
|
90
101
|
await run(['bootout', `${domain()}/${LEGACY_LABEL}`])
|
|
91
102
|
rmSync(plistPath(homedir(), LEGACY_LABEL), { force: true })
|
|
92
|
-
const
|
|
103
|
+
const sleep = o.sleep ?? Bun.sleep
|
|
104
|
+
let r = await run(['bootstrap', domain(), file])
|
|
105
|
+
for (let i = 1; i < BOOTSTRAP_TRIES && r.code !== 0; i++) {
|
|
106
|
+
await sleep(BOOTSTRAP_STEP_MS)
|
|
107
|
+
r = await run(['bootstrap', domain(), file])
|
|
108
|
+
}
|
|
93
109
|
if (r.code !== 0) {
|
|
94
|
-
o.print(`FAIL: launchctl bootstrap refused ${file} (exit ${r.code}): ${r.out}`)
|
|
110
|
+
o.print(`FAIL: launchctl bootstrap refused ${file} (exit ${r.code}, ${BOOTSTRAP_TRIES} tries): ${r.out}. pm is not loaded now: run \`shipkit pm install\` again`)
|
|
95
111
|
return 1
|
|
96
112
|
}
|
|
97
113
|
o.print(`ok: pm runs now and at every login, from ${o.root}, and runs the daemon and the web`)
|
package/src/home/home-routes.ts
CHANGED
|
@@ -16,8 +16,11 @@ import { tailLines } from './supervise.ts'
|
|
|
16
16
|
import { Hubs, type WorkerView } from './hubs.ts'
|
|
17
17
|
import type { Planner, PlanRow } from '../plan/planner.ts'
|
|
18
18
|
import type { Widgets, WidgetView } from './widgets.ts'
|
|
19
|
-
import { ActivityCache, windowMinutes } from './activity.ts'
|
|
19
|
+
import { ActivityCache, repoActivity, windowMinutes } from './activity.ts'
|
|
20
20
|
import { HouseRuns } from '../serve/house-runs.ts'
|
|
21
|
+
|
|
22
|
+
/** how far back a repo's git page lists its commits */
|
|
23
|
+
export const GIT_PAGE_MS = 7 * 24 * 3600_000
|
|
21
24
|
import type { HouseView } from '../ledger/house-runs.ts'
|
|
22
25
|
|
|
23
26
|
/** how often the daemon looks at every repo */
|
|
@@ -173,6 +176,14 @@ export function homeRoutes(d: HomeDeps) {
|
|
|
173
176
|
const q = new URL(req.url).searchParams
|
|
174
177
|
return Response.json(await activity.get(repos, windowMinutes(q.get('m')), { fresh: q.get('fresh') === '1' }))
|
|
175
178
|
}
|
|
179
|
+
// one repo's git for its git page: a week of commits, and HEAD's last
|
|
180
|
+
// one past that, whatever window the activity page shows
|
|
181
|
+
if (path === '/api/home/git.json') {
|
|
182
|
+
if (repos.length === 0) await refresh()
|
|
183
|
+
const r = find(new URL(req.url).searchParams.get('repo'))
|
|
184
|
+
if (r == null) return new Response('not a house repo', { status: 404 })
|
|
185
|
+
return Response.json(await repoActivity(r, Date.now() - GIT_PAGE_MS, (n) => d.plan?.declared(n) ?? null, (n) => d.plan?.titles(n) ?? null))
|
|
186
|
+
}
|
|
176
187
|
if (path === '/api/house-runs.json') {
|
|
177
188
|
if (repos.length === 0) await refresh()
|
|
178
189
|
const view: HouseView = { ...house.view(), held: d.plan?.held() ?? [] }
|
package/src/home/hubs.ts
CHANGED
|
@@ -132,7 +132,7 @@ export class Hubs {
|
|
|
132
132
|
const rt = await this.o.load(e.repo, io, { fresh: fresh || e.loads > 0 })
|
|
133
133
|
// a stop by hand came while it loaded, and wins
|
|
134
134
|
if (e.held) {
|
|
135
|
-
rt.stop(
|
|
135
|
+
rt.stop()
|
|
136
136
|
this.set(e, 'down', 'stopped by hand')
|
|
137
137
|
return
|
|
138
138
|
}
|
|
@@ -158,7 +158,7 @@ export class Hubs {
|
|
|
158
158
|
const names = new Set(repos.map((r) => r.name))
|
|
159
159
|
for (const [name, e] of this.entries) {
|
|
160
160
|
if (names.has(name)) continue
|
|
161
|
-
e.rt?.stop(
|
|
161
|
+
e.rt?.stop()
|
|
162
162
|
this.entries.delete(name)
|
|
163
163
|
}
|
|
164
164
|
for (const r of repos) {
|
|
@@ -179,7 +179,7 @@ export class Hubs {
|
|
|
179
179
|
stop(repo: WiredRepo): WorkerView {
|
|
180
180
|
const e = this.entry(repo)
|
|
181
181
|
e.held = true
|
|
182
|
-
e.rt?.stop(
|
|
182
|
+
e.rt?.stop()
|
|
183
183
|
e.rt = null
|
|
184
184
|
this.set(e, 'down', 'stopped by hand')
|
|
185
185
|
return this.view(e)
|
|
@@ -190,7 +190,7 @@ export class Hubs {
|
|
|
190
190
|
const e = this.entry(repo)
|
|
191
191
|
await e.loading
|
|
192
192
|
e.held = false
|
|
193
|
-
e.rt?.stop(
|
|
193
|
+
e.rt?.stop()
|
|
194
194
|
e.rt = null
|
|
195
195
|
await this.schedule(e, true)
|
|
196
196
|
return this.view(e)
|
|
@@ -279,7 +279,7 @@ export class Hubs {
|
|
|
279
279
|
return repos.map((r) => this.view(this.entry(r)))
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
/** every hub stops
|
|
282
|
+
/** every hub stops */
|
|
283
283
|
stopAll(): void {
|
|
284
284
|
for (const e of this.entries.values()) e.rt?.stop()
|
|
285
285
|
}
|