rvlib-shipkit 1.4.0 → 1.6.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 (199) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/assets/gen/comfyts-gen.ts +12 -8
  5. package/src/assets/server/studio-server.ts +14 -2
  6. package/src/check.ts +73 -103
  7. package/src/checks/SK001-package-json-shape.check.ts +11 -6
  8. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  9. package/src/checks/SK003-claude-md-shape.check.ts +7 -3
  10. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  11. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  12. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  13. package/src/checks/SK007-github-visibility.check.ts +4 -2
  14. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  15. package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
  16. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  17. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  18. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  19. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  20. package/src/checks/SK014-prose-ste.check.ts +4 -0
  21. package/src/checks/SK015-nested-repo.check.ts +4 -2
  22. package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
  23. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  24. package/src/checks/SK018-test-files-run.check.ts +6 -2
  25. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  26. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  27. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  28. package/src/checks/SK024-identity-colors.check.ts +5 -3
  29. package/src/checks/SK025-release-drift.check.ts +4 -5
  30. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  31. package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
  32. package/src/checks/SK028-readme.check.ts +5 -1
  33. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  34. package/src/checks/SK030-cache-audit.check.ts +34 -0
  35. package/src/checks/SK031-trace-audit.check.ts +39 -0
  36. package/src/checks/SK032-ignored-inputs.check.ts +79 -0
  37. package/src/cli/cmd_Asks.ts +5 -5
  38. package/src/cli/cmd_Assets.ts +17 -13
  39. package/src/cli/cmd_Bump.ts +12 -11
  40. package/src/cli/cmd_CacheAudit.ts +43 -0
  41. package/src/cli/cmd_Check.ts +8 -7
  42. package/src/cli/cmd_Checks.ts +7 -6
  43. package/src/cli/cmd_Ci.ts +12 -11
  44. package/src/cli/cmd_Commands.ts +16 -13
  45. package/src/cli/cmd_ConfigKeys.ts +7 -6
  46. package/src/cli/cmd_Contracts.ts +3 -3
  47. package/src/cli/cmd_CrossRepoActivity.ts +8 -6
  48. package/src/cli/cmd_Fleet.ts +9 -7
  49. package/src/cli/cmd_GlobalProcesses.ts +120 -7
  50. package/src/cli/cmd_Home.ts +26 -19
  51. package/src/cli/cmd_Hooks.ts +6 -5
  52. package/src/cli/cmd_Init.ts +5 -4
  53. package/src/cli/cmd_Keys.ts +20 -8
  54. package/src/cli/cmd_Mv.ts +7 -6
  55. package/src/cli/cmd_Needs.ts +4 -3
  56. package/src/cli/cmd_Overview.ts +5 -4
  57. package/src/cli/cmd_Pb.ts +2 -2
  58. package/src/cli/cmd_Planned.ts +6 -5
  59. package/src/cli/cmd_Prose.ts +5 -5
  60. package/src/cli/cmd_Reflect.ts +7 -7
  61. package/src/cli/cmd_Release.ts +6 -5
  62. package/src/cli/cmd_Releases.ts +5 -4
  63. package/src/cli/cmd_Serve.ts +6 -4
  64. package/src/cli/cmd_Smoke.ts +5 -4
  65. package/src/cli/cmd_Start.ts +7 -6
  66. package/src/cli/cmd_Status.ts +4 -3
  67. package/src/cli/cmd_TraceAudit.ts +62 -0
  68. package/src/cli/cmd_Tui.ts +3 -2
  69. package/src/cli/cmd_When.ts +71 -0
  70. package/src/cli/cmd_Wiki.ts +4 -4
  71. package/src/cli.ts +11 -2
  72. package/src/collections/checks.ts +2 -0
  73. package/src/collections/collection-types.ts +12 -4
  74. package/src/collections/providers.ts +5 -3
  75. package/src/core/app-icon.ts +154 -0
  76. package/src/core/audit-report.ts +21 -0
  77. package/src/core/cache-audit.ts +72 -0
  78. package/src/core/canonical-scripts.ts +2 -2
  79. package/src/core/check-ctx.ts +14 -5
  80. package/src/core/check-runner.ts +43 -23
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +23 -2
  83. package/src/core/floor-resolve.ts +5 -7
  84. package/src/core/git-root.ts +13 -0
  85. package/src/core/ignored-block.ts +94 -0
  86. package/src/core/ignored-inputs.ts +47 -0
  87. package/src/core/jobs.ts +9 -2
  88. package/src/core/legacy-shapes.ts +39 -10
  89. package/src/core/one-lib.ts +59 -4
  90. package/src/core/one-way-in-policy.ts +2 -2
  91. package/src/core/outcome-words.ts +18 -2
  92. package/src/core/port-check-cli.ts +52 -0
  93. package/src/core/port-check-file.ts +295 -0
  94. package/src/core/port-check-files.ts +31 -0
  95. package/src/core/read-set.ts +97 -9
  96. package/src/core/retention.ts +0 -14
  97. package/src/core/run-check.ts +12 -9
  98. package/src/core/run-ci.ts +14 -5
  99. package/src/core/run-fix.ts +30 -23
  100. package/src/core/run-fixers.ts +48 -5
  101. package/src/core/runs-matrix.ts +179 -0
  102. package/src/core/runs.ts +242 -0
  103. package/src/core/trace-audit.ts +120 -0
  104. package/src/core/update-policy.ts +20 -0
  105. package/src/core/when-block.ts +29 -30
  106. package/src/core/when.ts +41 -103
  107. package/src/core/wire.ts +10 -14
  108. package/src/fleet/scan-fleet.ts +20 -4
  109. package/src/fleet/update-fleet.ts +6 -6
  110. package/src/home/gui-app.ts +24 -27
  111. package/src/home/home-launchd.ts +20 -4
  112. package/src/home/home-routes.ts +12 -1
  113. package/src/home/home-start.ts +11 -5
  114. package/src/home/hubs.ts +5 -5
  115. package/src/home/open-app.ts +2 -2
  116. package/src/home/pm.ts +8 -0
  117. package/src/home/run-pm.ts +1 -0
  118. package/src/home/serve-web.ts +2 -2
  119. package/src/home/supervise.ts +24 -1
  120. package/src/home/via-hub.ts +14 -0
  121. package/src/hooks/run-hooks.ts +48 -4
  122. package/src/init-repo.ts +6 -6
  123. package/src/ledger/db.ts +2 -13
  124. package/src/ledger/gate-checks.ts +133 -61
  125. package/src/ledger/git-key.ts +153 -31
  126. package/src/ledger/git-scan.ts +5 -2
  127. package/src/ledger/ledger.ts +136 -21
  128. package/src/ledger/store.ts +14 -0
  129. package/src/ledger/toolchain-identity.ts +248 -45
  130. package/src/plan/repo-source.ts +18 -39
  131. package/src/problems/extract.ts +3 -0
  132. package/src/release/run-release.ts +5 -0
  133. package/src/repo-config.ts +46 -54
  134. package/src/rules/rule.ts +2 -2
  135. package/src/scaffold/files/agent-coding.md +1 -1
  136. package/src/scaffold/files/agent-index.md +1 -1
  137. package/src/scaffold/files/verify-skill.md +2 -2
  138. package/src/serve/hub-app/activity-st.ts +5 -4
  139. package/src/serve/hub-app/activity-view.tsx +1 -1
  140. package/src/serve/hub-app/admin-svc.ts +36 -3
  141. package/src/serve/hub-app/admin-view.tsx +46 -12
  142. package/src/serve/hub-app/check-view.tsx +66 -46
  143. package/src/serve/hub-app/check-words.ts +1 -11
  144. package/src/serve/hub-app/checks-svc.ts +14 -35
  145. package/src/serve/hub-app/col-card.tsx +1 -44
  146. package/src/serve/hub-app/col-tables-view.tsx +84 -11
  147. package/src/serve/hub-app/col-tables.ts +27 -0
  148. package/src/serve/hub-app/cols-svc.ts +5 -4
  149. package/src/serve/hub-app/dock-st.ts +3 -2
  150. package/src/serve/hub-app/dock-view.tsx +36 -0
  151. package/src/serve/hub-app/dock.css +6 -0
  152. package/src/serve/hub-app/float-cards.tsx +30 -0
  153. package/src/serve/hub-app/floating.tsx +41 -4
  154. package/src/serve/hub-app/git-st.ts +42 -0
  155. package/src/serve/hub-app/git-view.tsx +61 -0
  156. package/src/serve/hub-app/hash-route.ts +45 -1
  157. package/src/serve/hub-app/header-bar.tsx +62 -72
  158. package/src/serve/hub-app/health-svc.ts +3 -2
  159. package/src/serve/hub-app/home-st.ts +14 -2
  160. package/src/serve/hub-app/home-strip.css +43 -12
  161. package/src/serve/hub-app/home-strip.tsx +115 -64
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +32 -8
  164. package/src/serve/hub-app/hub-types.ts +5 -2
  165. package/src/serve/hub-app/hub.css +89 -63
  166. package/src/serve/hub-app/icons.tsx +11 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +15 -4
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +18 -22
  171. package/src/serve/hub-app/plan-view.tsx +51 -0
  172. package/src/serve/hub-app/planned-st.ts +9 -4
  173. package/src/serve/hub-app/processes-view.tsx +45 -0
  174. package/src/serve/hub-app/project-list.css +4 -1
  175. package/src/serve/hub-app/project-list.tsx +6 -3
  176. package/src/serve/hub-app/projects-svc.ts +25 -10
  177. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  178. package/src/serve/hub-app/reach.ts +36 -0
  179. package/src/serve/hub-app/readme-svc.ts +3 -2
  180. package/src/serve/hub-app/repo-sections.ts +131 -0
  181. package/src/serve/hub-app/repo-st.ts +70 -14
  182. package/src/serve/hub-app/sidebar.tsx +17 -42
  183. package/src/serve/hub-app/timeline-svc.ts +3 -2
  184. package/src/serve/hub-app/tree-build.ts +151 -155
  185. package/src/serve/hub-app/tree-row.tsx +55 -12
  186. package/src/serve/hub-app/tree-svc.ts +97 -56
  187. package/src/serve/hub-app/when-view.tsx +89 -0
  188. package/src/serve/hub-app/when.css +17 -0
  189. package/src/serve/hub-runtime.ts +7 -18
  190. package/src/serve/project-gutter-route.ts +22 -1
  191. package/src/serve/run-serve.ts +1 -1
  192. package/src/serve/serve-hub.ts +4 -14
  193. package/src/suite/judge-suites.ts +6 -0
  194. package/src/trace/interpose.c +655 -0
  195. package/src/trace/read-trace.ts +485 -0
  196. package/src/trace/tracer.ts +282 -0
  197. package/src/serve/hub-app/processes-svc.ts +0 -63
  198. package/src/serve/hub-app/repo-sections.tsx +0 -61
  199. package/src/serve/repo-processes.ts +0 -89
@@ -0,0 +1,61 @@
1
+ // A repo's git page: its branch against its upstream, what is not committed,
2
+ // and its commits of the last week (HEAD's last one when the week holds
3
+ // none), with the ways out to VS Code and Fork. Every repo at once: the
4
+ // activity page (⌘L).
5
+ import { observer } from 'mobx-react-lite'
6
+ import { useEffect } from 'react'
7
+ import { isMainBranch, upstreamWords } from '../../home/activity-shape.ts'
8
+ import { Commit } from './activity-view.tsx'
9
+ import { Icon, ICON } from './icons.tsx'
10
+ import type { RepoSt } from './repo-st.ts'
11
+
12
+ const KIND_WORD = { staged: 'staged', modified: 'modified', untracked: 'new' } as const
13
+
14
+ export const GitPage = observer(({ app }: { app: RepoSt }) => {
15
+ const g = app.git
16
+ useEffect(() => {
17
+ g.start()
18
+ return () => g.stop()
19
+ }, [g])
20
+ const r = g.row
21
+ const m = app.home.machine
22
+ const now = app.clock.now
23
+ const branch = r == null ? '' : r.branch ?? (r.detached == null ? '' : `detached ${r.detached}`)
24
+ return (
25
+ <div className="colcard checkcard">
26
+ <div className="chead">
27
+ <span className="tid">git</span>
28
+ <h3 className="ctitle">{app.name}</h3>
29
+ <span className="sp" />
30
+ {m == null ? null : <button className="hicon" title="open in VS Code" aria-label={`open ${app.key} in VS Code`} onClick={() => m.openInCode(app.key)}><Icon d={ICON.vscode} /></button>}
31
+ <button className="hicon" title="open in Fork" aria-label={`open ${app.key} in Fork`} onClick={() => void app.home.activity.openFork(app.key)}><Icon d={ICON.fork} /></button>
32
+ </div>
33
+ <div className="usays">its branch, what is not committed, and its commits of the last week. Every repo at once: the activity page (⌘L)</div>
34
+ {g.error !== '' ? <div className="warnline">{g.error}</div> : null}
35
+ {r == null ? (g.error === '' ? <div className="dim twait"><span className="spin" />reading git…</div> : null) : r.error != null ? <div className="tl red">git failed: {r.error}</div> : (
36
+ <>
37
+ <div className="cfacts">
38
+ <div className="cf"><div className="ck">branch</div><div className={`cv tl ${isMainBranch(r.branch) ? 'ok' : 'warn'}`}>{branch}</div></div>
39
+ <div className="cf"><div className="ck">upstream</div><div className="cv">{upstreamWords(r)}</div></div>
40
+ </div>
41
+ <section className="csec">
42
+ <div className="cstitle">not committed ({r.files.length})</div>
43
+ {r.files.length === 0 ? <div className="dim">nothing: the tree is clean</div> : (
44
+ <table className="gitfiles"><tbody>{r.files.map((f) => <tr key={f.path}><td className={`gitkind ${f.kind}`}>{KIND_WORD[f.kind]}</td><td className="gitpath">{f.path}</td></tr>)}</tbody></table>
45
+ )}
46
+ </section>
47
+ <section className="csec">
48
+ <div className="cstitle">commits, last week ({r.commits.length})</div>
49
+ <table className="gitcommits">
50
+ <tbody>
51
+ {r.commits.map((c) => <tr key={c.sha}><Commit c={c} now={now} old={false} /></tr>)}
52
+ {r.last != null ? <tr><Commit c={r.last} now={now} old /></tr> : null}
53
+ {r.commits.length === 0 && r.last == null ? <tr><td className="dim">no commit yet</td></tr> : null}
54
+ </tbody>
55
+ </table>
56
+ </section>
57
+ </>
58
+ )}
59
+ </div>
60
+ )
61
+ })
@@ -22,9 +22,25 @@ export type HashSel =
22
22
  | { t: 'tests'; check: string | null }
23
23
  /** what administers the repo: archive, reload its hub, keep it active */
24
24
  | { t: 'admin' }
25
+ /** the repo's README, what a repo shows with nothing else named */
26
+ | { t: 'readme' }
27
+ /** the global processes the repo declares, one card each, `id` the one
28
+ * a tree row opened (scrolled to and lit), null for none */
29
+ | { t: 'processes'; id: string | null }
30
+ /** what the machine plans to run in this repo, and when */
31
+ | { t: 'plan' }
32
+ /** the repo's git: branch, upstream, uncommitted files, recent commits */
33
+ | { t: 'git' }
34
+ /** what runs when: every check on every context, with the legend */
35
+ | { t: 'when' }
36
+ /** the checks one owner ships: shipkit's, or the repo's own */
37
+ | { t: 'checks'; from: CheckOwner }
38
+
39
+ /** whose a gate check is */
40
+ export type CheckOwner = 'shipkit' | 'repo'
25
41
 
26
42
  export type ParsedHash = {
27
- kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'admin' | 'none'
43
+ kind: 'asset' | 'cat' | 'col' | 'item' | 'status' | 'tests' | 'admin' | 'readme' | 'processes' | 'plan' | 'git' | 'when' | 'checks' | 'none'
28
44
  /** the collection, for an item. null for everything else */
29
45
  col: string | null
30
46
  id: string
@@ -40,6 +56,12 @@ const CAT = 'cat-'
40
56
  const STATUS = 'page:status'
41
57
  const TESTS = 'page:tests'
42
58
  const ADMIN = 'page:admin'
59
+ const README = 'page:readme'
60
+ const PROCESSES = 'page:processes'
61
+ const PLAN = 'page:plan'
62
+ const GIT = 'page:git'
63
+ const WHEN = 'page:when'
64
+ const CHECKS = 'page:checks:'
43
65
 
44
66
  /** what this selection puts in the url. null only for no selection at all,
45
67
  * and then the hash is left alone rather than wiped */
@@ -53,6 +75,12 @@ export function hashFor(sel: HashSel | null): string | null {
53
75
  case 'status': return STATUS
54
76
  case 'tests': return sel.check == null ? TESTS : `${TESTS}:${sel.check}`
55
77
  case 'admin': return ADMIN
78
+ case 'readme': return README
79
+ case 'processes': return sel.id == null ? PROCESSES : `${PROCESSES}:${sel.id}`
80
+ case 'plan': return PLAN
81
+ case 'git': return GIT
82
+ case 'when': return WHEN
83
+ case 'checks': return `${CHECKS}${sel.from}`
56
84
  }
57
85
  }
58
86
 
@@ -62,6 +90,16 @@ export function parseHash(raw: string): ParsedHash {
62
90
  if (raw === STATUS) return { kind: 'status', col: null, id: '', tools: null }
63
91
  if (raw === TESTS) return { kind: 'tests', col: null, id: '', tools: null }
64
92
  if (raw === ADMIN) return { kind: 'admin', col: null, id: '', tools: null }
93
+ if (raw === README) return { kind: 'readme', col: null, id: '', tools: null }
94
+ if (raw === PROCESSES) return { kind: 'processes', col: null, id: '', tools: null }
95
+ if (raw === PLAN) return { kind: 'plan', col: null, id: '', tools: null }
96
+ if (raw === GIT) return { kind: 'git', col: null, id: '', tools: null }
97
+ if (raw === WHEN) return { kind: 'when', col: null, id: '', tools: null }
98
+ if (raw === `${CHECKS}shipkit` || raw === `${CHECKS}repo`) return { kind: 'checks', col: null, id: raw.slice(CHECKS.length), tools: null }
99
+ if (raw.startsWith(`${PROCESSES}:`)) {
100
+ const id = raw.slice(PROCESSES.length + 1)
101
+ return id === '' ? none : { kind: 'processes', col: null, id, tools: null }
102
+ }
65
103
  // a check id carries colons of its own (`test:rvlib-shipkit`)
66
104
  if (raw.startsWith(`${TESTS}:`)) {
67
105
  const check = raw.slice(TESTS.length + 1)
@@ -104,6 +142,12 @@ export function selOf(p: ParsedHash): HashSel | null {
104
142
  case 'status': return { t: 'status' }
105
143
  case 'tests': return { t: 'tests', check: p.id === '' ? null : p.id }
106
144
  case 'admin': return { t: 'admin' }
145
+ case 'readme': return { t: 'readme' }
146
+ case 'processes': return { t: 'processes', id: p.id === '' ? null : p.id }
147
+ case 'plan': return { t: 'plan' }
148
+ case 'git': return { t: 'git' }
149
+ case 'when': return { t: 'when' }
150
+ case 'checks': return { t: 'checks', from: p.id as CheckOwner }
107
151
  }
108
152
  }
109
153
 
@@ -1,13 +1,18 @@
1
- // A repo's pieces of the top bar, beside its icon and its name: what runs now
2
- // and the two runs that are not one check (RepoRun), then on the right its
3
- // processes, the hub's own trouble and modernize while it is behind
4
- // (RepoTools), and at the far right end the editor icons (RepoOpeners). On
5
- // the home they ride in the home strip (home-strip.tsx); a hub of its own
1
+ // A repo's pieces of the top bar. On the right: modernize while it is behind
2
+ // and the hub's own processes (RepoTools), then a spinner while something
3
+ // runs, its words on hover, and one check… button that opens the two runs
4
+ // that are not one check (RepoRun), then at the far right end the editor
5
+ // icons (RepoOpeners). The home strip puts the repo's global processes right
6
+ // after its name (home-strip.tsx). The bar carries no trouble text:
7
+ // the hub's own trouble is a card floating under it (hubCards, float-cards.tsx).
8
+ // On the home they ride in the home strip (home-strip.tsx); a hub of its own
6
9
  // draws them in its own header, with its name. The repo's sections are the
7
- // left pane's (repo-sections.tsx). What runs, drawn, is the dock at the
8
- // bottom of the page (dock-view.tsx).
10
+ // top rows of the left pane's tree (repo-sections.ts). What runs, drawn, is
11
+ // the dock at the bottom of the page (dock-view.tsx).
9
12
  import { observer } from 'mobx-react-lite'
10
13
  import { Icon, ICON } from './icons.tsx'
14
+ import { Pop, Tip } from './floating.tsx'
15
+ import { FloatCards, type Card } from './float-cards.tsx'
11
16
  import type { RepoSt } from './repo-st.ts'
12
17
 
13
18
  /** the repo's hub has not answered on the home: its reason and its fix show
@@ -17,56 +22,64 @@ export const notUp = (app: RepoSt): boolean => {
17
22
  const m = app.home.machine
18
23
  if (m == null || app.started) return false
19
24
  if (!app.cols.cached) return true
20
- return m.fresh && m.worker(app.key)?.status !== 'up'
25
+ // the daemon silent: the hub's state is unknown, not down, so its last screen stays
26
+ return m.fresh && m.down === '' && m.worker(app.key)?.status !== 'up'
21
27
  }
22
28
 
23
- /** what is running, in one line: the hub's own run first, since it says the
24
- * most, else the newest run the record has in flight. Cut to its width, the
25
- * whole line on hover */
26
- const RunState = observer(({ app }: { app: RepoSt }) => {
29
+ /** what is running, in words: the hub's own run first, since it says the
30
+ * most, else the newest run the record has in flight. Null while idle */
31
+ export const runWords = (app: RepoSt): string | null => {
27
32
  const own = app.checks.strip
33
+ if (own !== '') return own
28
34
  const run = app.live.view?.runs[0]
29
- const text = own !== '' ? own : run != null ? `${run.label} · ${run.done} of ${run.total}${run.reds > 0 ? ` · ${run.reds} red so far` : ''}` : 'idle'
30
- const going = app.checks.busy || run != null
35
+ if (run != null) return `${run.label} · ${run.done} of ${run.total}${run.reds > 0 ? ` · ${run.reds} red so far` : ''}`
36
+ return app.checks.busy ? 'running' : null
37
+ }
38
+
39
+ /** a spinner while something runs, its words in a tooltip that opens at
40
+ * once; nothing at all while idle */
41
+ const RunState = observer(({ app }: { app: RepoSt }) => {
42
+ const text = runWords(app)
43
+ if (text == null) return null
31
44
  return (
32
- <span className={`hstate${going ? ' on' : ''}`} title={text}>
33
- {/* the spinner's slot is always there, so the text never moves */}
34
- <span className={`spin${going ? '' : ' idle'}`} />
35
- <span className="htext">{text}</span>
36
- </span>
45
+ <Tip className="hbusy" instant placement="bottom" tabIndex={0} role="status" aria-label={`running: ${text}`} content={text}>
46
+ <span className="spin" />
47
+ </Tip>
37
48
  )
38
49
  })
39
50
 
40
51
  export const HEALTH_TITLE = 'run the health pass: every check but the build steps, as shipkit check runs them (creds, probes, rules, contracts)'
41
52
  export const GATE_TITLE = 'run the gate: every check shipkit ci runs, the build steps included (lint, typecheck, tests)'
42
53
 
43
- /** the run state, then the two runs, compact: below a width their labels
44
- * give way to their icons, so the bar never wraps */
45
- export const RepoRun = observer(({ app }: { app: RepoSt }) => {
54
+ /** the two runs, each with what it does; an entry closes the menu as it starts */
55
+ export const CheckMenu = observer(({ app, close }: { app: RepoSt; close: () => void }) => {
46
56
  const u = app.checks
47
57
  return (
48
- <span className="hrun">
49
- <RunState app={app} />
50
- <button className="primary hbtn" disabled={u.busy} title={HEALTH_TITLE} aria-label="health pass" onClick={() => void u.run()}>
51
- <Icon d={ICON.health} /><span className="lbl">health pass</span>
58
+ <div className="hmenu">
59
+ <button className="hmitem" disabled={u.busy} onClick={() => { close(); void u.run() }}>
60
+ <Icon d={ICON.health} /><span><b>health pass</b><small>{HEALTH_TITLE}</small></span>
52
61
  </button>
53
- <button className="hbtn" disabled={u.busy} title={GATE_TITLE} aria-label="gate" onClick={() => void u.run(undefined, { gate: true, label: 'the gate' })}>
54
- <Icon d={ICON.gate} /><span className="lbl">gate</span>
62
+ <button className="hmitem" disabled={u.busy} onClick={() => { close(); void u.run(undefined, { gate: true, label: 'the gate' }) }}>
63
+ <Icon d={ICON.gate} /><span><b>gate</b><small>{GATE_TITLE}</small></span>
55
64
  </button>
56
- </span>
65
+ </div>
57
66
  )
58
67
  })
59
68
 
60
- /** what is wrong with the hub itself, one chip per problem. Nothing when all
61
- * is well: the bar states trouble, never health */
62
- const HubStatus = observer(({ app }: { app: RepoSt }) => (
63
- <>
64
- {app.cols.hubStatus.map((p) => (
65
- <span key={p.id} className={`hstatus ${p.severity === 'red' ? 'red' : 'warn'}`} title={p.text}>{p.text}</span>
66
- ))}
67
- </>
69
+ /** the spinner while a run goes, then one small check… button with the two
70
+ * runs behind it. The bar ends on the right, so the spinner comes and goes
71
+ * to the button's left and the button never moves */
72
+ export const RepoRun = observer(({ app }: { app: RepoSt }) => (
73
+ <span className="hrun">
74
+ <RunState app={app} />
75
+ <Pop className="ghost hcheck" aria-label="run a check" label="check…" placement="bottom-end">{(close) => <CheckMenu app={app} close={close} />}</Pop>
76
+ </span>
68
77
  ))
69
78
 
79
+ /** what is wrong with the hub itself, one card per problem */
80
+ export const hubCards = (app: RepoSt): Card[] =>
81
+ app.cols.hubStatus.map((p) => ({ key: `hub:${p.id}`, tone: p.severity === 'red' ? 'red' : 'warn', body: p.text }))
82
+
70
83
  /** modernize, while the repo runs an older shipkit than the home */
71
84
  const Modernize = observer(({ app }: { app: RepoSt }) => {
72
85
  const m = app.home.machine
@@ -88,48 +101,25 @@ export const RepoOpeners = observer(({ app }: { app: RepoSt }) => {
88
101
  )
89
102
  })
90
103
 
91
- /** the right side of the bar for a repo, before the machine's trouble and
92
- * the openers, which close it */
104
+ /** the right side of the bar for a repo, before the openers, which close it */
93
105
  export const RepoTools = observer(({ app }: { app: RepoSt }) => (
94
106
  <>
95
- <ProcessChips app={app} />
96
- <HubStatus app={app} />
97
107
  <Modernize app={app} />
98
108
  </>
99
109
  ))
100
110
 
101
111
  /** the bar of a hub of its own: its name, then the same pieces the home
102
- * strip carries for a repo */
112
+ * strip carries for a repo, and its trouble floating under it */
103
113
  export const HeaderBar = observer(({ app }: { app: RepoSt }) => (
104
- <header className="hdr">
105
- <span className="hname" style={app.name === '' ? undefined : { color: app.home.projects.identityOf(app.name).colorDark }}>{app.name !== '' ? app.name : 'hub'}</span>
106
- <RepoRun app={app} />
107
- <span className="sp" />
108
- <RepoTools app={app} />
109
- <RepoOpeners app={app} />
110
- </header>
114
+ <>
115
+ <header className="hdr">
116
+ <span className="hname" style={app.name === '' ? undefined : { color: app.home.projects.identityOf(app.name).colorDark }}>{app.name !== '' ? app.name : 'hub'}</span>
117
+ <span className="sp" />
118
+ <RepoTools app={app} />
119
+ <RepoRun app={app} />
120
+ <RepoOpeners app={app} />
121
+ </header>
122
+ <FloatCards cards={hubCards(app)} />
123
+ </>
111
124
  ))
112
125
 
113
- /** one switch per process the repo declares (serve.processes): its dot, its
114
- * id, a click starts or stops it, and a link once it serves */
115
- export const ProcessChips = observer(({ app }: { app: RepoSt }) => {
116
- const s = app.processes
117
- if (s.list.length === 0) return null
118
- return (
119
- <span className="hprocs">
120
- {s.list.map((p) => {
121
- const on = p.status === 'up' || p.status === 'starting'
122
- const title = [p.cmd, p.note, p.external ? 'started outside the hub' : null, p.tail.at(-1)].filter((x) => x != null && x !== '').join('\n')
123
- return (
124
- <span key={p.id} className="hproc">
125
- <button className={`ghost ${p.status}`} title={title} disabled={p.status === 'stopping' || (on && p.external)}
126
- onClick={() => void s.act(p.id, on ? 'stop' : 'start')}>
127
- <i className={`pdot ${p.status}`} />{p.id}
128
- </button>
129
- {p.url != null && p.status === 'up' ? <a href={p.url} target="_blank" rel="noreferrer" title={p.url}>↗</a> : null}
130
- </span>
131
- )
132
- })}
133
- </span>
134
- )
135
- })
@@ -6,6 +6,7 @@
6
6
  // exists so the whole text of a run stays reachable.
7
7
  import { makeAutoObservable, runInAction } from 'mobx'
8
8
  import type { RepoSt } from './repo-st.ts'
9
+ import { ownTrouble, reach } from './reach.ts'
9
10
 
10
11
  export class HealthSvc {
11
12
  statusMd: string | null = null
@@ -25,10 +26,10 @@ export class HealthSvc {
25
26
 
26
27
  async load(): Promise<void> {
27
28
  try {
28
- const s = (await (await fetch(this.st.url('/api/status.json'))).json()) as { statusMd: string | null }
29
+ const s = (await (await reach(this.st.url('/api/status.json'))).json()) as { statusMd: string | null }
29
30
  runInAction(() => { this.statusMd = s.statusMd })
30
31
  } catch (e) {
31
- runInAction(() => { this.statusMd = `FAIL status fetch broke: ${(e as Error).message}` })
32
+ runInAction(() => { this.statusMd = ownTrouble(e, this.st.home.machine, (m) => `FAIL status fetch broke: ${m}`) || this.statusMd })
32
33
  }
33
34
  }
34
35
 
@@ -20,6 +20,7 @@ import { PlannedSt } from './planned-st.ts'
20
20
  import { ActivitySt } from './activity-st.ts'
21
21
  import { ProjectsSvc } from './projects-svc.ts'
22
22
  import { parseFilter, RepoSt } from './repo-st.ts'
23
+ import { openSection, repoSections, sectionOfRepo } from './repo-sections.ts'
23
24
 
24
25
  /** the longest one repo's read ahead holds the next one back */
25
26
  export const PREFETCH_WAIT_MS = 10_000
@@ -169,8 +170,19 @@ export class HomeSt {
169
170
  for (const r of this.repos.values()) r.dispose()
170
171
  }
171
172
 
172
- /** a list click, a repo row: that repo, in place, as a history entry */
173
+ /** a list click, a repo row: that repo, in place, as a history entry,
174
+ * on the section the repo left showed. One already inside that section
175
+ * keeps its own page there, and one that has no such section opens its
176
+ * README. Written before the repo shows, so the one history entry
177
+ * already names it */
173
178
  select(name: string): void {
179
+ const from = this.page === 'repo' ? this.shown : null
180
+ const to = this.repo(name)
181
+ const want = from == null || from === to ? null : sectionOfRepo(from)
182
+ if (want != null && sectionOfRepo(to) !== want) {
183
+ const known = to.cols.loaded || to.cols.cached
184
+ openSection(to, known && !repoSections(to).some((s) => s.id === want && s.opens) ? 'readme' : want)
185
+ }
174
186
  this.show('repo', name, 'push')
175
187
  }
176
188
 
@@ -178,7 +190,7 @@ export class HomeSt {
178
190
  * before the repo is shown, so the one history entry already names it */
179
191
  openCheck(repo: string, check: string): void {
180
192
  this.repo(repo).cols.select('checks', `check:${check}`)
181
- this.select(repo)
193
+ this.show('repo', repo, 'push')
182
194
  }
183
195
 
184
196
  /** a link to a page of the home: the same page, in place */
@@ -6,18 +6,42 @@
6
6
  .hrepo{display:inline-flex;align-items:center;gap:var(--s2);min-width:0;flex:0 1 auto}
7
7
  .hrepo img,.hrico{flex:none;width:18px;height:18px;border-radius:4px;object-fit:contain}
8
8
  .hrname{font-size:13px;font-weight:650;letter-spacing:.04em;min-width:0;overflow:hidden;text-overflow:ellipsis}
9
- .hswatch{display:inline-flex;gap:2px}
10
- .hswatch input{width:16px;height:16px;padding:0;border:1px solid var(--line);border-radius:3px;background:none;cursor:pointer}
11
- .hswatch input::-webkit-color-swatch-wrapper{padding:0}
12
- .hswatch input::-webkit-color-swatch{border:none;border-radius:2px}
13
- .halert{display:inline-flex;align-items:center;font-size:12px;color:var(--errink)}
14
- .herr{font-size:12px;color:var(--errink);max-width:40ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
15
- /* the daemon or pm down: toasts over the bottom right corner of the page
16
- column (hub.css .appcol is their box), just above the dock, never taking
17
- room, so one coming or going moves nothing */
18
- .htoasts{position:absolute;right:var(--s3);bottom:var(--s3);z-index:25;display:flex;flex-direction:column;align-items:flex-end;gap:var(--s1);max-width:min(60ch,calc(100% - 2 * var(--s3)));pointer-events:none}
19
- .htoast{max-width:100%;padding:var(--s1) var(--s2);background:var(--panel);border:1px solid var(--red);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px;color:var(--errink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:auto}
20
- .htoast b{color:var(--red);margin-right:var(--s2)}
9
+ /* trouble, as cards (float-cards.tsx), never taking room, so one coming or
10
+ going moves nothing: at the top right, in a zero height box right after
11
+ the bar, so just under it whatever its height. The words wrap, never cut */
12
+ .htoasts{position:absolute;right:var(--s3);top:var(--s2);z-index:25;display:flex;flex-direction:column;align-items:flex-end;gap:var(--s1);max-width:min(60ch,calc(100% - 2 * var(--s3)));pointer-events:none}
13
+ .hanchor{position:relative;height:0;flex:none;z-index:25}
14
+ .htoast{max-width:100%;padding:var(--s1) var(--s2);background:var(--panel);border:1px solid var(--red);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px;line-height:1.45;color:var(--errink);white-space:normal;overflow-wrap:anywhere;pointer-events:auto}
15
+ .htoast b{color:var(--red);margin-right:var(--s1)}
16
+ .htoast.warn{border-color:var(--warn);color:var(--warn)}
17
+ .htoast.warn b{color:var(--warn)}
18
+ .htoast .hproc{vertical-align:middle}
19
+ /* the bar's run: one small check… button, then a spinner while a run goes,
20
+ its words in a tooltip (header-bar.tsx) */
21
+ .hcheck{height:22px;padding:0 var(--s2);font-size:11.5px;flex:none}
22
+ .hbusy{display:inline-flex;align-items:center;justify-content:center;width:18px;min-width:18px;height:22px;flex:none;border-radius:3px;cursor:default;outline:none}
23
+ .hbusy:focus-visible{box-shadow:0 0 0 1px var(--accent)}
24
+ /* a popover opened on a click (floating.tsx Pop) */
25
+ .fpop{z-index:40;width:max-content;max-width:min(44ch,90vw);padding:var(--s1);background:var(--panel2);border:1px solid var(--line);border-radius:4px;box-shadow:0 6px 20px var(--shadow);outline:none}
26
+ .hmenu{display:flex;flex-direction:column;gap:var(--s0)}
27
+ .fpopw{max-width:min(96ch,94vw)}
28
+ .hmenu2{display:flex;align-items:stretch}
29
+ .hmacts{flex:none;min-width:24ch;max-width:36ch}
30
+ .hmabout{flex:1 1 auto;min-width:0;max-width:56ch;max-height:60vh;overflow:auto;display:flex;flex-direction:column;gap:var(--s1);margin-left:var(--s1);padding:var(--s1) var(--s2);border-left:1px solid var(--line);user-select:text}
31
+ .hmabout .md{overflow-wrap:anywhere}
32
+ .hmabout .md>:first-child{margin-top:0}
33
+ .hmlog{display:block;font-size:11px;color:var(--dim);white-space:pre-wrap;overflow-wrap:anywhere}
34
+ @media (max-width:640px){.hmenu2{flex-direction:column}.hmabout{margin:var(--s1) 0 0;border-left:0;border-top:1px solid var(--line)}}
35
+ .hmitem{display:flex;align-items:flex-start;gap:var(--s2);width:100%;padding:var(--s1) var(--s2);border:0;border-radius:3px;background:none;color:var(--ink);text-align:left;white-space:normal;cursor:pointer}
36
+ .hmitem:hover:not(:disabled),.hmitem:focus-visible{background:var(--panel)}
37
+ .hmitem:disabled{opacity:.45;cursor:default}
38
+ .hmitem .hico{flex:none;width:14px;height:14px;margin-top:2px}
39
+ .hmitem span{display:flex;flex-direction:column;gap:2px}
40
+ .hmitem b{font-size:12.5px;font-weight:600}
41
+ .hmitem small{font-size:11.5px;line-height:1.4;color:var(--dim)}
42
+ .hmhead{display:flex;align-items:center;gap:var(--s1);padding:var(--s1) var(--s2) 0;font-size:12.5px}
43
+ .hmhead span{color:var(--dim)}
44
+ .hmnote{display:block;font-size:11.5px;line-height:1.4;color:var(--dim);overflow-wrap:anywhere}
21
45
  .hdot{display:inline-block;width:8px;height:8px;border-radius:4px;margin-right:var(--s1);background:var(--line)}
22
46
  .hdot.up{background:var(--ok)}.hdot.starting,.hdot.stopping{background:var(--warn)}.hdot.crashed{background:var(--red)}
23
47
  .hres{display:inline-flex;align-items:center;padding:var(--s0) var(--s2);font-size:12px}
@@ -126,3 +150,10 @@
126
150
  .wbar > i{display:block;height:100%;width:var(--p,0%);background:var(--ok)}
127
151
  .wbar.mid > i{background:var(--warn)}
128
152
  .wbar.hi > i{background:var(--red)}
153
+
154
+ /* a process card on the processes page: the menu's two panels at page width */
155
+ .pdetail .hmacts{max-width:none}
156
+ .pdetail .hmabout{max-width:none;max-height:none}
157
+ .pdetail>.cstitle{margin:var(--s3) 0 var(--s1)}
158
+ .pdetail>.cstitle:first-child{margin-top:0}
159
+ .pdetail .hlog{margin-top:0}