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.
Files changed (210) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +1 -1
  3. package/package.json +3 -1
  4. package/src/assets/run-assets.ts +3 -8
  5. package/src/check.ts +40 -2
  6. package/src/checks/SK001-package-json-shape.check.ts +14 -3
  7. package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
  8. package/src/checks/SK004-gitignore-policy.check.ts +11 -3
  9. package/src/checks/SK005-rvlib-versions.check.ts +13 -1
  10. package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
  11. package/src/checks/SK007-github-visibility.check.ts +9 -9
  12. package/src/checks/SK008-reviewer-agent.check.ts +14 -8
  13. package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
  14. package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
  15. package/src/checks/SK015-nested-repo.check.ts +5 -1
  16. package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
  17. package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
  18. package/src/checks/SK024-identity-colors.check.ts +54 -0
  19. package/src/checks/SK025-release-drift.check.ts +58 -0
  20. package/src/checks/SK026-asks-autoclose.check.ts +44 -0
  21. package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
  22. package/src/checks/SK028-readme.check.ts +79 -0
  23. package/src/cli/cmd_Assets.ts +1 -1
  24. package/src/cli/cmd_Check.ts +26 -1
  25. package/src/cli/cmd_Ci.ts +23 -5
  26. package/src/cli/cmd_CrossRepoActivity.ts +116 -0
  27. package/src/cli/cmd_GlobalProcesses.ts +155 -0
  28. package/src/cli/cmd_Home.ts +170 -0
  29. package/src/cli/cmd_Keys.ts +17 -3
  30. package/src/cli/cmd_Overview.ts +98 -0
  31. package/src/cli/cmd_Planned.ts +117 -0
  32. package/src/cli.ts +30 -2
  33. package/src/collections/checks.ts +3 -2
  34. package/src/collections/providers.ts +1 -1
  35. package/src/collections/test-time.ts +10 -1
  36. package/src/comfy/comfy-panel.ts +1 -16
  37. package/src/core/agent-spawn.ts +46 -0
  38. package/src/core/archived.ts +82 -0
  39. package/src/core/attach.ts +14 -9
  40. package/src/core/canonical-scripts.ts +12 -3
  41. package/src/core/check-file-names.ts +8 -0
  42. package/src/core/check-runner.ts +80 -30
  43. package/src/core/discover-checks.ts +38 -18
  44. package/src/core/file-stamp.ts +20 -0
  45. package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
  46. package/src/core/identity-color.ts +203 -0
  47. package/src/core/identity.ts +91 -0
  48. package/src/core/job-follow.ts +102 -0
  49. package/src/core/job-run.ts +27 -0
  50. package/src/core/jobs.ts +87 -27
  51. package/src/core/legacy-shapes.ts +1 -14
  52. package/src/core/machine-db.ts +83 -0
  53. package/src/core/one-lib.ts +85 -0
  54. package/src/core/port-holders.ts +43 -0
  55. package/src/core/repo-facts.ts +14 -0
  56. package/src/core/retention.ts +222 -0
  57. package/src/core/retired-checks.ts +7 -0
  58. package/src/core/run-check.ts +12 -4
  59. package/src/core/run-ci.ts +46 -12
  60. package/src/core/run-fix.ts +7 -2
  61. package/src/core/run-fixers.ts +10 -6
  62. package/src/core/slots.ts +5 -0
  63. package/src/core/statement-end.ts +17 -0
  64. package/src/core/wire.ts +11 -0
  65. package/src/fleet/scan-fleet.ts +2 -1
  66. package/src/home/activity-shape.ts +135 -0
  67. package/src/home/activity.ts +249 -0
  68. package/src/home/attach.ts +95 -0
  69. package/src/home/autostart.ts +108 -0
  70. package/src/home/global-units.ts +87 -0
  71. package/src/home/gui-app/ShipkitGui.swift +159 -0
  72. package/src/home/gui-app.ts +99 -0
  73. package/src/home/handoff.ts +51 -0
  74. package/src/home/home-launchd.ts +104 -0
  75. package/src/home/home-paths.ts +88 -0
  76. package/src/home/home-routes.ts +235 -0
  77. package/src/home/home-start.ts +105 -0
  78. package/src/home/home-state.ts +43 -0
  79. package/src/home/home-words.ts +12 -0
  80. package/src/home/hubs.ts +286 -0
  81. package/src/home/lib-watch.ts +139 -0
  82. package/src/home/machine-stores.ts +115 -0
  83. package/src/home/open-app.ts +16 -0
  84. package/src/home/pm-config.ts +37 -0
  85. package/src/home/pm.ts +174 -0
  86. package/src/home/process-rows.ts +205 -0
  87. package/src/home/resources.ts +365 -0
  88. package/src/home/run-daemon.ts +137 -0
  89. package/src/home/run-pm.ts +150 -0
  90. package/src/home/run-unit-action.ts +25 -0
  91. package/src/home/run-web.ts +37 -0
  92. package/src/home/serve-daemon.ts +31 -0
  93. package/src/home/serve-web.ts +175 -0
  94. package/src/home/supervise.ts +361 -0
  95. package/src/home/via-hub.ts +142 -0
  96. package/src/home/widget-text.ts +119 -0
  97. package/src/home/widgets.ts +102 -0
  98. package/src/hooks/run-hooks.ts +20 -3
  99. package/src/ledger/check-stats.ts +19 -45
  100. package/src/ledger/db.ts +69 -17
  101. package/src/ledger/gate-checks.ts +22 -3
  102. package/src/ledger/git-scan.ts +31 -7
  103. package/src/ledger/house-runs.ts +43 -0
  104. package/src/ledger/ledger.ts +40 -14
  105. package/src/ledger/live-timeline.ts +4 -19
  106. package/src/ledger/run-log.ts +92 -28
  107. package/src/ledger/store.ts +245 -103
  108. package/src/ledger/world-cache.ts +64 -15
  109. package/src/plan/cadence.ts +253 -0
  110. package/src/plan/catch-up.ts +37 -0
  111. package/src/plan/plan-timeline.ts +99 -0
  112. package/src/plan/planner.ts +428 -0
  113. package/src/plan/repo-source.ts +212 -0
  114. package/src/problems/problem-line.ts +1 -1
  115. package/src/release/run-release.ts +9 -2
  116. package/src/release/unit-resolve.ts +8 -12
  117. package/src/repo-config.ts +61 -4
  118. package/src/serve/house-runs.ts +53 -0
  119. package/src/serve/hub-app/activity-st.ts +168 -0
  120. package/src/serve/hub-app/activity-view.tsx +218 -0
  121. package/src/serve/hub-app/activity.css +78 -0
  122. package/src/serve/hub-app/admin-svc.ts +41 -0
  123. package/src/serve/hub-app/admin-view.tsx +42 -0
  124. package/src/serve/hub-app/chart-view.ts +12 -5
  125. package/src/serve/hub-app/check-view.tsx +9 -9
  126. package/src/serve/hub-app/check-words.ts +3 -1
  127. package/src/serve/hub-app/checks-svc.ts +12 -8
  128. package/src/serve/hub-app/col-card.tsx +4 -4
  129. package/src/serve/hub-app/cols-svc.ts +92 -13
  130. package/src/serve/hub-app/comfy-svc.ts +12 -6
  131. package/src/serve/hub-app/comfy-view.tsx +3 -3
  132. package/src/serve/hub-app/controls.css +32 -0
  133. package/src/serve/hub-app/cycle-button.tsx +36 -0
  134. package/src/serve/hub-app/dock-anim.ts +25 -0
  135. package/src/serve/hub-app/dock-st.ts +329 -0
  136. package/src/serve/hub-app/dock-view.tsx +68 -0
  137. package/src/serve/hub-app/dock.css +35 -0
  138. package/src/serve/hub-app/floating.tsx +49 -0
  139. package/src/serve/hub-app/geometry-view.tsx +4 -4
  140. package/src/serve/hub-app/hash-route.ts +8 -2
  141. package/src/serve/hub-app/header-bar.tsx +102 -95
  142. package/src/serve/hub-app/health-svc.ts +3 -3
  143. package/src/serve/hub-app/help.tsx +10 -7
  144. package/src/serve/hub-app/home-st.ts +278 -0
  145. package/src/serve/hub-app/home-strip.css +113 -0
  146. package/src/serve/hub-app/home-strip.tsx +376 -0
  147. package/src/serve/hub-app/home.html +21 -0
  148. package/src/serve/hub-app/hub-app.tsx +221 -177
  149. package/src/serve/hub-app/hub-types.ts +0 -3
  150. package/src/serve/hub-app/hub.css +112 -52
  151. package/src/serve/hub-app/icons.tsx +34 -0
  152. package/src/serve/hub-app/index.html +7 -1
  153. package/src/serve/hub-app/keys.ts +60 -0
  154. package/src/serve/hub-app/layout-st.ts +55 -0
  155. package/src/serve/hub-app/live-svc.ts +35 -72
  156. package/src/serve/hub-app/machine-st.ts +333 -0
  157. package/src/serve/hub-app/md.ts +38 -5
  158. package/src/serve/hub-app/media-svc.ts +63 -44
  159. package/src/serve/hub-app/media-view.tsx +20 -20
  160. package/src/serve/hub-app/next-run.ts +24 -30
  161. package/src/serve/hub-app/palette-st.ts +92 -0
  162. package/src/serve/hub-app/palette-view.tsx +47 -0
  163. package/src/serve/hub-app/pill-row.tsx +65 -0
  164. package/src/serve/hub-app/planned-st.ts +107 -0
  165. package/src/serve/hub-app/planned-view.tsx +114 -0
  166. package/src/serve/hub-app/planned.css +42 -0
  167. package/src/serve/hub-app/processes-svc.ts +63 -0
  168. package/src/serve/hub-app/project-list.css +119 -0
  169. package/src/serve/hub-app/project-list.tsx +266 -0
  170. package/src/serve/hub-app/projects-svc.ts +253 -23
  171. package/src/serve/hub-app/prose-hits-st.ts +3 -3
  172. package/src/serve/hub-app/prose-hits-view.tsx +6 -6
  173. package/src/serve/hub-app/readme-svc.ts +38 -0
  174. package/src/serve/hub-app/readme-view.tsx +19 -0
  175. package/src/serve/hub-app/readme.css +11 -0
  176. package/src/serve/hub-app/repo-sections.tsx +61 -0
  177. package/src/serve/hub-app/repo-st.ts +340 -0
  178. package/src/serve/hub-app/runs-tabs.ts +1 -1
  179. package/src/serve/hub-app/screen-cache.ts +74 -0
  180. package/src/serve/hub-app/sidebar.tsx +84 -0
  181. package/src/serve/hub-app/test-view.tsx +4 -4
  182. package/src/serve/hub-app/tests-svc.ts +3 -3
  183. package/src/serve/hub-app/tests-view.tsx +41 -14
  184. package/src/serve/hub-app/timeline-chart.tsx +55 -39
  185. package/src/serve/hub-app/timeline-geometry.ts +40 -5
  186. package/src/serve/hub-app/timeline-svc.ts +7 -6
  187. package/src/serve/hub-app/timeline-view.tsx +33 -16
  188. package/src/serve/hub-app/tree-row.tsx +2 -2
  189. package/src/serve/hub-app/tree-svc.ts +2 -2
  190. package/src/serve/hub-app/wikis-view.tsx +10 -6
  191. package/src/serve/hub-runtime.ts +550 -0
  192. package/src/serve/other-repos.ts +8 -45
  193. package/src/serve/project-gutter-route.ts +121 -20
  194. package/src/serve/project-gutter.ts +33 -11
  195. package/src/serve/readme-route.ts +57 -0
  196. package/src/serve/repo-processes.ts +89 -0
  197. package/src/serve/run-serve.ts +110 -402
  198. package/src/serve/serve-hub.ts +155 -52
  199. package/src/serve/stale-restart.ts +5 -0
  200. package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
  201. package/src/serve/warm-cell.ts +95 -0
  202. package/src/shipkit-files.ts +130 -0
  203. package/src/shipkit.ts +2 -1
  204. package/src/suite/kept-timings.ts +1 -1
  205. package/src/wiki/graph-positions.ts +1 -1
  206. package/src/checks/contracts.check.ts +0 -20
  207. package/src/serve/health-schedule.ts +0 -102
  208. package/src/serve/hub-app/hub-st.ts +0 -250
  209. package/src/serve/hub-app/project-gutter.css +0 -65
  210. 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
+ }