rvlib-shipkit 0.38.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +379 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/derived.ts +1 -1
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +49 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +16 -3
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +257 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +616 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +88 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +152 -189
- package/src/core/run-fix.ts +99 -0
- package/src/core/run-fixers.ts +64 -33
- package/src/core/self-update.ts +11 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/fleet/scan-fleet.ts +16 -0
- package/src/hooks/leak-guard.ts +20 -9
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +38 -26
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +157 -88
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +9 -4
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +209 -15
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/scan-md.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
package/src/serve/run-serve.ts
CHANGED
|
@@ -2,25 +2,49 @@
|
|
|
2
2
|
// comes from convention + repo.config's `serve` block: the wiki graph is
|
|
3
3
|
// always on (custom provider via serve.wikiGraph, else the generic md
|
|
4
4
|
// scan), the base-context wiki when a CLAUDE.md include names one, agent/
|
|
5
|
-
// graph when the dir exists, the studio when .
|
|
5
|
+
// graph when the dir exists, the studio when .shipkit/assets/
|
|
6
6
|
// kit.ts loads. Port: --port > serve.port > a deterministic house-range
|
|
7
7
|
// default (4800-4979, hashed from the repo name — stable across launches;
|
|
8
8
|
// pin serve.port when two repos collide).
|
|
9
|
-
import { existsSync, readFileSync } from 'fs'
|
|
9
|
+
import { existsSync, readFileSync, statSync } from 'fs'
|
|
10
10
|
import { homedir } from 'os'
|
|
11
11
|
import { basename, dirname, join, resolve, sep } from 'path'
|
|
12
12
|
import { fileURLToPath } from 'url'
|
|
13
13
|
import { loadKit, type ResolvedKit } from '../assets/kit-config.ts'
|
|
14
14
|
import { studioRoutes, type StudioRoutes } from '../assets/server/studio-server.ts'
|
|
15
15
|
import { projectNeeds } from '../collections/collection-types.ts'
|
|
16
|
-
import {
|
|
16
|
+
import { comfyCollection } from '../collections/comfy.ts'
|
|
17
|
+
import { ComfyPanel, comfyBox, defaultComfyPanelPort, scanComfyWorkflows } from '../comfy/comfy-panel.ts'
|
|
18
|
+
import { wikisCollection } from '../collections/wikis.ts'
|
|
17
19
|
import { pingTick } from '../collections/pings.ts'
|
|
18
20
|
import { buildCollections } from '../collections/providers.ts'
|
|
19
21
|
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
22
|
+
import { CheckCtx } from '../core/check-ctx.ts'
|
|
23
|
+
import { proseView } from '../prose/prose-view.ts'
|
|
24
|
+
import { allTasks, gateInventory } from '../ledger/gate-checks.ts'
|
|
25
|
+
import { runCheck } from '../core/run-check.ts'
|
|
26
|
+
import { runCi, type CiOpts } from '../core/run-ci.ts'
|
|
20
27
|
import type { GraphMount } from '../wiki/graph-mount.ts'
|
|
21
28
|
import { scanMdGraph } from '../wiki/scan-md.ts'
|
|
29
|
+
import { lastHealthPassAt, readInvocations, readLive, readRuns, readRunsSince, runsPath, sweepLive } from '../ledger/run-log.ts'
|
|
30
|
+
import { LIVE_HISTORY_MS, shapeLive, type LiveView } from '../ledger/live-timeline.ts'
|
|
31
|
+
import { parallelCap } from '../core/parallel.ts'
|
|
32
|
+
import { reclaimPreviousImage } from '../core/instance.ts'
|
|
33
|
+
import { isLinkedWorktree } from '../core/git-root.ts'
|
|
34
|
+
import { OtherRepos } from './other-repos.ts'
|
|
35
|
+
import { recordStamp } from './project-gutter.ts'
|
|
36
|
+
import { shapeTimeline } from '../ledger/run-timeline.ts'
|
|
37
|
+
import { readSuiteRuns, readSuites } from '../ledger/suite-log.ts'
|
|
38
|
+
import { shapeTestCheck, shapeTestsRoot } from '../collections/test-time.ts'
|
|
39
|
+
import type { TestFacts } from '../collections/collection-types.ts'
|
|
40
|
+
import { declaredSlowFiles } from '../suite/test-units.ts'
|
|
41
|
+
import { BUILD_TIMEOUT_MS, CHECK_TIMEOUT_MS } from '../collections/checks.ts'
|
|
42
|
+
import { HealthSchedule } from './health-schedule.ts'
|
|
43
|
+
import { GateTrigger, watchTracked } from './gate-trigger.ts'
|
|
44
|
+
import { ProblemBoard } from '../problems/problem-board.ts'
|
|
22
45
|
import { serveHub, type HubDeps } from './serve-hub.ts'
|
|
23
46
|
import { defaultHubPort } from './hub-port.ts'
|
|
47
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
24
48
|
export { defaultHubPort }
|
|
25
49
|
|
|
26
50
|
export type ServeIo = { print: (l: string) => void; printErr: (l: string) => void }
|
|
@@ -53,10 +77,42 @@ export function baseWikiRoot(root: string): string | null {
|
|
|
53
77
|
|
|
54
78
|
// `~/dev/<repo>/wiki` reads as "<repo> wiki" — a dir literally named wiki says
|
|
55
79
|
// nothing on its own, so the owning repo names the card
|
|
80
|
+
/** the tab of the repo's own wiki, beside the house wiki named for its repo */
|
|
81
|
+
const WIKI_LABEL = "this repo's wiki"
|
|
56
82
|
const wikiLabel = (dir: string): string => (basename(dir) === 'wiki' ? `${basename(dirname(dir))} wiki` : basename(dir))
|
|
57
83
|
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
/** the config file's bytes, cheaply: a re-read costs a new module instance,
|
|
85
|
+
* so it happens only when the file really moved */
|
|
86
|
+
function configStamp(root: string): string {
|
|
87
|
+
try {
|
|
88
|
+
const st = statSync(join(root, 'repo.config.ts'))
|
|
89
|
+
return `${st.mtimeMs}:${st.size}`
|
|
90
|
+
} catch {
|
|
91
|
+
return ''
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo, o?: { port?: number }): Promise<HubDeps> {
|
|
96
|
+
// THE SWITCH WRITES repo.config.ts, so the surfaces built on the config
|
|
97
|
+
// read the one on disk rather than the one this process booted with. A
|
|
98
|
+
// toggle that never reached the rows would read as a toggle that did
|
|
99
|
+
// nothing. What is resolved ONCE stays once: the kit, the graphs, the
|
|
100
|
+
// studio and the comfy panel are mounts, and remounting a server under a
|
|
101
|
+
// live page is a different question from re-reading a declaration.
|
|
102
|
+
let c = boot
|
|
103
|
+
let stamp = configStamp(root)
|
|
104
|
+
const refresh = async (): Promise<void> => {
|
|
105
|
+
const now = configStamp(root)
|
|
106
|
+
if (now === stamp || now === '') return
|
|
107
|
+
stamp = now
|
|
108
|
+
try {
|
|
109
|
+
c = await loadRepoConfig(root, { fresh: true })
|
|
110
|
+
} catch (e) {
|
|
111
|
+
io.print(`WARN repo.config.ts did not load, so the page is reading the one this process booted with — ${(e as Error).message}`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ONE list: the routes, the /hub.json tools and the wikis section all
|
|
60
116
|
// read it, so nothing can advertise a graph the server does not serve.
|
|
61
117
|
// The repo's OWN wiki mounts when it has one (a game repo's hub
|
|
62
118
|
// advertising an empty wiki route was startup noise), a config-declared
|
|
@@ -65,54 +121,181 @@ export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o
|
|
|
65
121
|
// agent working here obeys that wiki, so a reader can open it.
|
|
66
122
|
const graphs: GraphMount[] = []
|
|
67
123
|
if (c.serve?.wikiGraph != null)
|
|
68
|
-
graphs.push({ id: 'wiki', label:
|
|
124
|
+
graphs.push({ id: 'wiki', label: WIKI_LABEL, title: c.serve.wikiGraph.title ?? `${c.name} wiki`, load: c.serve.wikiGraph.load, lint: c.serve.wikiGraph.lint != null ? { checks: c.serve.wikiGraph.lint } : { root } })
|
|
69
125
|
else if (existsSync(join(root, 'wiki')))
|
|
70
|
-
graphs.push({ id: 'wiki', label:
|
|
126
|
+
graphs.push({ id: 'wiki', label: WIKI_LABEL, title: `${c.name} wiki`, load: () => scanMdGraph({ root }), lint: { root } })
|
|
71
127
|
const baseWiki = baseWikiRoot(root)
|
|
72
128
|
if (baseWiki != null)
|
|
73
129
|
graphs.push({ id: 'base', label: wikiLabel(baseWiki), title: wikiLabel(baseWiki), load: () => scanMdGraph({ root: baseWiki }), lint: null })
|
|
74
130
|
const agentRoot = join(root, 'agent')
|
|
75
131
|
if (existsSync(agentRoot))
|
|
76
|
-
graphs.push({ id: 'agent', label: 'agent
|
|
132
|
+
graphs.push({ id: 'agent', label: 'agent/', title: `${c.name} agent/`, load: () => scanMdGraph({ root: agentRoot, index: c.serve?.agentIndex }), lint: { root: agentRoot, index: c.serve?.agentIndex } })
|
|
77
133
|
// ONE page for every repo: the studio ALWAYS
|
|
78
134
|
// mounts — kit-less repos get an empty media section, never a second
|
|
79
135
|
// page. A broken kit degrades to the same empty kit, loudly.
|
|
80
136
|
let kit: ResolvedKit | null = null
|
|
81
|
-
if (existsSync(join(root, '.
|
|
137
|
+
if (existsSync(join(root, '.shipkit/assets/kit.ts'))) {
|
|
82
138
|
try {
|
|
83
139
|
kit = await loadKit(root)
|
|
84
140
|
} catch (e) {
|
|
85
141
|
io.print(`WARN kit not loaded (media section empty): ${(e as Error).message}`)
|
|
86
142
|
}
|
|
87
143
|
}
|
|
88
|
-
const emptyKit: ResolvedKit = { kinds: [], workspace: '.
|
|
144
|
+
const emptyKit: ResolvedKit = { kinds: [], workspace: '.shipkit/assets/out' }
|
|
89
145
|
let studio: StudioRoutes | null = null
|
|
90
146
|
try {
|
|
91
147
|
studio = await studioRoutes({ kit: kit ?? emptyKit, root })
|
|
92
148
|
} catch (e) {
|
|
93
149
|
io.print(`WARN media API not mounted: ${(e as Error).message}`)
|
|
94
150
|
}
|
|
95
|
-
|
|
151
|
+
// a repo with no workflow module gets NO comfy section and no panel
|
|
152
|
+
// process: an empty section header is a surface that teaches nothing
|
|
153
|
+
const hasComfy = scanComfyWorkflows(root).length > 0
|
|
154
|
+
const panel = hasComfy ? new ComfyPanel({ root, port: c.serve?.comfyPort ?? defaultComfyPanelPort(c.name) }) : null
|
|
96
155
|
const allCollections = () => [
|
|
97
156
|
...buildCollections(root, c, kit, { bootVersion: BOOT_VERSION, libVersion }),
|
|
98
|
-
|
|
157
|
+
...(hasComfy ? [comfyCollection(root)] : []),
|
|
158
|
+
wikisCollection(graphs, root),
|
|
99
159
|
]
|
|
100
160
|
const port = o?.port ?? c.serve?.port ?? defaultHubPort(c.name)
|
|
161
|
+
// every run this process has going, whoever started it: the daily timer
|
|
162
|
+
// never stacks a pass on top of one
|
|
163
|
+
let active = 0
|
|
164
|
+
// ONE run at a time, whoever asked: two gates over one tree would measure
|
|
165
|
+
// the same checks twice and race each other's repair pass
|
|
166
|
+
let chain: Promise<unknown> = Promise.resolve()
|
|
167
|
+
// the latest verdict of every check, printed as one problem frame after
|
|
168
|
+
// each run for the vscode problems pane (problems/problem-line.ts)
|
|
169
|
+
const board = new ProblemBoard()
|
|
170
|
+
const runChecks: HubDeps['runChecks'] = (tap, o) => {
|
|
171
|
+
const run = chain.then(async () => {
|
|
172
|
+
active++
|
|
173
|
+
try {
|
|
174
|
+
return await runIn({
|
|
175
|
+
line: tap.line,
|
|
176
|
+
check: (e) => {
|
|
177
|
+
// a frame per verdict that moved the board, so a tsc
|
|
178
|
+
// error reaches the pane when typecheck ends rather
|
|
179
|
+
// than when the whole gate does
|
|
180
|
+
if (board.take(e)) for (const l of board.frame()) io.print(l)
|
|
181
|
+
tap.check(e)
|
|
182
|
+
},
|
|
183
|
+
}, o)
|
|
184
|
+
} finally {
|
|
185
|
+
active--
|
|
186
|
+
// and one per run, so every run ends on a frame
|
|
187
|
+
for (const l of board.frame()) io.print(l)
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
chain = run.catch(() => {})
|
|
191
|
+
return run
|
|
192
|
+
}
|
|
193
|
+
const runIn: HubDeps['runChecks'] = async (tap, o) => {
|
|
194
|
+
const only = o?.only != null && o.only.length > 0 ? o.only : undefined
|
|
195
|
+
const by = o?.by ?? 'hub'
|
|
196
|
+
const buildIds = new Set((await allTasks(c, root)).tasks.map((t) => t.id))
|
|
197
|
+
const namedTasks = (only ?? []).filter((id) => buildIds.has(id))
|
|
198
|
+
// ONE runner, two questions: a build step is the gate's, so
|
|
199
|
+
// anything naming one (or asking for the gate itself) goes through
|
|
200
|
+
// runCi, and a set of everything else takes the health pass
|
|
201
|
+
if (o?.gate === true || namedTasks.length > 0)
|
|
202
|
+
return runCi(root, {
|
|
203
|
+
...hubGateOpts({ only, auto: o?.auto === true }),
|
|
204
|
+
buildOnly: only != null && namedTasks.length === only.length,
|
|
205
|
+
print: tap.line,
|
|
206
|
+
config: c,
|
|
207
|
+
onCheck: tap.check,
|
|
208
|
+
invokedBy: by,
|
|
209
|
+
})
|
|
210
|
+
// a NARROWED check run must not stamp STATUS.md: that file carries
|
|
211
|
+
// the verdict of a whole run, and one check is not one
|
|
212
|
+
return runCheck(root, { only, status: only == null, print: tap.line, config: c, onCheck: tap.check, invokedBy: by })
|
|
213
|
+
}
|
|
214
|
+
// resolved once: it shells out to git, and the page asks on every poll
|
|
215
|
+
let record = ''
|
|
216
|
+
try { record = runsPath(root) } catch { record = '' }
|
|
217
|
+
// nothing runs in this process yet, so a live row holding its pid was
|
|
218
|
+
// left by an earlier image of it: `bun --watch` restarts in place
|
|
219
|
+
if (record !== '') sweepLive(record, Date.now(), { ownPid: process.pid })
|
|
220
|
+
// the live strip asks every second, from every open tab: the record is
|
|
221
|
+
// read only when its files moved, and one shaped answer serves every
|
|
222
|
+
// request inside LIVE_SHARE_MS
|
|
223
|
+
const others = new OtherRepos(root)
|
|
224
|
+
let local: { stamp: string; at: number; live: ReturnType<typeof readLive>; runs: ReturnType<typeof readRunsSince>; invocations: ReturnType<typeof readInvocations> } | null = null
|
|
225
|
+
let shaped: { at: number; view: LiveView } | null = null
|
|
226
|
+
const LIVE_SHARE_MS = 500
|
|
227
|
+
const live = (): LiveView => {
|
|
228
|
+
const now = Date.now()
|
|
229
|
+
if (shaped != null && now - shaped.at < LIVE_SHARE_MS) return shaped.view
|
|
230
|
+
const stamp = recordStamp(record)
|
|
231
|
+
// the window slides even when nothing is written, so the history is
|
|
232
|
+
// re-read at least every few seconds
|
|
233
|
+
if (local == null || local.stamp !== stamp || now - local.at > 5_000) {
|
|
234
|
+
const since = now - LIVE_HISTORY_MS
|
|
235
|
+
local = { stamp, at: now, live: readLive(record), runs: readRunsSince(record, since), invocations: readInvocations(record, since) }
|
|
236
|
+
}
|
|
237
|
+
const view = shapeLive({ now, cap: parallelCap(), live: local.live, runs: local.runs, invocations: local.invocations, others: others.read() })
|
|
238
|
+
shaped = { at: now, view }
|
|
239
|
+
return view
|
|
240
|
+
}
|
|
241
|
+
const schedule = new HealthSchedule({
|
|
242
|
+
now: Date.now,
|
|
243
|
+
lastPassAt: () => lastHealthPassAt(record),
|
|
244
|
+
busy: () => active > 0,
|
|
245
|
+
run: async () => {
|
|
246
|
+
const t0 = Date.now()
|
|
247
|
+
io.print(`health pass: the daily run starts`)
|
|
248
|
+
const code = await runChecks({ line: () => {}, check: () => {} }, { by: 'schedule' })
|
|
249
|
+
io.print(`health pass: the daily run ended, exit ${code}, ${Math.round((Date.now() - t0) / 1000)}s`)
|
|
250
|
+
},
|
|
251
|
+
})
|
|
101
252
|
return {
|
|
102
253
|
root,
|
|
103
254
|
name: c.name,
|
|
104
255
|
port,
|
|
105
256
|
graphs,
|
|
106
257
|
studio,
|
|
107
|
-
|
|
258
|
+
refresh,
|
|
259
|
+
// the run happens IN THIS PROCESS, so every check result reaches the
|
|
260
|
+
// page as it finishes. A shelled run could only ever deliver one wall
|
|
261
|
+
// of text, which is what made seventy checks share one destination
|
|
262
|
+
runChecks,
|
|
263
|
+
schedule,
|
|
264
|
+
// every run in flight, whoever started it: they all write the same
|
|
265
|
+
// record, so a terminal gate shows here the same as a button's
|
|
266
|
+
live,
|
|
267
|
+
repoHub: (repo) => others.hubOf(repo),
|
|
268
|
+
timeline: async () => {
|
|
269
|
+
const ceilings: Record<string, number> = {}
|
|
270
|
+
const inventory = await gateInventory(root, c)
|
|
271
|
+
for (const row of inventory) ceilings[row.decl.id] = row.timeoutMs ?? (row.build ? BUILD_TIMEOUT_MS : CHECK_TIMEOUT_MS)
|
|
272
|
+
return shapeTimeline({
|
|
273
|
+
runs: readRuns(record),
|
|
274
|
+
invocations: readInvocations(record),
|
|
275
|
+
suites: readSuites(record),
|
|
276
|
+
ceilings,
|
|
277
|
+
inventory: inventory.map((row) => row.decl.id),
|
|
278
|
+
declaredSlow: declaredSlowFiles(c),
|
|
279
|
+
cap: parallelCap(),
|
|
280
|
+
})
|
|
281
|
+
},
|
|
282
|
+
tests: async (check) => {
|
|
283
|
+
// the SAME test files the tree lists, from the same provider
|
|
284
|
+
const col = allCollections().find((x) => x.id === 'tests')
|
|
285
|
+
const facts = col == null ? [] : (await col.list()).flatMap((i): TestFacts[] => (i.test == null ? [] : [i.test]))
|
|
286
|
+
const input = { facts, runs: readRuns(record), suiteRuns: readSuiteRuns(record) }
|
|
287
|
+
return check == null || check === '' ? shapeTestsRoot(input) : shapeTestCheck(check, input)
|
|
288
|
+
},
|
|
289
|
+
prose: async () => proseView(CheckCtx.open({ root, config: c }), c),
|
|
108
290
|
needs: () => projectNeeds(allCollections()),
|
|
109
291
|
collections: allCollections,
|
|
110
|
-
asksDir: c.asks?.dir ??
|
|
292
|
+
asksDir: c.asks?.dir ?? LAYOUT.asks,
|
|
293
|
+
comfy: panel == null ? null : { ensure: () => panel.ensure(), restart: () => panel.restart(), box: () => comfyBox(root), stop: () => panel.stop() },
|
|
111
294
|
// RED-only pings while serving; notify command is repo config data.
|
|
112
295
|
// Delivery failure must be LOUD — a silently broken notify path means
|
|
113
|
-
//
|
|
296
|
+
// a silent one reads as no blockers at all
|
|
114
297
|
pingTick: (needs) =>
|
|
115
|
-
pingTick(root, c.asks?.dir ??
|
|
298
|
+
pingTick(root, c.asks?.dir ?? LAYOUT.asks, needs, {
|
|
116
299
|
link: c.serve?.publicHost != null ? `http://${c.serve.publicHost}:${port}/?f=needs` : undefined,
|
|
117
300
|
run: (cmd) => {
|
|
118
301
|
const p = Bun.spawn(cmd, { stdout: 'ignore', stderr: 'pipe' })
|
|
@@ -125,6 +308,17 @@ export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o
|
|
|
125
308
|
}
|
|
126
309
|
}
|
|
127
310
|
|
|
311
|
+
/** how a gate the hub starts differs from `shipkit ci`. The gate repairs and
|
|
312
|
+
* stamps STATUS.md, a narrowed run does neither: one check is not the verdict
|
|
313
|
+
* of a gate. A run the hub starts ON ITS OWN never mutates the tree someone
|
|
314
|
+
* is editing: no repair pass, frozen (SK005 never updates a dependency), no
|
|
315
|
+
* status files, and the quick tier */
|
|
316
|
+
export function hubGateOpts(o: { only?: string[]; auto: boolean }): Pick<CiOpts, 'onlyIds' | 'fix' | 'status' | 'frozen' | 'tests'> {
|
|
317
|
+
const whole = o.only == null
|
|
318
|
+
if (o.auto) return { onlyIds: o.only, fix: false, status: false, frozen: true, tests: 'quick' }
|
|
319
|
+
return { onlyIds: o.only, fix: whole, status: whole }
|
|
320
|
+
}
|
|
321
|
+
|
|
128
322
|
// the hub must KNOW when it is outdated — a stale morning
|
|
129
323
|
// process serves broken-looking picks all day: version read at
|
|
130
324
|
// boot vs on disk per attention load; mismatch = red line telling him to
|
|
@@ -138,6 +332,12 @@ const BOOT_VERSION = libVersion()
|
|
|
138
332
|
// (src/collections/providers.ts) — never a parallel projection.
|
|
139
333
|
|
|
140
334
|
export async function runServe(root: string, io: ServeIo, o?: { port?: number }): Promise<number> {
|
|
335
|
+
// `bun --watch` restarts the hub in the same pid: what its previous image
|
|
336
|
+
// held is gone with it, and the check children it spawned are still ours
|
|
337
|
+
// and still running, so they die here, before this image spawns anything
|
|
338
|
+
const reclaimed = reclaimPreviousImage()
|
|
339
|
+
if (reclaimed.killed.length > 0)
|
|
340
|
+
io.print(`ok: this hub restarted in place: killed ${reclaimed.killed.length} check process(es) its previous image left running (pid ${reclaimed.killed.join(', ')})`)
|
|
141
341
|
const c = await loadRepoConfig(root)
|
|
142
342
|
const deps = await resolveHubDeps(root, c, io, o)
|
|
143
343
|
try {
|
|
@@ -148,8 +348,37 @@ export async function runServe(root: string, io: ServeIo, o?: { port?: number })
|
|
|
148
348
|
io.print(`${c.name} hub → ${base}`)
|
|
149
349
|
const routes = hub.tools.filter((t) => t.path !== '/').map((t) => `${t.label} ${t.path}`).join(' · ')
|
|
150
350
|
if (routes !== '') io.print(` also: ${routes}`)
|
|
351
|
+
// a page that cannot refresh itself looks identical to a broken one,
|
|
352
|
+
// and the only tell is that every change needs a reload key
|
|
353
|
+
if (!hub.liveReload) io.print(`WARN the page will not refresh itself: NODE_ENV=production strips bun hot reload, unset it and restart the hub`)
|
|
151
354
|
// the off-machine door (phone, other machines) — publicHost is config
|
|
152
355
|
if (c.serve?.publicHost != null) io.print(` phone: http://${c.serve.publicHost}:${hub.server.port}/`)
|
|
356
|
+
// said once at boot, so a silent phone is never mistaken for no blockers
|
|
357
|
+
if (c.notifyCommand != null && isLinkedWorktree(root)) io.print(` pings: off, this is a linked worktree (only the main checkout's hub notifies)`)
|
|
358
|
+
// the daily health pass rides the serving process, and only it: a hub
|
|
359
|
+
// built for a test never starts a timer
|
|
360
|
+
deps.schedule?.start((e) => io.printErr(`WARN the daily health pass broke: ${e.message}`))
|
|
361
|
+
// the quick gate at start, then after every save burst, so the
|
|
362
|
+
// problems pane follows the code. It only asks the runner, which
|
|
363
|
+
// measures or replays like any run: the watch decides WHEN, never
|
|
364
|
+
// what is true
|
|
365
|
+
const trigger = new GateTrigger({
|
|
366
|
+
run: () => deps.runChecks({ line: () => {}, check: () => {} }, { gate: true, by: 'hub', auto: true }),
|
|
367
|
+
onError: (e) => io.printErr(`WARN the hub's own gate run broke: ${e.message}`),
|
|
368
|
+
})
|
|
369
|
+
const watcher = watchTracked(root, () => trigger.touched(), { onError: (e) => io.printErr(`WARN the file watch stopped, so the problems pane refreshes only on a hub run: ${e.message}`) })
|
|
370
|
+
if (watcher.error != null) io.printErr(`WARN no file watch (${watcher.error}), so the problems pane refreshes only on a hub run`)
|
|
371
|
+
trigger.request()
|
|
372
|
+
// a child process (the comfy panel) outlives a parent nobody stopped,
|
|
373
|
+
// and the next launch then adopts one holding the PREVIOUS run's
|
|
374
|
+
// modules. A hard kill still leaks one, which is why adoption exists
|
|
375
|
+
for (const sig of ['SIGINT', 'SIGTERM'] as const)
|
|
376
|
+
process.on(sig, () => {
|
|
377
|
+
trigger.stop()
|
|
378
|
+
watcher.close()
|
|
379
|
+
hub.stop()
|
|
380
|
+
process.exit(0)
|
|
381
|
+
})
|
|
153
382
|
await new Promise(() => {}) // the hub owns the process — never resolve
|
|
154
383
|
return 0
|
|
155
384
|
} catch (e) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// the one guard on a request that changes something, shared by the hub and
|
|
2
|
+
// the standalone graph server.
|
|
3
|
+
/** 127.0.0.1 keeps other machines out, not other sites: any page open in the
|
|
4
|
+
* browser can POST to localhost. A request that changes something passes
|
|
5
|
+
* only from the hub's own page, or from a client that is not a browser at
|
|
6
|
+
* all (no origin header: curl, an agent). */
|
|
7
|
+
export function sameOrigin(req: Request): boolean {
|
|
8
|
+
if (req.method === 'GET' || req.method === 'HEAD') return true
|
|
9
|
+
const site = req.headers.get('sec-fetch-site')
|
|
10
|
+
if (site != null && site !== 'same-origin' && site !== 'none') return false
|
|
11
|
+
const origin = req.headers.get('origin')
|
|
12
|
+
if (origin == null) return true
|
|
13
|
+
try { return new URL(origin).host === new URL(req.url).host } catch { return false }
|
|
14
|
+
}
|