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
@@ -1,17 +1,16 @@
1
- // The resource tree, as state: which tab, what is expanded, where the
2
- // keyboard cursor sits. The trie itself is pure (tree-build.ts) and cached
3
- // here as computeds. Everything hand tuned persists to localStorage as
4
- // newline separated paths, text encodable and hand editable, and comes back
5
- // on reopen.
1
+ // The left pane's tree, as state: what is unfolded and where the keyboard
2
+ // cursor sits. The trie itself is pure (tree-build.ts) and cached here as
3
+ // computeds, its top rows the sections repo-sections.tsx draws. The folds
4
+ // persist to localStorage as newline separated keys, text encodable and hand
5
+ // editable, and come back on reopen. A live filter unfolds what matches
6
+ // without touching them, so clearing it restores them.
6
7
  import { makeAutoObservable } from 'mobx'
7
8
  import type { RepoSt } from './repo-st.ts'
8
9
  import type { Resource } from './hub-types.ts'
9
- import { buildPathTree, buildTypeTree, dirKeys, flattenTree, whenCounts, type TreeNode, type TreeRow } from './tree-build.ts'
10
-
11
- export type TreeTab = 'path' | 'type'
10
+ import { repoSections, type RepoSection } from './repo-sections.ts'
11
+ import { buildTree, dirKeys, flattenTree, matchesQuery, passesFilter, processResources, whenCounts, type TreeNode, type TreeRow } from './tree-build.ts'
12
12
 
13
13
  const EXPANDED_KEY = 'shipkit.tree.expanded'
14
- const TAB_KEY = 'shipkit.tree.tab'
15
14
 
16
15
  // every access is guarded: a private window, blocked site data or a stale
17
16
  // origin throws on read AND on write, and a tree that cannot remember is
@@ -32,44 +31,68 @@ const writeStore = (k: string, v: string): void => {
32
31
  }
33
32
 
34
33
  export class TreeSvc {
35
- tab: TreeTab = 'path'
36
34
  expanded = new Set<string>()
37
35
  /** the row key under the keyboard cursor */
38
36
  cursor: string | null = null
39
37
 
40
38
  constructor(private st: RepoSt) {
41
- this.tab = readStore(TAB_KEY) === 'type' ? 'type' : 'path'
42
39
  for (const p of readStore(EXPANDED_KEY).split('\n')) if (p !== '') this.expanded.add(p)
43
40
  makeAutoObservable<this, 'st'>(this, { st: false })
44
41
  }
45
42
 
46
- /** the filter and the search apply BEFORE the grouping, so a filtered
47
- * tree prunes to the branches that still hold something */
43
+ private get q(): string {
44
+ return this.st.query.trim().toLowerCase()
45
+ }
46
+
47
+ /** what the tree holds, filtered BEFORE the grouping so it prunes to the
48
+ * branches that still hold something. A search names a thing to find,
49
+ * so it reads every row: the severity group applies only while it is empty */
50
+ /** every row the tree can show: the hub's resources, then the repo's
51
+ * global processes, which pm reports and the hub never lists */
52
+ private get all(): Resource[] {
53
+ return [...this.st.cols.resources, ...processResources(this.st.globalProcesses)]
54
+ }
55
+
48
56
  get visible(): Resource[] {
57
+ const all = this.all
58
+ if (this.q !== '') return all.filter((r) => matchesQuery(r, this.q))
49
59
  const f = this.st.filter
50
- const q = this.st.query.trim().toLowerCase()
51
- return this.st.cols.resources.filter((r) => {
52
- // the filter reads the COLOUR, so what `red` shows is what reads
53
- // red on the tree. An ask is proactive work, so it is in needs
54
- // and never in red (collection-types stateOf owns that rule)
55
- if (f === 'red' && r.state !== 'red') return false
56
- if (f === 'needs' && r.severity == null) return false
57
- if (q !== '' && !r.title.toLowerCase().includes(q) && !r.path.toLowerCase().includes(q)) return false
58
- return true
59
- })
60
+ return all.filter((r) => passesFilter(r, f))
61
+ }
62
+
63
+ /** rows the severity group keeps out of the tree, said under it so a
64
+ * pruned tree never reads as a broken one */
65
+ get hidden(): number {
66
+ return this.q !== '' ? 0 : this.all.length - this.visible.length
67
+ }
68
+
69
+ /** the sections the pane draws, with their counts, keys and pages */
70
+ get sections(): RepoSection[] {
71
+ return repoSections(this.st)
60
72
  }
61
73
 
62
74
  get nodes(): TreeNode[] {
63
- return this.tab === 'type' ? buildTypeTree(this.visible) : buildPathTree(this.visible)
75
+ const all = buildTree(this.sections.map((s) => s.id), this.visible)
76
+ // a search hides every section with nothing matching, unless its own
77
+ // name is what was typed
78
+ if (this.q === '') return all
79
+ return all.filter((n) => n.children.length > 0 || n.label.includes(this.q))
80
+ }
81
+
82
+ /** what is unfolded right now: a live search must SHOW its matches, so
83
+ * the folds yield while it runs, and they are back once it is cleared */
84
+ get open(): Set<string> {
85
+ return this.q === '' ? this.expanded : new Set(dirKeys(this.nodes, false))
86
+ }
87
+ isOpen(key: string): boolean {
88
+ return this.open.has(key)
64
89
  }
65
90
 
66
91
  get rows(): TreeRow[] {
67
- // a live search must SHOW its matches: folds yield while it runs
68
- if (this.st.query.trim() !== '') return flattenTree(this.nodes, new Set(dirKeys(this.nodes, false)))
69
- return flattenTree(this.nodes, this.expanded)
92
+ return flattenTree(this.nodes, this.open)
70
93
  }
71
94
 
72
- /** the one line above the tree: what runs always and what does not */
95
+ /** the line at the top of the checks page: what runs always and what does not */
73
96
  get whenLine(): string {
74
97
  const c = whenCounts(this.st.cols.resources)
75
98
  const total = c.always + c.release + c.health + c.off
@@ -83,14 +106,8 @@ export class TreeSvc {
83
106
 
84
107
  private save(): void {
85
108
  writeStore(EXPANDED_KEY, [...this.expanded].join('\n'))
86
- writeStore(TAB_KEY, this.tab)
87
109
  }
88
110
 
89
- setTab(t: TreeTab): void {
90
- this.tab = t
91
- this.cursor = null
92
- this.save()
93
- }
94
111
  toggle(key: string): void {
95
112
  this.expanded.has(key) ? this.expanded.delete(key) : this.expanded.add(key)
96
113
  this.save()
@@ -111,14 +128,21 @@ export class TreeSvc {
111
128
  return this.rowAt(this.cursor)
112
129
  }
113
130
 
131
+ /** what a row opens: a section its page, a group with a page that page,
132
+ * a leaf its own page. Only a row with nothing to open folds */
133
+ private activate(row: TreeRow): void {
134
+ const sec = row.section == null ? null : this.sections.find((s) => s.id === row.section)
135
+ if (sec != null) sec.go()
136
+ else if (row.page != null) this.st.go(row.page)
137
+ else if (row.res != null) this.st.openResource(row.res)
138
+ else if (row.dir) this.toggle(row.key)
139
+ }
140
+
114
141
  /** a row click is both a cursor move and the open: one click, one result.
115
- * A folder with a page of its own opens the page, and only its chevron
116
- * folds (fold): the one convention for a row that is both */
142
+ * Only the chevron folds a row that opens something (fold) */
117
143
  click(row: TreeRow): void {
118
144
  this.cursor = row.key
119
- if (row.page != null) this.st.go(row.page)
120
- else if (row.dir) this.toggle(row.key)
121
- else if (row.res != null) this.st.openResource(row.res)
145
+ this.activate(row)
122
146
  }
123
147
  /** the chevron: fold or unfold, never open */
124
148
  fold(row: TreeRow): void {
@@ -126,39 +150,56 @@ export class TreeSvc {
126
150
  if (row.dir) this.toggle(row.key)
127
151
  }
128
152
 
153
+ /** where the arrows start: the cursor, else the section of the page shown */
154
+ private get from(): number {
155
+ const rows = this.rows
156
+ const i = rows.findIndex((r) => r.key === this.cursor)
157
+ if (i !== -1) return i
158
+ const on = this.sections.find((s) => s.on)?.id
159
+ return on == null ? -1 : rows.findIndex((r) => r.section === on)
160
+ }
161
+
162
+ /** an arrow lands on a row and opens its page at once. A branch with
163
+ * nothing to open only takes the cursor: a fold is the chevron's job */
164
+ private land(row: TreeRow): void {
165
+ this.cursor = row.key
166
+ if (row.section != null || row.page != null || row.res != null) this.st.walk(() => this.activate(row))
167
+ }
168
+
169
+ /** a greyed section with no page: the arrows step over it */
170
+ private inert(row: TreeRow): boolean {
171
+ return row.section != null && this.sections.find((s) => s.id === row.section)?.opens === false
172
+ }
173
+
129
174
  move(d: number): void {
130
175
  const rows = this.rows
131
176
  if (rows.length === 0) return
132
- const i = rows.findIndex((r) => r.key === this.cursor)
133
- const next = i === -1 ? (d > 0 ? 0 : rows.length - 1) : Math.min(rows.length - 1, Math.max(0, i + d))
134
- this.cursor = rows[next]!.key
177
+ const i = this.from
178
+ let next = i === -1 ? (d > 0 ? 0 : rows.length - 1) : i + d
179
+ while (next >= 0 && next < rows.length && this.inert(rows[next]!)) next += d
180
+ if (next < 0 || next >= rows.length || next === i) return
181
+ this.land(rows[next]!)
135
182
  }
136
- /** right descends: a collapsed directory opens, anything else steps down */
183
+ /** right descends: a folded row unfolds, anything else steps down */
137
184
  right(): void {
138
185
  const r = this.cur
139
- if (r != null && r.dir && !this.expanded.has(r.key)) this.toggle(r.key)
186
+ if (r != null && r.dir && !this.isOpen(r.key)) this.toggle(r.key)
140
187
  else this.move(1)
141
188
  }
142
- /** left ascends: an open directory closes, anything else climbs to its parent */
189
+ /** left ascends: an unfolded row folds, anything else climbs to its parent */
143
190
  left(): void {
144
191
  const r = this.cur
145
192
  if (r == null) return this.move(-1)
146
- if (r.dir && this.expanded.has(r.key)) return this.toggle(r.key)
193
+ if (r.dir && this.isOpen(r.key)) return this.toggle(r.key)
147
194
  const rows = this.rows
148
195
  const i = rows.findIndex((x) => x.key === r.key)
149
196
  for (let n = i - 1; n >= 0; n--) {
150
- if (rows[n]!.depth < r.depth) {
151
- this.cursor = rows[n]!.key
152
- return
153
- }
197
+ if (rows[n]!.depth < r.depth) return this.land(rows[n]!)
154
198
  }
155
199
  }
156
- open(): void {
200
+ enter(): void {
157
201
  const r = this.cur
158
- if (r == null) return
159
- if (r.page != null) this.st.go(r.page)
160
- else if (r.dir) this.toggle(r.key)
161
- else if (r.res != null) this.st.openResource(r.res)
202
+ if (r != null) this.activate(r)
162
203
  }
163
204
 
164
205
  /** true = the key was ours, so the page must not also scroll */
@@ -167,7 +208,7 @@ export class TreeSvc {
167
208
  else if (k === 'ArrowUp') this.move(-1)
168
209
  else if (k === 'ArrowRight') this.right()
169
210
  else if (k === 'ArrowLeft') this.left()
170
- else if (k === 'Enter' || k === ' ') this.open()
211
+ else if (k === 'Enter' || k === ' ') this.enter()
171
212
  else return false
172
213
  return true
173
214
  }
@@ -0,0 +1,89 @@
1
+ // A repo's when page: what runs when. The legend first (every context, every
2
+ // value, what it means), then one row per check with its value on each axis,
3
+ // read from the facts every check row already carries (core/runs-matrix.ts),
4
+ // so the page reads nothing of its own and never disagrees with `shipkit when`.
5
+ import { observer } from 'mobx-react-lite'
6
+ import { useState } from 'react'
7
+ import { MATRIX_AXES, RUNS_LEGEND, inColumn, type MatrixAxis } from '../../core/runs-matrix.ts'
8
+ import type { RunsCells } from './hub-types.ts'
9
+ import type { RepoSt } from './repo-st.ts'
10
+
11
+ type Row = { id: string; title: string; cells: RunsCells; blocking: boolean | null; build: boolean }
12
+
13
+ const shortOf = (k: MatrixAxis): string => RUNS_LEGEND.find((a) => a.axis === k)!.short
14
+
15
+ const Legend = () => (
16
+ <table className="whlegend">
17
+ <tbody>
18
+ {RUNS_LEGEND.map((a) => (
19
+ <tr key={a.axis}>
20
+ <td className="whaxis">{a.axis}</td>
21
+ <td>
22
+ <div>{a.context}</div>
23
+ {a.values.map((v) => <div key={v.value} className="whval"><code>{v.value}</code> <span className="dim">{v.means}</span></div>)}
24
+ </td>
25
+ </tr>
26
+ ))}
27
+ </tbody>
28
+ </table>
29
+ )
30
+
31
+ const Cell = ({ r, k }: { r: Row; k: MatrixAxis }) => {
32
+ const c = r.cells.cells[k]
33
+ return (
34
+ <td className={`whc${c.on ? ' on' : ''}`} title={`${k}: ${c.text} · from ${c.source}${r.cells.derivedWhy != null ? ` (${r.cells.derivedWhy})` : ''}`}>
35
+ {c.text}
36
+ </td>
37
+ )
38
+ }
39
+
40
+ export const WhenPage = observer(({ app }: { app: RepoSt }) => {
41
+ const [only, setOnly] = useState<MatrixAxis | null>(null)
42
+ const all: Row[] = app.cols.resources
43
+ .filter((r) => r.collection === 'checks' && r.check != null)
44
+ .map((r) => ({ id: r.check!.checkId, title: r.text, cells: r.check!.runs, blocking: r.check!.blocking, build: r.check!.build }))
45
+ const live = all.filter((r) => r.cells.off == null)
46
+ const off = all.filter((r) => r.cells.off != null)
47
+ const shown = only == null ? live : live.filter((r) => inColumn(r.cells, only))
48
+ return (
49
+ <div className="colcard checkcard whpage">
50
+ <div className="chead">
51
+ <span className="tid">when</span>
52
+ <h3 className="ctitle">what runs when</h3>
53
+ </div>
54
+ <div className="usays">every check declares every context, with no default. Click a column to see only what that context runs. `shipkit when` prints the same</div>
55
+ <section className="csec"><div className="cstitle">the legend</div><Legend /></section>
56
+ <section className="csec">
57
+ <div className="cstitle">{only == null ? `${live.length} checks` : `${shown.length} of ${live.length} checks run on ${only}`}{only != null ? <> · <a href="#" onClick={(e) => { e.preventDefault(); setOnly(null) }}>show all</a></> : null}</div>
58
+ <table className="whtable">
59
+ <thead>
60
+ <tr>
61
+ <th>check</th>
62
+ {MATRIX_AXES.map((k) => (
63
+ <th key={k} className={only === k ? 'on' : ''} title={`${k}: ${RUNS_LEGEND.find((a) => a.axis === k)!.context}. Click: only what it runs (${live.filter((r) => inColumn(r.cells, k)).length})`} onClick={() => setOnly(only === k ? null : k)}>
64
+ {shortOf(k)} <span className="dim">{live.filter((r) => inColumn(r.cells, k)).length}</span>
65
+ </th>
66
+ ))}
67
+ <th>cache</th>
68
+ </tr>
69
+ </thead>
70
+ <tbody>
71
+ {shown.map((r) => (
72
+ <tr key={r.id}>
73
+ <td className="whid"><a href="#" title={r.title} onClick={(e) => { e.preventDefault(); app.timeline.openCheck(r.id) }}>{r.id}</a>{r.build ? <span className="dim"> build</span> : r.blocking === false ? <span className="dim"> warns</span> : null}</td>
74
+ {MATRIX_AXES.map((k) => <Cell key={k} r={r} k={k} />)}
75
+ <td className="whcache dim" title={r.cells.cache}>{r.cells.cache}</td>
76
+ </tr>
77
+ ))}
78
+ </tbody>
79
+ </table>
80
+ </section>
81
+ {off.length > 0 ? (
82
+ <section className="csec">
83
+ <div className="cstitle">off in this repo ({off.length}): repo.config.ts `off`, or the check does not apply here</div>
84
+ {off.map((r) => <div key={r.id} className="whoff"><span className="whid">{r.id}</span> <span className="dim">{r.cells.off}</span></div>)}
85
+ </section>
86
+ ) : null}
87
+ </div>
88
+ )
89
+ })
@@ -0,0 +1,17 @@
1
+ /* the when page (when-view.tsx): the legend, then the matrix. A cell that
2
+ runs is washed with the accent, a cell the repo moved carries a star, and
3
+ nothing here picks a colour of its own. */
4
+ .whpage{font-size:13px}
5
+ .whlegend{border-collapse:collapse;margin-bottom:var(--s2)}
6
+ .whlegend td{vertical-align:top;padding:var(--s1) var(--s2);border-bottom:1px solid var(--line)}
7
+ .whaxis{font-family:ui-monospace,monospace;color:var(--accent);white-space:nowrap}
8
+ .whval{padding-left:var(--s3)}
9
+ .whtable{border-collapse:collapse;width:100%}
10
+ .whtable th{position:sticky;top:0;background:var(--panel);text-align:left;font-weight:500;padding:var(--s1) var(--s2);cursor:pointer;white-space:nowrap;border-bottom:1px solid var(--line)}
11
+ .whtable th.on{color:var(--accent);border-bottom-color:var(--accent)}
12
+ .whtable td{padding:var(--s0) var(--s2);border-bottom:1px solid var(--line);white-space:nowrap}
13
+ .whc{color:var(--dim);font-family:ui-monospace,monospace}
14
+ .whc.on{color:var(--ink);background:var(--wash2)}
15
+ .whid{font-family:ui-monospace,monospace}
16
+ .whcache{max-width:320px;overflow:hidden;text-overflow:ellipsis}
17
+ .whoff{padding:var(--s0) var(--s2)}
@@ -44,7 +44,6 @@ import { RUN_JOB } from '../home/home-words.ts'
44
44
  import { OtherRepos } from './other-repos.ts'
45
45
  import { HouseRuns } from './house-runs.ts'
46
46
  import { recordStamp } from './project-gutter.ts'
47
- import { RepoProcesses } from './repo-processes.ts'
48
47
  import { shapeTimeline } from '../ledger/run-timeline.ts'
49
48
  import { readSuiteRuns, readSuites } from '../ledger/suite-log.ts'
50
49
  import { shapeTestCheck, shapeTestsRoot } from '../collections/test-time.ts'
@@ -203,7 +202,9 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
203
202
  // ONE way a run executes. In process for a hub of its own; as a detached
204
203
  // job in the daemon, which follows it: its lines and verdicts reach the
205
204
  // same taps, and a reload of the daemon leaves it running
206
- const exec = async (kind: 'ci' | 'check', opts: Record<string, unknown>, tap: CheckTap, background: boolean): Promise<number> => {
205
+ const exec = async (kind: 'ci' | 'check', sent: Record<string, unknown>, tap: CheckTap, background: boolean): Promise<number> => {
206
+ // the switches a terminal set ride beside the options, into the job's env
207
+ const { env, ...opts } = sent as { env?: Record<string, string> }
207
208
  if (jobs == null) {
208
209
  const turn = background ? await takeSlot(autoGatePool(), { repo: c.name, check: 'auto gate' }, { pollMs: 250 }) : null
209
210
  try {
@@ -215,7 +216,7 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
215
216
  }
216
217
  // a job still running here, started before this image or by a terminal's --detach
217
218
  while (liveJobs(root).length > 0) await Bun.sleep(jobs.pollMs ?? 250)
218
- const j = startJob(root, { cli: jobs.cli, kind, args: [], job: { opts, oneLib: true, turn: background } })
219
+ const j = startJob(root, { cli: jobs.cli, kind, args: [], job: { opts, oneLib: true, turn: background, ...(env == null ? {} : { env }) } })
219
220
  tap.job?.(j.id)
220
221
  return (await followJob(root, j.id, tap, { pollMs: jobs.pollMs })) ?? 1
221
222
  }
@@ -333,7 +334,6 @@ export async function resolveHubDeps(root: string, boot: RepoConfig, io: ServeIo
333
334
  root,
334
335
  name: c.name,
335
336
  uses: c.serve?.uses ?? [],
336
- processes: new RepoProcesses(root, c.serve?.processes ?? []),
337
337
  port,
338
338
  graphs,
339
339
  studio,
@@ -443,10 +443,8 @@ export type HubRuntime = {
443
443
  ping: () => Promise<void>
444
444
  /** a run is in flight: a reload now would kill it */
445
445
  busy: () => boolean
446
- /** everything this runtime started stops, and it leaves the plan. `procs`
447
- * also stops the repo's declared processes: a real stop of the hub,
448
- * never a process restart */
449
- stop: (o?: { procs?: boolean }) => void
446
+ /** everything this runtime started stops, and it leaves the plan */
447
+ stop: () => void
450
448
  }
451
449
 
452
450
  export type LoadHubOpts = {
@@ -489,7 +487,6 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
489
487
  return ok
490
488
  }
491
489
  const handler = hubHandler(deps, { base: o.base, projects: o.projects })
492
- const procs = deps.processes
493
490
  // tick failures are LOUD (the hub's own lines), never swallowed
494
491
  const ping = async (): Promise<void> => {
495
492
  if (deps.pingTick == null) return
@@ -519,13 +516,6 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
519
516
  // the answers a page waits on, computed now, one hub at a time
520
517
  // across the process, so a first visit reads them from memory
521
518
  warming = warming.then(() => handler.warm())
522
- // the repo's own processes: the auto ones start, a restart adopts the
523
- // ones still running from their pid records
524
- void procs?.startAuto()
525
- if (procs != null && procs.size > 0) {
526
- const t = setInterval(() => void procs.tick(), 2_000)
527
- live.push({ stop: () => clearInterval(t) })
528
- }
529
519
  if (o.ownTimers) {
530
520
  // one tick now (the state file guarantees a restart never re-fires), then every minute
531
521
  void ping()
@@ -541,9 +531,8 @@ export async function loadHub(root: string, io: ServeIo, o: LoadHubOpts): Promis
541
531
  start,
542
532
  ping,
543
533
  busy: () => deps.busy?.() === true,
544
- stop: (s) => {
534
+ stop: () => {
545
535
  for (const l of live.splice(0)) l.stop()
546
- if (s?.procs === true) procs?.stopAll()
547
536
  handler.stop()
548
537
  },
549
538
  }
@@ -4,7 +4,8 @@
4
4
  // and the uncommitted count ask git only when a stat says a commit, a tag or
5
5
  // the index moved, a few repos per poll, and a repo's run record is opened
6
6
  // only when it moved, so a steady poll spawns ps and lsof and nothing else. The icon route
7
- // serves only the icon files a discovered repo carries, and POST
7
+ // serves only the icon files a discovered repo carries, the app-icon route
8
+ // draws the app icon (core/app-icon.ts) for the admin page's preview, and POST
8
9
  // /api/projects/open opens a discovered repo in VS Code, POST
9
10
  // /api/projects/fork in Fork: a request names a repo, never a path. The rail ORDER is one text file for the whole machine
10
11
  // (ORDER_FILE), so every hub shows the same order and a hand edit is seen
@@ -21,11 +22,14 @@ import {
21
22
  import { readLastStatusReadonly } from '../ledger/run-log.ts'
22
23
  import { identityOfText, identityProblem, type Identity } from '../core/identity-color.ts'
23
24
  import { writeIdentity } from '../core/identity.ts'
25
+ import { appIconOf, appIconPngs, identityOfRoot } from '../core/app-icon.ts'
24
26
  import { writeArchived } from '../core/archived.ts'
25
27
 
26
28
  export type { Gate }
27
29
 
28
30
  export const POLL_MS = 5_000
31
+ /** the app icon preview: sharp on a 2x screen at the admin page's 128px */
32
+ const PREVIEW_PX = 256
29
33
  /** how long a release answer holds for a checkout whose `.git` is not a
30
34
  * folder, since no stat can say when it moved */
31
35
  export const RELEASE_TTL_MS = 60_000
@@ -377,6 +381,23 @@ export function projectGutter(d: GutterDeps) {
377
381
  headers: { 'content-type': ICON_TYPES[r.icon.split('.').pop()!] ?? 'application/octet-stream', 'cache-control': 'max-age=86400' },
378
382
  })
379
383
  }
384
+ if (path.startsWith('/api/projects/app-icon/')) {
385
+ if (repos.length === 0) await answer()
386
+ const r = findAny(decodeURIComponent(path.slice('/api/projects/app-icon/'.length)))
387
+ if (r == null) return new Response('not a house repo', { status: 404 })
388
+ const icon = appIconOf(r.path)
389
+ if (icon == null) return new Response('no png mark: the app wears the default icon', { status: 404 })
390
+ // the colours a picker is on right now, before they are saved; else the config's
391
+ const q = new URL(req.url).searchParams
392
+ const asked = { colorLight: q.get('light') ?? '', colorDark: q.get('dark') ?? '' }
393
+ const id = identityProblem(asked) == null ? asked : identityOfRoot(r.path)
394
+ try {
395
+ const [png] = await appIconPngs(await readFile(icon), id, [PREVIEW_PX])
396
+ return new Response(new Uint8Array(png!), { headers: { 'content-type': 'image/png', 'cache-control': 'max-age=60' } })
397
+ } catch (e) {
398
+ return new Response(`could not draw the icon from ${icon}: ${(e as Error).message}`, { status: 422 })
399
+ }
400
+ }
380
401
  if (path === '/api/projects/identity' && req.method === 'POST') {
381
402
  if (repos.length === 0) await answer()
382
403
  const b = (await req.json().catch(() => ({}))) as { name?: unknown; colorLight?: unknown; colorDark?: unknown }
@@ -142,7 +142,7 @@ export async function runServe(root: string, io: ServeIo, o?: { port?: number })
142
142
  stale.stop()
143
143
  clearInterval(planClock)
144
144
  plan.stop()
145
- rt.stop({ procs: true })
145
+ rt.stop()
146
146
  hub.stop()
147
147
  process.exit(0)
148
148
  })
@@ -28,7 +28,6 @@ import type { HubProblems } from './hub-problems.ts'
28
28
  import hubIndex from './hub-app/index.html'
29
29
  import { homePort, homeUp } from '../home/home-paths.ts'
30
30
  import { RUN_EXIT } from '../home/home-words.ts'
31
- import type { RepoProcesses } from './repo-processes.ts'
32
31
  import { projectGutter } from './project-gutter-route.ts'
33
32
  import { sameOrigin } from './same-origin.ts'
34
33
  import { LAYOUT } from '../core/layout.ts'
@@ -54,8 +53,6 @@ export type HubDeps = {
54
53
  name: string
55
54
  /** the machine resources the repo declares in serve.uses */
56
55
  uses?: string[]
57
- /** the long running processes the repo declares in serve.processes */
58
- processes?: RepoProcesses
59
56
  /** a terminal's `shipkit ci` or `check`, run in this process; resolves to its exit code */
60
57
  runCli?: (kind: 'ci' | 'check', opts: Record<string, unknown>, print: (l: string) => void) => Promise<number>
61
58
  /** 0 = random (tests); real launches pass repo.config/default */
@@ -235,13 +232,6 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
235
232
  })
236
233
  return new Response(stream, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } })
237
234
  }
238
- if (path === '/api/processes.json') return Response.json({ processes: d.processes?.views() ?? [] })
239
- if (path === '/api/processes/act' && req.method === 'POST') {
240
- const b = (await req.json().catch(() => ({}))) as { id?: string; act?: string }
241
- if (b.act !== 'start' && b.act !== 'stop' && b.act !== 'restart') return new Response('act must be start, stop or restart', { status: 400 })
242
- const v = d.processes?.act(b.id ?? '', b.act)
243
- return v == null ? new Response(`no process '${b.id ?? ''}' in serve.processes`, { status: 404 }) : Response.json(v)
244
- }
245
235
  // the three surfaces built on the config read the config on disk
246
236
  if (path === '/api/needs.json' || path === '/api/resources.json' || path === '/api/checks.json' || path === '/api/collections.json') await d.refresh?.()
247
237
  if (path === '/api/needs.json') {
@@ -249,9 +239,9 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
249
239
  return Response.json({ needs })
250
240
  }
251
241
  if (path === '/api/resources.json') {
252
- // ONE flat list for the whole repo: the page groups it by
253
- // path and by type, and a second endpoint would be a second
254
- // answer to drift from this one
242
+ // ONE flat list for the whole repo: the page groups it
243
+ // under its sections, and a second endpoint would be a
244
+ // second answer to drift from this one
255
245
  const r = await rows.get({ fresh: fresh(req) })
256
246
  return Response.json({
257
247
  collections: r.collections,
@@ -377,7 +367,7 @@ export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } =
377
367
  if (req.method === 'POST') {
378
368
  try {
379
369
  const body = (await req.json()) as GateEdit & { expect?: string }
380
- const b = await writeGateBlock(d.root, { when: body.when, slow: body.slow, slowRemove: body.slowRemove, words: body.words }, { expect: body.expect })
370
+ const b = await writeGateBlock(d.root, { off: body.off, slow: body.slow, slowRemove: body.slowRemove, words: body.words }, { expect: body.expect })
381
371
  await d.refresh?.()
382
372
  return Response.json({ ...b, dirty: configDirty(d.root) })
383
373
  } catch (e) {
@@ -294,6 +294,10 @@ export function aloneRunsNeeded(suspects: Timed[], parallelCpu: Record<string, P
294
294
  return { rerun, cleared }
295
295
  }
296
296
 
297
+ /** the recap bun prints after the last file, "3 tests failed:", repeats
298
+ * every (fail) line under no header of its own: it closes the last file */
299
+ export const BUN_RECAP = /^\s*\d+ tests? failed:\s*$/
300
+
297
301
  /** the suites with a failing test, read off bun's report: a file that failed
298
302
  * prints its path as a header line, then its `(fail)` lines under it. A
299
303
  * passing file prints nothing at all in a piped run, which is why this reads
@@ -304,6 +308,7 @@ export function failedSuites(output: string): string[] {
304
308
  for (const line of output.split('\n')) {
305
309
  const head = line.match(/^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/)
306
310
  if (head != null) { cur = normSuitePath(head[1]!); continue }
311
+ if (BUN_RECAP.test(line)) { cur = null; continue }
307
312
  // a file that cannot load prints no `(fail)` line, only this banner
308
313
  if (cur != null && (/^\(fail\) /.test(line) || line.startsWith('# Unhandled error between tests'))) out.add(cur)
309
314
  }
@@ -322,6 +327,7 @@ export function failLines(output: string): Map<string, string> {
322
327
  const line = raw.trimEnd()
323
328
  const head = line.match(/^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/)
324
329
  if (head != null) { cur = normSuitePath(head[1]!); err = null; continue }
330
+ if (BUN_RECAP.test(line)) { cur = null; continue }
325
331
  if (cur == null || out.has(cur)) continue
326
332
  if (err == null && /^\s*error: /.test(line)) err = line.trim()
327
333
  const fail = line.match(/^\(fail\) (.*?)(?: \[[\d.]+m?s\])?$/)