rvlib-shipkit 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// the machine's global processes (ComfyUI, a TTS server, an app panel), each
|
|
2
|
+
// declared once in a repo's `<name>.shipkit.ts` as `globalProcess({...})`
|
|
3
|
+
// (shipkit-files.ts) and owned by pm: a switch, a restart, its log, its
|
|
4
|
+
// action buttons, and its url opened in the default browser or as a shipkit
|
|
5
|
+
// app window.
|
|
6
|
+
//
|
|
7
|
+
// two kinds: with no `down`, the command IS the service, supervised like a
|
|
8
|
+
// hub (restarted on a crash, signalled on stop). With `down`, the command is
|
|
9
|
+
// a bring-up that exits (an idempotent ladder script), `down` its reverse,
|
|
10
|
+
// and the probe is the only truth.
|
|
11
|
+
import { mkdirSync, openSync, closeSync } from 'fs'
|
|
12
|
+
import { dirname, join } from 'path'
|
|
13
|
+
import { fileURLToPath } from 'url'
|
|
14
|
+
import type { GlobalProcessDecl } from './global-units.ts'
|
|
15
|
+
import { iconImageType } from '../shipkit-files.ts'
|
|
16
|
+
import { Proc, realDeps, tailLines, type ProcStatus, type SuperviseDeps } from './supervise.ts'
|
|
17
|
+
import { resourceLog, resourcePid } from './home-paths.ts'
|
|
18
|
+
import { groupOf, localPort, portHolders } from '../core/port-holders.ts'
|
|
19
|
+
|
|
20
|
+
export type ResourceView = {
|
|
21
|
+
id: string
|
|
22
|
+
label: string
|
|
23
|
+
description: string | null
|
|
24
|
+
where: string | null
|
|
25
|
+
status: ProcStatus
|
|
26
|
+
/** the pid pm holds for it, null for a bring-up or one started elsewhere */
|
|
27
|
+
pid: number | null
|
|
28
|
+
/** epoch ms its uptime counts from while up (its pid's start when it has
|
|
29
|
+
* one, else its last status change), its crash time when crashed */
|
|
30
|
+
since: number
|
|
31
|
+
/** what the owner last asked for */
|
|
32
|
+
want: 'up' | 'down'
|
|
33
|
+
note: string | null
|
|
34
|
+
/** a bring-up can always be taken down; a kept process not started by pm cannot */
|
|
35
|
+
canStop: boolean
|
|
36
|
+
/** it runs, started outside the supervisor: whether pm can stop that copy
|
|
37
|
+
* and start its own (and why not). null while there is nothing to take over */
|
|
38
|
+
takeOver: { ok: true } | { ok: false; why: string } | null
|
|
39
|
+
/** the last lines of its log, what the page shows on hover */
|
|
40
|
+
tail: string[]
|
|
41
|
+
/** the repo and the file that declare it */
|
|
42
|
+
repo: string
|
|
43
|
+
file: string
|
|
44
|
+
url: string | null
|
|
45
|
+
/** what its card wears, as declared: null leaves the card its repo's icon */
|
|
46
|
+
icon: ProcessIcon | null
|
|
47
|
+
actions: { id: string; label: string }[]
|
|
48
|
+
/** the action running now, or null */
|
|
49
|
+
running: string | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** an emoji, or the url pm serves a declared image file at */
|
|
53
|
+
export type ProcessIcon = { text: string } | { src: string }
|
|
54
|
+
|
|
55
|
+
/** the page's url for a process's declared icon */
|
|
56
|
+
export const processIconOf = (d: GlobalProcessDecl): ProcessIcon | null =>
|
|
57
|
+
d.icon == null ? null : iconImageType(d.icon) != null ? { src: `/api/pm/icon/${encodeURIComponent(d.id)}` } : { text: d.icon }
|
|
58
|
+
|
|
59
|
+
export type HttpProbe = (url: string) => Promise<boolean>
|
|
60
|
+
|
|
61
|
+
export const probeUrl: HttpProbe = async (url) => {
|
|
62
|
+
try {
|
|
63
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(2_000) })
|
|
64
|
+
return r.ok
|
|
65
|
+
} catch {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** runs a command to its end, output appended to the log; the exit code */
|
|
71
|
+
export type RunOnce = (cmd: string[], cwd: string, log: string) => Promise<number>
|
|
72
|
+
|
|
73
|
+
export const runOnce: RunOnce = async (cmd, cwd, log) => {
|
|
74
|
+
mkdirSync(dirname(log), { recursive: true })
|
|
75
|
+
const fd = openSync(log, 'a')
|
|
76
|
+
try {
|
|
77
|
+
const p = Bun.spawn(cmd, { cwd, stdio: ['ignore', fd, fd] })
|
|
78
|
+
return await p.exited
|
|
79
|
+
} catch (e) {
|
|
80
|
+
throw new Error(`could not run ${cmd.join(' ')}: ${(e as Error).message}`)
|
|
81
|
+
} finally {
|
|
82
|
+
closeSync(fd)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** opens a shipkit app window on a url; what it did lands in the log */
|
|
87
|
+
export type OpenApp = (url: string, log: string) => void
|
|
88
|
+
|
|
89
|
+
/** its own process: what building the app needs never loads in pm */
|
|
90
|
+
const OPEN_APP = fileURLToPath(new URL('./open-app.ts', import.meta.url))
|
|
91
|
+
|
|
92
|
+
const openApp: OpenApp = (url, log) => {
|
|
93
|
+
mkdirSync(dirname(log), { recursive: true })
|
|
94
|
+
const fd = openSync(log, 'a')
|
|
95
|
+
try {
|
|
96
|
+
Bun.spawn([process.execPath, OPEN_APP, url], { stdio: ['ignore', fd, fd] })
|
|
97
|
+
} finally {
|
|
98
|
+
closeSync(fd)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** opens a url in the default browser; what it did lands in the log */
|
|
103
|
+
export type Browse = (url: string, log: string) => void
|
|
104
|
+
|
|
105
|
+
const browse: Browse = (url, log) => {
|
|
106
|
+
mkdirSync(dirname(log), { recursive: true })
|
|
107
|
+
const fd = openSync(log, 'a')
|
|
108
|
+
try {
|
|
109
|
+
const cmd = process.platform === 'darwin' ? ['open', url] : process.platform === 'win32' ? ['cmd', '/c', 'start', '', url] : ['xdg-open', url]
|
|
110
|
+
Bun.spawn(cmd, { stdio: ['ignore', fd, fd] })
|
|
111
|
+
} finally {
|
|
112
|
+
closeSync(fd)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** the function action runner: imports the unit file, calls the action */
|
|
117
|
+
const ACTION_RUNNER = fileURLToPath(new URL('./run-unit-action.ts', import.meta.url))
|
|
118
|
+
|
|
119
|
+
export type ResourceOpts = {
|
|
120
|
+
probe?: HttpProbe
|
|
121
|
+
run?: RunOnce
|
|
122
|
+
deps?: SuperviseDeps
|
|
123
|
+
/** the app window */
|
|
124
|
+
open?: OpenApp
|
|
125
|
+
/** the default browser */
|
|
126
|
+
browse?: Browse
|
|
127
|
+
/** the pids listening on a local port, and the process group of a pid */
|
|
128
|
+
holders?: (port: number) => number[]
|
|
129
|
+
groupOf?: (pid: number) => number | null
|
|
130
|
+
/** pm's own process group: never signalled by a take over */
|
|
131
|
+
ownGroup?: number
|
|
132
|
+
sleep?: (ms: number) => Promise<void>
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** where a process started elsewhere is found: the port of its url, else of its probe */
|
|
136
|
+
const ownerPort = (d: GlobalProcessDecl): number | null => localPort(d.url) ?? localPort(d.probe)
|
|
137
|
+
|
|
138
|
+
/** what pm's own group is, read once */
|
|
139
|
+
let pmGroup: number | null | undefined
|
|
140
|
+
|
|
141
|
+
/** one switchable process */
|
|
142
|
+
export class Resource {
|
|
143
|
+
private proc: Proc | null = null
|
|
144
|
+
status: ProcStatus = 'down'
|
|
145
|
+
want: 'up' | 'down' = 'down'
|
|
146
|
+
note: string | null = null
|
|
147
|
+
running: string | null = null
|
|
148
|
+
/** how its last action ended, kept beside what the supervisor says */
|
|
149
|
+
private actionNote: string | null = null
|
|
150
|
+
private busy = false
|
|
151
|
+
/** the last bring-up exited non zero */
|
|
152
|
+
private failed = false
|
|
153
|
+
/** open it once it answers: in the default browser, or as an app window */
|
|
154
|
+
private openWhenUp: 'browser' | 'app' | null = null
|
|
155
|
+
/** the status a tick last saw, and since when, for the bring-up kind */
|
|
156
|
+
private seen: ProcStatus = 'down'
|
|
157
|
+
private seenAt: number
|
|
158
|
+
readonly log: string
|
|
159
|
+
|
|
160
|
+
constructor(readonly decl: GlobalProcessDecl, dir: string, private o: ResourceOpts = {}) {
|
|
161
|
+
this.log = resourceLog(dir, decl.id)
|
|
162
|
+
this.seenAt = this.now()
|
|
163
|
+
if (decl.down == null)
|
|
164
|
+
this.proc = new Proc({ id: decl.id, cmd: decl.cmd, cwd: decl.cwd, log: this.log, pidKey: resourcePid(dir, decl.id), probe: decl.probe == null ? undefined : () => this.probe() }, o.deps ?? realDeps)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private now(): number {
|
|
168
|
+
return this.o.deps?.now() ?? Date.now()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private probe(): Promise<boolean> {
|
|
172
|
+
return this.decl.probe == null ? Promise.resolve(true) : (this.o.probe ?? probeUrl)(this.decl.probe)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get canStop(): boolean {
|
|
176
|
+
return this.proc == null || !this.proc.external
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** null while it is ours or not running; else whether it can be taken over */
|
|
180
|
+
get takeOverView(): ResourceView['takeOver'] {
|
|
181
|
+
if (this.proc == null || !this.proc.external) return null
|
|
182
|
+
if (ownerPort(this.decl) == null) return { ok: false, why: 'it declares no local url or probe with a port, so the process that runs it cannot be found: stop it where it was started' }
|
|
183
|
+
return { ok: true }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** it runs, started elsewhere: stop that copy (the process group that
|
|
187
|
+
* listens on its port, SIGTERM then SIGKILL after the grace) and start
|
|
188
|
+
* the supervised one, in one go */
|
|
189
|
+
async takeOver(): Promise<void> {
|
|
190
|
+
const p = this.proc
|
|
191
|
+
const v = this.takeOverView
|
|
192
|
+
if (p == null || v == null) throw new Error(`${this.decl.id} does not run outside the supervisor: nothing to take over`)
|
|
193
|
+
if (!v.ok) throw new Error(`${this.decl.id}: ${v.why}`)
|
|
194
|
+
const port = ownerPort(this.decl)!
|
|
195
|
+
const owners = (this.o.holders ?? portHolders)(port).filter((pid) => pid !== process.pid)
|
|
196
|
+
if (owners.length === 0) throw new Error(`${this.decl.id} answers, but no process listens on port ${port}: nothing to stop`)
|
|
197
|
+
const own = this.o.ownGroup ?? (pmGroup === undefined ? (pmGroup = groupOf(process.pid)) : pmGroup)
|
|
198
|
+
const targets = [...new Set(owners.map((pid) => {
|
|
199
|
+
const g = (this.o.groupOf ?? groupOf)(pid)
|
|
200
|
+
// a group of one's own (pm's, a shell's login group) is never signalled whole
|
|
201
|
+
return g != null && g > 1 && g !== own ? g : pid
|
|
202
|
+
}))]
|
|
203
|
+
this.want = 'up'
|
|
204
|
+
this.openWhenUp = null
|
|
205
|
+
await p.takeOver(owners, targets, this.o.sleep ?? ((ms) => Bun.sleep(ms)))
|
|
206
|
+
await this.tick()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** switch it on or off. The bring-up kind reports its command's failure */
|
|
210
|
+
async set(want: 'up' | 'down'): Promise<void> {
|
|
211
|
+
this.want = want
|
|
212
|
+
if (want === 'down') this.openWhenUp = null
|
|
213
|
+
if (this.proc != null) {
|
|
214
|
+
if (want === 'up') await this.proc.ensure()
|
|
215
|
+
else this.proc.stop()
|
|
216
|
+
await this.tick()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
if (this.busy) return
|
|
220
|
+
const cmd = want === 'up' ? this.decl.cmd : this.decl.down!
|
|
221
|
+
this.busy = true
|
|
222
|
+
this.status = want === 'up' ? 'starting' : 'stopping'
|
|
223
|
+
this.note = null
|
|
224
|
+
this.failed = false
|
|
225
|
+
try {
|
|
226
|
+
const code = await (this.o.run ?? runOnce)(cmd, this.decl.cwd, this.log)
|
|
227
|
+
if (code !== 0) {
|
|
228
|
+
this.failed = true
|
|
229
|
+
this.note = `\`${cmd.join(' ')}\` exited ${code}: see the log`
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
this.failed = true
|
|
233
|
+
this.note = (e as Error).message
|
|
234
|
+
} finally {
|
|
235
|
+
this.busy = false
|
|
236
|
+
}
|
|
237
|
+
await this.tick()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async restart(): Promise<void> {
|
|
241
|
+
if (this.proc != null) {
|
|
242
|
+
this.want = 'up'
|
|
243
|
+
this.proc.restart()
|
|
244
|
+
await this.tick()
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
await this.set('down')
|
|
248
|
+
await this.set('up')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** its url in the default browser, or as a shipkit app window: at once
|
|
252
|
+
* while it answers, else it starts and opens once it does */
|
|
253
|
+
async open(how: 'browser' | 'app'): Promise<void> {
|
|
254
|
+
if (this.decl.url == null) throw new Error(`${this.decl.id} declares no url to open`)
|
|
255
|
+
if (this.status === 'up') {
|
|
256
|
+
this.show(how, this.decl.url)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
this.openWhenUp = how
|
|
260
|
+
await this.set('up')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private show(how: 'browser' | 'app', url: string): void {
|
|
264
|
+
if (how === 'app') (this.o.open ?? openApp)(url, this.log)
|
|
265
|
+
else (this.o.browse ?? browse)(url, this.log)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** one declared button. A function action runs in its own bun process
|
|
269
|
+
* that imports the unit file */
|
|
270
|
+
async action(id: string): Promise<void> {
|
|
271
|
+
const a = this.decl.actions.find((x) => x.id === id)
|
|
272
|
+
if (a == null) throw new Error(`${this.decl.id} declares no action '${id}'`)
|
|
273
|
+
if (this.running != null) throw new Error(`${this.decl.id} is already running its action '${this.running}'`)
|
|
274
|
+
const cmd = a.cmd ?? [process.execPath, ACTION_RUNNER, join(this.decl.root, this.decl.file), id]
|
|
275
|
+
this.running = id
|
|
276
|
+
this.actionNote = null
|
|
277
|
+
try {
|
|
278
|
+
const code = await (this.o.run ?? runOnce)(cmd, this.decl.root, this.log)
|
|
279
|
+
this.actionNote = code === 0 ? `${a.label}: done` : `${a.label} exited ${code}: see the log`
|
|
280
|
+
} catch (e) {
|
|
281
|
+
this.actionNote = `${a.label}: ${(e as Error).message}`
|
|
282
|
+
} finally {
|
|
283
|
+
this.running = null
|
|
284
|
+
}
|
|
285
|
+
this.note = this.actionNote
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async tick(): Promise<void> {
|
|
289
|
+
if (this.proc != null) {
|
|
290
|
+
await this.proc.tick()
|
|
291
|
+
this.status = this.proc.status
|
|
292
|
+
this.note = this.proc.note ?? this.actionNote
|
|
293
|
+
this.want = this.proc.want
|
|
294
|
+
} else if (!this.busy) {
|
|
295
|
+
const up = await this.probe()
|
|
296
|
+
this.status = up ? 'up' : this.failed && this.want === 'up' ? 'crashed' : 'down'
|
|
297
|
+
}
|
|
298
|
+
if (this.status !== this.seen) {
|
|
299
|
+
this.seen = this.status
|
|
300
|
+
this.seenAt = this.now()
|
|
301
|
+
}
|
|
302
|
+
if (this.openWhenUp != null && this.status === 'up' && this.decl.url != null) {
|
|
303
|
+
const how = this.openWhenUp
|
|
304
|
+
this.openWhenUp = null
|
|
305
|
+
this.show(how, this.decl.url)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
view(): ResourceView {
|
|
310
|
+
const d = this.decl
|
|
311
|
+
return {
|
|
312
|
+
id: d.id,
|
|
313
|
+
label: d.label,
|
|
314
|
+
description: d.description,
|
|
315
|
+
where: d.where,
|
|
316
|
+
status: this.status,
|
|
317
|
+
pid: this.proc?.pid ?? null,
|
|
318
|
+
// up with a pid: since that pid started, so a pm reload never resets its uptime
|
|
319
|
+
since: (this.status === 'up' ? this.proc?.startedAt() : null) ?? this.proc?.since ?? this.seenAt,
|
|
320
|
+
want: this.want,
|
|
321
|
+
note: this.note,
|
|
322
|
+
canStop: this.canStop,
|
|
323
|
+
takeOver: this.takeOverView,
|
|
324
|
+
tail: tailLines(this.log, 20),
|
|
325
|
+
repo: d.repo,
|
|
326
|
+
file: d.file,
|
|
327
|
+
url: d.url,
|
|
328
|
+
icon: processIconOf(d),
|
|
329
|
+
actions: d.actions.map((a) => ({ id: a.id, label: a.label })),
|
|
330
|
+
running: this.running,
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export class Resources {
|
|
336
|
+
list: Resource[]
|
|
337
|
+
/** the unit files that would not load, as pm last read them */
|
|
338
|
+
errors: string[] = []
|
|
339
|
+
|
|
340
|
+
constructor(decls: GlobalProcessDecl[], private dir: string, private o: ResourceOpts = {}) {
|
|
341
|
+
this.list = decls.map((d) => new Resource(d, dir, o))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** the declarations as they are now: an unchanged one keeps its state, a
|
|
345
|
+
* changed or new one starts from its pid record, a dropped one leaves
|
|
346
|
+
* its process running for whoever declares it next */
|
|
347
|
+
sync(decls: GlobalProcessDecl[], errors: string[] = []): void {
|
|
348
|
+
const key = (d: GlobalProcessDecl): string => JSON.stringify(d)
|
|
349
|
+
const had = new Map(this.list.map((r) => [key(r.decl), r]))
|
|
350
|
+
this.list = decls.map((d) => had.get(key(d)) ?? new Resource(d, this.dir, this.o))
|
|
351
|
+
this.errors = errors
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
get(id: string): Resource | null {
|
|
355
|
+
return this.list.find((r) => r.decl.id === id) ?? null
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async tick(): Promise<void> {
|
|
359
|
+
await Promise.all(this.list.map((r) => r.tick()))
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
views(): ResourceView[] {
|
|
363
|
+
return this.list.map((r) => r.view())
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// `shipkit daemon`: resolve what the home needs from the repo it runs in (the
|
|
2
|
+
// house repos around it), and serve its api on the
|
|
3
|
+
// internal port. A second launch sees the first and leaves. Every house repo's
|
|
4
|
+
// hub runs in this process (hubs.ts), on this process's copy of the lib
|
|
5
|
+
// (one-lib.ts). Every gate runs as a detached job it follows (core/jobs.ts), so
|
|
6
|
+
// it runs with no bun watch and an edit of its own code restarts it in place
|
|
7
|
+
// at once (lib-watch.ts): the new image follows the jobs still running. The
|
|
8
|
+
// machine resources are pm's.
|
|
9
|
+
import { fileURLToPath } from 'url'
|
|
10
|
+
import { useOneLib } from '../core/one-lib.ts'
|
|
11
|
+
import { reclaimPreviousImage } from '../core/instance.ts'
|
|
12
|
+
import { reloadOnOwnCode } from './lib-watch.ts'
|
|
13
|
+
import { loadHub } from '../serve/hub-runtime.ts'
|
|
14
|
+
import { houseRepos } from '../serve/project-gutter.ts'
|
|
15
|
+
import { projectGutter } from '../serve/project-gutter-route.ts'
|
|
16
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
17
|
+
import { ensureIdentity } from '../core/identity.ts'
|
|
18
|
+
import { homedir } from 'os'
|
|
19
|
+
import { daemonPort, homeDir, homePort, homeUp, hubBase, WORKER_ENV } from './home-paths.ts'
|
|
20
|
+
import { HomeState } from './home-state.ts'
|
|
21
|
+
import { Hubs } from './hubs.ts'
|
|
22
|
+
import { Planner } from '../plan/planner.ts'
|
|
23
|
+
import { machineCatchUp } from '../plan/catch-up.ts'
|
|
24
|
+
import { serveDaemon } from './serve-daemon.ts'
|
|
25
|
+
import { startSweeps } from './machine-stores.ts'
|
|
26
|
+
import { Widgets } from './widgets.ts'
|
|
27
|
+
|
|
28
|
+
const CLI = fileURLToPath(new URL('../cli.ts', import.meta.url))
|
|
29
|
+
|
|
30
|
+
/** SHIPKIT_DAEMON_ONLY=a,b loads those house repos alone */
|
|
31
|
+
const onlyRepos = (): Set<string> | undefined => {
|
|
32
|
+
const v = process.env.SHIPKIT_DAEMON_ONLY
|
|
33
|
+
return v == null || v.trim() === '' ? undefined : new Set(v.split(',').map((x) => x.trim()).filter((x) => x !== ''))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type HomeIo = { print: (l: string) => void; printErr: (l: string) => void }
|
|
37
|
+
|
|
38
|
+
export async function runDaemon(root: string, io: HomeIo, o: { port?: number } = {}): Promise<number> {
|
|
39
|
+
const own = o.port ?? daemonPort()
|
|
40
|
+
// the port a page reaches every hub on: the web's
|
|
41
|
+
const port = homePort()
|
|
42
|
+
const house = houseRepos(root, homedir())
|
|
43
|
+
if (house.index == null) {
|
|
44
|
+
io.printErr(`FAIL: ${root} includes no house wiki index in its CLAUDE.md, so the daemon has no repos to load. Run it from the repo that hosts the house wiki`)
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
// before the first repo config loads: every one of them, and every check
|
|
48
|
+
// file, then imports this copy of the lib
|
|
49
|
+
useOneLib()
|
|
50
|
+
// a process this one starts runs its gate and its hub in process
|
|
51
|
+
process.env[WORKER_ENV] = '1'
|
|
52
|
+
// a restart in place keeps the pid: the check children its previous
|
|
53
|
+
// image spawned are still running, and die here
|
|
54
|
+
// a bun watch over this process follows every repo config it loads, so
|
|
55
|
+
// one repo's edit would restart every hub and kill every gate
|
|
56
|
+
if (process.execArgv.includes('--watch') || process.execArgv.includes('--hot'))
|
|
57
|
+
io.printErr(`WARN the daemon runs under bun --watch, so an edit of any repo config restarts every hub. Run \`shipkit pm install\` again for an agent without it`)
|
|
58
|
+
const reclaimed = reclaimPreviousImage()
|
|
59
|
+
if (reclaimed.killed.length > 0) io.print(`ok: the daemon restarted in place: killed ${reclaimed.killed.length} check process(es) its previous image left running`)
|
|
60
|
+
const c = await loadRepoConfig(root)
|
|
61
|
+
const dir = homeDir()
|
|
62
|
+
// the machine's one plan: every repo and check pair, ticked by the daemon.
|
|
63
|
+
// Its catch up window lives in the machine db, so a restart in place keeps it
|
|
64
|
+
const plan = new Planner({ catchUp: machineCatchUp, onError: (repo, e) => io.printErr(`WARN the plan of ${repo} broke: ${e.message}`) })
|
|
65
|
+
// a repo's colour counts only the checks its plan holds today
|
|
66
|
+
const gutter = projectGutter({ root, warn: (l) => io.printErr(l), declared: (name) => plan.declared(name) })
|
|
67
|
+
const hubs = new Hubs({
|
|
68
|
+
dir,
|
|
69
|
+
port,
|
|
70
|
+
// every repo carries its identity colours: the first load writes the
|
|
71
|
+
// missing key, one line, and leaves the commit to the repo (SK024)
|
|
72
|
+
prepare: async (repo, hubIo) => {
|
|
73
|
+
const r = await ensureIdentity(repo.path)
|
|
74
|
+
if (r.changed && r.id != null) hubIo.print(`ok: ${repo.name}'s repo.config.ts gets identity ${r.id.colorLight} / ${r.id.colorDark}, from ${r.from}`)
|
|
75
|
+
else if (r.refused != null) hubIo.print(`note: ${repo.name} has no identity colours: ${r.refused}`)
|
|
76
|
+
},
|
|
77
|
+
load: async (repo, hubIo, o) => {
|
|
78
|
+
const rt = await loadHub(repo.path, hubIo, {
|
|
79
|
+
base: hubBase(repo.name),
|
|
80
|
+
port,
|
|
81
|
+
projects: gutter,
|
|
82
|
+
plan,
|
|
83
|
+
planName: repo.name,
|
|
84
|
+
gateAtStart: false,
|
|
85
|
+
ownTimers: false,
|
|
86
|
+
fresh: o.fresh,
|
|
87
|
+
// every gate is a detached job: a reload of this process never kills one
|
|
88
|
+
jobs: { cli: CLI },
|
|
89
|
+
})
|
|
90
|
+
hubIo.print(`${rt.name} hub → http://localhost:${port}${hubBase(repo.name)}/`)
|
|
91
|
+
return { ...rt, uses: rt.deps.uses ?? [] }
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
try {
|
|
95
|
+
const home = serveDaemon({
|
|
96
|
+
root,
|
|
97
|
+
name: c.name,
|
|
98
|
+
port,
|
|
99
|
+
daemonPort: own,
|
|
100
|
+
dir,
|
|
101
|
+
hubs,
|
|
102
|
+
state: new HomeState(dir),
|
|
103
|
+
gutter,
|
|
104
|
+
plan,
|
|
105
|
+
only: onlyRepos(),
|
|
106
|
+
widgets: new Widgets(),
|
|
107
|
+
})
|
|
108
|
+
io.print(`daemon → http://127.0.0.1:${home.server.port} (the page: http://localhost:${port})`)
|
|
109
|
+
// nothing grows unbound: every store of every repo it loads, and the
|
|
110
|
+
// machine's, swept on boot and hourly (core/retention.ts)
|
|
111
|
+
const only = onlyRepos()
|
|
112
|
+
const sweeps = startSweeps({ roots: () => house.repos.filter((r) => only == null || only.has(r.name)).map((r) => r.path), dir, printErr: io.printErr })
|
|
113
|
+
const code = reloadOnOwnCode({ role: 'daemon', entry: fileURLToPath(import.meta.url), busy: () => hubs.busy(), before: () => { sweeps.stop(); home.stop() }, io })
|
|
114
|
+
io.print(` ${house.repos.length} repos, state in ${dir}, reloads on ${code.files} lib files`)
|
|
115
|
+
for (const sig of ['SIGINT', 'SIGTERM'] as const)
|
|
116
|
+
process.on(sig, () => {
|
|
117
|
+
// the repo processes it started keep running: they are their
|
|
118
|
+
// own process groups, and the next daemon adopts them
|
|
119
|
+
code.stop()
|
|
120
|
+
sweeps.stop()
|
|
121
|
+
home.stop()
|
|
122
|
+
process.exit(0)
|
|
123
|
+
})
|
|
124
|
+
await new Promise(() => {})
|
|
125
|
+
return 0
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (e instanceof Error && /in use|EADDRINUSE/.test(e.message)) {
|
|
128
|
+
if (await homeUp(own)) {
|
|
129
|
+
io.print(`daemon → http://127.0.0.1:${own} (already running)`)
|
|
130
|
+
return 0
|
|
131
|
+
}
|
|
132
|
+
io.printErr(`FAIL: port ${own} is taken by another process. Set SHIPKIT_DAEMON_PORT to a free one`)
|
|
133
|
+
return 1
|
|
134
|
+
}
|
|
135
|
+
throw e
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// `shipkit pm`: the one process launchd keeps alive. It runs the daemon and
|
|
2
|
+
// the web as detached children, starts one again when it dies, owns the
|
|
3
|
+
// machine's global processes, and serves its api on its own port. It restarts itself
|
|
4
|
+
// in place on an edit of its own code, and adopts its children when it does.
|
|
5
|
+
// It never loads the repo config itself (pm-config.ts), and it runs from this
|
|
6
|
+
// file, never from cli.ts (execPm): cli.ts loads every command, about 170 MB
|
|
7
|
+
// against pm's own 20.
|
|
8
|
+
import { fileURLToPath } from 'url'
|
|
9
|
+
import { daemonPort, homeDir, homePort, homeUp, homeUrl, pmPort, pmUp, WORKER_ENV } from './home-paths.ts'
|
|
10
|
+
import { reloadOnOwnCode } from './lib-watch.ts'
|
|
11
|
+
import { Pm, PM_TICK_MS, type Role } from './pm.ts'
|
|
12
|
+
import { Resources } from './resources.ts'
|
|
13
|
+
import type { HomeIo } from './run-daemon.ts'
|
|
14
|
+
import type { PmConfig } from './pm-config.ts'
|
|
15
|
+
|
|
16
|
+
const CLI = fileURLToPath(new URL('../cli.ts', import.meta.url))
|
|
17
|
+
const PM_CONFIG = fileURLToPath(new URL('./pm-config.ts', import.meta.url))
|
|
18
|
+
|
|
19
|
+
const RUN_PM = fileURLToPath(import.meta.url)
|
|
20
|
+
|
|
21
|
+
/** how often pm reads the house repos' global processes again */
|
|
22
|
+
export const PM_RESCAN_MS = 15_000
|
|
23
|
+
|
|
24
|
+
/** replace this process's image with pm run from this file: same pid, same
|
|
25
|
+
* cwd, same env, none of cli.ts loaded. False when the runtime has no
|
|
26
|
+
* execve, and the caller runs pm in process */
|
|
27
|
+
export function execPm(o: { port?: number } = {}): boolean {
|
|
28
|
+
const execve = (process as { execve?: (file: string, args: string[], env: Record<string, string>) => never }).execve
|
|
29
|
+
if (typeof execve !== 'function') return false
|
|
30
|
+
const env = Object.fromEntries(Object.entries(process.env).filter((e): e is [string, string] => e[1] != null))
|
|
31
|
+
return execve.call(process, process.execPath, [process.execPath, RUN_PM, ...(o.port != null ? ['--port', String(o.port)] : [])], env)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** the house repo's side of pm, read by a child process */
|
|
35
|
+
export async function readPmConfig(root: string): Promise<PmConfig> {
|
|
36
|
+
const p = Bun.spawn([process.execPath, PM_CONFIG, root], { cwd: root, stdout: 'pipe', stderr: 'pipe' })
|
|
37
|
+
const [out, err, code] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text(), p.exited])
|
|
38
|
+
if (code !== 0) throw new Error(`could not read the repo config of ${root} (exit ${code}): ${err.trim() || out.trim()}`)
|
|
39
|
+
return JSON.parse(out) as PmConfig
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** true when the role serves: the daemon as a home on its port, the web as
|
|
43
|
+
* any http answer on the home port (it answers 502 while the daemon is down,
|
|
44
|
+
* and still holds its port) */
|
|
45
|
+
export async function roleAnswers(role: Role): Promise<boolean> {
|
|
46
|
+
if (role === 'daemon') return homeUp(daemonPort())
|
|
47
|
+
try {
|
|
48
|
+
await fetch(`${homeUrl(homePort())}/`, { signal: AbortSignal.timeout(400) })
|
|
49
|
+
return true
|
|
50
|
+
} catch {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runPm(root: string, io: HomeIo, o: { port?: number } = {}): Promise<number> {
|
|
56
|
+
const port = o.port ?? pmPort()
|
|
57
|
+
// every process pm starts runs its shipkit in process, and never starts pm
|
|
58
|
+
process.env[WORKER_ENV] = '1'
|
|
59
|
+
let c: PmConfig
|
|
60
|
+
try {
|
|
61
|
+
c = await readPmConfig(root)
|
|
62
|
+
} catch (e) {
|
|
63
|
+
io.printErr(`FAIL: ${(e as Error).message}`)
|
|
64
|
+
return 1
|
|
65
|
+
}
|
|
66
|
+
if (c.index == null) {
|
|
67
|
+
io.printErr(`FAIL: ${root} includes no house wiki index in its CLAUDE.md, so pm has no daemon to run. Run it from the repo that hosts the house wiki`)
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
if (await pmUp(port)) {
|
|
71
|
+
io.print(`pm → http://127.0.0.1:${port} (already running)`)
|
|
72
|
+
return 0
|
|
73
|
+
}
|
|
74
|
+
const dir = homeDir()
|
|
75
|
+
const pm = new Pm({
|
|
76
|
+
dir,
|
|
77
|
+
root,
|
|
78
|
+
port,
|
|
79
|
+
cmd: (role) => [process.execPath, CLI, role],
|
|
80
|
+
probe: roleAnswers,
|
|
81
|
+
resources: new Resources(c.processes, dir),
|
|
82
|
+
})
|
|
83
|
+
let server: Bun.Server<unknown>
|
|
84
|
+
try {
|
|
85
|
+
server = Bun.serve({ port, hostname: '127.0.0.1', fetch: (req) => pm.handle(req) })
|
|
86
|
+
} catch (e) {
|
|
87
|
+
if (e instanceof Error && /in use|EADDRINUSE/.test(e.message)) {
|
|
88
|
+
io.printErr(`FAIL: port ${port} is taken by another process. Set SHIPKIT_PM_PORT to a free one`)
|
|
89
|
+
return 1
|
|
90
|
+
}
|
|
91
|
+
throw e
|
|
92
|
+
}
|
|
93
|
+
await pm.boot()
|
|
94
|
+
for (const p of pm.children.values()) {
|
|
95
|
+
const v = p.view()
|
|
96
|
+
io.print(` ${v.id}: ${p.adopted ? 'adopted' : v.status}${v.pid != null ? ` pid ${v.pid}` : ''}${v.note != null ? ` (${v.note})` : ''}`)
|
|
97
|
+
}
|
|
98
|
+
pm.resources.errors = c.errors
|
|
99
|
+
for (const e of c.errors) io.printErr(`WARN ${e}`)
|
|
100
|
+
// a unit file added, edited or removed in any house repo: read again,
|
|
101
|
+
// in its child process, and the unchanged processes keep their state
|
|
102
|
+
let reading = false
|
|
103
|
+
const reread = setInterval(() => {
|
|
104
|
+
if (reading) return
|
|
105
|
+
reading = true
|
|
106
|
+
void readPmConfig(root)
|
|
107
|
+
.then((n) => pm.resources.sync(n.processes, n.errors))
|
|
108
|
+
.catch((e) => io.printErr(`WARN pm could not read the global processes again: ${(e as Error).message}`))
|
|
109
|
+
.finally(() => { reading = false })
|
|
110
|
+
}, PM_RESCAN_MS)
|
|
111
|
+
let ticking = false
|
|
112
|
+
const timer = setInterval(() => {
|
|
113
|
+
if (ticking) return
|
|
114
|
+
ticking = true
|
|
115
|
+
void pm.tick().catch((e) => io.printErr(`WARN the pm tick broke: ${(e as Error).message}`)).finally(() => { ticking = false })
|
|
116
|
+
}, PM_TICK_MS)
|
|
117
|
+
const code = reloadOnOwnCode({
|
|
118
|
+
role: 'pm',
|
|
119
|
+
entry: fileURLToPath(import.meta.url),
|
|
120
|
+
// the children keep running, and the next image adopts them
|
|
121
|
+
before: () => {
|
|
122
|
+
clearInterval(timer)
|
|
123
|
+
clearInterval(reread)
|
|
124
|
+
server.stop(true)
|
|
125
|
+
},
|
|
126
|
+
io,
|
|
127
|
+
})
|
|
128
|
+
io.print(`pm → http://127.0.0.1:${server.port} (the page: http://localhost:${homePort()}, the daemon: :${daemonPort()}), ${c.processes.length} global processes, logs in ${dir}, reloads on ${code.files} lib files`)
|
|
129
|
+
let stopping = false
|
|
130
|
+
for (const sig of ['SIGINT', 'SIGTERM'] as const)
|
|
131
|
+
process.on(sig, () => {
|
|
132
|
+
// a stop asked of pm stops its children; the global processes keep running,
|
|
133
|
+
// their own process groups, for the next pm to adopt
|
|
134
|
+
if (stopping) return
|
|
135
|
+
stopping = true
|
|
136
|
+
code.stop()
|
|
137
|
+
clearInterval(timer)
|
|
138
|
+
clearInterval(reread)
|
|
139
|
+
server.stop(true)
|
|
140
|
+
void pm.stopAll().finally(() => process.exit(0))
|
|
141
|
+
})
|
|
142
|
+
await new Promise(() => {})
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (import.meta.main) {
|
|
147
|
+
const i = process.argv.indexOf('--port')
|
|
148
|
+
const port = i < 0 ? undefined : Number(process.argv[i + 1])
|
|
149
|
+
process.exitCode = await runPm(process.cwd(), { print: (l) => console.log(l), printErr: (l) => console.error(l) }, { port })
|
|
150
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// runs one function action of a global process, in its own bun process so a
|
|
2
|
+
// slow or crashing action never touches pm:
|
|
3
|
+
//
|
|
4
|
+
// bun run-unit-action.ts <unit file> <action id>
|
|
5
|
+
//
|
|
6
|
+
// the unit file imports the lib by name, so this copy is registered first.
|
|
7
|
+
import type { GlobalProcessUnit } from '../shipkit-files.ts'
|
|
8
|
+
|
|
9
|
+
if (import.meta.main) {
|
|
10
|
+
const [file, id] = process.argv.slice(2)
|
|
11
|
+
if (file == null || id == null) {
|
|
12
|
+
console.error('usage: bun run-unit-action.ts <unit file> <action id>')
|
|
13
|
+
process.exit(2)
|
|
14
|
+
}
|
|
15
|
+
const { useOneLib } = await import('../core/one-lib.ts')
|
|
16
|
+
useOneLib()
|
|
17
|
+
const u = ((await import(file)) as { default?: GlobalProcessUnit }).default
|
|
18
|
+
const run = u?.actions?.[id]?.run
|
|
19
|
+
if (run == null) {
|
|
20
|
+
console.error(`FAIL: ${file} has no function action '${id}'`)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
console.log(`▶ ${u!.id}: ${u!.actions![id]!.label}`)
|
|
24
|
+
await run()
|
|
25
|
+
}
|