rvlib-shipkit 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
package/src/home/pm.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// pm, the machine's process manager: it keeps the daemon and the web running
|
|
2
|
+
// (supervise.ts: detached, pid records, crash restarts with backoff, adoption)
|
|
3
|
+
// and owns the machine's global processes (resources.ts). Its api answers on
|
|
4
|
+
// its own port, and the web proxies /api/pm/* to it, so the page switches a
|
|
5
|
+
// process and restarts a child through the one origin it knows.
|
|
6
|
+
//
|
|
7
|
+
// the one constraint a reader breaks first: pm never takes a child down with
|
|
8
|
+
// it. A reload of pm, or pm killed, leaves both running, and the next pm
|
|
9
|
+
// adopts them from their pid records. Only a stop asked of pm stops them.
|
|
10
|
+
import { existsSync } from 'fs'
|
|
11
|
+
import { join } from 'path'
|
|
12
|
+
import { LIB_VERSION } from '../core/lib-version.ts'
|
|
13
|
+
import { iconImageType } from '../shipkit-files.ts'
|
|
14
|
+
import { sameOrigin } from '../serve/same-origin.ts'
|
|
15
|
+
import { childLog, childPid, daemonPort, homePort, pmLog, pmPort } from './home-paths.ts'
|
|
16
|
+
import type { Resources, ResourceView } from './resources.ts'
|
|
17
|
+
import { capLog, Proc, realDeps, tailLines, type ProcView, type SuperviseDeps } from './supervise.ts'
|
|
18
|
+
|
|
19
|
+
export type Role = 'daemon' | 'web'
|
|
20
|
+
export const ROLES: Role[] = ['daemon', 'web']
|
|
21
|
+
|
|
22
|
+
/** how often pm looks at its children and the global processes */
|
|
23
|
+
export const PM_TICK_MS = 2_000
|
|
24
|
+
|
|
25
|
+
export type ChildView = ProcView & { tail: string[] }
|
|
26
|
+
|
|
27
|
+
/** where pm, the daemon and the web listen */
|
|
28
|
+
export type CorePorts = { pm: number; daemon: number; web: number }
|
|
29
|
+
|
|
30
|
+
/** how many log lines each of shipkit's own processes hands the page */
|
|
31
|
+
const CORE_TAIL = 20
|
|
32
|
+
|
|
33
|
+
export type PmAnswer = {
|
|
34
|
+
pm: 'shipkit-pm'
|
|
35
|
+
pid: number
|
|
36
|
+
/** epoch ms this pm started; absent from a pm older than the field */
|
|
37
|
+
since?: number
|
|
38
|
+
version: string
|
|
39
|
+
/** where pm and its children listen; absent from a pm older than the field */
|
|
40
|
+
ports?: CorePorts
|
|
41
|
+
/** the last lines of pm's own log; absent from a pm older than the field */
|
|
42
|
+
tail?: string[]
|
|
43
|
+
children: ChildView[]
|
|
44
|
+
resources: ResourceView[]
|
|
45
|
+
/** the `.shipkit.ts` files that would not load, as pm last read them */
|
|
46
|
+
unitErrors: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type PmDeps = {
|
|
50
|
+
dir: string
|
|
51
|
+
/** the house repo: where the children run */
|
|
52
|
+
root: string
|
|
53
|
+
/** the port pm serves on (default: SHIPKIT_PM_PORT, else 4797) */
|
|
54
|
+
port?: number
|
|
55
|
+
/** the command that runs one role, e.g. [bun, cli.ts, 'daemon'] */
|
|
56
|
+
cmd: (role: Role) => string[]
|
|
57
|
+
/** true once the role serves */
|
|
58
|
+
probe: (role: Role) => Promise<boolean>
|
|
59
|
+
resources: Resources
|
|
60
|
+
deps?: SuperviseDeps
|
|
61
|
+
/** runs a stop or restart of the web once its answer is out: the web
|
|
62
|
+
* proxies that very request (default: 200 ms later) */
|
|
63
|
+
later?: (f: () => void) => void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const WEB_ANSWER_MS = 200
|
|
67
|
+
|
|
68
|
+
export class Pm {
|
|
69
|
+
readonly children: Map<Role, Proc>
|
|
70
|
+
private readonly startedAt = Date.now()
|
|
71
|
+
|
|
72
|
+
constructor(private d: PmDeps) {
|
|
73
|
+
this.children = new Map(ROLES.map((role) => [role, new Proc({
|
|
74
|
+
id: role,
|
|
75
|
+
cmd: d.cmd(role),
|
|
76
|
+
cwd: d.root,
|
|
77
|
+
log: childLog(d.dir, role),
|
|
78
|
+
pidKey: childPid(d.dir, role),
|
|
79
|
+
probe: () => d.probe(role),
|
|
80
|
+
// a crash loop while its code is being edited heals itself
|
|
81
|
+
neverGiveUp: true,
|
|
82
|
+
}, d.deps ?? realDeps)]))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get resources(): Resources {
|
|
86
|
+
return this.d.resources
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** every child up: adopted from its pid record, else started, unless
|
|
90
|
+
* something else already answers its port */
|
|
91
|
+
async boot(): Promise<void> {
|
|
92
|
+
await Promise.all([...this.children.values()].map((p) => p.ensure()))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async tick(): Promise<void> {
|
|
96
|
+
await Promise.all([...[...this.children.values()].map((p) => p.tick()), this.d.resources.tick()])
|
|
97
|
+
for (const role of ROLES) capLog(childLog(this.d.dir, role))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** stop both children and wait until they are gone, SIGKILL included */
|
|
101
|
+
async stopAll(o: { waitMs?: number; stepMs?: number } = {}): Promise<void> {
|
|
102
|
+
for (const p of this.children.values()) p.stop()
|
|
103
|
+
const until = Date.now() + (o.waitMs ?? 12_000)
|
|
104
|
+
while (Date.now() < until && [...this.children.values()].some((p) => p.status === 'stopping')) {
|
|
105
|
+
await Bun.sleep(o.stepMs ?? 200)
|
|
106
|
+
await Promise.all([...this.children.values()].map((p) => p.tick()))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
answer(): PmAnswer {
|
|
111
|
+
// up: since its pid started, so a pm reload never resets its uptime
|
|
112
|
+
const children = [...this.children.values()].map((p) => {
|
|
113
|
+
const v = p.view()
|
|
114
|
+
return { ...v, since: (v.status === 'up' ? p.startedAt() : null) ?? v.since, tail: tailLines(p.spec.log, CORE_TAIL) }
|
|
115
|
+
})
|
|
116
|
+
const ports = { pm: this.d.port ?? pmPort(), daemon: daemonPort(), web: homePort() }
|
|
117
|
+
return { pm: 'shipkit-pm', pid: process.pid, since: this.startedAt, version: LIB_VERSION, ports, tail: tailLines(pmLog(this.d.dir), CORE_TAIL), children, resources: this.d.resources.views(), unitErrors: this.d.resources.errors }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** the image a global process declares as its icon, and no other file */
|
|
121
|
+
private icon(id: string): Response {
|
|
122
|
+
const d = this.d.resources.get(id)?.decl
|
|
123
|
+
const type = d?.icon == null ? null : iconImageType(d.icon)
|
|
124
|
+
if (d?.icon == null || type == null) return new Response(`no image icon for '${id}'`, { status: 404 })
|
|
125
|
+
const file = join(d.root, d.icon)
|
|
126
|
+
if (!existsSync(file)) return new Response(`${d.repo}/${d.icon} does not exist: the icon of '${id}'`, { status: 404 })
|
|
127
|
+
return new Response(Bun.file(file), { headers: { 'content-type': type, 'cache-control': 'no-cache' } })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async handle(req: Request): Promise<Response> {
|
|
131
|
+
const path = new URL(req.url).pathname
|
|
132
|
+
if (!sameOrigin(req)) return new Response('refused: a request that changes something must come from the home page', { status: 403 })
|
|
133
|
+
if (path === '/api/pm/status.json') return Response.json(this.answer())
|
|
134
|
+
if (path.startsWith('/api/pm/icon/') && req.method === 'GET') return this.icon(decodeURIComponent(path.slice('/api/pm/icon/'.length)))
|
|
135
|
+
if (req.method !== 'POST') return new Response('not found', { status: 404 })
|
|
136
|
+
const b = (await req.json().catch(() => ({}))) as Record<string, unknown>
|
|
137
|
+
if (path === '/api/pm/child') {
|
|
138
|
+
const p = this.children.get(b.id as Role)
|
|
139
|
+
if (p == null) return new Response(`no child '${String(b.id)}': pm runs ${ROLES.join(' and ')}`, { status: 404 })
|
|
140
|
+
if (b.act === 'start') p.start()
|
|
141
|
+
else if (b.act === 'stop' || b.act === 'restart') {
|
|
142
|
+
const act = b.act === 'stop' ? () => p.stop() : () => p.restart()
|
|
143
|
+
if (b.id === 'web') (this.d.later ?? ((f) => { setTimeout(f, WEB_ANSWER_MS) }))(act)
|
|
144
|
+
else act()
|
|
145
|
+
} else return new Response('act must be start, stop or restart', { status: 400 })
|
|
146
|
+
return Response.json(p.view())
|
|
147
|
+
}
|
|
148
|
+
if (path === '/api/pm/resource') {
|
|
149
|
+
const r = this.d.resources.get(String(b.id))
|
|
150
|
+
if (r == null) return new Response(`no global process '${String(b.id)}': none of the house repos' .shipkit.ts files declares it`, { status: 404 })
|
|
151
|
+
const act = b.act ?? (b.on === true ? 'start' : b.on === false ? 'stop' : null)
|
|
152
|
+
// a bring-up or an action can take minutes: the page follows it on its poll
|
|
153
|
+
const report = (p: Promise<void>): void => { p.catch((e: Error) => { r.note = e.message }) }
|
|
154
|
+
if (act === 'start') report(r.set('up'))
|
|
155
|
+
else if (act === 'stop') report(r.set('down'))
|
|
156
|
+
else if (act === 'restart') report(r.restart())
|
|
157
|
+
else if (act === 'open' || act === 'app') {
|
|
158
|
+
if (r.decl.url == null) return new Response(`${r.decl.id} declares no url to open`, { status: 400 })
|
|
159
|
+
report(r.open(act === 'app' ? 'app' : 'browser'))
|
|
160
|
+
} else if (act === 'takeover') {
|
|
161
|
+
const v = r.takeOverView
|
|
162
|
+
if (v == null) return new Response(`${r.decl.id} does not run outside the supervisor: nothing to take over`, { status: 409 })
|
|
163
|
+
if (!v.ok) return new Response(`${r.decl.id}: ${v.why}`, { status: 400 })
|
|
164
|
+
report(r.takeOver())
|
|
165
|
+
} else if (act === 'action') {
|
|
166
|
+
if (!r.decl.actions.some((a) => a.id === b.action)) return new Response(`${r.decl.id} declares no action '${String(b.action)}'`, { status: 404 })
|
|
167
|
+
if (r.running != null) return new Response(`${r.decl.id} is already running its action '${r.running}'`, { status: 409 })
|
|
168
|
+
report(r.action(String(b.action)))
|
|
169
|
+
} else return new Response('act must be start, stop, restart, open, app, takeover or action', { status: 400 })
|
|
170
|
+
return Response.json(r.view())
|
|
171
|
+
}
|
|
172
|
+
return new Response('not found', { status: 404 })
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// the processes as rows: shipkit's own first (pm, the daemon, the web: the
|
|
2
|
+
// core group), then one group per declaring repo, one row per process, in the
|
|
3
|
+
// order the page and `shipkit global-processes` both show.
|
|
4
|
+
// The page's table and the command's text and json are all drawn from these
|
|
5
|
+
// rows, so the two can never disagree. A row carries no clock: ages are words
|
|
6
|
+
// computed at render time from `since`.
|
|
7
|
+
import type { GlobalProcessDecl } from './global-units.ts'
|
|
8
|
+
import type { CorePorts, PmAnswer } from './pm.ts'
|
|
9
|
+
import type { ProcessIcon, ResourceView } from './resources.ts'
|
|
10
|
+
import type { ProcStatus } from './supervise.ts'
|
|
11
|
+
|
|
12
|
+
/** a status as pm reports it, or unknown while pm does not answer */
|
|
13
|
+
export type RowStatus = ProcStatus | 'unknown'
|
|
14
|
+
|
|
15
|
+
/** the dot a row wears: green up, red crashed, grey stopped or unknown, orange on its way */
|
|
16
|
+
export type RowDot = 'up' | 'crashed' | 'down' | 'starting' | 'unknown'
|
|
17
|
+
|
|
18
|
+
export type ProcessRow = {
|
|
19
|
+
id: string
|
|
20
|
+
label: string
|
|
21
|
+
repo: string
|
|
22
|
+
file: string
|
|
23
|
+
status: RowStatus
|
|
24
|
+
dot: RowDot
|
|
25
|
+
url: string | null
|
|
26
|
+
/** the url without its scheme, what the table shows dimmed */
|
|
27
|
+
host: string | null
|
|
28
|
+
pid: number | null
|
|
29
|
+
/** epoch ms its state counts from (up: its pid's start when it has one), null when pm never said */
|
|
30
|
+
since: number | null
|
|
31
|
+
note: string | null
|
|
32
|
+
description: string | null
|
|
33
|
+
where: string | null
|
|
34
|
+
/** the last line of its log, null when it has none */
|
|
35
|
+
lastLog: string | null
|
|
36
|
+
tail: string[]
|
|
37
|
+
canStop: boolean
|
|
38
|
+
/** the page offers restart: a global process pm runs and can stop, while
|
|
39
|
+
* up; the daemon and the web always, through pm; never pm itself */
|
|
40
|
+
canRestart: boolean
|
|
41
|
+
/** one of shipkit's own: always on, no stop, no open */
|
|
42
|
+
core: boolean
|
|
43
|
+
/** the shipkit version it runs, when known (core rows only) */
|
|
44
|
+
version: string | null
|
|
45
|
+
takeOver: ResourceView['takeOver']
|
|
46
|
+
actions: { id: string; label: string }[]
|
|
47
|
+
running: string | null
|
|
48
|
+
icon: ProcessIcon | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** a repo's processes, or shipkit's own (`core`, always the first group) */
|
|
52
|
+
export type ProcessGroup = { repo: string; core?: true; rows: ProcessRow[] }
|
|
53
|
+
|
|
54
|
+
/** the core group's name */
|
|
55
|
+
export const CORE_GROUP = 'shipkit'
|
|
56
|
+
|
|
57
|
+
export const dotOf = (s: RowStatus): RowDot =>
|
|
58
|
+
s === 'up' ? 'up' : s === 'crashed' ? 'crashed' : s === 'starting' || s === 'stopping' ? 'starting' : s === 'down' ? 'down' : 'unknown'
|
|
59
|
+
|
|
60
|
+
const hostOf = (url: string | null): string | null => (url == null ? null : url.replace(/^https?:\/\//, '').replace(/\/$/, ''))
|
|
61
|
+
|
|
62
|
+
/** a process as pm serves it. A pm older than pid and since reads them as null */
|
|
63
|
+
export function rowOf(r: ResourceView): ProcessRow {
|
|
64
|
+
return {
|
|
65
|
+
id: r.id,
|
|
66
|
+
label: r.label,
|
|
67
|
+
repo: r.repo,
|
|
68
|
+
file: r.file,
|
|
69
|
+
status: r.status,
|
|
70
|
+
dot: dotOf(r.status),
|
|
71
|
+
url: r.url,
|
|
72
|
+
host: hostOf(r.url),
|
|
73
|
+
pid: r.pid ?? null,
|
|
74
|
+
since: r.since ?? null,
|
|
75
|
+
note: r.note,
|
|
76
|
+
description: r.description,
|
|
77
|
+
where: r.where,
|
|
78
|
+
lastLog: r.tail.at(-1) ?? null,
|
|
79
|
+
tail: r.tail,
|
|
80
|
+
canStop: r.canStop,
|
|
81
|
+
canRestart: r.status === 'up' && r.canStop,
|
|
82
|
+
core: false,
|
|
83
|
+
version: null,
|
|
84
|
+
takeOver: r.takeOver,
|
|
85
|
+
actions: r.actions,
|
|
86
|
+
running: r.running,
|
|
87
|
+
icon: r.icon,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** a declared process while pm does not answer: everything but its state */
|
|
92
|
+
export function declaredRow(d: GlobalProcessDecl): ProcessRow {
|
|
93
|
+
return {
|
|
94
|
+
id: d.id,
|
|
95
|
+
label: d.label,
|
|
96
|
+
repo: d.repo,
|
|
97
|
+
file: d.file,
|
|
98
|
+
status: 'unknown',
|
|
99
|
+
dot: 'unknown',
|
|
100
|
+
url: d.url,
|
|
101
|
+
host: hostOf(d.url),
|
|
102
|
+
pid: null,
|
|
103
|
+
since: null,
|
|
104
|
+
note: null,
|
|
105
|
+
description: d.description,
|
|
106
|
+
where: d.where,
|
|
107
|
+
lastLog: null,
|
|
108
|
+
tail: [],
|
|
109
|
+
canStop: false,
|
|
110
|
+
canRestart: false,
|
|
111
|
+
core: false,
|
|
112
|
+
version: null,
|
|
113
|
+
takeOver: null,
|
|
114
|
+
actions: d.actions.map((a) => ({ id: a.id, label: a.label })),
|
|
115
|
+
running: null,
|
|
116
|
+
// an image is pm's to serve, and pm does not answer
|
|
117
|
+
icon: d.icon == null || /\.[a-z0-9]+$/i.test(d.icon) ? null : { text: d.icon },
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** one group per repo, in `order` (the project list), the repos it does not
|
|
122
|
+
* list after it by name; a group keeps pm's order */
|
|
123
|
+
export function groupRows(rows: ProcessRow[], order: string[]): ProcessGroup[] {
|
|
124
|
+
const by = new Map<string, ProcessRow[]>()
|
|
125
|
+
for (const r of rows) by.set(r.repo, [...(by.get(r.repo) ?? []), r])
|
|
126
|
+
const rank = (repo: string): number => {
|
|
127
|
+
const i = order.indexOf(repo)
|
|
128
|
+
return i < 0 ? order.length : i
|
|
129
|
+
}
|
|
130
|
+
return [...by.keys()]
|
|
131
|
+
.sort((a, b) => rank(a) - rank(b) || a.localeCompare(b))
|
|
132
|
+
.map((repo) => ({ repo, rows: by.get(repo)! }))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const CORE_ABOUT = {
|
|
136
|
+
pm: 'the process manager, the LaunchAgent com.shipkit.pm: launchd starts it at login and again when it dies, so the page has no restart for it (from a terminal: launchctl kickstart -k gui/$(id -u)/com.shipkit.pm)',
|
|
137
|
+
daemon: 'loads every house repo hub in one process and serves their api, restarted through pm',
|
|
138
|
+
web: 'serves this page and passes every api request to the daemon, restarted through pm',
|
|
139
|
+
} as const
|
|
140
|
+
|
|
141
|
+
export type CoreOpts = {
|
|
142
|
+
/** the daemon's own version, asked of the daemon; null when it does not answer */
|
|
143
|
+
daemonVersion?: string | null
|
|
144
|
+
/** where each listens, when pm does not say (an older pm, or pm down) */
|
|
145
|
+
ports?: CorePorts | null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function coreRow(id: 'pm' | 'daemon' | 'web', o: { status: RowStatus; pid: number | null; since: number | null; note: string | null; tail: string[]; version: string | null; port: number | null; restart: boolean }): ProcessRow {
|
|
149
|
+
const url = o.port == null ? null : `http://127.0.0.1:${o.port}`
|
|
150
|
+
return {
|
|
151
|
+
id, label: id, repo: CORE_GROUP, file: '', status: o.status, dot: dotOf(o.status), url, host: hostOf(url), pid: o.pid, since: o.since, note: o.note,
|
|
152
|
+
description: CORE_ABOUT[id], where: null, lastLog: o.tail.at(-1) ?? null, tail: o.tail,
|
|
153
|
+
canStop: false, canRestart: o.restart, core: true, version: o.version, takeOver: null, actions: [], running: null, icon: null,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** shipkit's own processes, one row each: pm as it answers (down when it does
|
|
158
|
+
* not), then the daemon and the web as pm reports them (unknown without pm) */
|
|
159
|
+
export function coreGroup(pm: PmAnswer | null, o: CoreOpts = {}): ProcessGroup {
|
|
160
|
+
const ports = pm?.ports ?? o.ports ?? null
|
|
161
|
+
const rows: ProcessRow[] = [pm == null
|
|
162
|
+
? coreRow('pm', { status: 'down', pid: null, since: null, note: 'pm does not answer', tail: [], version: null, port: ports?.pm ?? null, restart: false })
|
|
163
|
+
: coreRow('pm', { status: 'up', pid: pm.pid, since: pm.since ?? null, note: null, tail: pm.tail ?? [], version: pm.version, port: ports?.pm ?? null, restart: false })]
|
|
164
|
+
for (const id of ['daemon', 'web'] as const) {
|
|
165
|
+
const c = pm?.children.find((x) => x.id === id)
|
|
166
|
+
const port = ports?.[id] ?? null
|
|
167
|
+
const version = id === 'daemon' ? (o.daemonVersion ?? null) : null
|
|
168
|
+
if (c == null) rows.push(coreRow(id, { status: 'unknown', pid: null, since: null, note: null, tail: [], version, port, restart: false }))
|
|
169
|
+
else {
|
|
170
|
+
const note = [c.note, c.restarts > 0 ? `${c.restarts} restart(s)` : null].filter((x) => x != null).join(', ')
|
|
171
|
+
rows.push(coreRow(id, { status: c.status, pid: c.pid, since: c.since, note: note === '' ? null : note, tail: c.tail, version, port, restart: true }))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { repo: CORE_GROUP, core: true, rows }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** what the page's table holds, from pm's answer: the core group, then the
|
|
178
|
+
* global processes by repo */
|
|
179
|
+
export const processGroups = (pm: PmAnswer | null, order: string[], o: CoreOpts = {}): ProcessGroup[] =>
|
|
180
|
+
[coreGroup(pm, o), ...groupRows((pm?.resources ?? []).map(rowOf), order)]
|
|
181
|
+
|
|
182
|
+
/** the declared icon, else the repo's own, else none */
|
|
183
|
+
export const processIcon = (r: { icon: ProcessIcon | null }, repoIcon: string | null): ProcessIcon | null =>
|
|
184
|
+
r.icon ?? (repoIcon == null ? null : { src: repoIcon })
|
|
185
|
+
|
|
186
|
+
/** a span in its largest whole unit: 45s, 3m, 2h, 4d */
|
|
187
|
+
export function spanWords(ms: number): string {
|
|
188
|
+
const s = Math.max(0, Math.floor(ms / 1000))
|
|
189
|
+
if (s < 60) return `${s}s`
|
|
190
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`
|
|
191
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`
|
|
192
|
+
return `${Math.floor(s / 86400)}d`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** the state in words: `up 2h`, `crashed 3m ago`, `stopped`, `starting` */
|
|
196
|
+
export function stateWords(r: Pick<ProcessRow, 'status' | 'since'>, now: number): string {
|
|
197
|
+
const age = r.since == null ? null : spanWords(now - r.since)
|
|
198
|
+
switch (r.status) {
|
|
199
|
+
case 'up': return age == null ? 'up' : `up ${age}`
|
|
200
|
+
case 'crashed': return age == null ? 'crashed' : `crashed ${age} ago`
|
|
201
|
+
case 'down': return 'stopped'
|
|
202
|
+
case 'unknown': return 'unknown (pm down)'
|
|
203
|
+
default: return r.status
|
|
204
|
+
}
|
|
205
|
+
}
|