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,130 @@
1
+ // THE `<name>.shipkit.ts` file: a repo drops one anywhere and default-exports
2
+ // what it declares, and the kind says who reads it.
3
+ //
4
+ // check({...}) a gate check, read by discovery like a `.check.ts`
5
+ // globalProcess({...}) a long running process for the whole machine: pm
6
+ // supervises it, the home page switches it
7
+ // widget({...}) html the daemon renders every few seconds, mounted
8
+ // on the home page
9
+ //
10
+ // the one constraint a reader breaks first: every kind is IMPORTED by the
11
+ // process that reads it (discovery, pm's config child, the daemon), so a file
12
+ // keeps its top level cheap and puts heavy work inside a function.
13
+ import { check, type CheckDef } from './check.ts'
14
+
15
+ export { check }
16
+
17
+ /** one button beside a global process: a command, or a function of the file */
18
+ export type ProcessAction = {
19
+ /** the words on the button */
20
+ label: string
21
+ /** argv, run from the repo root, output appended to the process log */
22
+ cmd?: string[]
23
+ /** a function run in its own bun process that imports this file, from
24
+ * the repo root. Its output lands in the process log too */
25
+ run?: () => void | Promise<void>
26
+ }
27
+
28
+ export type GlobalProcessDef = {
29
+ /** kebab-case, unique on the machine: what a repo names in `serve.uses` */
30
+ id: string
31
+ /** the name on the switch (default the id) */
32
+ label?: string
33
+ /** one line: what it is for */
34
+ description?: string
35
+ /** the machine it runs on, as a hint beside the switch */
36
+ where?: string
37
+ /** argv, run from the repo root (or `cwd`). Without `down` it IS the
38
+ * service: pm keeps it running, restarts it on a crash, signals it on
39
+ * stop. With `down` it is a bring-up that exits once the service answers */
40
+ cmd: string[]
41
+ /** argv that takes a bring-up down again. Absent: `cmd` is the service */
42
+ down?: string[]
43
+ /** where both run, repo-relative (default the repo root) */
44
+ cwd?: string
45
+ /** the url it serves: shown as a link, probed when `probe` is absent */
46
+ url?: string
47
+ /** an http url that answers 2xx while it is up (default `url`). A
48
+ * bring-up needs one of the two: its probe is the only truth */
49
+ probe?: string
50
+ /** a button each beside the switch, by key */
51
+ actions?: Record<string, ProcessAction>
52
+ /** what its card wears: an emoji, or a repo-relative image path
53
+ * (png, svg, ico, jpg, webp). Absent: the declaring repo's own icon */
54
+ icon?: string
55
+ }
56
+
57
+ export type WidgetDef = {
58
+ /** kebab-case, unique in its repo */
59
+ id: string
60
+ /** the heading over it (default the id) */
61
+ title?: string
62
+ /** how often the daemon renders it again (default 5000, at least 1000) */
63
+ everyMs?: number
64
+ /** the html it shows, rendered in the daemon. A throw shows as its error */
65
+ render: () => string | Promise<string>
66
+ }
67
+
68
+ export type GlobalProcessUnit = GlobalProcessDef & { kind: 'globalProcess' }
69
+ export type WidgetUnit = WidgetDef & { kind: 'widget' }
70
+
71
+ export const UNIT_KINDS = ['globalProcess', 'widget'] as const
72
+
73
+ const KEBAB = /^[a-z0-9][a-z0-9-]*$/
74
+ const words = (v: unknown): boolean => Array.isArray(v) && v.length > 0 && v.every((w) => typeof w === 'string')
75
+ const httpUrl = (v: unknown): boolean => typeof v === 'string' && /^https?:\/\//.test(v)
76
+
77
+ /** the image types an icon file can be, by extension */
78
+ export const ICON_IMAGE_TYPES: Record<string, string> = { png: 'image/png', svg: 'image/svg+xml', ico: 'image/x-icon', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp' }
79
+
80
+ /** the image an icon names (its extension), or null when it is text */
81
+ export const iconImageType = (icon: string): string | null => ICON_IMAGE_TYPES[/\.([a-z]+)$/i.exec(icon)?.[1]?.toLowerCase() ?? ''] ?? null
82
+
83
+ function iconProblem(icon: unknown): string | null {
84
+ if (typeof icon !== 'string' || icon.trim() === '') return 'icon is an emoji or a repo relative image path'
85
+ if (iconImageType(icon) != null) return icon.startsWith('/') || icon.split(/[\\/]/).includes('..') ? `icon '${icon}' must be a repo relative path` : null
86
+ if (/[\\/.]/.test(icon) || [...icon].length > 8) return `icon '${icon}' is neither an emoji nor an image path (${Object.keys(ICON_IMAGE_TYPES).join(', ')})`
87
+ return null
88
+ }
89
+
90
+ /** why a global process declaration cannot run, or null */
91
+ export function globalProcessProblem(d: GlobalProcessDef): string | null {
92
+ const at = `globalProcess '${String(d.id)}'`
93
+ if (!KEBAB.test(d.id ?? '')) return `${at}: id must be kebab-case`
94
+ if (!words(d.cmd)) return `${at}: cmd is a list of words`
95
+ if (d.down != null && !words(d.down)) return `${at}: down is a list of words`
96
+ if (d.url != null && !httpUrl(d.url)) return `${at}: url must be an http url`
97
+ if (d.probe != null && !httpUrl(d.probe)) return `${at}: probe must be an http url`
98
+ if (d.down != null && d.probe == null && d.url == null) return `${at}: a bring-up (one with down) needs probe or url, the only truth of whether it is up`
99
+ if (d.icon !== undefined) {
100
+ const why = iconProblem(d.icon)
101
+ if (why != null) return `${at}: ${why}`
102
+ }
103
+ for (const [k, a] of Object.entries(d.actions ?? {})) {
104
+ if (!KEBAB.test(k)) return `${at}: action '${k}' must be kebab-case`
105
+ if ((a.cmd != null) === (a.run != null)) return `${at}: action '${k}' declares exactly one of cmd or run`
106
+ if (a.cmd != null && !words(a.cmd)) return `${at}: action '${k}'.cmd is a list of words`
107
+ }
108
+ return null
109
+ }
110
+
111
+ export function globalProcess(d: GlobalProcessDef): GlobalProcessUnit {
112
+ const why = globalProcessProblem(d)
113
+ if (why != null) throw new Error(why)
114
+ return { ...d, kind: 'globalProcess' }
115
+ }
116
+
117
+ export function widget(d: WidgetDef): WidgetUnit {
118
+ if (!KEBAB.test(d.id ?? '')) throw new Error(`widget '${String(d.id)}': id must be kebab-case`)
119
+ if (typeof d.render !== 'function') throw new Error(`widget '${d.id}': render is a function returning html`)
120
+ if (d.everyMs != null && !(d.everyMs >= 1000)) throw new Error(`widget '${d.id}': everyMs is at least 1000`)
121
+ return { ...d, kind: 'widget' }
122
+ }
123
+
124
+ /** the kind a default export says it is: a unit, else a check */
125
+ export function unitKind(d: unknown): 'globalProcess' | 'widget' | 'check' {
126
+ const k = (d as { kind?: unknown } | null)?.kind
127
+ return k === 'globalProcess' || k === 'widget' ? k : 'check'
128
+ }
129
+
130
+ export type { CheckDef }
package/src/shipkit.ts CHANGED
@@ -15,7 +15,8 @@ export { contractsConfigFrom, activeContracts, applyContractsResult } from './co
15
15
  export { serveHub, hubTools } from './serve/serve-hub.ts'
16
16
  export type { Hub, HubDeps, HubTool } from './serve/serve-hub.ts'
17
17
  export type { GraphMount } from './wiki/graph-mount.ts'
18
- export { runServe, resolveHubDeps, defaultHubPort, baseWikiRoot } from './serve/run-serve.ts'
18
+ export { runServe } from './serve/run-serve.ts'
19
+ export { resolveHubDeps, defaultHubPort, baseWikiRoot, loadHub } from './serve/hub-runtime.ts'
19
20
  export { projectNeeds, redCount } from './collections/collection-types.ts'
20
21
  export type { Collection, Instance, NeedItem, NeedSeverity, CurationVerb } from './collections/collection-types.ts'
21
22
  export { buildCollections } from './collections/providers.ts'
@@ -15,7 +15,7 @@
15
15
  import { copyFileSync, mkdirSync, readFileSync } from 'fs'
16
16
  import { dirname, join } from 'path'
17
17
  import { atomicWrite } from '../core/locked-write.ts'
18
- import { shipkitRoot } from '../ledger/store.ts'
18
+ import { shipkitRoot } from '../ledger/db.ts'
19
19
 
20
20
  /** where a unit's durations live, beside everything else the repo remembers */
21
21
  export const timingsHome = (root: string, unit: string): string => join(shipkitRoot(root), 'suite-timings', `${unit === '' ? 'root' : unit}.json`)
@@ -4,7 +4,7 @@
4
4
  import { mkdirSync, readFileSync } from 'fs'
5
5
  import { dirname, join } from 'path'
6
6
  import { atomicWrite } from '../core/locked-write.ts'
7
- import { shipkitRoot } from '../ledger/store.ts'
7
+ import { shipkitRoot } from '../ledger/db.ts'
8
8
  import type { MdGraph } from './md-graph.ts'
9
9
 
10
10
  export type Positions = Record<string, [number, number]>
@@ -1,20 +0,0 @@
1
- // the contracts check — `.shipkit/*` release-contract fulfillness. It shares ONE
2
- // evaluation with check SK002 (the `contractsResult` scan), and it tags with
3
- // that id, because the invariant has one owner and two surfaces report it. A
4
- // venue deploy step deps on it BY NAME, which needs nothing from this end.
5
- import { applyContractsResult, contractsResult } from '../contracts/apply-contracts.ts'
6
- import { check } from '../check.ts'
7
- import { contractsFulfillness } from './SK002-contracts-fulfillness.check.ts'
8
-
9
- export const contractsCheck = check({
10
- id: 'contracts',
11
- title: 'contracts',
12
- description: 'every declared venue has the `.shipkit/*` release contract it demands',
13
- owner: 'meta/contracts.md',
14
- blocking: true,
15
- run: async (c) => {
16
- applyContractsResult(c, await c.use(contractsResult), contractsFulfillness)
17
- },
18
- })
19
-
20
- export default contractsCheck
@@ -1,102 +0,0 @@
1
- // THE HEALTH PASS TIMER. While the hub serves, the whole health pass runs once
2
- // a day, and once at startup when the last whole pass on record is older than
3
- // that. One timer for the repo, no per check schedule: a check's own `ttl` is
4
- // its cadence, and this pass is only the heartbeat that replays an answer
5
- // still fresh and re-measures one that expired.
6
- //
7
- // The run is the hub's own in process runner, the same path as the button, so
8
- // its rows land in the same run record. "When was the last pass" is read off
9
- // that record, which is what makes a `shipkit check` in a terminal count too.
10
-
11
- /** how often the health pass runs while the hub serves */
12
- export const HEALTH_EVERY_MS = 24 * 60 * 60 * 1000
13
-
14
- /** how often the timer looks. The pass itself is daily, this only bounds how
15
- * late after its due time it starts */
16
- export const SCHEDULE_LOOK_MS = 5 * 60 * 1000
17
-
18
- export type ScheduleState = {
19
- everyMs: number
20
- /** when the last whole health pass on record started, null = none */
21
- lastPassAt: number | null
22
- /** when the next one starts: now when it is overdue */
23
- nextAt: number
24
- /** the timer's own pass is running right now */
25
- running: boolean
26
- }
27
-
28
- export type ScheduleDeps = {
29
- now: () => number
30
- /** the last whole health pass on record, whoever ran it */
31
- lastPassAt: () => number | null
32
- /** another run in this process is going: the timer never stacks on it */
33
- busy: () => boolean
34
- /** the pass itself */
35
- run: () => Promise<void>
36
- everyMs?: number
37
- }
38
-
39
- export type TickResult = 'ran' | 'fresh' | 'busy'
40
-
41
- export class HealthSchedule {
42
- readonly everyMs: number
43
- running = false
44
- /** when this timer last STARTED a pass. A pass that dies before it writes
45
- * a record would otherwise be due again at the very next look */
46
- private attemptedAt: number | null = null
47
- private timer: ReturnType<typeof setInterval> | null = null
48
-
49
- constructor(private readonly d: ScheduleDeps) {
50
- this.everyMs = d.everyMs ?? HEALTH_EVERY_MS
51
- }
52
-
53
- /** the newest of the record and this timer's own last attempt */
54
- private lastAt(): number | null {
55
- const rec = this.d.lastPassAt()
56
- if (rec == null) return this.attemptedAt
57
- return this.attemptedAt == null ? rec : Math.max(rec, this.attemptedAt)
58
- }
59
-
60
- due(): boolean {
61
- const last = this.lastAt()
62
- return last == null || this.d.now() - last >= this.everyMs
63
- }
64
-
65
- state(): ScheduleState {
66
- const now = this.d.now()
67
- const last = this.lastAt()
68
- return {
69
- everyMs: this.everyMs,
70
- lastPassAt: this.d.lastPassAt(),
71
- nextAt: last == null ? now : Math.max(now, last + this.everyMs),
72
- running: this.running,
73
- }
74
- }
75
-
76
- /** one look: run the pass when it is due and nothing else is running */
77
- async tick(): Promise<TickResult> {
78
- if (this.running || this.d.busy()) return 'busy'
79
- if (!this.due()) return 'fresh'
80
- this.running = true
81
- this.attemptedAt = this.d.now()
82
- try {
83
- await this.d.run()
84
- } finally {
85
- this.running = false
86
- }
87
- return 'ran'
88
- }
89
-
90
- /** look now, then every SCHEDULE_LOOK_MS. A second start is a no-op */
91
- start(onError: (e: Error) => void, lookMs = SCHEDULE_LOOK_MS): void {
92
- if (this.timer != null) return
93
- const look = (): void => void this.tick().catch((e: Error) => onError(e))
94
- look()
95
- this.timer = setInterval(look, lookMs)
96
- }
97
-
98
- stop(): void {
99
- if (this.timer != null) clearInterval(this.timer)
100
- this.timer = null
101
- }
102
- }
@@ -1,250 +0,0 @@
1
- // HubSt — the ONE root store (build/app-state-tree: one central tree,
2
- // instantiated once at boot, services with backrefs, exposed as
3
- // window.app). Owns cross-service state: selection, the severity filter
4
- // (an ADDRESS: /?f=needs · /?f=red), and the hash router: every page and
5
- // every tab is a selection, every selection has an address (hash-route.ts),
6
- // and setSel is the one place both move together.
7
- import { RUNS_ID } from '../../collections/collection-types.ts'
8
- import { makeAutoObservable } from 'mobx'
9
- import { ClockSvc } from './clock-svc.ts'
10
- import { ColsSvc } from './cols-svc.ts'
11
- import { canonicalSel, currentHash, hashFor, parseHash, selOf, type HashSel } from './hash-route.ts'
12
- import { ComfySvc } from './comfy-svc.ts'
13
- import { HealthSvc } from './health-svc.ts'
14
- import { LiveSvc } from './live-svc.ts'
15
- import { MediaSvc } from './media-svc.ts'
16
- import { TimelineSvc } from './timeline-svc.ts'
17
- import { TreeSvc } from './tree-svc.ts'
18
- import { ChecksSvc } from './checks-svc.ts'
19
- import { ProjectsSvc } from './projects-svc.ts'
20
- import { TestsSvc } from './tests-svc.ts'
21
- import type { Resource } from './hub-types.ts'
22
-
23
- export type Sel = HashSel | null
24
-
25
- /** one cmd+j target — run() fires the SAME action clicking would */
26
- export type Jump = { id: string; label: string; section: string; run: () => void }
27
-
28
- /** all · needs (yellow+red) · red — one severity ladder for every section */
29
- export type Filter = 'all' | 'needs' | 'red'
30
- const parseFilter = (search: string): Filter => {
31
- const f = new URLSearchParams(search).get('f')
32
- return f === 'needs' || f === 'red' ? f : 'all'
33
- }
34
-
35
- export class HubSt {
36
- name = ''
37
- /** where this repo sits on disk, from /hub.json: what makes a source
38
- * path on a page a link that opens his editor */
39
- root = ''
40
- /** the one second tick every age on the page reads (clock-svc.ts) */
41
- clock = new ClockSvc()
42
- media = new MediaSvc(this)
43
- cols = new ColsSvc(this)
44
- tree = new TreeSvc(this)
45
- health = new HealthSvc(this)
46
- checks = new ChecksSvc(this)
47
- comfy = new ComfySvc(this)
48
- timeline = new TimelineSvc(this)
49
- /** the runs in flight, whoever started them (live-svc.ts) */
50
- live = new LiveSvc(this)
51
- projects = new ProjectsSvc(this)
52
- /** the pages behind the test groups of the tree (tests-svc.ts) */
53
- tests = new TestsSvc(this)
54
- sel: Sel = null
55
- filter: Filter = parseFilter(location.search)
56
- /** sidebar search — live substring filter over asset + collection rows */
57
- query = ''
58
- palOpen = false
59
- palQuery = ''
60
- palIdx = 0
61
- private booted = false
62
-
63
- constructor() {
64
- makeAutoObservable<this, 'booted'>(this, { media: false, cols: false, tree: false, health: false, checks: false, comfy: false, timeline: false, live: false, projects: false, tests: false, booted: false })
65
- }
66
-
67
- boot(): void {
68
- void fetch('/hub.json')
69
- .then((r) => r.json() as Promise<{ name: string; root?: string }>)
70
- .then((h) => { this.setHub(h.name, h.root ?? '') })
71
- .catch(() => {})
72
- this.clock.start()
73
- this.media.start()
74
- this.cols.start()
75
- this.live.start()
76
- void this.checks.loadGate()
77
- void this.health.load()
78
- // back, forward, a pasted link and a hand edited hash all arrive
79
- // here; applyHash is idempotent, so the two events firing together
80
- // for one move cost nothing
81
- window.addEventListener('hashchange', () => this.applyHash())
82
- window.addEventListener('popstate', () => this.applyHash())
83
- this.applyHash()
84
- }
85
- private setHub(n: string, root: string): void {
86
- this.name = n
87
- this.root = root
88
- document.title = `${n} · hub`
89
- }
90
-
91
- setFilter(v: Filter): void {
92
- this.filter = v
93
- const u = new URL(location.href)
94
- v === 'all' ? u.searchParams.delete('f') : u.searchParams.set('f', v)
95
- history.replaceState(null, '', u)
96
- }
97
- setQuery(v: string): void {
98
- this.query = v
99
- }
100
-
101
- // ── cmd+j palette ─────────────────────────────────────────────────────
102
- palToggle(): void {
103
- this.palOpen = !this.palOpen
104
- this.palQuery = ''
105
- this.palIdx = 0
106
- }
107
- palClose(): void {
108
- this.palOpen = false
109
- }
110
- setPalQuery(v: string): void {
111
- this.palQuery = v
112
- this.palIdx = 0
113
- }
114
- setPalIdx(n: number): void {
115
- this.palIdx = n
116
- }
117
- palMove(d: number): void {
118
- const n = this.palResults.length
119
- if (n > 0) this.palIdx = (this.palIdx + d + n) % n
120
- }
121
- palRun(): void {
122
- const j = this.palResults[this.palIdx]
123
- if (j == null) return
124
- this.palOpen = false
125
- j.run()
126
- }
127
- /** every jump target: section titles, media assets, collection instances */
128
- get jumps(): Jump[] {
129
- const out: Jump[] = []
130
- for (const cat of new Set(this.media.assets.map((a) => a.cat)))
131
- out.push({ id: `cat:${cat}`, label: cat, section: 'media', run: () => this.showCat(cat) })
132
- for (const a of this.media.assets)
133
- out.push({ id: `asset:${a.id}`, label: a.label, section: a.cat, run: () => this.selectAsset(a.id) })
134
- // the runs page is a page, not a resource, so no collection lists it
135
- out.push({ id: 'page:runs', label: 'runs', section: 'page', run: () => this.cols.select('runs', RUNS_ID) })
136
- for (const col of this.cols.sidebarCols) {
137
- out.push({ id: `col:${col.id}`, label: col.title, section: 'section', run: () => this.showCol(col.id) })
138
- for (const i of col.instances)
139
- out.push({ id: `item:${i.id}`, label: i.text, section: col.title, run: () => this.cols.select(col.id, i.id) })
140
- }
141
- return out
142
- }
143
- /** ranked: prefix matches first, then substring; capped at 12 */
144
- get palResults(): Jump[] {
145
- const q = this.palQuery.trim().toLowerCase()
146
- if (q === '') return this.jumps.slice(0, 12)
147
- const pre: Jump[] = []
148
- const sub: Jump[] = []
149
- for (const j of this.jumps) {
150
- const l = j.label.toLowerCase()
151
- if (l.startsWith(q)) pre.push(j)
152
- else if (l.includes(q)) sub.push(j)
153
- }
154
- return [...pre, ...sub].slice(0, 12)
155
- }
156
-
157
- // ── selection + hash deep-links ───────────────────────────────────────
158
- selectAsset(id: string): void {
159
- this.setSel({ t: 'asset', id })
160
- void this.media.select(id)
161
- }
162
- showCat(cat: string): void {
163
- this.setSel({ t: 'cat', cat })
164
- this.media.openCats.add(cat)
165
- void this.media.fillCat()
166
- }
167
- showStatus(): void {
168
- this.setSel({ t: 'status' })
169
- void this.health.load()
170
- }
171
- /** where the test checks' time goes: the `tests` group (null) or one test
172
- * check's group. Fetched as the page opens, never on the poll */
173
- showTests(check: string | null): void {
174
- this.setSel({ t: 'tests', check })
175
- void this.tests.load(check)
176
- }
177
-
178
- /** the ONE place a selection is written, so the url always describes what
179
- * is on screen. A new page is a history entry, so back returns to the
180
- * page before it. The same address is never pushed twice: applyHash
181
- * lands here too, with the hash already naming this selection */
182
- setSel(sel: Sel): void {
183
- this.sel = sel
184
- const h = hashFor(sel)
185
- if (h == null) return
186
- const cur = parseHash(currentHash())
187
- const next = parseHash(h)
188
- // the tool set rides after the asset id (media-svc syncHash), so an
189
- // address is the same page whatever tools it carries
190
- if (cur.kind === next.kind && cur.col === next.col && cur.id === next.id) return
191
- history.pushState(null, '', `#${h}`)
192
- }
193
- /** one tree row, opened: an asset goes to the atelier, everything else to
194
- * its own item card. A gate check has a page of its own now, so nothing
195
- * lands on the raw wall of text unless it is asked for by name */
196
- openResource(r: Resource): void {
197
- const assetId = r.id.replace(/^media:/, '')
198
- if (r.collection === 'media' && this.media.assets.some((a) => a.id === assetId)) {
199
- this.selectAsset(assetId)
200
- return
201
- }
202
- this.cols.select(r.collection, r.id)
203
- }
204
-
205
- /** collection view — one card per instance */
206
- showCol(id: string): void {
207
- this.setSel({ t: 'col', id })
208
- }
209
- /** open whatever a selection names, through the same calls a click makes */
210
- go(sel: HashSel): void {
211
- switch (sel.t) {
212
- case 'asset': return this.selectAsset(sel.id)
213
- case 'cat': return this.showCat(sel.cat)
214
- case 'col': return this.showCol(sel.id)
215
- case 'item': return this.cols.select(sel.col, sel.id)
216
- case 'status': return this.showStatus()
217
- case 'tests': return this.showTests(sel.check)
218
- }
219
- }
220
- /** the url decides the page: a reload, back, forward and a pasted link
221
- * all land here, and only a hash naming something else moves the view */
222
- applyHash(): void {
223
- const p = parseHash(currentHash())
224
- const named = selOf(p)
225
- if (named == null) return
226
- // an address naming what this build opens under another address is
227
- // rewritten in place: a replace, so back never lands on the stale one
228
- const sel = canonicalSel(named)
229
- if (sel !== named) history.replaceState(null, '', `#${hashFor(sel)}`)
230
- // media addresses resolve only once the first heartbeat listed the
231
- // assets (afterFirstBeat calls back)
232
- if (sel.t === 'asset' && !this.media.assets.some((a) => a.id === sel.id)) return
233
- if (sel.t === 'cat' && !this.media.assets.some((a) => a.cat === sel.cat)) return
234
- if (sel.t === 'asset' && this.media.curId === sel.id) {
235
- // same asset, new tool set (a pasted share link): apply NOW, a
236
- // parked pendingTools would hijack the next selection instead
237
- if (p.tools != null) this.media.applyTab(p.tools)
238
- return
239
- }
240
- if (hashFor(sel) === hashFor(this.sel)) return
241
- if (sel.t === 'asset') this.media.pendingTools = p.tools
242
- this.go(sel)
243
- }
244
- /** first heartbeat has the asset list — only then can a hash resolve */
245
- afterFirstBeat(): void {
246
- if (this.booted) return
247
- this.booted = true
248
- this.applyHash()
249
- }
250
- }
@@ -1,65 +0,0 @@
1
- /* the project gutter. It is FIXED to the left edge, so it takes no cell of
2
- the .app grid, and the page makes room for it by padding, whatever the
3
- grid holds. Colours are hub.css tokens only, spacing is its scale:
4
- tests/hub-css-invariants.test.ts reads this sheet too. */
5
- .app:has(> .pgut){--gut:44px;padding-left:var(--gut)}
6
- .pgut{position:fixed;left:0;top:0;bottom:0;width:var(--gut,44px);z-index:5;display:flex;flex-direction:column;align-items:center;
7
- gap:var(--s1);padding:var(--s2) 0;background:var(--bg);border-right:1px solid var(--line);overflow-y:auto;scrollbar-width:none}
8
- .pgt{position:relative;flex:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;padding:0;
9
- background:transparent;border:0;border-radius:6px;cursor:pointer}
10
- .pgt:hover{background:var(--panel2)}
11
- .pgt.cur{background:var(--sel)}
12
- .pgt img{width:24px;height:24px;border-radius:5px;object-fit:cover;pointer-events:none}
13
- /* the initial on a hue hashed from the name (projects-svc hueOf) */
14
- .pgi{width:24px;height:24px;border-radius:5px;display:flex;align-items:center;justify-content:center;
15
- font-size:12px;font-weight:600;user-select:none;color:var(--c);background:color-mix(in srgb,var(--c) 22%,var(--panel2))}
16
- .pgi.h1{--c:var(--k1)}
17
- .pgi.h2{--c:var(--k2)}
18
- .pgi.h3{--c:var(--k3)}
19
- .pgi.h4{--c:var(--k4)}
20
- .pgi.h5{--c:var(--k5)}
21
- .pgi.h6{--c:var(--k6)}
22
- /* the face says three things and no more (projects-svc faceOf) */
23
- /* closed in VS Code: the icon dims, the badge does not */
24
- .pgt.dim img,.pgt.dim .pgi{opacity:.45}
25
- /* the one badge: open asks, top right */
26
- .pga{position:absolute;top:0;right:0;min-width:14px;height:14px;padding:0 var(--s0);border-radius:7px;
27
- font-size:9px;font-weight:700;line-height:14px;text-align:center;color:var(--bg);background:var(--warn)}
28
- /* Claude sessions running: a thin edge on the left that breathes */
29
- .pgs{position:absolute;left:0;top:6px;bottom:6px;width:2px;border-radius:1px;background:var(--accent);pointer-events:none;animation:pgpulse 2.4s ease-in-out infinite}
30
- @keyframes pgpulse{0%,100%{opacity:1}50%{opacity:.25}}
31
- @media (prefers-reduced-motion:reduce){.pgs{animation:none}}
32
- /* the hover card: right of the rail, one at a time, never interactive.
33
- Header, then what needs a human, then the label and value rows of the
34
- check page (.wfact .wk .wv from hub.css) */
35
- .pgcard{position:fixed;left:calc(var(--gut,44px) + var(--s2));z-index:20;width:300px;padding:var(--s3);pointer-events:none;
36
- background:var(--panel);border:1px solid var(--line);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px;line-height:1.45;color:var(--ink)}
37
- .pgch{display:flex;align-items:center;gap:var(--s2);min-width:0}
38
- .pgcface{flex:none;display:flex}
39
- .pgcface img,.pgcface .pgi{width:20px;height:20px;border-radius:4px;font-size:11px}
40
- .pgcname{flex:1;min-width:0;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
41
- .pgcthis{font-weight:400;font-size:11px;color:var(--dim)}
42
- .pgcver{flex:none;color:var(--dim);font-size:11px;font-variant-numeric:tabular-nums}
43
- .pgcline{margin-top:var(--s1);color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
44
- .pgcdesc{margin-top:var(--s1);color:var(--dim);font-size:11px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
45
- /* NEEDS YOU: a warn toned bullet, set apart from the facts */
46
- .pgcneeds{margin-top:var(--s2);padding:var(--s1) var(--s2);border-left:2px solid var(--warn);background:var(--panel2);border-radius:0 3px 3px 0}
47
- .pgcneed{display:flex;gap:var(--s1);min-width:0;color:var(--warn)}
48
- .pgcneed.red{color:var(--red);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
49
- .pgcask{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
50
- .pgcmore{flex:none}
51
- .pgcfacts{margin-top:var(--s2)}
52
- .pgcfacts .wk{flex-basis:64px;white-space:nowrap}
53
- .pgcfacts .wv{text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;word-break:normal}
54
- .pgcfacts .wv.warn{color:var(--warn)}
55
- .pgerr{font-size:12px;font-weight:700;color:var(--red);cursor:help}
56
- /* the one time sort by last activity, above the icons */
57
- .pgsort{flex:none;width:32px;height:20px;padding:0;background:transparent;border:1px solid var(--line);border-radius:3px;
58
- color:var(--dim);font-size:12px;line-height:18px;cursor:pointer}
59
- .pgsort:hover{color:var(--ink);border-color:var(--accent)}
60
- /* a drag: the tile being moved fades, and the gap it would land in is a line
61
- drawn on a tile edge, centred in the gap between two tiles */
62
- .pgt.dragging{opacity:.4}
63
- .pgt.dropb::before,.pgt.dropa::after{content:'';position:absolute;left:2px;right:2px;height:2px;border-radius:1px;background:var(--accent);pointer-events:none}
64
- .pgt.dropb::before{top:-3px}
65
- .pgt.dropa::after{bottom:-3px}