rvlib-shipkit 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/package.json +3 -1
  3. package/src/check.ts +24 -0
  4. package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
  5. package/src/checks/SK023-stores-under-cap.check.ts +49 -0
  6. package/src/checks/SK024-identity-colors.check.ts +49 -0
  7. package/src/cli/cmd_Check.ts +26 -1
  8. package/src/cli/cmd_Ci.ts +18 -5
  9. package/src/cli/cmd_Home.ts +170 -0
  10. package/src/cli.ts +22 -2
  11. package/src/comfy/comfy-panel.ts +1 -16
  12. package/src/core/attach.ts +14 -9
  13. package/src/core/check-file-names.ts +8 -0
  14. package/src/core/check-runner.ts +40 -18
  15. package/src/core/discover-checks.ts +36 -18
  16. package/src/core/identity-color.ts +203 -0
  17. package/src/core/identity.ts +91 -0
  18. package/src/core/job-follow.ts +102 -0
  19. package/src/core/job-run.ts +27 -0
  20. package/src/core/jobs.ts +87 -27
  21. package/src/core/legacy-shapes.ts +1 -14
  22. package/src/core/machine-db.ts +83 -0
  23. package/src/core/one-lib.ts +85 -0
  24. package/src/core/port-holders.ts +43 -0
  25. package/src/core/retention.ts +213 -0
  26. package/src/core/run-check.ts +6 -2
  27. package/src/core/run-ci.ts +3 -0
  28. package/src/core/slots.ts +5 -0
  29. package/src/core/statement-end.ts +17 -0
  30. package/src/home/attach.ts +95 -0
  31. package/src/home/autostart.ts +108 -0
  32. package/src/home/global-units.ts +86 -0
  33. package/src/home/gui-app/ShipkitGui.swift +159 -0
  34. package/src/home/gui-app.ts +99 -0
  35. package/src/home/handoff.ts +51 -0
  36. package/src/home/home-launchd.ts +104 -0
  37. package/src/home/home-paths.ts +88 -0
  38. package/src/home/home-routes.ts +208 -0
  39. package/src/home/home-start.ts +105 -0
  40. package/src/home/home-state.ts +43 -0
  41. package/src/home/home-words.ts +12 -0
  42. package/src/home/hubs.ts +286 -0
  43. package/src/home/lib-watch.ts +139 -0
  44. package/src/home/machine-stores.ts +115 -0
  45. package/src/home/open-app.ts +16 -0
  46. package/src/home/pm-config.ts +37 -0
  47. package/src/home/pm.ts +141 -0
  48. package/src/home/resources.ts +338 -0
  49. package/src/home/run-daemon.ts +137 -0
  50. package/src/home/run-pm.ts +149 -0
  51. package/src/home/run-unit-action.ts +25 -0
  52. package/src/home/run-web.ts +37 -0
  53. package/src/home/serve-daemon.ts +31 -0
  54. package/src/home/serve-web.ts +172 -0
  55. package/src/home/supervise.ts +346 -0
  56. package/src/home/via-hub.ts +142 -0
  57. package/src/home/widgets.ts +102 -0
  58. package/src/ledger/check-stats.ts +19 -45
  59. package/src/ledger/db.ts +69 -17
  60. package/src/ledger/gate-checks.ts +5 -0
  61. package/src/ledger/git-scan.ts +31 -7
  62. package/src/ledger/ledger.ts +40 -14
  63. package/src/ledger/run-log.ts +58 -23
  64. package/src/ledger/store.ts +228 -103
  65. package/src/ledger/world-cache.ts +15 -13
  66. package/src/plan/cadence.ts +237 -0
  67. package/src/plan/planner.ts +303 -0
  68. package/src/plan/repo-source.ts +100 -0
  69. package/src/problems/problem-line.ts +1 -1
  70. package/src/repo-config.ts +51 -2
  71. package/src/serve/hub-app/check-view.tsx +9 -9
  72. package/src/serve/hub-app/check-words.ts +1 -1
  73. package/src/serve/hub-app/checks-svc.ts +12 -8
  74. package/src/serve/hub-app/col-card.tsx +4 -4
  75. package/src/serve/hub-app/cols-svc.ts +92 -13
  76. package/src/serve/hub-app/comfy-svc.ts +12 -6
  77. package/src/serve/hub-app/comfy-view.tsx +3 -3
  78. package/src/serve/hub-app/floating.tsx +49 -0
  79. package/src/serve/hub-app/geometry-view.tsx +4 -4
  80. package/src/serve/hub-app/hash-route.ts +1 -1
  81. package/src/serve/hub-app/header-bar.tsx +44 -12
  82. package/src/serve/hub-app/health-svc.ts +3 -3
  83. package/src/serve/hub-app/help.tsx +10 -7
  84. package/src/serve/hub-app/home-st.ts +241 -0
  85. package/src/serve/hub-app/home-strip.css +70 -0
  86. package/src/serve/hub-app/home-strip.tsx +245 -0
  87. package/src/serve/hub-app/home.html +17 -0
  88. package/src/serve/hub-app/hub-app.tsx +201 -93
  89. package/src/serve/hub-app/hub-types.ts +0 -3
  90. package/src/serve/hub-app/hub.css +43 -23
  91. package/src/serve/hub-app/icons.tsx +14 -0
  92. package/src/serve/hub-app/index.html +3 -1
  93. package/src/serve/hub-app/keys.ts +25 -0
  94. package/src/serve/hub-app/layout-st.ts +55 -0
  95. package/src/serve/hub-app/live-svc.ts +12 -9
  96. package/src/serve/hub-app/machine-st.ts +270 -0
  97. package/src/serve/hub-app/media-svc.ts +63 -44
  98. package/src/serve/hub-app/media-view.tsx +20 -20
  99. package/src/serve/hub-app/next-run.ts +24 -30
  100. package/src/serve/hub-app/palette-st.ts +88 -0
  101. package/src/serve/hub-app/planned-st.ts +171 -0
  102. package/src/serve/hub-app/planned-view.tsx +109 -0
  103. package/src/serve/hub-app/planned.css +39 -0
  104. package/src/serve/hub-app/processes-svc.ts +63 -0
  105. package/src/serve/hub-app/project-list.css +97 -0
  106. package/src/serve/hub-app/project-list.tsx +167 -0
  107. package/src/serve/hub-app/projects-svc.ts +139 -17
  108. package/src/serve/hub-app/prose-hits-st.ts +3 -3
  109. package/src/serve/hub-app/prose-hits-view.tsx +6 -6
  110. package/src/serve/hub-app/{hub-st.ts → repo-st.ts} +155 -84
  111. package/src/serve/hub-app/runs-tabs.ts +1 -1
  112. package/src/serve/hub-app/screen-cache.ts +74 -0
  113. package/src/serve/hub-app/test-view.tsx +4 -4
  114. package/src/serve/hub-app/tests-svc.ts +3 -3
  115. package/src/serve/hub-app/tests-view.tsx +10 -10
  116. package/src/serve/hub-app/timeline-chart.tsx +24 -12
  117. package/src/serve/hub-app/timeline-svc.ts +5 -4
  118. package/src/serve/hub-app/timeline-view.tsx +13 -13
  119. package/src/serve/hub-app/tree-row.tsx +2 -2
  120. package/src/serve/hub-app/tree-svc.ts +2 -2
  121. package/src/serve/hub-app/wikis-view.tsx +10 -6
  122. package/src/serve/hub-runtime.ts +542 -0
  123. package/src/serve/project-gutter-route.ts +21 -0
  124. package/src/serve/project-gutter.ts +3 -2
  125. package/src/serve/repo-processes.ts +89 -0
  126. package/src/serve/run-serve.ts +110 -402
  127. package/src/serve/serve-hub.ts +142 -44
  128. package/src/serve/stale-restart.ts +5 -0
  129. package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
  130. package/src/serve/warm-cell.ts +95 -0
  131. package/src/shipkit-files.ts +114 -0
  132. package/src/shipkit.ts +2 -1
  133. package/src/suite/kept-timings.ts +1 -1
  134. package/src/wiki/graph-positions.ts +1 -1
  135. package/src/serve/health-schedule.ts +0 -102
  136. package/src/serve/hub-app/project-gutter.css +0 -65
  137. package/src/serve/hub-app/project-gutter.tsx +0 -128
@@ -0,0 +1,245 @@
1
+ // the home's strip over every page it serves: one quiet line. On a repo page
2
+ // the shown repo (its icon, its name in its own colour, the two swatches that
3
+ // set those colours); on the overview and the plan, the page's name. On the
4
+ // right, only machine wide trouble, and only while it lasts (pm or the daemon
5
+ // down, a child or a global process crashed). The places themselves are the
6
+ // two buttons at the top of the project list (⌘J, ⌘P) and the first two
7
+ // entries of the picker.
8
+ // What belongs to one repo (its hub state, restart, the active flag, VS
9
+ // Code, modernize) is RepoControls, in that repo's own header. NotUp stands
10
+ // in for the hub page while its hub cannot answer, with the fix. Every link
11
+ // of the home is a page of the same app: a plain click moves it in place.
12
+ import { observer } from 'mobx-react-lite'
13
+ import type { ReactNode } from 'react'
14
+ import type { ResourceView } from '../../home/resources.ts'
15
+ import type { WorkerView } from '../../home/hubs.ts'
16
+ import { NOT_INSTALLED } from '../../home/home-words.ts'
17
+ import type { MachineSt } from './machine-st.ts'
18
+ import { plainClick, type HomeSt } from './home-st.ts'
19
+ import { Icon, ICON } from './icons.tsx'
20
+ import { Tip } from './floating.tsx'
21
+
22
+ const STATUS_WORD: Record<WorkerView['status'], string> = { up: 'up', starting: 'starting', crashed: 'crashed', down: 'down', stopping: 'stopping' }
23
+
24
+ export const Dot = ({ s }: { s: WorkerView['status'] }) => <i className={`hdot ${s}`} />
25
+
26
+ /** a link to a page of the home: a plain click shows it in place, any other
27
+ * click keeps the browser's own behaviour */
28
+ export const HomeLink = ({ app, to, className, title, children }: { app: HomeSt; to: string; className?: string; title?: string; children: ReactNode }) => (
29
+ <a className={className} href={to} title={title} onClick={(e) => {
30
+ if (!plainClick(e)) return
31
+ e.preventDefault()
32
+ app.navigate(to)
33
+ }}>{children}</a>
34
+ )
35
+
36
+ const Log = ({ lines }: { lines: string[] }) => (lines.length === 0 ? null : <pre className="hlog">{lines.join('\n')}</pre>)
37
+
38
+ /** one switch per global process; its log floats under it on hover */
39
+ export const ResourceChip = observer(({ m, r }: { m: MachineSt; r: ResourceView }) => {
40
+ const on = r.status === 'up' || r.status === 'starting'
41
+ const title = [r.description, r.where != null ? `on ${r.where}` : null, r.note].filter((x) => x != null).join(' · ')
42
+ const log = r.tail.length > 0 ? `${title !== '' ? `${title}\n\n` : ''}${r.tail.join('\n')}` : null
43
+ return (
44
+ <Tip className="hproc" tipClass="hpop" placement="bottom-start" interactive content={log} data-log={log == null ? undefined : 'yes'}>
45
+ <button className={`hres ${r.status}`} title={log != null ? undefined : title}
46
+ disabled={r.status === 'stopping' || (on && !r.canStop)}
47
+ onClick={() => void m.processAct(r.id, on ? 'stop' : 'start')}>
48
+ <Dot s={r.status} />{r.label}
49
+ </button>
50
+ </Tip>
51
+ )
52
+ })
53
+
54
+ /** a global process on the overview, as one card: its switch (its log
55
+ * floats on hover), its state in words, the repo and file that declare it,
56
+ * its url, then its buttons: restart, take over when a copy started
57
+ * elsewhere runs, open in the browser, open as an app window (app: true
58
+ * only), its declared actions */
59
+ const ProcessCard = observer(({ m, r }: { m: MachineSt; r: ResourceView }) => {
60
+ const up = r.status === 'up'
61
+ return (
62
+ <section className="hcard">
63
+ <div className="hchead">
64
+ <ResourceChip m={m} r={r} />
65
+ <span className={`hcstate ${r.status}`}>{STATUS_WORD[r.status]}</span>
66
+ </div>
67
+ <div className="hdim hcfile">{r.repo}/{r.file}</div>
68
+ {r.url != null ? (
69
+ <a className="hcurl" href={r.url} title={`open ${r.url} in the default browser`} onClick={(e) => {
70
+ // the default browser, through pm, never inside the shipkit window
71
+ e.preventDefault()
72
+ void m.processAct(r.id, 'open')
73
+ }}>{r.url.replace(/^https?:\/\//, '')}</a>
74
+ ) : null}
75
+ {r.description != null || r.where != null ? <div className="hdim">{[r.description, r.where != null ? `on ${r.where}` : null].filter((x) => x != null).join(' · ')}</div> : null}
76
+ <div className="hcbtns">
77
+ {up && r.canStop ? <button className="ghost" onClick={() => void m.processAct(r.id, 'restart')} title={`stop ${r.label} and start it again`}>restart</button> : null}
78
+ {r.takeOver?.ok === true ? <button className="ghost" onClick={() => void m.processAct(r.id, 'takeover')} title={`stop the copy of ${r.label} started elsewhere (the process group listening on its port) and start one pm supervises, in one go`}>take over</button> : null}
79
+ {r.url != null ? <button className="ghost" onClick={() => void m.processAct(r.id, 'open')} title={up ? `open ${r.url} in the default browser` : `start ${r.label}, then open ${r.url} in the default browser`}>{up ? 'open' : 'start and open'}</button> : null}
80
+ {r.app && r.url != null ? <button className="ghost" onClick={() => void m.processAct(r.id, 'app')} title={up ? `open ${r.url} as a shipkit app window` : `start ${r.label}, then open ${r.url} as a shipkit app window`}>open as app</button> : null}
81
+ {r.actions.map((a) => (
82
+ <button key={a.id} className="ghost" disabled={r.running != null} onClick={() => void m.processAct(r.id, 'action', a.id)}>{r.running === a.id ? <span className="spin" /> : null}{a.label}</button>
83
+ ))}
84
+ </div>
85
+ {r.takeOver?.ok === false ? <div className="hnote">cannot take over: {r.takeOver.why}</div> : null}
86
+ {r.note != null ? <div className="hnote">{r.note}</div> : null}
87
+ </section>
88
+ )
89
+ })
90
+
91
+ /** every house repo's widgets: the html the daemon last rendered, each under its title */
92
+ const Monitor = observer(({ m }: { m: MachineSt }) => {
93
+ const w = m.widgets
94
+ if (w == null || (w.widgets.length === 0 && w.errors.length === 0)) return null
95
+ return (
96
+ <section className="hsec">
97
+ <h2><Icon d={ICON.monitor} />monitor</h2>
98
+ <div className="hmon">
99
+ {w.widgets.map((x) => (
100
+ <section key={`${x.repo}/${x.id}`} className="hwidget" title={`${x.repo}/${x.file}`}>
101
+ <h3>{x.title}<span className="hdim"> · {x.repo}</span></h3>
102
+ {x.error != null ? <p className="hnote">{x.error}</p> : null}
103
+ {x.html == null ? <p className="twait"><span className="spin" /></p> : <div dangerouslySetInnerHTML={{ __html: x.html }} />}
104
+ </section>
105
+ ))}
106
+ </div>
107
+ {w.errors.map((e) => <p key={e} className="hnote">{e}</p>)}
108
+ </section>
109
+ )
110
+ })
111
+
112
+ /** the two colours of a repo, a picker each: the page draws the dark one */
113
+ export const IdentitySwatches = observer(({ app, name }: { app: HomeSt; name: string }) => {
114
+ const id = app.projects.identityOf(name)
115
+ return (
116
+ <span className="hswatch">
117
+ <input type="color" aria-label="colour on a dark background" title={`${name} on a dark background (this page): ${id.colorDark}, written to repo.config.ts identity.colorDark`}
118
+ value={id.colorDark} onChange={(e) => app.projects.setIdentity(name, { colorDark: e.currentTarget.value })} />
119
+ <input type="color" aria-label="colour on a light background" title={`${name} on a light background: ${id.colorLight}, written to repo.config.ts identity.colorLight`}
120
+ value={id.colorLight} onChange={(e) => app.projects.setIdentity(name, { colorLight: e.currentTarget.value })} />
121
+ </span>
122
+ )
123
+ })
124
+
125
+ /** the shown repo in the strip: its icon, its name in its colour */
126
+ const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
127
+ const p = app.projects.list.find((x) => x.name === name)
128
+ const id = app.projects.identityOf(name)
129
+ return (
130
+ <span className="hrepo">
131
+ {/* the icon's box is there before the project list is, so the name never moves */}
132
+ {p?.icon != null ? <img src={p.icon} alt="" width={18} height={18} /> : <i className="hrico" />}
133
+ <b className="hrname" style={{ color: id.colorDark }}>{name}</b>
134
+ <IdentitySwatches app={app} name={name} />
135
+ </span>
136
+ )
137
+ })
138
+
139
+ /** what is wrong with the machine itself, one chip each. Nothing when all is
140
+ * well: the strip states trouble, never health */
141
+ const MachineAlerts = observer(({ m }: { m: MachineSt }) => (
142
+ <>
143
+ {(m.pm?.children ?? []).filter((c) => c.status === 'crashed' || c.status === 'down').map((c) => (
144
+ <span key={c.id} className="halert" title={c.note ?? c.tail.at(-1) ?? ''}><Dot s={c.status} />{c.id} {STATUS_WORD[c.status]}</span>
145
+ ))}
146
+ {(m.pm?.resources ?? []).filter((r) => r.status === 'crashed').map((r) => <ResourceChip key={r.id} m={m} r={r} />)}
147
+ {m.error !== '' ? <span className="herr" title={m.error}>{m.error}</span> : null}
148
+ </>
149
+ ))
150
+
151
+ export const HomeStrip = observer(({ app }: { app: HomeSt }) => {
152
+ const m = app.machine
153
+ if (m == null) return null
154
+ const name = m.repo
155
+ return (
156
+ <>
157
+ {m.down !== '' ? <div className="hdown" role="alert"><b>daemon down</b> {m.down}</div> : null}
158
+ {m.pmDown !== '' ? <div className="hdown" role="alert"><b>pm down</b> {m.pmDown}</div> : null}
159
+ <div className="hstrip">
160
+ {name == null ? <span className="hplace">{app.page === 'planned' ? 'planned' : 'overview'}</span> : <ShownRepo app={app} name={name} />}
161
+ <span className="sp" />
162
+ <MachineAlerts m={m} />
163
+ </div>
164
+ </>
165
+ )
166
+ })
167
+
168
+ /** one repo's own controls on the home, in that repo's header: its hub
169
+ * state, restart, the active flag, VS Code, and modernize when it is behind */
170
+ export const RepoControls = observer(({ app, name }: { app: HomeSt; name: string }) => {
171
+ const m = app.machine
172
+ if (m == null) return null
173
+ const w = m.worker(name)
174
+ const behind = m.data?.behind[name]
175
+ return (
176
+ <span className="hctl">
177
+ {w == null || w.status === 'up' ? null : <span className="hstat"><Dot s={w.status} />{STATUS_WORD[w.status]}</span>}
178
+ <button className="ghost" onClick={() => void m.hub(name, 'restart')} title="load this repo's hub again, its config read fresh">restart</button>
179
+ <label className="hflag" title="an active repo runs the checks the plan holds for it, and gets its pings, even with no agent in it">
180
+ <input type="checkbox" checked={m.isActive(name)} onChange={(e) => void m.setActive(name, e.currentTarget.checked)} />active
181
+ </label>
182
+ <button className="ghost" onClick={() => m.openInCode(name)} title="open the folder in VS Code (or double click its icon)">VS Code</button>
183
+ {behind == null ? null : <button className="primary" onClick={() => void m.modernize(name)} title={`this repo runs shipkit ${behind}, the home runs ${m.data?.version}`}>modernize {behind} → {m.data?.version}</button>}
184
+ </span>
185
+ )
186
+ })
187
+
188
+ /** why the shown repo's hub does not answer, and what to do about it */
189
+ export const NotUp = observer(({ app }: { app: HomeSt }) => {
190
+ const m = app.machine
191
+ const name = m?.repo
192
+ if (m == null || name == null) return null
193
+ const w = m.shown
194
+ const mod = m.data?.modernize[name]
195
+ if (w == null) return <div className="hwait"><p className="twait"><span className="spin" />{m.down !== '' ? `waiting for the daemon to load ${name}…` : `looking for ${name}…`}</p></div>
196
+ return (
197
+ <div className="hwait">
198
+ {w.blocked != null ? <p>{w.blocked}</p> : <p><Dot s={w.status} />{name}'s hub is {STATUS_WORD[w.status]}{w.note != null ? `: ${w.note}` : ''}</p>}
199
+ {w.blocked == null && (w.status === 'down' || w.status === 'crashed') ? <button className="primary" onClick={() => void m.hub(name, 'restart')}>start it</button> : null}
200
+ {w.blocked != null && mod?.status !== 'running' ? <button className="primary" onClick={() => void m.modernize(name)}>{w.blocked === NOT_INSTALLED ? 'bun install' : 'modernize'}</button> : null}
201
+ {mod == null ? null : <><p>modernize: {mod.status}{mod.code != null ? `, exit ${mod.code}` : ''}</p><Log lines={mod.tail} /></>}
202
+ <Log lines={w.tail} />
203
+ </div>
204
+ )
205
+ })
206
+
207
+ /** the overview at `/`: shipkit's own processes, a card per global process,
208
+ * then the widgets. The repos are the project list's, not a section here */
209
+ export const Overview = observer(({ app }: { app: HomeSt }) => {
210
+ const m = app.machine
211
+ if (m == null) return null
212
+ if (m.data == null) return <div className="hwait"><p className="twait"><span className="spin" />{m.down !== '' ? 'the daemon does not answer yet' : 'loading…'}</p></div>
213
+ const procs = m.pm?.resources ?? []
214
+ return (
215
+ <div className="hall">
216
+ <div className="hstack">
217
+ <section className="hsec">
218
+ <h2><Icon d={ICON.shipkit} />shipkit<span className="sp" /><button className="ghost" title="open one more window on this page" onClick={() => m.newWindow()}>new window</button></h2>
219
+ {m.pm == null ? <p className="hdim">pm does not answer, so its children and the global processes are unknown</p> : (
220
+ <div className="hcards"><section className="hcard">
221
+ <div className="hrow"><span className="hlink"><Dot s="up" />pm</span><span className="hdim">pid {m.pm.pid} · shipkit {m.pm.version}</span></div>
222
+ {m.pm.children.map((c) => (
223
+ <div key={c.id} className="hrow">
224
+ <span className="hlink"><Dot s={c.status} />{c.id}</span>
225
+ <span className="hdim">{STATUS_WORD[c.status]}{c.pid != null ? ` · pid ${c.pid}` : ''}{c.restarts > 0 ? ` · ${c.restarts} restart(s)` : ''}</span>
226
+ {c.note != null ? <span className="hnote">{c.note}</span> : null}
227
+ <span className="sp" />
228
+ <button className="ghost" onClick={() => void m.restartChild(c.id as 'daemon' | 'web')} title={`stop the ${c.id} and start it again, through pm`}>restart</button>
229
+ {c.status === 'crashed' ? <Log lines={c.tail} /> : null}
230
+ </div>
231
+ ))}
232
+ </section></div>
233
+ )}
234
+ </section>
235
+ <section className="hsec">
236
+ <h2><Icon d={ICON.process} />global processes</h2>
237
+ {procs.length === 0 ? <p className="hdim">{m.pm == null ? 'unknown while pm does not answer' : 'none declared: a repo declares one in a <name>.shipkit.ts file, as export default globalProcess({...})'}</p> : null}
238
+ <div className="hcards">{procs.map((r) => <ProcessCard key={r.id} m={m} r={r} />)}</div>
239
+ {(m.pm?.unitErrors ?? []).map((e) => <p key={e} className="hnote">{e}</p>)}
240
+ </section>
241
+ <Monitor m={m} />
242
+ </div>
243
+ </div>
244
+ )
245
+ })
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>home</title>
7
+ <link rel="stylesheet" href="./hub.css" />
8
+ <link rel="stylesheet" href="./project-list.css" />
9
+ <link rel="stylesheet" href="./prose-hits.css" />
10
+ <link rel="stylesheet" href="./home-strip.css" />
11
+ <link rel="stylesheet" href="./planned.css" />
12
+ </head>
13
+ <body data-home>
14
+ <div id="root"></div>
15
+ <script type="module" src="./hub-app.tsx"></script>
16
+ </body>
17
+ </html>