rvlib-shipkit 1.1.0 → 1.2.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 +67 -0
- package/package.json +3 -1
- package/src/check.ts +24 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
- package/src/checks/SK023-stores-under-cap.check.ts +49 -0
- package/src/checks/SK024-identity-colors.check.ts +49 -0
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +18 -5
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli.ts +22 -2
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/attach.ts +14 -9
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +40 -18
- package/src/core/discover-checks.ts +36 -18
- 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/retention.ts +213 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +3 -0
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +86 -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 +208 -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 +141 -0
- package/src/home/resources.ts +338 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +149 -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 +172 -0
- package/src/home/supervise.ts +346 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widgets.ts +102 -0
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +5 -0
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/run-log.ts +58 -23
- package/src/ledger/store.ts +228 -103
- package/src/ledger/world-cache.ts +15 -13
- package/src/plan/cadence.ts +237 -0
- package/src/plan/planner.ts +303 -0
- package/src/plan/repo-source.ts +100 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/repo-config.ts +51 -2
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +1 -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/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +1 -1
- package/src/serve/hub-app/header-bar.tsx +44 -12
- 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 +241 -0
- package/src/serve/hub-app/home-strip.css +70 -0
- package/src/serve/hub-app/home-strip.tsx +245 -0
- package/src/serve/hub-app/home.html +17 -0
- package/src/serve/hub-app/hub-app.tsx +201 -93
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +43 -23
- package/src/serve/hub-app/icons.tsx +14 -0
- package/src/serve/hub-app/index.html +3 -1
- package/src/serve/hub-app/keys.ts +25 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +12 -9
- package/src/serve/hub-app/machine-st.ts +270 -0
- 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 +88 -0
- package/src/serve/hub-app/planned-st.ts +171 -0
- package/src/serve/hub-app/planned-view.tsx +109 -0
- package/src/serve/hub-app/planned.css +39 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +97 -0
- package/src/serve/hub-app/project-list.tsx +167 -0
- package/src/serve/hub-app/projects-svc.ts +139 -17
- 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/{hub-st.ts → repo-st.ts} +155 -84
- 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/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +10 -10
- package/src/serve/hub-app/timeline-chart.tsx +24 -12
- package/src/serve/hub-app/timeline-svc.ts +5 -4
- package/src/serve/hub-app/timeline-view.tsx +13 -13
- 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 +542 -0
- package/src/serve/project-gutter-route.ts +21 -0
- package/src/serve/project-gutter.ts +3 -2
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +142 -44
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +114 -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/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// the long running processes a repo declares in `serve.processes` (a dev
|
|
2
|
+
// server, a watcher), supervised by its hub with the same supervisor the home
|
|
3
|
+
// uses for resources. Each runs detached with a pid record in the machine db
|
|
4
|
+
// (named by its path under .shipkit/tmp, where an older copy kept a pid file),
|
|
5
|
+
// so a hub restart in place adopts it, and only a real stop of the hub stops it.
|
|
6
|
+
import { join, resolve } from 'path'
|
|
7
|
+
import { Proc, realDeps, tailLines, type ProcView, type SuperviseDeps } from '../home/supervise.ts'
|
|
8
|
+
import type { ProcessDecl } from '../repo-config.ts'
|
|
9
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
10
|
+
|
|
11
|
+
export type ProcessView = ProcView & { url: string | null; auto: boolean; cmd: string; tail: string[] }
|
|
12
|
+
|
|
13
|
+
export const processDir = (root: string): string => join(root, LAYOUT.tmp, 'processes')
|
|
14
|
+
|
|
15
|
+
const probeUrl = async (url: string): Promise<boolean> => {
|
|
16
|
+
try {
|
|
17
|
+
// any answer means it serves: a dev server may 404 its root
|
|
18
|
+
await fetch(url, { signal: AbortSignal.timeout(1_500) })
|
|
19
|
+
return true
|
|
20
|
+
} catch {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class RepoProcesses {
|
|
26
|
+
private procs: { decl: ProcessDecl; proc: Proc }[]
|
|
27
|
+
|
|
28
|
+
constructor(root: string, decls: ProcessDecl[], o: { deps?: SuperviseDeps; probe?: (url: string) => Promise<boolean> } = {}) {
|
|
29
|
+
const dir = processDir(root)
|
|
30
|
+
this.procs = decls.map((decl) => ({
|
|
31
|
+
decl,
|
|
32
|
+
proc: new Proc({
|
|
33
|
+
id: decl.id,
|
|
34
|
+
cmd: decl.cmd,
|
|
35
|
+
cwd: resolve(root, decl.cwd ?? '.'),
|
|
36
|
+
log: join(dir, `${decl.id}.log`),
|
|
37
|
+
pidKey: join(dir, `${decl.id}.pid`),
|
|
38
|
+
probe: decl.url == null ? undefined : () => (o.probe ?? probeUrl)(decl.url!),
|
|
39
|
+
}, o.deps ?? realDeps),
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** the auto ones start with the hub; an adopted one is left as it runs */
|
|
44
|
+
async startAuto(): Promise<void> {
|
|
45
|
+
await Promise.all(this.procs.filter((p) => p.decl.auto === true && p.proc.pid == null).map((p) => p.proc.ensure()))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
act(id: string, act: 'start' | 'stop' | 'restart'): ProcessView | null {
|
|
49
|
+
const p = this.procs.find((x) => x.decl.id === id)
|
|
50
|
+
if (p == null) return null
|
|
51
|
+
if (act === 'stop' || act === 'restart') p.proc.stop()
|
|
52
|
+
if (act === 'start') void p.proc.ensure()
|
|
53
|
+
if (act === 'restart') this.restartWhenGone(p.proc)
|
|
54
|
+
return this.view(p)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private restartWhenGone(proc: Proc): void {
|
|
58
|
+
const wait = async (): Promise<void> => {
|
|
59
|
+
for (let i = 0; i < 40 && proc.status === 'stopping'; i++) {
|
|
60
|
+
await Bun.sleep(250)
|
|
61
|
+
await proc.tick()
|
|
62
|
+
}
|
|
63
|
+
proc.start()
|
|
64
|
+
}
|
|
65
|
+
void wait()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async tick(): Promise<void> {
|
|
69
|
+
await Promise.all(this.procs.map((p) => p.proc.tick()))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** a real stop of the hub: every process it started stops with it */
|
|
73
|
+
stopAll(): void {
|
|
74
|
+
for (const p of this.procs) if (p.proc.pid != null) p.proc.stop()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private view(p: { decl: ProcessDecl; proc: Proc }): ProcessView {
|
|
78
|
+
const v = p.proc.view()
|
|
79
|
+
return { ...v, url: p.decl.url ?? null, auto: p.decl.auto === true, cmd: p.decl.cmd.join(' '), tail: v.status === 'up' ? [] : tailLines(p.proc.spec.log, 20) }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
views(): ProcessView[] {
|
|
83
|
+
return this.procs.map((p) => this.view(p))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get size(): number {
|
|
87
|
+
return this.procs.length
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/serve/run-serve.ts
CHANGED
|
@@ -1,426 +1,72 @@
|
|
|
1
|
-
// shipkit serve
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
// kit.ts loads. Port: --port > serve.port > a deterministic house-range
|
|
7
|
-
// default (4800-4979, hashed from the repo name — stable across launches;
|
|
8
|
-
// pin serve.port when two repos collide).
|
|
9
|
-
import { existsSync, readFileSync, statSync } from 'fs'
|
|
10
|
-
import { homedir } from 'os'
|
|
11
|
-
import { basename, dirname, join, resolve, sep } from 'path'
|
|
12
|
-
import { loadKit, type ResolvedKit } from '../assets/kit-config.ts'
|
|
13
|
-
import { studioRoutes, type StudioRoutes } from '../assets/server/studio-server.ts'
|
|
14
|
-
import { projectNeeds } from '../collections/collection-types.ts'
|
|
15
|
-
import { comfyCollection } from '../collections/comfy.ts'
|
|
16
|
-
import { ComfyPanel, comfyBox, defaultComfyPanelPort, scanComfyWorkflows } from '../comfy/comfy-panel.ts'
|
|
17
|
-
import { wikisCollection } from '../collections/wikis.ts'
|
|
18
|
-
import { pingTick } from '../collections/pings.ts'
|
|
19
|
-
import { buildCollections } from '../collections/providers.ts'
|
|
20
|
-
import { loadRepoConfig, type RepoConfig } from '../repo-config.ts'
|
|
21
|
-
import { CheckCtx } from '../core/check-ctx.ts'
|
|
22
|
-
import { proseView } from '../prose/prose-view.ts'
|
|
23
|
-
import { allTasks, gateInventory } from '../ledger/gate-checks.ts'
|
|
24
|
-
import { runCheck } from '../core/run-check.ts'
|
|
25
|
-
import { runCi, type CiOpts } from '../core/run-ci.ts'
|
|
26
|
-
import type { GraphMount } from '../wiki/graph-mount.ts'
|
|
27
|
-
import { scanMdGraph } from '../wiki/scan-md.ts'
|
|
28
|
-
import { lastHealthPassAt, readInvocations, readLive, readRuns, readRunsSince, runsPath, sweepLive } from '../ledger/run-log.ts'
|
|
29
|
-
import { LIVE_HISTORY_MS, shapeLive, type LiveView } from '../ledger/live-timeline.ts'
|
|
30
|
-
import { parallelCap } from '../core/parallel.ts'
|
|
1
|
+
// shipkit serve: the process around one repo's hub. On a machine with a home
|
|
2
|
+
// it hands the hub to the home and streams its lines; else it serves the hub
|
|
3
|
+
// runtime (hub-runtime.ts) behind a Bun.serve of its own, restarts in place
|
|
4
|
+
// when the lib moved under it, and stops everything on a signal.
|
|
5
|
+
import { basename } from 'path'
|
|
31
6
|
import { reclaimPreviousImage } from '../core/instance.ts'
|
|
32
7
|
import { isLinkedWorktree } from '../core/git-root.ts'
|
|
33
|
-
import { OtherRepos } from './other-repos.ts'
|
|
34
|
-
import { recordStamp } from './project-gutter.ts'
|
|
35
|
-
import { shapeTimeline } from '../ledger/run-timeline.ts'
|
|
36
|
-
import { readSuiteRuns, readSuites } from '../ledger/suite-log.ts'
|
|
37
|
-
import { shapeTestCheck, shapeTestsRoot } from '../collections/test-time.ts'
|
|
38
|
-
import type { TestFacts } from '../collections/collection-types.ts'
|
|
39
|
-
import { declaredSlowFiles } from '../suite/test-units.ts'
|
|
40
|
-
import { BUILD_TIMEOUT_MS, CHECK_TIMEOUT_MS } from '../collections/checks.ts'
|
|
41
|
-
import { HealthSchedule } from './health-schedule.ts'
|
|
42
|
-
import { GateTrigger, watchTracked } from './gate-trigger.ts'
|
|
43
|
-
import { ProblemBoard } from '../problems/problem-board.ts'
|
|
44
|
-
import { HubProblems } from './hub-problems.ts'
|
|
45
|
-
import { StaleRestart } from './stale-restart.ts'
|
|
46
|
-
import { reexecBlocker, reexecSelf } from '../core/reexec.ts'
|
|
47
|
-
import { serveHub, type HubDeps } from './serve-hub.ts'
|
|
48
|
-
import { defaultHubPort } from './hub-port.ts'
|
|
49
|
-
import { LAYOUT } from '../core/layout.ts'
|
|
50
8
|
import { diskLibVersion, LIB_VERSION } from '../core/lib-version.ts'
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
9
|
+
import { reexecBlocker, reexecSelf } from '../core/reexec.ts'
|
|
10
|
+
import { attachToHome } from '../home/attach.ts'
|
|
11
|
+
import { daemonUp, WORKER_ENV } from '../home/home-paths.ts'
|
|
12
|
+
import { homeWanted, startHome } from '../home/home-start.ts'
|
|
13
|
+
import { loadHub, type ServeIo } from './hub-runtime.ts'
|
|
14
|
+
import { homeRepo } from './project-gutter.ts'
|
|
15
|
+
import { serveHub } from './serve-hub.ts'
|
|
16
|
+
import { StaleRestart } from './stale-restart.ts'
|
|
17
|
+
import { Planner } from '../plan/planner.ts'
|
|
55
18
|
|
|
56
|
-
|
|
57
|
-
// points at (`@~/dev/<other-repo>/wiki/index.md`). It lives in ANOTHER repo and
|
|
58
|
-
// binds every agent working here, so the hub makes it browsable. Never linted
|
|
59
|
-
// from here. The local-include forms (`@./agent/index.md`,
|
|
60
|
-
// `@./CLAUDE.local.md`) and any index inside this repo are skipped: those are
|
|
61
|
-
// already their own mounts.
|
|
62
|
-
export function baseWikiRoot(root: string): string | null {
|
|
63
|
-
const abs = resolve(root)
|
|
64
|
-
for (const f of ['CLAUDE.md', 'CLAUDE.local.md', 'CLAUDE.private.md']) {
|
|
65
|
-
const p = join(abs, f)
|
|
66
|
-
if (!existsSync(p)) continue
|
|
67
|
-
for (const line of readFileSync(p, 'utf8').split('\n')) {
|
|
68
|
-
const m = line.trim().match(/^@(\S+\.md)$/)
|
|
69
|
-
if (m == null) continue
|
|
70
|
-
const raw = m[1]!
|
|
71
|
-
const target = resolve(abs, raw.startsWith('~/') ? join(homedir(), raw.slice(2)) : raw)
|
|
72
|
-
const dir = dirname(target)
|
|
73
|
-
if (!existsSync(target)) continue
|
|
74
|
-
if (dir === abs || dir.startsWith(abs + sep)) continue
|
|
75
|
-
return dir
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
19
|
+
export type { ServeIo }
|
|
80
20
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
/** the tab of the repo's own wiki, beside the house wiki named for its repo */
|
|
84
|
-
const WIKI_LABEL = "this repo's wiki"
|
|
85
|
-
const wikiLabel = (dir: string): string => (basename(dir) === 'wiki' ? `${basename(dirname(dir))} wiki` : basename(dir))
|
|
21
|
+
/** how often a hub of its own looks at its plan */
|
|
22
|
+
export const PLAN_TICK_MS = 2_000
|
|
86
23
|
|
|
87
|
-
/**
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
24
|
+
/** what a `shipkit serve` does with its hub. On a machine with a home, no
|
|
25
|
+
* hub runs outside it: a home that answers takes it, and an installed one
|
|
26
|
+
* that does not is started first. `serve` only inside the home, a linked
|
|
27
|
+
* worktree (its hub is its own checkout), a machine with no home, and
|
|
28
|
+
* SHIPKIT_NO_HOME=1, the one lift */
|
|
29
|
+
export async function serveMode(root: string, env: Record<string, string | undefined> = process.env, o: { up?: () => Promise<boolean>; installed?: () => boolean } = {}): Promise<'serve' | 'attach' | 'start'> {
|
|
30
|
+
const set = (v: string | undefined): boolean => v != null && v !== ''
|
|
31
|
+
if (set(env[WORKER_ENV]) || set(env.SHIPKIT_NO_HOME)) return 'serve'
|
|
32
|
+
if (isLinkedWorktree(root)) return 'serve'
|
|
33
|
+
if (await (o.up ?? (() => daemonUp()))()) return 'attach'
|
|
34
|
+
return (o.installed ?? (() => homeWanted(env)))() ? 'start' : 'serve'
|
|
96
35
|
}
|
|
97
36
|
|
|
98
|
-
export async function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// nothing. What is resolved ONCE stays once: the kit, the graphs, the
|
|
103
|
-
// studio and the comfy panel are mounts, and remounting a server under a
|
|
104
|
-
// live page is a different question from re-reading a declaration.
|
|
105
|
-
let c = boot
|
|
106
|
-
let stamp = configStamp(root)
|
|
107
|
-
const refresh = async (): Promise<void> => {
|
|
108
|
-
const now = configStamp(root)
|
|
109
|
-
if (now === stamp || now === '') return
|
|
110
|
-
stamp = now
|
|
111
|
-
try {
|
|
112
|
-
c = await loadRepoConfig(root, { fresh: true })
|
|
113
|
-
} catch (e) {
|
|
114
|
-
io.print(`WARN repo.config.ts did not load, so the page is reading the one this process booted with — ${(e as Error).message}`)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ONE list: the routes, the /hub.json tools and the wikis section all
|
|
119
|
-
// read it, so nothing can advertise a graph the server does not serve.
|
|
120
|
-
// The repo's OWN wiki mounts when it has one (a game repo's hub
|
|
121
|
-
// advertising an empty wiki route was startup noise), a config-declared
|
|
122
|
-
// provider always mounts, and the base-context wiki (the shared one named
|
|
123
|
-
// by the CLAUDE.md include chain) mounts wherever it is reachable: an
|
|
124
|
-
// agent working here obeys that wiki, so a reader can open it.
|
|
125
|
-
const graphs: GraphMount[] = []
|
|
126
|
-
if (c.serve?.wikiGraph != null)
|
|
127
|
-
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 } })
|
|
128
|
-
else if (existsSync(join(root, 'wiki')))
|
|
129
|
-
graphs.push({ id: 'wiki', label: WIKI_LABEL, title: `${c.name} wiki`, load: () => scanMdGraph({ root }), lint: { root } })
|
|
130
|
-
const baseWiki = baseWikiRoot(root)
|
|
131
|
-
if (baseWiki != null)
|
|
132
|
-
graphs.push({ id: 'base', label: wikiLabel(baseWiki), title: wikiLabel(baseWiki), load: () => scanMdGraph({ root: baseWiki }), lint: null })
|
|
133
|
-
const agentRoot = join(root, 'agent')
|
|
134
|
-
if (existsSync(agentRoot))
|
|
135
|
-
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 } })
|
|
136
|
-
// ONE page for every repo: the studio ALWAYS
|
|
137
|
-
// mounts — kit-less repos get an empty media section, never a second
|
|
138
|
-
// page. A broken kit degrades to the same empty kit, loudly.
|
|
139
|
-
let kit: ResolvedKit | null = null
|
|
140
|
-
if (existsSync(join(root, '.shipkit/assets/kit.ts'))) {
|
|
37
|
+
export async function runServe(root: string, io: ServeIo, o?: { port?: number }): Promise<number> {
|
|
38
|
+
const mode = await serveMode(root)
|
|
39
|
+
if (mode === 'start') {
|
|
40
|
+
io.print(`the home is installed but not running: starting it`)
|
|
141
41
|
try {
|
|
142
|
-
|
|
42
|
+
await startHome()
|
|
143
43
|
} catch (e) {
|
|
144
|
-
io.
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
const emptyKit: ResolvedKit = { kinds: [], workspace: '.shipkit/assets/out' }
|
|
148
|
-
let studio: StudioRoutes | null = null
|
|
149
|
-
try {
|
|
150
|
-
studio = await studioRoutes({ kit: kit ?? emptyKit, root })
|
|
151
|
-
} catch (e) {
|
|
152
|
-
io.print(`WARN media API not mounted: ${(e as Error).message}`)
|
|
153
|
-
}
|
|
154
|
-
// a repo with no workflow module gets NO comfy section and no panel
|
|
155
|
-
// process: an empty section header is a surface that teaches nothing
|
|
156
|
-
const hasComfy = scanComfyWorkflows(root).length > 0
|
|
157
|
-
const panel = hasComfy ? new ComfyPanel({ root, port: c.serve?.comfyPort ?? defaultComfyPanelPort(c.name) }) : null
|
|
158
|
-
const allCollections = () => [
|
|
159
|
-
...buildCollections(root, c, kit),
|
|
160
|
-
...(hasComfy ? [comfyCollection(root)] : []),
|
|
161
|
-
wikisCollection(graphs, root),
|
|
162
|
-
]
|
|
163
|
-
const port = o?.port ?? c.serve?.port ?? defaultHubPort(c.name)
|
|
164
|
-
// every run this process has going, whoever started it: the daily timer
|
|
165
|
-
// never stacks a pass on top of one
|
|
166
|
-
let active = 0
|
|
167
|
-
// ONE run at a time, whoever asked: two gates over one tree would measure
|
|
168
|
-
// the same checks twice and race each other's repair pass
|
|
169
|
-
let chain: Promise<unknown> = Promise.resolve()
|
|
170
|
-
// the latest verdict of every check, printed as one problem frame after
|
|
171
|
-
// each run for the vscode problems pane (problems/problem-line.ts)
|
|
172
|
-
const board = new ProblemBoard()
|
|
173
|
-
// what is wrong with the hub itself rides the same frame, the moment it
|
|
174
|
-
// moves, so the pane never reads 0 while the page shows a hub red
|
|
175
|
-
const hubProblems = new HubProblems((all) => {
|
|
176
|
-
if (board.setHub(all)) for (const l of board.frame()) io.print(l)
|
|
177
|
-
})
|
|
178
|
-
const runChecks: HubDeps['runChecks'] = (tap, o) => {
|
|
179
|
-
const run = chain.then(async () => {
|
|
180
|
-
active++
|
|
181
|
-
try {
|
|
182
|
-
return await runIn({
|
|
183
|
-
line: tap.line,
|
|
184
|
-
check: (e) => {
|
|
185
|
-
// a frame per verdict that moved the board, so a tsc
|
|
186
|
-
// error reaches the pane when typecheck ends rather
|
|
187
|
-
// than when the whole gate does
|
|
188
|
-
if (board.take(e)) for (const l of board.frame()) io.print(l)
|
|
189
|
-
tap.check(e)
|
|
190
|
-
},
|
|
191
|
-
}, o)
|
|
192
|
-
} finally {
|
|
193
|
-
active--
|
|
194
|
-
// and one per run, so every run ends on a frame
|
|
195
|
-
for (const l of board.frame()) io.print(l)
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
chain = run.catch(() => {})
|
|
199
|
-
return run
|
|
200
|
-
}
|
|
201
|
-
const runIn: HubDeps['runChecks'] = async (tap, o) => {
|
|
202
|
-
const only = o?.only != null && o.only.length > 0 ? o.only : undefined
|
|
203
|
-
const by = o?.by ?? 'hub'
|
|
204
|
-
const buildIds = new Set((await allTasks(c, root)).tasks.map((t) => t.id))
|
|
205
|
-
const namedTasks = (only ?? []).filter((id) => buildIds.has(id))
|
|
206
|
-
// ONE runner, two questions: a build step is the gate's, so
|
|
207
|
-
// anything naming one (or asking for the gate itself) goes through
|
|
208
|
-
// runCi, and a set of everything else takes the health pass
|
|
209
|
-
if (o?.gate === true || namedTasks.length > 0)
|
|
210
|
-
return runCi(root, {
|
|
211
|
-
...hubGateOpts({ only, auto: o?.auto === true }),
|
|
212
|
-
buildOnly: only != null && namedTasks.length === only.length,
|
|
213
|
-
print: tap.line,
|
|
214
|
-
config: c,
|
|
215
|
-
onCheck: tap.check,
|
|
216
|
-
invokedBy: by,
|
|
217
|
-
})
|
|
218
|
-
// a NARROWED check run must not stamp STATUS.md: that file carries
|
|
219
|
-
// the verdict of a whole run, and one check is not one
|
|
220
|
-
return runCheck(root, { only, status: only == null, print: tap.line, config: c, onCheck: tap.check, invokedBy: by })
|
|
221
|
-
}
|
|
222
|
-
// resolved once: it shells out to git, and the page asks on every poll
|
|
223
|
-
let record = ''
|
|
224
|
-
try { record = runsPath(root) } catch { record = '' }
|
|
225
|
-
// nothing runs in this process yet, so a live row holding its pid was
|
|
226
|
-
// left by an earlier image of it: `bun --watch` restarts in place
|
|
227
|
-
if (record !== '') sweepLive(record, Date.now(), { ownPid: process.pid })
|
|
228
|
-
// the live strip asks every second, from every open tab: the record is
|
|
229
|
-
// read only when its files moved, and one shaped answer serves every
|
|
230
|
-
// request inside LIVE_SHARE_MS
|
|
231
|
-
const others = new OtherRepos(root)
|
|
232
|
-
let local: { stamp: string; at: number; live: ReturnType<typeof readLive>; runs: ReturnType<typeof readRunsSince>; invocations: ReturnType<typeof readInvocations> } | null = null
|
|
233
|
-
let shaped: { at: number; view: LiveView } | null = null
|
|
234
|
-
const LIVE_SHARE_MS = 500
|
|
235
|
-
const live = (): LiveView => {
|
|
236
|
-
const now = Date.now()
|
|
237
|
-
if (shaped != null && now - shaped.at < LIVE_SHARE_MS) return shaped.view
|
|
238
|
-
const stamp = recordStamp(record)
|
|
239
|
-
// the window slides even when nothing is written, so the history is
|
|
240
|
-
// re-read at least every few seconds
|
|
241
|
-
if (local == null || local.stamp !== stamp || now - local.at > 5_000) {
|
|
242
|
-
const since = now - LIVE_HISTORY_MS
|
|
243
|
-
local = { stamp, at: now, live: readLive(record), runs: readRunsSince(record, since), invocations: readInvocations(record, since) }
|
|
44
|
+
io.printErr(`FAIL: ${(e as Error).message}. No hub runs outside the home on this machine: SHIPKIT_NO_HOME=1 serves this one in process`)
|
|
45
|
+
return 1
|
|
244
46
|
}
|
|
245
|
-
const view = shapeLive({ now, cap: parallelCap(), live: local.live, runs: local.runs, invocations: local.invocations, others: others.read() })
|
|
246
|
-
shaped = { at: now, view }
|
|
247
|
-
return view
|
|
248
47
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const t0 = Date.now()
|
|
255
|
-
io.print(`health pass: the daily run starts`)
|
|
256
|
-
const code = await runChecks({ line: () => {}, check: () => {} }, { by: 'schedule' })
|
|
257
|
-
io.print(`health pass: the daily run ended, exit ${code}, ${Math.round((Date.now() - t0) / 1000)}s`)
|
|
258
|
-
},
|
|
259
|
-
})
|
|
260
|
-
return {
|
|
261
|
-
root,
|
|
262
|
-
name: c.name,
|
|
263
|
-
port,
|
|
264
|
-
graphs,
|
|
265
|
-
studio,
|
|
266
|
-
refresh,
|
|
267
|
-
// the run happens IN THIS PROCESS, so every check result reaches the
|
|
268
|
-
// page as it finishes. A shelled run could only ever deliver one wall
|
|
269
|
-
// of text, which is what made seventy checks share one destination
|
|
270
|
-
runChecks,
|
|
271
|
-
schedule,
|
|
272
|
-
// every run in flight, whoever started it: they all write the same
|
|
273
|
-
// record, so a terminal gate shows here the same as a button's
|
|
274
|
-
live,
|
|
275
|
-
repoHub: (repo) => others.hubOf(repo),
|
|
276
|
-
timeline: async () => {
|
|
277
|
-
const ceilings: Record<string, number> = {}
|
|
278
|
-
const budgets: Record<string, number> = {}
|
|
279
|
-
const inventory = await gateInventory(root, c)
|
|
280
|
-
for (const row of inventory) {
|
|
281
|
-
ceilings[row.decl.id] = row.timeoutMs ?? (row.build ? BUILD_TIMEOUT_MS : CHECK_TIMEOUT_MS)
|
|
282
|
-
budgets[row.decl.id] = row.budgetMs
|
|
283
|
-
}
|
|
284
|
-
return shapeTimeline({
|
|
285
|
-
runs: readRuns(record),
|
|
286
|
-
invocations: readInvocations(record),
|
|
287
|
-
suites: readSuites(record),
|
|
288
|
-
ceilings,
|
|
289
|
-
budgets,
|
|
290
|
-
inventory: inventory.map((row) => row.decl.id),
|
|
291
|
-
declaredSlow: declaredSlowFiles(c),
|
|
292
|
-
cap: parallelCap(),
|
|
293
|
-
})
|
|
294
|
-
},
|
|
295
|
-
tests: async (check) => {
|
|
296
|
-
// the SAME test files the tree lists, from the same provider
|
|
297
|
-
const col = allCollections().find((x) => x.id === 'tests')
|
|
298
|
-
const facts = col == null ? [] : (await col.list()).flatMap((i): TestFacts[] => (i.test == null ? [] : [i.test]))
|
|
299
|
-
const input = { facts, runs: readRuns(record), suiteRuns: readSuiteRuns(record) }
|
|
300
|
-
return check == null || check === '' ? shapeTestsRoot(input) : shapeTestCheck(check, input)
|
|
301
|
-
},
|
|
302
|
-
prose: async () => proseView(CheckCtx.open({ root, config: c }), c),
|
|
303
|
-
needs: () => projectNeeds(allCollections(), hubProblems.collector),
|
|
304
|
-
hubProblems,
|
|
305
|
-
busy: () => active > 0,
|
|
306
|
-
collections: allCollections,
|
|
307
|
-
asksDir: c.asks?.dir ?? LAYOUT.asks,
|
|
308
|
-
comfy: panel == null ? null : { ensure: () => panel.ensure(), restart: () => panel.restart(), box: () => comfyBox(root), stop: () => panel.stop() },
|
|
309
|
-
// RED-only pings while serving; notify command is repo config data.
|
|
310
|
-
// Delivery failure must be LOUD — a silently broken notify path means
|
|
311
|
-
// a silent one reads as no blockers at all
|
|
312
|
-
pingTick: (needs) =>
|
|
313
|
-
pingTick(root, c.asks?.dir ?? LAYOUT.asks, needs, {
|
|
314
|
-
link: c.serve?.publicHost != null ? `http://${c.serve.publicHost}:${port}/?f=needs` : undefined,
|
|
315
|
-
run: (cmd) => {
|
|
316
|
-
const p = Bun.spawn(cmd, { stdout: 'ignore', stderr: 'pipe' })
|
|
317
|
-
void p.exited.then(async (code) => {
|
|
318
|
-
if (code !== 0) io.printErr(`WARN ping delivery failed (exit ${code}): ${await new Response(p.stderr).text().then((s) => s.trim().slice(0, 160))}`)
|
|
319
|
-
})
|
|
320
|
-
},
|
|
321
|
-
notifyCommand: c.notifyCommand,
|
|
322
|
-
}),
|
|
48
|
+
if (mode !== 'serve') {
|
|
49
|
+
// the home serves every repo on its one port, so a forced one cannot apply here
|
|
50
|
+
if (o?.port != null) io.printErr(`WARN --port ${o.port} is ignored: the home serves this hub on its own port. SHIPKIT_NO_HOME=1 serves it in process on ${o.port}`)
|
|
51
|
+
const r = await attachToHome(basename(homeRepo(root)), io)
|
|
52
|
+
if (r !== 'serve') return r
|
|
323
53
|
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/** how a gate the hub starts differs from `shipkit ci`. The gate repairs and
|
|
327
|
-
* stamps STATUS.md, a narrowed run does neither: one check is not the verdict
|
|
328
|
-
* of a gate. A run the hub starts ON ITS OWN never mutates the tree someone
|
|
329
|
-
* is editing: no repair pass, frozen (SK005 never updates a dependency), no
|
|
330
|
-
* status files, and the quick tier */
|
|
331
|
-
export function hubGateOpts(o: { only?: string[]; auto: boolean }): Pick<CiOpts, 'onlyIds' | 'fix' | 'status' | 'frozen' | 'tests'> {
|
|
332
|
-
const whole = o.only == null
|
|
333
|
-
if (o.auto) return { onlyIds: o.only, fix: false, status: false, frozen: true, tests: 'quick' }
|
|
334
|
-
return { onlyIds: o.only, fix: whole, status: whole }
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// the / dashboard's data now comes from the collection providers
|
|
339
|
-
// (src/collections/providers.ts) — never a parallel projection.
|
|
340
|
-
|
|
341
|
-
export async function runServe(root: string, io: ServeIo, o?: { port?: number }): Promise<number> {
|
|
342
54
|
// `bun --watch` restarts the hub in the same pid: what its previous image
|
|
343
55
|
// held is gone with it, and the check children it spawned are still ours
|
|
344
56
|
// and still running, so they die here, before this image spawns anything
|
|
345
57
|
const reclaimed = reclaimPreviousImage()
|
|
346
58
|
if (reclaimed.killed.length > 0)
|
|
347
59
|
io.print(`ok: this hub restarted in place: killed ${reclaimed.killed.length} check process(es) its previous image left running (pid ${reclaimed.killed.join(', ')})`)
|
|
348
|
-
|
|
349
|
-
|
|
60
|
+
// a hub of its own plans its one repo, which counts as active: someone
|
|
61
|
+
// runs it. The interval below is that plan's clock
|
|
62
|
+
const plan = new Planner({ onError: (_repo, e) => io.printErr(`WARN the plan broke: ${e.message}`) })
|
|
63
|
+
const rt = await loadHub(root, io, { port: o?.port, plan, gateAtStart: true, ownTimers: true })
|
|
64
|
+
const { config: c, deps } = rt
|
|
65
|
+
let hub: ReturnType<typeof serveHub>
|
|
350
66
|
try {
|
|
351
|
-
|
|
352
|
-
const base = `http://localhost:${hub.server.port}`
|
|
353
|
-
// a banner, not a checklist: one clickable line, then what is inside,
|
|
354
|
-
// dimmed by indentation. Wire prefixes stay reserved for problems.
|
|
355
|
-
io.print(`${c.name} hub → ${base}`)
|
|
356
|
-
const routes = hub.tools.filter((t) => t.path !== '/').map((t) => `${t.label} ${t.path}`).join(' · ')
|
|
357
|
-
if (routes !== '') io.print(` also: ${routes}`)
|
|
358
|
-
// a page that cannot refresh itself looks identical to a broken one,
|
|
359
|
-
// and the only tell is that every change needs a reload key
|
|
360
|
-
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`)
|
|
361
|
-
// the off-machine door (phone, other machines) — publicHost is config
|
|
362
|
-
if (c.serve?.publicHost != null) io.print(` phone: http://${c.serve.publicHost}:${hub.server.port}/`)
|
|
363
|
-
// said once at boot, so a silent phone is never mistaken for no blockers
|
|
364
|
-
if (c.notifyCommand != null && isLinkedWorktree(root)) io.print(` pings: off, this is a linked worktree (only the main checkout's hub notifies)`)
|
|
365
|
-
// the daily health pass rides the serving process, and only it: a hub
|
|
366
|
-
// built for a test never starts a timer
|
|
367
|
-
deps.schedule?.start((e) => io.printErr(`WARN the daily health pass broke: ${e.message}`))
|
|
368
|
-
// the quick gate at start, then after every save burst, so the
|
|
369
|
-
// problems pane follows the code. It only asks the runner, which
|
|
370
|
-
// measures or replays like any run: the watch decides WHEN, never
|
|
371
|
-
// what is true
|
|
372
|
-
const trigger = new GateTrigger({
|
|
373
|
-
run: () => deps.runChecks({ line: () => {}, check: () => {} }, { gate: true, by: 'hub', auto: true }),
|
|
374
|
-
onError: (e) => io.printErr(`WARN the hub's own gate run broke: ${e.message}`),
|
|
375
|
-
})
|
|
376
|
-
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}`) })
|
|
377
|
-
if (watcher.error != null) io.printErr(`WARN no file watch (${watcher.error}), so the problems pane refreshes only on a hub run`)
|
|
378
|
-
// a lib that moved under this process (under bun --watch the version
|
|
379
|
-
// import already restarted it, so what is left is a plain serve), or
|
|
380
|
-
// a file table the watch restarts leaked: it re-executes itself in
|
|
381
|
-
// place (same pid, argv, port) once no run is in flight. Only a
|
|
382
|
-
// restart that throws is said, in the hub's own status
|
|
383
|
-
const stale = new StaleRestart({
|
|
384
|
-
boot: LIB_VERSION,
|
|
385
|
-
disk: diskLibVersion,
|
|
386
|
-
busy: () => deps.busy?.() === true,
|
|
387
|
-
restart: (why) => {
|
|
388
|
-
// a refusal throws while the hub still serves, so the page can say it
|
|
389
|
-
const blocked = reexecBlocker()
|
|
390
|
-
if (blocked != null) throw new Error(blocked)
|
|
391
|
-
io.print(`ok: ${why}: restarting in place`)
|
|
392
|
-
stale.stop()
|
|
393
|
-
trigger.stop()
|
|
394
|
-
watcher.close()
|
|
395
|
-
hub.stop()
|
|
396
|
-
try {
|
|
397
|
-
reexecSelf()
|
|
398
|
-
} catch (e) {
|
|
399
|
-
// the server is already down: nothing is left to show a status
|
|
400
|
-
io.printErr(`FAIL: this hub stopped to restart and could not: ${(e as Error).message}. Restart the hub task`)
|
|
401
|
-
process.exit(1)
|
|
402
|
-
}
|
|
403
|
-
},
|
|
404
|
-
say: (text) => deps.hubProblems?.set('stale', text == null ? null : { text, severity: 'red' }),
|
|
405
|
-
})
|
|
406
|
-
stale.start()
|
|
407
|
-
// a watch restart that leaked sheds it before the first run spawns anything
|
|
408
|
-
stale.tick()
|
|
409
|
-
trigger.request()
|
|
410
|
-
// a child process (the comfy panel) outlives a parent nobody stopped,
|
|
411
|
-
// and the next launch then adopts one holding the PREVIOUS run's
|
|
412
|
-
// modules. A hard kill still leaks one, which is why adoption exists
|
|
413
|
-
for (const sig of ['SIGINT', 'SIGTERM'] as const)
|
|
414
|
-
process.on(sig, () => {
|
|
415
|
-
stale.stop()
|
|
416
|
-
trigger.stop()
|
|
417
|
-
watcher.close()
|
|
418
|
-
hub.stop()
|
|
419
|
-
process.exit(0)
|
|
420
|
-
})
|
|
421
|
-
await new Promise(() => {}) // the hub owns the process — never resolve
|
|
422
|
-
return 0
|
|
67
|
+
hub = serveHub(deps, rt.handler)
|
|
423
68
|
} catch (e) {
|
|
69
|
+
rt.stop()
|
|
424
70
|
if (e instanceof Error && e.message.includes('in use')) {
|
|
425
71
|
// same repo in another window is fine; a different repo hashed
|
|
426
72
|
// onto this port is a real collision — never claim its hub
|
|
@@ -440,4 +86,66 @@ export async function runServe(root: string, io: ServeIo, o?: { port?: number })
|
|
|
440
86
|
}
|
|
441
87
|
throw e
|
|
442
88
|
}
|
|
89
|
+
const base = `http://localhost:${hub.server.port}`
|
|
90
|
+
// a banner, not a checklist: one clickable line, then what is inside,
|
|
91
|
+
// dimmed by indentation. Wire prefixes stay reserved for problems.
|
|
92
|
+
io.print(`${c.name} hub → ${base}`)
|
|
93
|
+
const routes = hub.tools.filter((t) => t.path !== '/').map((t) => `${t.label} ${t.path}`).join(' · ')
|
|
94
|
+
if (routes !== '') io.print(` also: ${routes}`)
|
|
95
|
+
// a page that cannot refresh itself looks identical to a broken one,
|
|
96
|
+
// and the only tell is that every change needs a reload key
|
|
97
|
+
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`)
|
|
98
|
+
// the off-machine door (phone, other machines) — publicHost is config
|
|
99
|
+
if (c.serve?.publicHost != null) io.print(` phone: http://${c.serve.publicHost}:${hub.server.port}/`)
|
|
100
|
+
// said once at boot, so a silent phone is never mistaken for no blockers
|
|
101
|
+
if (c.notifyCommand != null && isLinkedWorktree(root)) io.print(` pings: off, this is a linked worktree (only the main checkout's hub notifies)`)
|
|
102
|
+
// a lib that moved under this process (under bun --watch the version
|
|
103
|
+
// import already restarted it, so what is left is a plain serve), or
|
|
104
|
+
// a file table the watch restarts leaked: it re-executes itself in
|
|
105
|
+
// place (same pid, argv, port) once no run is in flight. Only a
|
|
106
|
+
// restart that throws is said, in the hub's own status
|
|
107
|
+
const stale = new StaleRestart({
|
|
108
|
+
boot: LIB_VERSION,
|
|
109
|
+
disk: diskLibVersion,
|
|
110
|
+
busy: () => deps.busy?.() === true,
|
|
111
|
+
restart: (why) => {
|
|
112
|
+
// a refusal throws while the hub still serves, so the page can say it
|
|
113
|
+
const blocked = reexecBlocker()
|
|
114
|
+
if (blocked != null) throw new Error(blocked)
|
|
115
|
+
io.print(`ok: ${why}: restarting in place`)
|
|
116
|
+
stale.stop()
|
|
117
|
+
clearInterval(planClock)
|
|
118
|
+
rt.stop()
|
|
119
|
+
hub.stop()
|
|
120
|
+
try {
|
|
121
|
+
reexecSelf()
|
|
122
|
+
} catch (e) {
|
|
123
|
+
// the server is already down: nothing is left to show a status
|
|
124
|
+
io.printErr(`FAIL: this hub stopped to restart and could not: ${(e as Error).message}. Restart the hub task`)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
say: (text) => deps.hubProblems?.set('stale', text == null ? null : { text, severity: 'red' }),
|
|
129
|
+
})
|
|
130
|
+
stale.start()
|
|
131
|
+
// a watch restart that leaked sheds it before the first run spawns anything
|
|
132
|
+
stale.tick()
|
|
133
|
+
// the save watch, the processes and the pings, and the checks whose
|
|
134
|
+
// answer would not replay once the start settles
|
|
135
|
+
rt.start()
|
|
136
|
+
const planClock = setInterval(() => plan.tick(), PLAN_TICK_MS)
|
|
137
|
+
// a child process (the comfy panel) outlives a parent nobody stopped,
|
|
138
|
+
// and the next launch then adopts one holding the PREVIOUS run's
|
|
139
|
+
// modules. A hard kill still leaks one, which is why adoption exists
|
|
140
|
+
for (const sig of ['SIGINT', 'SIGTERM'] as const)
|
|
141
|
+
process.on(sig, () => {
|
|
142
|
+
stale.stop()
|
|
143
|
+
clearInterval(planClock)
|
|
144
|
+
plan.stop()
|
|
145
|
+
rt.stop({ procs: true })
|
|
146
|
+
hub.stop()
|
|
147
|
+
process.exit(0)
|
|
148
|
+
})
|
|
149
|
+
await new Promise(() => {}) // the hub owns the process — never resolve
|
|
150
|
+
return 0
|
|
443
151
|
}
|