rvlib-shipkit 1.2.0 → 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.
- package/CHANGELOG.md +67 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +20 -6
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
- package/src/checks/SK024-identity-colors.check.ts +5 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Ci.ts +5 -0
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +9 -1
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-runner.ts +40 -12
- package/src/core/discover-checks.ts +2 -0
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +1 -1
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +14 -5
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +43 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/global-units.ts +3 -2
- package/src/home/home-routes.ts +32 -5
- package/src/home/pm.ts +37 -4
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +35 -8
- package/src/home/run-daemon.ts +6 -6
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +6 -3
- package/src/home/supervise.ts +16 -1
- package/src/home/widget-text.ts +119 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/db.ts +1 -1
- package/src/ledger/gate-checks.ts +17 -3
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +39 -10
- package/src/ledger/store.ts +19 -2
- package/src/ledger/world-cache.ts +51 -4
- package/src/plan/cadence.ts +21 -5
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +138 -13
- package/src/plan/repo-source.ts +120 -8
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +11 -3
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-words.ts +2 -0
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/hash-route.ts +7 -1
- package/src/serve/hub-app/header-bar.tsx +79 -104
- package/src/serve/hub-app/home-st.ts +49 -12
- package/src/serve/hub-app/home-strip.css +65 -22
- package/src/serve/hub-app/home-strip.tsx +228 -97
- package/src/serve/hub-app/home.html +4 -0
- package/src/serve/hub-app/hub-app.tsx +96 -160
- package/src/serve/hub-app/hub.css +77 -37
- package/src/serve/hub-app/icons.tsx +23 -3
- package/src/serve/hub-app/index.html +4 -0
- package/src/serve/hub-app/keys.ts +42 -7
- package/src/serve/hub-app/live-svc.ts +24 -64
- package/src/serve/hub-app/machine-st.ts +64 -1
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/next-run.ts +1 -1
- package/src/serve/hub-app/palette-st.ts +11 -7
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +21 -85
- package/src/serve/hub-app/planned-view.tsx +12 -7
- package/src/serve/hub-app/planned.css +5 -2
- package/src/serve/hub-app/project-list.css +43 -21
- package/src/serve/hub-app/project-list.tsx +122 -23
- package/src/serve/hub-app/projects-svc.ts +120 -12
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +25 -6
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/tests-view.tsx +31 -4
- package/src/serve/hub-app/timeline-chart.tsx +31 -27
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +2 -2
- package/src/serve/hub-app/timeline-view.tsx +20 -3
- package/src/serve/hub-runtime.ts +17 -9
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +100 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/serve-hub.ts +13 -8
- package/src/serve/tracked-watch.ts +33 -1
- package/src/shipkit-files.ts +20 -4
- package/src/checks/SK023-stores-under-cap.check.ts +0 -49
- package/src/checks/contracts.check.ts +0 -20
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// the processes as rows: shipkit's own first (pm, the daemon, the web: the
|
|
2
|
+
// core group), then one group per declaring repo, one row per process, in the
|
|
3
|
+
// order the page and `shipkit global-processes` both show.
|
|
4
|
+
// The page's table and the command's text and json are all drawn from these
|
|
5
|
+
// rows, so the two can never disagree. A row carries no clock: ages are words
|
|
6
|
+
// computed at render time from `since`.
|
|
7
|
+
import type { GlobalProcessDecl } from './global-units.ts'
|
|
8
|
+
import type { CorePorts, PmAnswer } from './pm.ts'
|
|
9
|
+
import type { ProcessIcon, ResourceView } from './resources.ts'
|
|
10
|
+
import type { ProcStatus } from './supervise.ts'
|
|
11
|
+
|
|
12
|
+
/** a status as pm reports it, or unknown while pm does not answer */
|
|
13
|
+
export type RowStatus = ProcStatus | 'unknown'
|
|
14
|
+
|
|
15
|
+
/** the dot a row wears: green up, red crashed, grey stopped or unknown, orange on its way */
|
|
16
|
+
export type RowDot = 'up' | 'crashed' | 'down' | 'starting' | 'unknown'
|
|
17
|
+
|
|
18
|
+
export type ProcessRow = {
|
|
19
|
+
id: string
|
|
20
|
+
label: string
|
|
21
|
+
repo: string
|
|
22
|
+
file: string
|
|
23
|
+
status: RowStatus
|
|
24
|
+
dot: RowDot
|
|
25
|
+
url: string | null
|
|
26
|
+
/** the url without its scheme, what the table shows dimmed */
|
|
27
|
+
host: string | null
|
|
28
|
+
pid: number | null
|
|
29
|
+
/** epoch ms its state counts from (up: its pid's start when it has one), null when pm never said */
|
|
30
|
+
since: number | null
|
|
31
|
+
note: string | null
|
|
32
|
+
description: string | null
|
|
33
|
+
where: string | null
|
|
34
|
+
/** the last line of its log, null when it has none */
|
|
35
|
+
lastLog: string | null
|
|
36
|
+
tail: string[]
|
|
37
|
+
canStop: boolean
|
|
38
|
+
/** the page offers restart: a global process pm runs and can stop, while
|
|
39
|
+
* up; the daemon and the web always, through pm; never pm itself */
|
|
40
|
+
canRestart: boolean
|
|
41
|
+
/** one of shipkit's own: always on, no stop, no open */
|
|
42
|
+
core: boolean
|
|
43
|
+
/** the shipkit version it runs, when known (core rows only) */
|
|
44
|
+
version: string | null
|
|
45
|
+
takeOver: ResourceView['takeOver']
|
|
46
|
+
actions: { id: string; label: string }[]
|
|
47
|
+
running: string | null
|
|
48
|
+
icon: ProcessIcon | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** a repo's processes, or shipkit's own (`core`, always the first group) */
|
|
52
|
+
export type ProcessGroup = { repo: string; core?: true; rows: ProcessRow[] }
|
|
53
|
+
|
|
54
|
+
/** the core group's name */
|
|
55
|
+
export const CORE_GROUP = 'shipkit'
|
|
56
|
+
|
|
57
|
+
export const dotOf = (s: RowStatus): RowDot =>
|
|
58
|
+
s === 'up' ? 'up' : s === 'crashed' ? 'crashed' : s === 'starting' || s === 'stopping' ? 'starting' : s === 'down' ? 'down' : 'unknown'
|
|
59
|
+
|
|
60
|
+
const hostOf = (url: string | null): string | null => (url == null ? null : url.replace(/^https?:\/\//, '').replace(/\/$/, ''))
|
|
61
|
+
|
|
62
|
+
/** a process as pm serves it. A pm older than pid and since reads them as null */
|
|
63
|
+
export function rowOf(r: ResourceView): ProcessRow {
|
|
64
|
+
return {
|
|
65
|
+
id: r.id,
|
|
66
|
+
label: r.label,
|
|
67
|
+
repo: r.repo,
|
|
68
|
+
file: r.file,
|
|
69
|
+
status: r.status,
|
|
70
|
+
dot: dotOf(r.status),
|
|
71
|
+
url: r.url,
|
|
72
|
+
host: hostOf(r.url),
|
|
73
|
+
pid: r.pid ?? null,
|
|
74
|
+
since: r.since ?? null,
|
|
75
|
+
note: r.note,
|
|
76
|
+
description: r.description,
|
|
77
|
+
where: r.where,
|
|
78
|
+
lastLog: r.tail.at(-1) ?? null,
|
|
79
|
+
tail: r.tail,
|
|
80
|
+
canStop: r.canStop,
|
|
81
|
+
canRestart: r.status === 'up' && r.canStop,
|
|
82
|
+
core: false,
|
|
83
|
+
version: null,
|
|
84
|
+
takeOver: r.takeOver,
|
|
85
|
+
actions: r.actions,
|
|
86
|
+
running: r.running,
|
|
87
|
+
icon: r.icon,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** a declared process while pm does not answer: everything but its state */
|
|
92
|
+
export function declaredRow(d: GlobalProcessDecl): ProcessRow {
|
|
93
|
+
return {
|
|
94
|
+
id: d.id,
|
|
95
|
+
label: d.label,
|
|
96
|
+
repo: d.repo,
|
|
97
|
+
file: d.file,
|
|
98
|
+
status: 'unknown',
|
|
99
|
+
dot: 'unknown',
|
|
100
|
+
url: d.url,
|
|
101
|
+
host: hostOf(d.url),
|
|
102
|
+
pid: null,
|
|
103
|
+
since: null,
|
|
104
|
+
note: null,
|
|
105
|
+
description: d.description,
|
|
106
|
+
where: d.where,
|
|
107
|
+
lastLog: null,
|
|
108
|
+
tail: [],
|
|
109
|
+
canStop: false,
|
|
110
|
+
canRestart: false,
|
|
111
|
+
core: false,
|
|
112
|
+
version: null,
|
|
113
|
+
takeOver: null,
|
|
114
|
+
actions: d.actions.map((a) => ({ id: a.id, label: a.label })),
|
|
115
|
+
running: null,
|
|
116
|
+
// an image is pm's to serve, and pm does not answer
|
|
117
|
+
icon: d.icon == null || /\.[a-z0-9]+$/i.test(d.icon) ? null : { text: d.icon },
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** one group per repo, in `order` (the project list), the repos it does not
|
|
122
|
+
* list after it by name; a group keeps pm's order */
|
|
123
|
+
export function groupRows(rows: ProcessRow[], order: string[]): ProcessGroup[] {
|
|
124
|
+
const by = new Map<string, ProcessRow[]>()
|
|
125
|
+
for (const r of rows) by.set(r.repo, [...(by.get(r.repo) ?? []), r])
|
|
126
|
+
const rank = (repo: string): number => {
|
|
127
|
+
const i = order.indexOf(repo)
|
|
128
|
+
return i < 0 ? order.length : i
|
|
129
|
+
}
|
|
130
|
+
return [...by.keys()]
|
|
131
|
+
.sort((a, b) => rank(a) - rank(b) || a.localeCompare(b))
|
|
132
|
+
.map((repo) => ({ repo, rows: by.get(repo)! }))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const CORE_ABOUT = {
|
|
136
|
+
pm: 'the process manager, the LaunchAgent com.shipkit.pm: launchd starts it at login and again when it dies, so the page has no restart for it (from a terminal: launchctl kickstart -k gui/$(id -u)/com.shipkit.pm)',
|
|
137
|
+
daemon: 'loads every house repo hub in one process and serves their api, restarted through pm',
|
|
138
|
+
web: 'serves this page and passes every api request to the daemon, restarted through pm',
|
|
139
|
+
} as const
|
|
140
|
+
|
|
141
|
+
export type CoreOpts = {
|
|
142
|
+
/** the daemon's own version, asked of the daemon; null when it does not answer */
|
|
143
|
+
daemonVersion?: string | null
|
|
144
|
+
/** where each listens, when pm does not say (an older pm, or pm down) */
|
|
145
|
+
ports?: CorePorts | null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function coreRow(id: 'pm' | 'daemon' | 'web', o: { status: RowStatus; pid: number | null; since: number | null; note: string | null; tail: string[]; version: string | null; port: number | null; restart: boolean }): ProcessRow {
|
|
149
|
+
const url = o.port == null ? null : `http://127.0.0.1:${o.port}`
|
|
150
|
+
return {
|
|
151
|
+
id, label: id, repo: CORE_GROUP, file: '', status: o.status, dot: dotOf(o.status), url, host: hostOf(url), pid: o.pid, since: o.since, note: o.note,
|
|
152
|
+
description: CORE_ABOUT[id], where: null, lastLog: o.tail.at(-1) ?? null, tail: o.tail,
|
|
153
|
+
canStop: false, canRestart: o.restart, core: true, version: o.version, takeOver: null, actions: [], running: null, icon: null,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** shipkit's own processes, one row each: pm as it answers (down when it does
|
|
158
|
+
* not), then the daemon and the web as pm reports them (unknown without pm) */
|
|
159
|
+
export function coreGroup(pm: PmAnswer | null, o: CoreOpts = {}): ProcessGroup {
|
|
160
|
+
const ports = pm?.ports ?? o.ports ?? null
|
|
161
|
+
const rows: ProcessRow[] = [pm == null
|
|
162
|
+
? coreRow('pm', { status: 'down', pid: null, since: null, note: 'pm does not answer', tail: [], version: null, port: ports?.pm ?? null, restart: false })
|
|
163
|
+
: coreRow('pm', { status: 'up', pid: pm.pid, since: pm.since ?? null, note: null, tail: pm.tail ?? [], version: pm.version, port: ports?.pm ?? null, restart: false })]
|
|
164
|
+
for (const id of ['daemon', 'web'] as const) {
|
|
165
|
+
const c = pm?.children.find((x) => x.id === id)
|
|
166
|
+
const port = ports?.[id] ?? null
|
|
167
|
+
const version = id === 'daemon' ? (o.daemonVersion ?? null) : null
|
|
168
|
+
if (c == null) rows.push(coreRow(id, { status: 'unknown', pid: null, since: null, note: null, tail: [], version, port, restart: false }))
|
|
169
|
+
else {
|
|
170
|
+
const note = [c.note, c.restarts > 0 ? `${c.restarts} restart(s)` : null].filter((x) => x != null).join(', ')
|
|
171
|
+
rows.push(coreRow(id, { status: c.status, pid: c.pid, since: c.since, note: note === '' ? null : note, tail: c.tail, version, port, restart: true }))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { repo: CORE_GROUP, core: true, rows }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** what the page's table holds, from pm's answer: the core group, then the
|
|
178
|
+
* global processes by repo */
|
|
179
|
+
export const processGroups = (pm: PmAnswer | null, order: string[], o: CoreOpts = {}): ProcessGroup[] =>
|
|
180
|
+
[coreGroup(pm, o), ...groupRows((pm?.resources ?? []).map(rowOf), order)]
|
|
181
|
+
|
|
182
|
+
/** the declared icon, else the repo's own, else none */
|
|
183
|
+
export const processIcon = (r: { icon: ProcessIcon | null }, repoIcon: string | null): ProcessIcon | null =>
|
|
184
|
+
r.icon ?? (repoIcon == null ? null : { src: repoIcon })
|
|
185
|
+
|
|
186
|
+
/** a span in its largest whole unit: 45s, 3m, 2h, 4d */
|
|
187
|
+
export function spanWords(ms: number): string {
|
|
188
|
+
const s = Math.max(0, Math.floor(ms / 1000))
|
|
189
|
+
if (s < 60) return `${s}s`
|
|
190
|
+
if (s < 3600) return `${Math.floor(s / 60)}m`
|
|
191
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h`
|
|
192
|
+
return `${Math.floor(s / 86400)}d`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** the state in words: `up 2h`, `crashed 3m ago`, `stopped`, `starting` */
|
|
196
|
+
export function stateWords(r: Pick<ProcessRow, 'status' | 'since'>, now: number): string {
|
|
197
|
+
const age = r.since == null ? null : spanWords(now - r.since)
|
|
198
|
+
switch (r.status) {
|
|
199
|
+
case 'up': return age == null ? 'up' : `up ${age}`
|
|
200
|
+
case 'crashed': return age == null ? 'crashed' : `crashed ${age} ago`
|
|
201
|
+
case 'down': return 'stopped'
|
|
202
|
+
case 'unknown': return 'unknown (pm down)'
|
|
203
|
+
default: return r.status
|
|
204
|
+
}
|
|
205
|
+
}
|
package/src/home/resources.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// the machine's global processes (ComfyUI, a TTS server, an app panel), each
|
|
2
2
|
// declared once in a repo's `<name>.shipkit.ts` as `globalProcess({...})`
|
|
3
3
|
// (shipkit-files.ts) and owned by pm: a switch, a restart, its log, its
|
|
4
|
-
// action buttons, and
|
|
4
|
+
// action buttons, and its url opened in the default browser or as a shipkit
|
|
5
|
+
// app window.
|
|
5
6
|
//
|
|
6
7
|
// two kinds: with no `down`, the command IS the service, supervised like a
|
|
7
8
|
// hub (restarted on a crash, signalled on stop). With `down`, the command is
|
|
@@ -11,6 +12,7 @@ import { mkdirSync, openSync, closeSync } from 'fs'
|
|
|
11
12
|
import { dirname, join } from 'path'
|
|
12
13
|
import { fileURLToPath } from 'url'
|
|
13
14
|
import type { GlobalProcessDecl } from './global-units.ts'
|
|
15
|
+
import { iconImageType } from '../shipkit-files.ts'
|
|
14
16
|
import { Proc, realDeps, tailLines, type ProcStatus, type SuperviseDeps } from './supervise.ts'
|
|
15
17
|
import { resourceLog, resourcePid } from './home-paths.ts'
|
|
16
18
|
import { groupOf, localPort, portHolders } from '../core/port-holders.ts'
|
|
@@ -21,6 +23,11 @@ export type ResourceView = {
|
|
|
21
23
|
description: string | null
|
|
22
24
|
where: string | null
|
|
23
25
|
status: ProcStatus
|
|
26
|
+
/** the pid pm holds for it, null for a bring-up or one started elsewhere */
|
|
27
|
+
pid: number | null
|
|
28
|
+
/** epoch ms its uptime counts from while up (its pid's start when it has
|
|
29
|
+
* one, else its last status change), its crash time when crashed */
|
|
30
|
+
since: number
|
|
24
31
|
/** what the owner last asked for */
|
|
25
32
|
want: 'up' | 'down'
|
|
26
33
|
note: string | null
|
|
@@ -35,13 +42,20 @@ export type ResourceView = {
|
|
|
35
42
|
repo: string
|
|
36
43
|
file: string
|
|
37
44
|
url: string | null
|
|
38
|
-
/**
|
|
39
|
-
|
|
45
|
+
/** what its card wears, as declared: null leaves the card its repo's icon */
|
|
46
|
+
icon: ProcessIcon | null
|
|
40
47
|
actions: { id: string; label: string }[]
|
|
41
48
|
/** the action running now, or null */
|
|
42
49
|
running: string | null
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
/** an emoji, or the url pm serves a declared image file at */
|
|
53
|
+
export type ProcessIcon = { text: string } | { src: string }
|
|
54
|
+
|
|
55
|
+
/** the page's url for a process's declared icon */
|
|
56
|
+
export const processIconOf = (d: GlobalProcessDecl): ProcessIcon | null =>
|
|
57
|
+
d.icon == null ? null : iconImageType(d.icon) != null ? { src: `/api/pm/icon/${encodeURIComponent(d.id)}` } : { text: d.icon }
|
|
58
|
+
|
|
45
59
|
export type HttpProbe = (url: string) => Promise<boolean>
|
|
46
60
|
|
|
47
61
|
export const probeUrl: HttpProbe = async (url) => {
|
|
@@ -138,14 +152,22 @@ export class Resource {
|
|
|
138
152
|
private failed = false
|
|
139
153
|
/** open it once it answers: in the default browser, or as an app window */
|
|
140
154
|
private openWhenUp: 'browser' | 'app' | null = null
|
|
155
|
+
/** the status a tick last saw, and since when, for the bring-up kind */
|
|
156
|
+
private seen: ProcStatus = 'down'
|
|
157
|
+
private seenAt: number
|
|
141
158
|
readonly log: string
|
|
142
159
|
|
|
143
160
|
constructor(readonly decl: GlobalProcessDecl, dir: string, private o: ResourceOpts = {}) {
|
|
144
161
|
this.log = resourceLog(dir, decl.id)
|
|
162
|
+
this.seenAt = this.now()
|
|
145
163
|
if (decl.down == null)
|
|
146
164
|
this.proc = new Proc({ id: decl.id, cmd: decl.cmd, cwd: decl.cwd, log: this.log, pidKey: resourcePid(dir, decl.id), probe: decl.probe == null ? undefined : () => this.probe() }, o.deps ?? realDeps)
|
|
147
165
|
}
|
|
148
166
|
|
|
167
|
+
private now(): number {
|
|
168
|
+
return this.o.deps?.now() ?? Date.now()
|
|
169
|
+
}
|
|
170
|
+
|
|
149
171
|
private probe(): Promise<boolean> {
|
|
150
172
|
return this.decl.probe == null ? Promise.resolve(true) : (this.o.probe ?? probeUrl)(this.decl.probe)
|
|
151
173
|
}
|
|
@@ -226,12 +248,10 @@ export class Resource {
|
|
|
226
248
|
await this.set('up')
|
|
227
249
|
}
|
|
228
250
|
|
|
229
|
-
/** its url in the default browser, or as a shipkit app window
|
|
230
|
-
*
|
|
231
|
-
* it does */
|
|
251
|
+
/** its url in the default browser, or as a shipkit app window: at once
|
|
252
|
+
* while it answers, else it starts and opens once it does */
|
|
232
253
|
async open(how: 'browser' | 'app'): Promise<void> {
|
|
233
254
|
if (this.decl.url == null) throw new Error(`${this.decl.id} declares no url to open`)
|
|
234
|
-
if (how === 'app' && !this.decl.app) throw new Error(`${this.decl.id} does not declare app: true, so it has no app window: open it in the browser`)
|
|
235
255
|
if (this.status === 'up') {
|
|
236
256
|
this.show(how, this.decl.url)
|
|
237
257
|
return
|
|
@@ -275,6 +295,10 @@ export class Resource {
|
|
|
275
295
|
const up = await this.probe()
|
|
276
296
|
this.status = up ? 'up' : this.failed && this.want === 'up' ? 'crashed' : 'down'
|
|
277
297
|
}
|
|
298
|
+
if (this.status !== this.seen) {
|
|
299
|
+
this.seen = this.status
|
|
300
|
+
this.seenAt = this.now()
|
|
301
|
+
}
|
|
278
302
|
if (this.openWhenUp != null && this.status === 'up' && this.decl.url != null) {
|
|
279
303
|
const how = this.openWhenUp
|
|
280
304
|
this.openWhenUp = null
|
|
@@ -290,6 +314,9 @@ export class Resource {
|
|
|
290
314
|
description: d.description,
|
|
291
315
|
where: d.where,
|
|
292
316
|
status: this.status,
|
|
317
|
+
pid: this.proc?.pid ?? null,
|
|
318
|
+
// up with a pid: since that pid started, so a pm reload never resets its uptime
|
|
319
|
+
since: (this.status === 'up' ? this.proc?.startedAt() : null) ?? this.proc?.since ?? this.seenAt,
|
|
293
320
|
want: this.want,
|
|
294
321
|
note: this.note,
|
|
295
322
|
canStop: this.canStop,
|
|
@@ -298,7 +325,7 @@ export class Resource {
|
|
|
298
325
|
repo: d.repo,
|
|
299
326
|
file: d.file,
|
|
300
327
|
url: d.url,
|
|
301
|
-
|
|
328
|
+
icon: processIconOf(d),
|
|
302
329
|
actions: d.actions.map((a) => ({ id: a.id, label: a.label })),
|
|
303
330
|
running: this.running,
|
|
304
331
|
}
|
package/src/home/run-daemon.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { daemonPort, homeDir, homePort, homeUp, hubBase, WORKER_ENV } from './ho
|
|
|
20
20
|
import { HomeState } from './home-state.ts'
|
|
21
21
|
import { Hubs } from './hubs.ts'
|
|
22
22
|
import { Planner } from '../plan/planner.ts'
|
|
23
|
+
import { machineCatchUp } from '../plan/catch-up.ts'
|
|
23
24
|
import { serveDaemon } from './serve-daemon.ts'
|
|
24
25
|
import { startSweeps } from './machine-stores.ts'
|
|
25
26
|
import { Widgets } from './widgets.ts'
|
|
@@ -58,10 +59,11 @@ export async function runDaemon(root: string, io: HomeIo, o: { port?: number } =
|
|
|
58
59
|
if (reclaimed.killed.length > 0) io.print(`ok: the daemon restarted in place: killed ${reclaimed.killed.length} check process(es) its previous image left running`)
|
|
59
60
|
const c = await loadRepoConfig(root)
|
|
60
61
|
const dir = homeDir()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// the machine's one plan: every repo and check pair, ticked by the daemon.
|
|
63
|
+
// Its catch up window lives in the machine db, so a restart in place keeps it
|
|
64
|
+
const plan = new Planner({ catchUp: machineCatchUp, onError: (repo, e) => io.printErr(`WARN the plan of ${repo} broke: ${e.message}`) })
|
|
65
|
+
// a repo's colour counts only the checks its plan holds today
|
|
66
|
+
const gutter = projectGutter({ root, warn: (l) => io.printErr(l), declared: (name) => plan.declared(name) })
|
|
65
67
|
const hubs = new Hubs({
|
|
66
68
|
dir,
|
|
67
69
|
port,
|
|
@@ -84,8 +86,6 @@ export async function runDaemon(root: string, io: HomeIo, o: { port?: number } =
|
|
|
84
86
|
fresh: o.fresh,
|
|
85
87
|
// every gate is a detached job: a reload of this process never kills one
|
|
86
88
|
jobs: { cli: CLI },
|
|
87
|
-
// a lane click of the live strip opens that repo's page, on the page's own origin
|
|
88
|
-
repoHub: async (other) => (names.has(other) ? `${hubBase(other)}/` : null),
|
|
89
89
|
})
|
|
90
90
|
hubIo.print(`${rt.name} hub → http://localhost:${port}${hubBase(repo.name)}/`)
|
|
91
91
|
return { ...rt, uses: rt.deps.uses ?? [] }
|
package/src/home/run-pm.ts
CHANGED
package/src/home/serve-web.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// the web: ONE Bun.serve on the home port. It serves the one page bundle (the
|
|
2
|
-
// overview at `/`, the plan at `/planned`, a repo's hub page at `/r/<repo>/`,
|
|
2
|
+
// overview at `/`, the plan at `/planned`, the activity at `/activity`, a repo's hub page at `/r/<repo>/`,
|
|
3
3
|
// all views of one app) and proxies every api,
|
|
4
4
|
// graph and media request to the daemon, streams included, and /api/pm/* to
|
|
5
5
|
// pm. A browser only ever sees this origin.
|
|
@@ -14,7 +14,7 @@ import { hubBase } from './home-paths.ts'
|
|
|
14
14
|
/** the page routes: ONE app for the overview, the plan, and the hub page of
|
|
15
15
|
* each known repo, which moves between them in place (home-st.ts) */
|
|
16
16
|
export function pageRoutes<T>(names: string[], page: T): Record<string, T> {
|
|
17
|
-
const out: Record<string, T> = { '/': page, '/planned': page }
|
|
17
|
+
const out: Record<string, T> = { '/': page, '/planned': page, '/activity': page, '/archived': page }
|
|
18
18
|
for (const n of names) out[`${hubBase(n)}/`] = page
|
|
19
19
|
return out
|
|
20
20
|
}
|
|
@@ -51,6 +51,9 @@ export const REPO_ANSWER_MS = 30_000
|
|
|
51
51
|
export const LIST_EVERY_MS = 30_000
|
|
52
52
|
export const LIST_RETRY_MS = 1_000
|
|
53
53
|
|
|
54
|
+
/** the one home answer that can wait on git: the activity page's first read of a window */
|
|
55
|
+
export const readsGit = (path: string): boolean => path === '/api/home/activity.json'
|
|
56
|
+
|
|
54
57
|
/** a run or a check run streams for minutes, and a silent one is never cut */
|
|
55
58
|
export const streams = (path: string): boolean => /\/api\/(run|check\/run)$/.test(path)
|
|
56
59
|
|
|
@@ -94,7 +97,7 @@ export function serveWeb<T extends Bun.HTMLBundle | Response>(d: WebDeps<T>) {
|
|
|
94
97
|
const path = url.pathname
|
|
95
98
|
const pm = toPm(path)
|
|
96
99
|
const target = pm ? d.pm : d.daemon
|
|
97
|
-
const wait = streams(path) ? null : path.startsWith('/r/') ? (d.repoMs ?? REPO_ANSWER_MS) : (d.machineMs ?? MACHINE_ANSWER_MS)
|
|
100
|
+
const wait = streams(path) ? null : path.startsWith('/r/') || readsGit(path) ? (d.repoMs ?? REPO_ANSWER_MS) : (d.machineMs ?? MACHINE_ANSWER_MS)
|
|
98
101
|
// the wait covers the answer headers only: a body already flowing is never cut
|
|
99
102
|
const ctl = new AbortController()
|
|
100
103
|
const timer = wait == null ? null : setTimeout(() => ctl.abort(new Error(`no answer in ${wait / 1000}s: it runs but hangs`)), wait)
|
package/src/home/supervise.ts
CHANGED
|
@@ -143,19 +143,33 @@ export class Proc {
|
|
|
143
143
|
external = false
|
|
144
144
|
/** the pid came from the pid record an earlier image wrote, not from a start */
|
|
145
145
|
adopted = false
|
|
146
|
+
/** when the pid started, kept per pid: a pid's start never moves, so the
|
|
147
|
+
* OS is asked once for an adopted pid and never for one this image spawned */
|
|
148
|
+
private started: { pid: number; at: number | null } | null = null
|
|
146
149
|
|
|
147
150
|
constructor(readonly spec: ProcSpec, private d: SuperviseDeps = realDeps) {
|
|
148
151
|
this.since = d.now()
|
|
149
152
|
const rec = readPid(spec.pidKey)
|
|
150
153
|
// an earlier image started it and it still runs: adopt, never double
|
|
151
|
-
|
|
154
|
+
const at = rec != null && d.alive(rec.pid) ? (d.startedAt?.(rec.pid) ?? null) : undefined
|
|
155
|
+
if (rec != null && at !== undefined && !((at ?? 0) > rec.at + RECORD_SLACK_MS)) {
|
|
152
156
|
this.pid = rec.pid
|
|
157
|
+
this.started = { pid: rec.pid, at }
|
|
153
158
|
this.adopted = true
|
|
154
159
|
this.want = 'up'
|
|
155
160
|
this.set('starting')
|
|
156
161
|
} else if (rec != null) dropPid(spec.pidKey)
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
/** epoch ms its pid started, what its uptime counts from; null without
|
|
165
|
+
* a pid, or when the OS cannot say */
|
|
166
|
+
startedAt(): number | null {
|
|
167
|
+
const pid = this.pid
|
|
168
|
+
if (pid == null) return null
|
|
169
|
+
if (this.started?.pid !== pid) this.started = { pid, at: this.d.startedAt?.(pid) ?? null }
|
|
170
|
+
return this.started.at
|
|
171
|
+
}
|
|
172
|
+
|
|
159
173
|
private set(s: ProcStatus, note: string | null = null): void {
|
|
160
174
|
if (s !== this.status) this.since = this.d.now()
|
|
161
175
|
this.status = s
|
|
@@ -185,6 +199,7 @@ export class Proc {
|
|
|
185
199
|
try {
|
|
186
200
|
const pid = this.d.spawn(this.spec)
|
|
187
201
|
this.pid = pid
|
|
202
|
+
this.started = { pid, at: this.d.now() }
|
|
188
203
|
this.adopted = false
|
|
189
204
|
writePid(this.spec.pidKey, { pid, cmd: this.spec.cmd, at: this.d.now() })
|
|
190
205
|
this.set('starting')
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// a widget's html as console lines, for `shipkit overview`: the html the
|
|
2
|
+
// daemon rendered for the page, tags stripped, a table's cells aligned in
|
|
3
|
+
// columns, a block element on its own line, and a `.wbar` meter (its width
|
|
4
|
+
// in `--p:NN%`) drawn as `█████░░░░░`. It reads, never runs: no script, no
|
|
5
|
+
// style, no layout, so a widget that draws only with css reads as its text.
|
|
6
|
+
|
|
7
|
+
const BLOCK = new Set(['div', 'p', 'section', 'article', 'header', 'footer', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'hr', 'dl', 'dt', 'dd', 'figure', 'figcaption', 'blockquote'])
|
|
8
|
+
const SKIP = new Set(['script', 'style', 'template', 'svg'])
|
|
9
|
+
const BAR_CELLS = 10
|
|
10
|
+
|
|
11
|
+
const ENTITY: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', middot: '·', deg: '°', hellip: '…', times: '×' }
|
|
12
|
+
|
|
13
|
+
export function decodeEntities(s: string): string {
|
|
14
|
+
return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, e: string) => {
|
|
15
|
+
if (e[0] === '#') {
|
|
16
|
+
const n = e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : Number(e.slice(1))
|
|
17
|
+
return Number.isFinite(n) ? String.fromCodePoint(n) : m
|
|
18
|
+
}
|
|
19
|
+
return ENTITY[e.toLowerCase()] ?? m
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** a meter of BAR_CELLS cells, `p` in 0..100 */
|
|
24
|
+
export function barText(p: number): string {
|
|
25
|
+
const on = Math.round((Math.min(100, Math.max(0, p)) / 100) * BAR_CELLS)
|
|
26
|
+
return '█'.repeat(on) + '░'.repeat(BAR_CELLS - on)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Block = { kind: 'line'; text: string } | { kind: 'table'; rows: string[][] }
|
|
30
|
+
|
|
31
|
+
const attr = (attrs: string, name: string): string | null => {
|
|
32
|
+
const m = new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i').exec(attrs)
|
|
33
|
+
return m == null ? null : (m[2] ?? m[3] ?? m[4] ?? '')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const squash = (s: string): string => s.replace(/\s+/g, ' ').trim()
|
|
37
|
+
|
|
38
|
+
/** a table's rows as aligned lines: each column as wide as its widest cell */
|
|
39
|
+
export function alignRows(rows: string[][]): string[] {
|
|
40
|
+
const w: number[] = []
|
|
41
|
+
for (const r of rows) r.forEach((c, i) => { w[i] = Math.max(w[i] ?? 0, c.length) })
|
|
42
|
+
return rows.map((r) => r.map((c, i) => (i === r.length - 1 ? c : c.padEnd(w[i]!))).join(' ').trimEnd())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** a widget's leading `<header>`: the words it puts on its card's title
|
|
46
|
+
* line (state, age), apart from the body drawn under that line */
|
|
47
|
+
export function splitWidgetHead(html: string): { head: string | null; body: string } {
|
|
48
|
+
const m = /^\s*<header\b[^>]*>([\s\S]*?)<\/header>/i.exec(html)
|
|
49
|
+
return m == null ? { head: null, body: html } : { head: m[1]!, body: html.slice(m[0].length) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** the html as lines, no empty one */
|
|
53
|
+
export function widgetText(html: string): string[] {
|
|
54
|
+
const blocks: Block[] = []
|
|
55
|
+
let line = ''
|
|
56
|
+
let table: string[][] | null = null
|
|
57
|
+
let cell: string | null = null
|
|
58
|
+
let skip: string | null = null
|
|
59
|
+
const put = (s: string): void => {
|
|
60
|
+
if (cell != null) cell += s
|
|
61
|
+
else line += s
|
|
62
|
+
}
|
|
63
|
+
const flush = (): void => {
|
|
64
|
+
for (const part of line.split('\n')) if (squash(part) !== '') blocks.push({ kind: 'line', text: squash(part) })
|
|
65
|
+
line = ''
|
|
66
|
+
}
|
|
67
|
+
const endCell = (): void => {
|
|
68
|
+
if (cell != null && table != null) table.at(-1)?.push(squash(cell))
|
|
69
|
+
cell = null
|
|
70
|
+
}
|
|
71
|
+
const tokens = /<!--[\s\S]*?-->|<(\/?)([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>|([^<]+)|</g
|
|
72
|
+
for (const m of html.matchAll(tokens)) {
|
|
73
|
+
const [whole, close, rawTag, attrs = '', text] = m
|
|
74
|
+
if (text != null || whole === '<') {
|
|
75
|
+
if (skip == null) put(decodeEntities(text ?? '<'))
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (rawTag == null) continue
|
|
79
|
+
const tag = rawTag.toLowerCase()
|
|
80
|
+
if (skip != null) {
|
|
81
|
+
if (close === '/' && tag === skip) skip = null
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
if (SKIP.has(tag) && close !== '/' && !attrs.trim().endsWith('/')) {
|
|
85
|
+
skip = tag
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
if (tag === 'table') {
|
|
89
|
+
endCell()
|
|
90
|
+
if (close === '/') {
|
|
91
|
+
if (table != null) blocks.push({ kind: 'table', rows: table.filter((r) => r.some((c) => c !== '')) })
|
|
92
|
+
table = null
|
|
93
|
+
} else {
|
|
94
|
+
flush()
|
|
95
|
+
table = []
|
|
96
|
+
}
|
|
97
|
+
} else if (tag === 'tr' && table != null) {
|
|
98
|
+
endCell()
|
|
99
|
+
if (close !== '/') table.push([])
|
|
100
|
+
} else if ((tag === 'td' || tag === 'th') && table != null) {
|
|
101
|
+
endCell()
|
|
102
|
+
if (close !== '/') {
|
|
103
|
+
if (table.length === 0) table.push([])
|
|
104
|
+
cell = ''
|
|
105
|
+
}
|
|
106
|
+
} else if (tag === 'br') put(cell != null ? ' ' : '\n')
|
|
107
|
+
else if (BLOCK.has(tag)) {
|
|
108
|
+
if (cell != null) put(' ')
|
|
109
|
+
else flush()
|
|
110
|
+
} else if (close !== '/' && /\bwbar\b/.test(attr(attrs, 'class') ?? '')) {
|
|
111
|
+
const p = /--p\s*:\s*(-?\d+(?:\.\d+)?)%/.exec(attr(attrs, 'style') ?? '')
|
|
112
|
+
if (p != null) put(` ${barText(Number(p[1]))} `)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
endCell()
|
|
116
|
+
if (table != null) blocks.push({ kind: 'table', rows: table })
|
|
117
|
+
flush()
|
|
118
|
+
return blocks.flatMap((b) => (b.kind === 'line' ? [b.text] : alignRows(b.rows)))
|
|
119
|
+
}
|
package/src/hooks/run-hooks.ts
CHANGED
|
@@ -43,7 +43,10 @@ export type HooksConfig = {
|
|
|
43
43
|
* hooks/leak-guard.ts */
|
|
44
44
|
leakAllow?: LeakAllowEntry[]
|
|
45
45
|
/** staged-path prefixes that can never enter git (local state like
|
|
46
|
-
* '.comfy-ts/'). A gitignore line is advisory and editable. This gate is not.
|
|
46
|
+
* '.comfy-ts/'). A gitignore line is advisory and editable. This gate is not.
|
|
47
|
+
* A repo declaring `visibility: 'private'` still commits
|
|
48
|
+
* '.comfy-ts/drafts/' through a broader prefix. Public and undeclared
|
|
49
|
+
* repos block it. */
|
|
47
50
|
blockedPaths?: string[]
|
|
48
51
|
/** words the commit-message language gate never counts as French
|
|
49
52
|
* (proper nouns that carry accents without being a language choice) */
|
|
@@ -191,6 +194,20 @@ function waivedLines(waived: LeakWaiver[]): string[] {
|
|
|
191
194
|
)
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
const COMFY_DRAFTS = '.comfy-ts/drafts/'
|
|
198
|
+
|
|
199
|
+
/** the staged files the `blocked-paths` gate refuses. A repo DECLARED private
|
|
200
|
+
* (never by default) commits its comfy drafts through a
|
|
201
|
+
* broader block: a draft names private loras, which a private tree may hold.
|
|
202
|
+
* A prefix naming the drafts folder itself keeps them blocked. */
|
|
203
|
+
export function blockedStaged(staged: string[], config: RepoConfig): string[] {
|
|
204
|
+
const prefixes = config.hooks?.blockedPaths ?? []
|
|
205
|
+
const open = draftsOpen(config)
|
|
206
|
+
return staged.filter((f) => prefixes.some((p) => f.startsWith(p) && !(open && f.startsWith(COMFY_DRAFTS) && !p.startsWith(COMFY_DRAFTS))))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const draftsOpen = (c: RepoConfig): boolean => c.visibility === 'private'
|
|
210
|
+
|
|
194
211
|
/** the pre-commit gate: built-in leak guard, then the repo's own commands.
|
|
195
212
|
* A commit takes ~1s (build/shipkit.md): the gate times itself, and going
|
|
196
213
|
* over budget WARNs naming the fix, so drift back to slow commits is loud. */
|
|
@@ -226,7 +243,7 @@ export async function runPreCommit(root: string, config: RepoConfig): Promise<Ga
|
|
|
226
243
|
if ((h.blockedPaths ?? []).length > 0) {
|
|
227
244
|
const names = Bun.spawnSync([gitBin(), 'diff', '--cached', '--name-only'], { cwd: root })
|
|
228
245
|
const staged = names.stdout.toString().split('\n').filter(Boolean)
|
|
229
|
-
const blocked = staged
|
|
246
|
+
const blocked = blockedStaged(staged, config)
|
|
230
247
|
out.push(blocked.length === 0
|
|
231
248
|
? { id: 'blocked-paths', ok: true, lines: [`ok: blocked-paths clean (${h.blockedPaths!.join(', ')})`] }
|
|
232
249
|
: {
|
|
@@ -327,7 +344,7 @@ export function listGates(config: RepoConfig, root = process.cwd()): string[] {
|
|
|
327
344
|
const leakAllowState = waivers === 0 ? '' : `, ${(h.leakAllow ?? []).length} repo waiver(s)${globalAllowFile() == null ? '' : ' + ~/.shipkit/private/leak-allow.json'}`
|
|
328
345
|
return [
|
|
329
346
|
`pre-commit leak-guard${leakState} — staged added lines + paths vs ${kw} + ~/.shipkit/private/banned-keywords.txt${leakAllowState}`,
|
|
330
|
-
...((h.blockedPaths ?? []).length > 0 ? [`pre-commit blocked-paths — nothing under ${h.blockedPaths!.join(', ')} enters git`] : []),
|
|
347
|
+
...((h.blockedPaths ?? []).length > 0 ? [`pre-commit blocked-paths — nothing under ${h.blockedPaths!.join(', ')} enters git${draftsOpen(config) && blockedStaged([COMFY_DRAFTS], config).length === 0 && h.blockedPaths!.some((p) => COMFY_DRAFTS.startsWith(p)) ? `, except ${COMFY_DRAFTS} (declared private)` : ''}`] : []),
|
|
331
348
|
`pre-commit root-identity${off.has('root-identity') ? ' (DISABLED by config)' : ''} — the repo's own name in package.json + repo.config.ts, and scratch files`,
|
|
332
349
|
...(h.preCommit ?? []).map((c) => `pre-commit ${c.id} — ${c.cmd.join(' ')}`),
|
|
333
350
|
`commit-msg commit-message${off.has('commit-message') ? ' (DISABLED by config)' : ''} — dash punctuation + AI attribution + French${off.has('commit-language') ? ' (commit-language DISABLED by config)' : ''}`,
|
package/src/ledger/db.ts
CHANGED
|
@@ -287,7 +287,7 @@ function migrate(db: Database): void {
|
|
|
287
287
|
/** give the file back its free pages once more than half of it is free and
|
|
288
288
|
* that is worth it: a DELETE frees pages for reuse but never shrinks the
|
|
289
289
|
* file. VACUUM holds the write lock while it runs: a gate that asks then
|
|
290
|
-
* measures instead of waiting. The daemon's sweep and `check --fix
|
|
290
|
+
* measures instead of waiting. The daemon's sweep and `check --fix stores-under-cap` (house repo)
|
|
291
291
|
* call it. True when it ran */
|
|
292
292
|
export function compactDb(path: string, o: { minFreeBytes?: number } = {}): boolean {
|
|
293
293
|
const opened = openDb(path, { create: false })
|