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
@@ -0,0 +1,235 @@
1
+ // the daemon's api, apart from the page so a test reaches it without a bundle:
2
+ // its own /api/home/* and every repo's hub under /r/<repo>/. The daemon
3
+ // decides WHEN a hub loads and never what is true: every verdict still comes
4
+ // from each repo's own runner and ledger. The machine resources are pm's
5
+ // (run-pm.ts), and the page reaches them through the web at /api/pm/*.
6
+ import { existsSync } from 'fs'
7
+ import { dirname, join } from 'path'
8
+ import { fileURLToPath } from 'url'
9
+ import { installedVersion, isBehind } from '../fleet/scan-fleet.ts'
10
+ import { LIB_VERSION } from '../core/lib-version.ts'
11
+ import { projectGutter, type ProjectRow } from '../serve/project-gutter-route.ts'
12
+ import type { WiredRepo } from '../serve/project-gutter.ts'
13
+ import { sameOrigin } from '../serve/same-origin.ts'
14
+ import type { HomeState } from './home-state.ts'
15
+ import { tailLines } from './supervise.ts'
16
+ import { Hubs, type WorkerView } from './hubs.ts'
17
+ import type { Planner, PlanRow } from '../plan/planner.ts'
18
+ import type { Widgets, WidgetView } from './widgets.ts'
19
+ import { ActivityCache, windowMinutes } from './activity.ts'
20
+ import { HouseRuns } from '../serve/house-runs.ts'
21
+ import type { HouseView } from '../ledger/house-runs.ts'
22
+
23
+ /** how often the daemon looks at every repo */
24
+ export const TICK_MS = 2_000
25
+
26
+ export type ModernizeView = { status: 'running' | 'done' | 'failed'; code: number | null; tail: string[] }
27
+
28
+ export type HomeAnswer = {
29
+ version: string
30
+ workers: WorkerView[]
31
+ /** the repos flagged active by hand */
32
+ active: string[]
33
+ /** repo → its installed shipkit, for every repo behind this home's */
34
+ behind: Record<string, string>
35
+ modernize: Record<string, ModernizeView>
36
+ }
37
+
38
+ export type HomeDeps = {
39
+ root: string
40
+ name: string
41
+ port: number
42
+ dir: string
43
+ hubs: Hubs
44
+ state: HomeState
45
+ /** the rail rows, the same measure a hub's rail reads */
46
+ gutter: ReturnType<typeof projectGutter>
47
+ /** the machine's one plan, which this tick drives. Absent = nothing runs
48
+ * on its own (a test of the routes alone) */
49
+ plan?: Planner
50
+ /** the repos it loads, by name; absent = every house repo. A smoke on a
51
+ * real machine keeps the others out, so no save there starts a gate */
52
+ only?: Set<string>
53
+ /** every house repo's widgets, rendered on the daemon's tick. Absent =
54
+ * none (a test of the routes alone) */
55
+ widgets?: Widgets
56
+ /** brings a repo current (modernizeCmd); resolves to its exit code */
57
+ modernize?: (repo: WiredRepo, log: string) => Promise<number>
58
+ }
59
+
60
+ const CLI = fileURLToPath(new URL('../cli.ts', import.meta.url))
61
+
62
+ /** what bringing a repo current runs: its first install when it has none,
63
+ * else the fleet update, which goes through the repo's own gate */
64
+ export function modernizeCmd(repo: WiredRepo, root: string): { cmd: string[]; cwd: string } {
65
+ if (!existsSync(join(repo.path, 'node_modules'))) return { cmd: ['bun', 'install'], cwd: repo.path }
66
+ return { cmd: ['bun', CLI, 'fleet', '--update', '--only', repo.name], cwd: root }
67
+ }
68
+
69
+ export const runModernize = async (repo: WiredRepo, root: string, log: string): Promise<number> => {
70
+ const { mkdirSync, openSync, closeSync } = await import('fs')
71
+ mkdirSync(dirname(log), { recursive: true })
72
+ const fd = openSync(log, 'w')
73
+ try {
74
+ const { cmd, cwd } = modernizeCmd(repo, root)
75
+ const p = Bun.spawn(cmd, { cwd, stdio: ['ignore', fd, fd] })
76
+ return await p.exited
77
+ } finally {
78
+ closeSync(fd)
79
+ }
80
+ }
81
+
82
+ /** active = an agent works in it, VS Code has it open, or it is flagged by
83
+ * hand. The house repo is active whenever any repo is: its checks serve
84
+ * every repo (a login, a shared key), so they follow the machine's work */
85
+ export function activeSet(rows: ProjectRow[], flagged: string[], house?: string): Set<string> {
86
+ const out = new Set(flagged)
87
+ for (const r of rows) if ((r.sessions != null && r.sessions > 0) || r.vscode === true) out.add(r.name)
88
+ if (house != null && out.size > 0) out.add(house)
89
+ return out
90
+ }
91
+
92
+ /** the monitor area's data: every widget's last html */
93
+ export type WidgetsAnswer = { widgets: WidgetView[]; errors: string[] }
94
+
95
+ /** the planned page's data: every row of the machine's plan */
96
+ export type PlannedAnswer = {
97
+ now: number
98
+ rows: PlanRow[]
99
+ errors: { repo: string; error: string }[]
100
+ running: boolean
101
+ /** what holds work back, one line each (Planner.held) */
102
+ held: string[]
103
+ }
104
+
105
+ export function homeRoutes(d: HomeDeps) {
106
+ let repos: WiredRepo[] = []
107
+ let rows: ProjectRow[] = []
108
+ const modernizing = new Map<string, ModernizeView & { log: string }>()
109
+ // read only while the activity page asks, never on the tick
110
+ const activity = new ActivityCache({ declared: (name) => d.plan?.declared(name) ?? null, titles: (name) => d.plan?.titles(name) ?? null })
111
+ const modernizeLog = (repo: string): string => join(d.dir, 'modernize', `${repo}.log`)
112
+ // the dock: every loaded repo's recent runs, read only while a page asks
113
+ const house = new HouseRuns(() => repos)
114
+
115
+ async function refresh(): Promise<void> {
116
+ const a = await d.gutter.answer()
117
+ rows = a.projects
118
+ repos = a.projects.filter((p) => d.only == null || d.only.has(p.name)).map((p) => ({ name: p.name, path: p.path, icon: null }))
119
+ d.hubs.sync(repos)
120
+ }
121
+
122
+ let active = new Set<string>()
123
+ if (d.plan != null) d.plan.active = (name) => active.has(name)
124
+
125
+ async function tick(): Promise<void> {
126
+ await refresh()
127
+ active = activeSet(rows, d.state.data.active, rows.find((r) => r.path === d.root)?.name ?? d.name)
128
+ // the plan's clock: one look per tick, and it starts at most one batch
129
+ d.plan?.tick()
130
+ // a widget render never holds the tick back
131
+ void d.widgets?.tick(repos.map((r) => ({ name: r.name, path: r.path }))).catch((e) => console.error(`WARN the widgets broke: ${(e as Error).message}`))
132
+ await d.hubs.tick(active)
133
+ }
134
+
135
+ function answer(): HomeAnswer {
136
+ const behind: Record<string, string> = {}
137
+ for (const r of repos) {
138
+ const v = installedVersion(r.path, 'rvlib-shipkit')
139
+ if (v != null && isBehind(v, LIB_VERSION)) behind[r.name] = v
140
+ }
141
+ const modernize: Record<string, ModernizeView> = {}
142
+ for (const [k, m] of modernizing) modernize[k] = { status: m.status, code: m.code, tail: tailLines(m.log, 40) }
143
+ return { version: LIB_VERSION, workers: d.hubs.views(repos), active: d.state.data.active, behind, modernize }
144
+ }
145
+
146
+ const find = (name: unknown): WiredRepo | null => repos.find((r) => r.name === name) ?? null
147
+ const body = async (req: Request): Promise<Record<string, unknown>> => (await req.json().catch(() => ({}))) as Record<string, unknown>
148
+
149
+ async function handle(req: Request, srv?: Parameters<Hubs['dispatch']>[2]): Promise<Response | null> {
150
+ const path = new URL(req.url).pathname
151
+ // every repo's hub, which makes its own same origin check
152
+ if (path.startsWith('/r/')) {
153
+ if (repos.length === 0) await refresh()
154
+ return d.hubs.dispatch(req, path, srv as Parameters<Hubs['dispatch']>[2])
155
+ }
156
+ if (path === '/hub.json')
157
+ // the identity a client probes (homeUp), through the web or here
158
+ return Response.json({ hub: 'shipkit-home', name: d.name, root: d.root, version: LIB_VERSION }, { headers: { 'access-control-allow-origin': '*' } })
159
+ if (!sameOrigin(req)) return new Response('refused: a request that changes something must come from the home page', { status: 403 })
160
+ if (path.startsWith('/api/projects')) return d.gutter.handle(req, path)
161
+ // the web asks which page routes to serve: one per house repo
162
+ if (path === '/api/home/repos.json') {
163
+ if (repos.length === 0) await refresh()
164
+ return Response.json({ repos: d.hubs.names() })
165
+ }
166
+ if (path === '/api/home/planned.json') {
167
+ if (repos.length === 0) await refresh()
168
+ const out: PlannedAnswer = { now: Date.now(), rows: d.plan?.rows() ?? [], errors: d.plan?.errors() ?? [], running: d.plan?.busy() === true, held: d.plan?.held() ?? [] }
169
+ return Response.json(out)
170
+ }
171
+ if (path === '/api/home/activity.json') {
172
+ if (repos.length === 0) await refresh()
173
+ const q = new URL(req.url).searchParams
174
+ return Response.json(await activity.get(repos, windowMinutes(q.get('m')), { fresh: q.get('fresh') === '1' }))
175
+ }
176
+ if (path === '/api/house-runs.json') {
177
+ if (repos.length === 0) await refresh()
178
+ const view: HouseView = { ...house.view(), held: d.plan?.held() ?? [] }
179
+ return Response.json(view, { headers: { 'cache-control': 'no-store' } })
180
+ }
181
+ if (path === '/api/home/widgets.json') {
182
+ const out: WidgetsAnswer = { widgets: d.widgets?.views() ?? [], errors: d.widgets?.errors ?? [] }
183
+ return Response.json(out)
184
+ }
185
+ if (path === '/api/home.json') {
186
+ if (repos.length === 0) await refresh()
187
+ return Response.json(answer())
188
+ }
189
+ if (req.method !== 'POST') return null
190
+ if (path === '/api/home/hub') {
191
+ const b = await body(req)
192
+ if (repos.length === 0) await refresh()
193
+ const r = find(b.name)
194
+ if (r == null) return new Response(`not a house repo: ${String(b.name)}`, { status: 404 })
195
+ if (b.act === 'ensure') return Response.json(d.hubs.ensure(r))
196
+ if (b.act === 'stop') return Response.json(d.hubs.stop(r))
197
+ if (b.act === 'restart') return Response.json(await d.hubs.restart(r))
198
+ return new Response(`act must be ensure, stop or restart`, { status: 400 })
199
+ }
200
+ if (path === '/api/home/planned/run') {
201
+ const b = await body(req)
202
+ if (d.plan == null || !d.plan.ask(String(b.repo), String(b.check))) return new Response(`no planned check ${String(b.check)} in ${String(b.repo)}`, { status: 404 })
203
+ d.plan.tick()
204
+ return Response.json({ ok: true })
205
+ }
206
+ if (path === '/api/home/active') {
207
+ const b = await body(req)
208
+ if (find(b.name) == null) return new Response(`not a house repo: ${String(b.name)}`, { status: 404 })
209
+ d.state.setActive(String(b.name), b.on === true)
210
+ return Response.json({ active: d.state.data.active })
211
+ }
212
+ if (path === '/api/home/modernize') {
213
+ const b = await body(req)
214
+ const r = find(b.name)
215
+ if (r == null) return new Response(`not a house repo: ${String(b.name)}`, { status: 404 })
216
+ if (modernizing.get(r.name)?.status === 'running') return new Response(`already modernizing ${r.name}`, { status: 409 })
217
+ const log = modernizeLog(r.name)
218
+ const m = { status: 'running' as ModernizeView['status'], code: null as number | null, tail: [], log }
219
+ modernizing.set(r.name, m)
220
+ void (d.modernize ?? ((repo, l) => runModernize(repo, d.root, l)))(r, log)
221
+ .then(async (code) => {
222
+ m.status = code === 0 ? 'done' : 'failed'
223
+ m.code = code
224
+ // the hub loads again on what the update installed
225
+ if (code === 0) await d.hubs.restart(r)
226
+ })
227
+ .catch(() => { m.status = 'failed' })
228
+ return Response.json({ ok: true })
229
+ }
230
+ return null
231
+ }
232
+
233
+ return { handle, tick, answer, refresh }
234
+ }
235
+
@@ -0,0 +1,105 @@
1
+ // getting to a running home from anywhere: is pm installed, start it, find
2
+ // the house repo it runs from, open a window on it. `shipkit gui` and the
3
+ // serve handoff both come through here.
4
+ import { existsSync, mkdirSync, readFileSync } from 'fs'
5
+ import { homedir } from 'os'
6
+ import { dirname, join, resolve } from 'path'
7
+ import { gitRoot } from '../core/git-root.ts'
8
+ import { homeRepo, houseIndex } from '../serve/project-gutter.ts'
9
+ import { BUNDLE_ID, canBuildGui, ensureGuiApp } from './gui-app.ts'
10
+ import { LABEL, plistPath } from './home-launchd.ts'
11
+ import { childLog, daemonPort, daemonUp, homeDir, homePort, homeUp, pmLog } from './home-paths.ts'
12
+
13
+ /** the pm LaunchAgent is installed: on this machine no hub runs outside the home */
14
+ export const homeInstalled = (): boolean => existsSync(plistPath())
15
+
16
+ /** set, a machine with no LaunchAgent still sends its hubs and gates to the
17
+ * home that answers on SHIPKIT_HOME_PORT (a home started by hand, a test) */
18
+ export const USE_HOME_ENV = 'SHIPKIT_USE_HOME'
19
+
20
+ /** this machine runs its hubs and gates in the home */
21
+ export const homeWanted = (env: Record<string, string | undefined> = process.env, installed: () => boolean = homeInstalled): boolean =>
22
+ (env[USE_HOME_ENV] != null && env[USE_HOME_ENV] !== '') || installed()
23
+
24
+ /** how long a start waits for the home to answer */
25
+ export const START_WAIT_MS = 15_000
26
+
27
+ /** kick the installed pm agent and wait until the role answers: the daemon
28
+ * for a terminal client, the web for a window. Throws, loud, when it does
29
+ * not: a caller must never fall back to a hub of its own */
30
+ export async function startHome(o: { wait?: number; need?: 'daemon' | 'web' } = {}): Promise<void> {
31
+ const need = o.need ?? 'daemon'
32
+ const up = (): Promise<boolean> => (need === 'daemon' ? daemonUp() : homeUp(homePort()))
33
+ if (await up()) return
34
+ const p = Bun.spawn(['launchctl', 'kickstart', `gui/${process.getuid?.() ?? 501}/${LABEL}`], { stdout: 'ignore', stderr: 'pipe' })
35
+ const err = (await new Response(p.stderr).text()).trim()
36
+ if ((await p.exited) !== 0) throw new Error(`launchctl kickstart ${LABEL} failed: ${err}. Reinstall it with \`shipkit pm install\` from the house repo`)
37
+ const until = Date.now() + (o.wait ?? START_WAIT_MS)
38
+ while (Date.now() < until) {
39
+ if (await up()) return
40
+ await Bun.sleep(250)
41
+ }
42
+ const port = need === 'daemon' ? daemonPort() : homePort()
43
+ throw new Error(`the ${need} did not answer on :${port} within ${Math.round((o.wait ?? START_WAIT_MS) / 1000)}s: see ${childLog(homeDir(), need)} and ${pmLog()}`)
44
+ }
45
+
46
+ /** the cli the home runs from its repo: the one that repo's own `hub` script
47
+ * runs (an installed copy, or the lib source in the repo that hosts it),
48
+ * else this one */
49
+ export function houseCli(root: string, own: string): string {
50
+ try {
51
+ const hub = (JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { scripts?: Record<string, string> }).scripts?.['hub'] ?? ''
52
+ const cli = /(\S*cli\.ts)\b/.exec(hub)?.[1]
53
+ if (cli != null) {
54
+ const abs = resolve(root, cli)
55
+ if (existsSync(abs)) return abs
56
+ }
57
+ } catch { /* no package.json: this cli */ }
58
+ return own
59
+ }
60
+
61
+ /** the repo that hosts the house wiki, seen from any house repo, or null */
62
+ export function houseRootOf(cwd: string): string | null {
63
+ const index = houseIndex(homeRepo(cwd), homedir())
64
+ if (index == null) return null
65
+ return gitRoot(dirname(index))?.top ?? null
66
+ }
67
+
68
+ export const CHROME = process.env.CHROME ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
69
+ /** the window's own Chrome profile: its own dock icon and cookies, apart from the everyday browser */
70
+ export const guiProfile = (): string => join(homeDir(), 'gui-profile')
71
+ const GUI_DISK_CACHE = 32 * 1024 * 1024
72
+
73
+ /** the command that opens one more home window: a Chrome app window (no tabs,
74
+ * no address bar) on its own profile, else the default browser */
75
+ export function windowCmd(url: string, o: { chrome?: string; platform?: string } = {}): string[] {
76
+ const chrome = o.chrome ?? CHROME
77
+ const platform = o.platform ?? process.platform
78
+ // no component downloads and a small disk cache: the profile shows one
79
+ // page, and those two are most of what grows it (retention.ts caps it)
80
+ if (existsSync(chrome)) return [chrome, `--app=${url}`, `--user-data-dir=${guiProfile()}`, '--no-first-run', '--no-default-browser-check', '--disable-component-update', `--disk-cache-size=${GUI_DISK_CACHE}`]
81
+ return platform === 'darwin' ? ['open', url] : ['xdg-open', url]
82
+ }
83
+
84
+ /** one more home window: the native app where it can be built (its own
85
+ * dock icon, the house mark), else a Chrome app window. `warn` hears why the
86
+ * native one was not used */
87
+ export function openWindow(url: string, o: { root?: string | null; version?: string; warn?: (l: string) => void } = {}): string[] {
88
+ if (canBuildGui()) {
89
+ try {
90
+ const { app, rebuilt } = ensureGuiApp({ root: o.root ?? houseRootOf(process.cwd()) ?? process.cwd(), version: o.version ?? '0' })
91
+ // a running copy is the old build: it quits, and the new one opens the window
92
+ if (rebuilt) Bun.spawnSync(['osascript', '-e', `quit app id "${BUNDLE_ID}"`], { stdout: 'ignore', stderr: 'ignore' })
93
+ const cmd = ['open', '-a', app, url]
94
+ Bun.spawn(cmd, { stdio: ['ignore', 'ignore', 'ignore'] })
95
+ return cmd
96
+ } catch (e) {
97
+ o.warn?.(`WARN the shipkit app could not be built, so this window is a Chrome one: ${(e as Error).message}`)
98
+ }
99
+ }
100
+ mkdirSync(guiProfile(), { recursive: true })
101
+ const cmd = windowCmd(url)
102
+ // detached: the window outlives the command that opened it
103
+ Bun.spawn(cmd, { stdio: ['ignore', 'ignore', 'ignore'], detached: true }).unref()
104
+ return cmd
105
+ }
@@ -0,0 +1,43 @@
1
+ // what the home remembers across restarts, and only that: the repos flagged
2
+ // active by hand. One row of the machine db (core/machine-db.ts), edited by
3
+ // the page.
4
+ import { importMachineFile, machineGet, machineSet } from '../core/machine-db.ts'
5
+ import { stateFile } from './home-paths.ts'
6
+
7
+ export type HomeStateData = {
8
+ /** repos whose hub runs whether or not an agent works in them */
9
+ active: string[]
10
+ }
11
+
12
+ const KEY = 'home.active'
13
+
14
+ export class HomeState {
15
+ data: HomeStateData
16
+
17
+ /** `dir` is the home dir, where the json file an older copy wrote is
18
+ * read once into the db */
19
+ constructor(dir: string) {
20
+ importMachineFile<string[]>(KEY, stateFile(dir), (t) => HomeState.parse(t).active)
21
+ this.data = { active: HomeState.clean(machineGet<unknown>(KEY)) }
22
+ }
23
+
24
+ static parse(text: string): HomeStateData {
25
+ try {
26
+ return { active: HomeState.clean((JSON.parse(text) as Partial<HomeStateData>).active) }
27
+ } catch {
28
+ return { active: [] }
29
+ }
30
+ }
31
+
32
+ private static clean(v: unknown): string[] {
33
+ return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []
34
+ }
35
+
36
+ setActive(name: string, on: boolean): void {
37
+ const set = new Set(this.data.active)
38
+ if (on) set.add(name)
39
+ else set.delete(name)
40
+ this.data = { ...this.data, active: [...set].sort() }
41
+ if (!machineSet(KEY, this.data.active)) throw new Error('the machine db could not be written, so the active flag is not saved')
42
+ }
43
+ }
@@ -0,0 +1,12 @@
1
+ // the words the home's server and page both need, in a module with no node
2
+ // import, so the page bundle can take it.
3
+
4
+ /** the blocker the page answers with an install button */
5
+ export const NOT_INSTALLED = 'rvlib-shipkit is not installed here: run `bun install`'
6
+
7
+ /** the last line of a run a hub streams to a terminal: its exit code follows */
8
+ export const RUN_EXIT = '\u0000shipkit-exit '
9
+
10
+ /** a line of a run a hub streams to a terminal: the gate job it started, so a
11
+ * terminal whose stream is cut follows the rest from the job folder */
12
+ export const RUN_JOB = '\u0000shipkit-job '