rvlib-shipkit 1.3.0 → 1.5.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 +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +7 -1
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
package/src/home/pm.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type ChildView = ProcView & { tail: string[] }
|
|
|
27
27
|
/** where pm, the daemon and the web listen */
|
|
28
28
|
export type CorePorts = { pm: number; daemon: number; web: number }
|
|
29
29
|
|
|
30
|
+
/** a child whose pid lives but has not answered for this long is hung and
|
|
31
|
+
* gets killed: a healthy daemon answers within about 20 s of its start */
|
|
32
|
+
export const CHILD_HANG_MS = 90_000
|
|
33
|
+
|
|
30
34
|
/** how many log lines each of shipkit's own processes hands the page */
|
|
31
35
|
const CORE_TAIL = 20
|
|
32
36
|
|
|
@@ -61,6 +65,8 @@ export type PmDeps = {
|
|
|
61
65
|
/** runs a stop or restart of the web once its answer is out: the web
|
|
62
66
|
* proxies that very request (default: 200 ms later) */
|
|
63
67
|
later?: (f: () => void) => void
|
|
68
|
+
/** where pm says what it did on its own, a hung child killed */
|
|
69
|
+
say?: (line: string) => void
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
const WEB_ANSWER_MS = 200
|
|
@@ -79,6 +85,8 @@ export class Pm {
|
|
|
79
85
|
probe: () => d.probe(role),
|
|
80
86
|
// a crash loop while its code is being edited heals itself
|
|
81
87
|
neverGiveUp: true,
|
|
88
|
+
hangMs: CHILD_HANG_MS,
|
|
89
|
+
say: d.say,
|
|
82
90
|
}, d.deps ?? realDeps)]))
|
|
83
91
|
}
|
|
84
92
|
|
|
@@ -154,9 +162,9 @@ export class Pm {
|
|
|
154
162
|
if (act === 'start') report(r.set('up'))
|
|
155
163
|
else if (act === 'stop') report(r.set('down'))
|
|
156
164
|
else if (act === 'restart') report(r.restart())
|
|
157
|
-
else if (act === 'open' || act === 'app') {
|
|
165
|
+
else if (act === 'open' || act === 'chrome' || act === 'app') {
|
|
158
166
|
if (r.decl.url == null) return new Response(`${r.decl.id} declares no url to open`, { status: 400 })
|
|
159
|
-
report(r.open(act === '
|
|
167
|
+
report(r.open(act === 'open' ? 'browser' : act))
|
|
160
168
|
} else if (act === 'takeover') {
|
|
161
169
|
const v = r.takeOverView
|
|
162
170
|
if (v == null) return new Response(`${r.decl.id} does not run outside the supervisor: nothing to take over`, { status: 409 })
|
|
@@ -166,7 +174,7 @@ export class Pm {
|
|
|
166
174
|
if (!r.decl.actions.some((a) => a.id === b.action)) return new Response(`${r.decl.id} declares no action '${String(b.action)}'`, { status: 404 })
|
|
167
175
|
if (r.running != null) return new Response(`${r.decl.id} is already running its action '${r.running}'`, { status: 409 })
|
|
168
176
|
report(r.action(String(b.action)))
|
|
169
|
-
} else return new Response('act must be start, stop, restart, open, app, takeover or action', { status: 400 })
|
|
177
|
+
} else return new Response('act must be start, stop, restart, open, chrome, app, takeover or action', { status: 400 })
|
|
170
178
|
return Response.json(r.view())
|
|
171
179
|
}
|
|
172
180
|
return new Response('not found', { status: 404 })
|
package/src/home/process-rows.ts
CHANGED
|
@@ -9,8 +9,9 @@ import type { CorePorts, PmAnswer } from './pm.ts'
|
|
|
9
9
|
import type { ProcessIcon, ResourceView } from './resources.ts'
|
|
10
10
|
import type { ProcStatus } from './supervise.ts'
|
|
11
11
|
|
|
12
|
-
/** a status as pm reports it,
|
|
13
|
-
|
|
12
|
+
/** a status as pm reports it, unknown while pm does not answer, silent
|
|
13
|
+
* while pm runs it but the page gets no answer from it */
|
|
14
|
+
export type RowStatus = ProcStatus | 'unknown' | 'silent'
|
|
14
15
|
|
|
15
16
|
/** the dot a row wears: green up, red crashed, grey stopped or unknown, orange on its way */
|
|
16
17
|
export type RowDot = 'up' | 'crashed' | 'down' | 'starting' | 'unknown'
|
|
@@ -46,6 +47,8 @@ export type ProcessRow = {
|
|
|
46
47
|
actions: { id: string; label: string }[]
|
|
47
48
|
running: string | null
|
|
48
49
|
icon: ProcessIcon | null
|
|
50
|
+
/** its own app is being built or opened */
|
|
51
|
+
appBusy: boolean
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
/** a repo's processes, or shipkit's own (`core`, always the first group) */
|
|
@@ -55,7 +58,7 @@ export type ProcessGroup = { repo: string; core?: true; rows: ProcessRow[] }
|
|
|
55
58
|
export const CORE_GROUP = 'shipkit'
|
|
56
59
|
|
|
57
60
|
export const dotOf = (s: RowStatus): RowDot =>
|
|
58
|
-
s === 'up' ? 'up' : s === 'crashed' ? 'crashed' : s === 'starting' || s === 'stopping' ? 'starting' : s === 'down' ? 'down' : 'unknown'
|
|
61
|
+
s === 'up' ? 'up' : s === 'crashed' || s === 'silent' ? 'crashed' : s === 'starting' || s === 'stopping' ? 'starting' : s === 'down' ? 'down' : 'unknown'
|
|
59
62
|
|
|
60
63
|
const hostOf = (url: string | null): string | null => (url == null ? null : url.replace(/^https?:\/\//, '').replace(/\/$/, ''))
|
|
61
64
|
|
|
@@ -85,6 +88,7 @@ export function rowOf(r: ResourceView): ProcessRow {
|
|
|
85
88
|
actions: r.actions,
|
|
86
89
|
running: r.running,
|
|
87
90
|
icon: r.icon,
|
|
91
|
+
appBusy: r.appBusy === true,
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|
|
@@ -113,6 +117,7 @@ export function declaredRow(d: GlobalProcessDecl): ProcessRow {
|
|
|
113
117
|
takeOver: null,
|
|
114
118
|
actions: d.actions.map((a) => ({ id: a.id, label: a.label })),
|
|
115
119
|
running: null,
|
|
120
|
+
appBusy: false,
|
|
116
121
|
// an image is pm's to serve, and pm does not answer
|
|
117
122
|
icon: d.icon == null || /\.[a-z0-9]+$/i.test(d.icon) ? null : { text: d.icon },
|
|
118
123
|
}
|
|
@@ -143,6 +148,10 @@ export type CoreOpts = {
|
|
|
143
148
|
daemonVersion?: string | null
|
|
144
149
|
/** where each listens, when pm does not say (an older pm, or pm down) */
|
|
145
150
|
ports?: CorePorts | null
|
|
151
|
+
/** one of shipkit's own not answering the page, and why: its row says
|
|
152
|
+
* no answer whatever pm last reported. pm silent, every other row is
|
|
153
|
+
* the last answer's with its state unknown, so the table keeps its lines */
|
|
154
|
+
silent?: Partial<Record<'pm' | 'daemon' | 'web', string>>
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
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 {
|
|
@@ -150,7 +159,7 @@ function coreRow(id: 'pm' | 'daemon' | 'web', o: { status: RowStatus; pid: numbe
|
|
|
150
159
|
return {
|
|
151
160
|
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
161
|
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,
|
|
162
|
+
canStop: false, canRestart: o.restart, core: true, version: o.version, takeOver: null, actions: [], running: null, icon: null, appBusy: false,
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
165
|
|
|
@@ -171,13 +180,20 @@ export function coreGroup(pm: PmAnswer | null, o: CoreOpts = {}): ProcessGroup {
|
|
|
171
180
|
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
181
|
}
|
|
173
182
|
}
|
|
174
|
-
|
|
183
|
+
const s = o.silent ?? {}
|
|
184
|
+
return { repo: CORE_GROUP, core: true, rows: rows.map((r) => {
|
|
185
|
+
const why = s[r.id as 'pm' | 'daemon' | 'web']
|
|
186
|
+
return why != null && why !== '' ? { ...r, status: 'silent', dot: 'crashed', note: why } : s.pm ? stale(r) : r
|
|
187
|
+
}) }
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
/** what the page's table holds, from pm's answer: the core group, then the
|
|
178
191
|
* global processes by repo */
|
|
179
192
|
export const processGroups = (pm: PmAnswer | null, order: string[], o: CoreOpts = {}): ProcessGroup[] =>
|
|
180
|
-
[coreGroup(pm, o), ...groupRows((pm?.resources ?? []).map(rowOf), order)]
|
|
193
|
+
[coreGroup(pm, o), ...groupRows((pm?.resources ?? []).map((r) => (o.silent?.pm ? stale(rowOf(r)) : rowOf(r))), order)]
|
|
194
|
+
|
|
195
|
+
/** a row from an answer pm can no longer confirm: its state unknown, nothing to click */
|
|
196
|
+
const stale = (r: ProcessRow): ProcessRow => ({ ...r, status: 'unknown', dot: 'unknown', canStop: false, canRestart: false, takeOver: null, running: null, appBusy: false })
|
|
181
197
|
|
|
182
198
|
/** the declared icon, else the repo's own, else none */
|
|
183
199
|
export const processIcon = (r: { icon: ProcessIcon | null }, repoIcon: string | null): ProcessIcon | null =>
|
|
@@ -200,6 +216,7 @@ export function stateWords(r: Pick<ProcessRow, 'status' | 'since'>, now: number)
|
|
|
200
216
|
case 'crashed': return age == null ? 'crashed' : `crashed ${age} ago`
|
|
201
217
|
case 'down': return 'stopped'
|
|
202
218
|
case 'unknown': return 'unknown (pm down)'
|
|
219
|
+
case 'silent': return 'no answer'
|
|
203
220
|
default: return r.status
|
|
204
221
|
}
|
|
205
222
|
}
|
package/src/home/resources.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
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 its url opened in the default browser
|
|
5
|
-
// app
|
|
4
|
+
// action buttons, and its url opened in the default browser, in Chrome, or
|
|
5
|
+
// as its own app (home/gui-app.ts builds that app in its repo on first use).
|
|
6
6
|
//
|
|
7
7
|
// two kinds: with no `down`, the command IS the service, supervised like a
|
|
8
8
|
// hub (restarted on a crash, signalled on stop). With `down`, the command is
|
|
9
9
|
// a bring-up that exits (an idempotent ladder script), `down` its reverse,
|
|
10
10
|
// and the probe is the only truth.
|
|
11
|
-
import { mkdirSync, openSync
|
|
11
|
+
import { closeSync, existsSync, mkdirSync, openSync } from 'fs'
|
|
12
|
+
import { homedir } from 'os'
|
|
12
13
|
import { dirname, join } from 'path'
|
|
13
14
|
import { fileURLToPath } from 'url'
|
|
14
15
|
import type { GlobalProcessDecl } from './global-units.ts'
|
|
@@ -47,8 +48,14 @@ export type ResourceView = {
|
|
|
47
48
|
actions: { id: string; label: string }[]
|
|
48
49
|
/** the action running now, or null */
|
|
49
50
|
running: string | null
|
|
51
|
+
/** its own app is being built or opened: the page shows a spinner on
|
|
52
|
+
* that button. Absent from an older pm */
|
|
53
|
+
appBusy?: boolean
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/** where a url opens: the default browser, Chrome, or the process's own app */
|
|
57
|
+
export type OpenHow = 'browser' | 'chrome' | 'app'
|
|
58
|
+
|
|
52
59
|
/** an emoji, or the url pm serves a declared image file at */
|
|
53
60
|
export type ProcessIcon = { text: string } | { src: string }
|
|
54
61
|
|
|
@@ -83,17 +90,19 @@ export const runOnce: RunOnce = async (cmd, cwd, log) => {
|
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
/** opens a
|
|
87
|
-
|
|
93
|
+
/** opens a process's own app, building it first when it is missing or
|
|
94
|
+
* stale; what it did lands in the log. Resolves with the exit code */
|
|
95
|
+
export type OpenApp = (d: GlobalProcessDecl, log: string) => Promise<number>
|
|
88
96
|
|
|
89
97
|
/** its own process: what building the app needs never loads in pm */
|
|
90
98
|
const OPEN_APP = fileURLToPath(new URL('./open-app.ts', import.meta.url))
|
|
91
99
|
|
|
92
|
-
const openApp: OpenApp = (
|
|
100
|
+
const openApp: OpenApp = async (d, log) => {
|
|
93
101
|
mkdirSync(dirname(log), { recursive: true })
|
|
94
102
|
const fd = openSync(log, 'a')
|
|
95
103
|
try {
|
|
96
|
-
|
|
104
|
+
const spec = { id: d.id, label: d.label, url: d.url, icon: d.icon, root: d.root }
|
|
105
|
+
return await Bun.spawn([process.execPath, OPEN_APP, '--process', JSON.stringify(spec)], { stdio: ['ignore', fd, fd] }).exited
|
|
97
106
|
} finally {
|
|
98
107
|
closeSync(fd)
|
|
99
108
|
}
|
|
@@ -113,6 +122,26 @@ const browse: Browse = (url, log) => {
|
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
|
|
125
|
+
const CHROME_APPS = ['/Applications/Google Chrome.app', join(homedir(), 'Applications', 'Google Chrome.app')]
|
|
126
|
+
|
|
127
|
+
/** the command that opens a url in Chrome, else in the default browser */
|
|
128
|
+
export function chromeCmd(url: string, o: { platform?: string; has?: (path: string) => boolean } = {}): string[] {
|
|
129
|
+
const platform = o.platform ?? process.platform
|
|
130
|
+
const has = o.has ?? existsSync
|
|
131
|
+
if (platform === 'darwin') return CHROME_APPS.some(has) ? ['open', '-a', 'Google Chrome', url] : ['open', url]
|
|
132
|
+
return platform === 'win32' ? ['cmd', '/c', 'start', 'chrome', url] : ['xdg-open', url]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const chrome: Browse = (url, log) => {
|
|
136
|
+
mkdirSync(dirname(log), { recursive: true })
|
|
137
|
+
const fd = openSync(log, 'a')
|
|
138
|
+
try {
|
|
139
|
+
Bun.spawn(chromeCmd(url), { stdio: ['ignore', fd, fd] })
|
|
140
|
+
} finally {
|
|
141
|
+
closeSync(fd)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
116
145
|
/** the function action runner: imports the unit file, calls the action */
|
|
117
146
|
const ACTION_RUNNER = fileURLToPath(new URL('./run-unit-action.ts', import.meta.url))
|
|
118
147
|
|
|
@@ -120,10 +149,12 @@ export type ResourceOpts = {
|
|
|
120
149
|
probe?: HttpProbe
|
|
121
150
|
run?: RunOnce
|
|
122
151
|
deps?: SuperviseDeps
|
|
123
|
-
/** the app
|
|
152
|
+
/** the process's own app */
|
|
124
153
|
open?: OpenApp
|
|
125
154
|
/** the default browser */
|
|
126
155
|
browse?: Browse
|
|
156
|
+
/** Chrome */
|
|
157
|
+
chrome?: Browse
|
|
127
158
|
/** the pids listening on a local port, and the process group of a pid */
|
|
128
159
|
holders?: (port: number) => number[]
|
|
129
160
|
groupOf?: (pid: number) => number | null
|
|
@@ -150,8 +181,10 @@ export class Resource {
|
|
|
150
181
|
private busy = false
|
|
151
182
|
/** the last bring-up exited non zero */
|
|
152
183
|
private failed = false
|
|
153
|
-
/** open it once it answers
|
|
154
|
-
private openWhenUp:
|
|
184
|
+
/** open it once it answers */
|
|
185
|
+
private openWhenUp: OpenHow | null = null
|
|
186
|
+
/** its own app is being built or opened */
|
|
187
|
+
appBusy = false
|
|
155
188
|
/** the status a tick last saw, and since when, for the bring-up kind */
|
|
156
189
|
private seen: ProcStatus = 'down'
|
|
157
190
|
private seenAt: number
|
|
@@ -248,9 +281,9 @@ export class Resource {
|
|
|
248
281
|
await this.set('up')
|
|
249
282
|
}
|
|
250
283
|
|
|
251
|
-
/** its url in the default browser, or as
|
|
284
|
+
/** its url in the default browser, in Chrome, or as its own app: at once
|
|
252
285
|
* while it answers, else it starts and opens once it does */
|
|
253
|
-
async open(how:
|
|
286
|
+
async open(how: OpenHow): Promise<void> {
|
|
254
287
|
if (this.decl.url == null) throw new Error(`${this.decl.id} declares no url to open`)
|
|
255
288
|
if (this.status === 'up') {
|
|
256
289
|
this.show(how, this.decl.url)
|
|
@@ -260,9 +293,27 @@ export class Resource {
|
|
|
260
293
|
await this.set('up')
|
|
261
294
|
}
|
|
262
295
|
|
|
263
|
-
private show(how:
|
|
264
|
-
if (how === '
|
|
265
|
-
else (this.o.
|
|
296
|
+
private show(how: OpenHow, url: string): void {
|
|
297
|
+
if (how === 'browser') (this.o.browse ?? browse)(url, this.log)
|
|
298
|
+
else if (how === 'chrome') (this.o.chrome ?? chrome)(url, this.log)
|
|
299
|
+
else if (!this.appBusy) void this.showApp()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** off the request path: the build takes seconds the first time, and
|
|
303
|
+
* the page follows `appBusy` on its poll */
|
|
304
|
+
private async showApp(): Promise<void> {
|
|
305
|
+
this.appBusy = true
|
|
306
|
+
let failed: string | null = null
|
|
307
|
+
try {
|
|
308
|
+
const code = await (this.o.open ?? openApp)(this.decl, this.log)
|
|
309
|
+
if (code !== 0) failed = `opening ${this.decl.label} as an app exited ${code}: see the log`
|
|
310
|
+
} catch (e) {
|
|
311
|
+
failed = `opening ${this.decl.label} as an app: ${(e as Error).message}`
|
|
312
|
+
} finally {
|
|
313
|
+
this.appBusy = false
|
|
314
|
+
}
|
|
315
|
+
// kept beside what the supervisor says, like an action's outcome
|
|
316
|
+
if (failed != null) this.note = this.actionNote = failed
|
|
266
317
|
}
|
|
267
318
|
|
|
268
319
|
/** one declared button. A function action runs in its own bun process
|
|
@@ -328,6 +379,7 @@ export class Resource {
|
|
|
328
379
|
icon: processIconOf(d),
|
|
329
380
|
actions: d.actions.map((a) => ({ id: a.id, label: a.label })),
|
|
330
381
|
running: this.running,
|
|
382
|
+
appBusy: this.appBusy,
|
|
331
383
|
}
|
|
332
384
|
}
|
|
333
385
|
}
|
package/src/home/run-daemon.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { Planner } from '../plan/planner.ts'
|
|
|
23
23
|
import { machineCatchUp } from '../plan/catch-up.ts'
|
|
24
24
|
import { serveDaemon } from './serve-daemon.ts'
|
|
25
25
|
import { startSweeps } from './machine-stores.ts'
|
|
26
|
+
import { landMotherhome } from '../core/motherhome.ts'
|
|
26
27
|
import { Widgets } from './widgets.ts'
|
|
27
28
|
|
|
28
29
|
const CLI = fileURLToPath(new URL('../cli.ts', import.meta.url))
|
|
@@ -109,7 +110,11 @@ export async function runDaemon(root: string, io: HomeIo, o: { port?: number } =
|
|
|
109
110
|
// nothing grows unbound: every store of every repo it loads, and the
|
|
110
111
|
// machine's, swept on boot and hourly (core/retention.ts)
|
|
111
112
|
const only = onlyRepos()
|
|
112
|
-
const sweeps = startSweeps({
|
|
113
|
+
const sweeps = startSweeps({
|
|
114
|
+
roots: () => house.repos.filter((r) => only == null || only.has(r.name)).map((r) => r.path), dir, printErr: io.printErr,
|
|
115
|
+
// the rows every gate filed land here, one commit a day at most
|
|
116
|
+
also: (now) => { for (const n of landMotherhome({ now })) io.printErr(n) },
|
|
117
|
+
})
|
|
113
118
|
const code = reloadOnOwnCode({ role: 'daemon', entry: fileURLToPath(import.meta.url), busy: () => hubs.busy(), before: () => { sweeps.stop(); home.stop() }, io })
|
|
114
119
|
io.print(` ${house.repos.length} repos, state in ${dir}, reloads on ${code.files} lib files`)
|
|
115
120
|
for (const sig of ['SIGINT', 'SIGTERM'] as const)
|
package/src/home/run-pm.ts
CHANGED
|
@@ -11,16 +11,37 @@ import { reloadOnOwnCode } from './lib-watch.ts'
|
|
|
11
11
|
import { Pm, PM_TICK_MS, type Role } from './pm.ts'
|
|
12
12
|
import { Resources } from './resources.ts'
|
|
13
13
|
import type { HomeIo } from './run-daemon.ts'
|
|
14
|
-
import type
|
|
14
|
+
import { stampOf, type PmConfig } from './pm-inputs.ts'
|
|
15
15
|
|
|
16
16
|
const CLI = fileURLToPath(new URL('../cli.ts', import.meta.url))
|
|
17
17
|
const PM_CONFIG = fileURLToPath(new URL('./pm-config.ts', import.meta.url))
|
|
18
18
|
|
|
19
19
|
const RUN_PM = fileURLToPath(import.meta.url)
|
|
20
20
|
|
|
21
|
-
/** how often pm
|
|
21
|
+
/** how often pm stats what its last read of the house side saw */
|
|
22
22
|
export const PM_RESCAN_MS = 15_000
|
|
23
23
|
|
|
24
|
+
/** pm's reread of the house side: the child runs again only once a stat of
|
|
25
|
+
* a path the last answer read moved. A few ms of stats per poll, against a
|
|
26
|
+
* child that imports every unit file */
|
|
27
|
+
export class PmConfigWatch {
|
|
28
|
+
private last: PmConfig | null = null
|
|
29
|
+
|
|
30
|
+
constructor(private readonly read: () => Promise<PmConfig>) {}
|
|
31
|
+
|
|
32
|
+
seed(c: PmConfig): void {
|
|
33
|
+
this.last = c
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** the new answer, or null when nothing it read moved */
|
|
37
|
+
async poll(): Promise<PmConfig | null> {
|
|
38
|
+
if (this.last != null && stampOf(this.last.inputs) === this.last.stamp) return null
|
|
39
|
+
const c = await this.read()
|
|
40
|
+
this.last = c
|
|
41
|
+
return c
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
/** replace this process's image with pm run from this file: same pid, same
|
|
25
46
|
* cwd, same env, none of cli.ts loaded. False when the runtime has no
|
|
26
47
|
* execve, and the caller runs pm in process */
|
|
@@ -79,6 +100,7 @@ export async function runPm(root: string, io: HomeIo, o: { port?: number } = {})
|
|
|
79
100
|
cmd: (role) => [process.execPath, CLI, role],
|
|
80
101
|
probe: roleAnswers,
|
|
81
102
|
resources: new Resources(c.processes, dir),
|
|
103
|
+
say: io.printErr,
|
|
82
104
|
})
|
|
83
105
|
let server: Bun.Server<unknown>
|
|
84
106
|
try {
|
|
@@ -98,13 +120,16 @@ export async function runPm(root: string, io: HomeIo, o: { port?: number } = {})
|
|
|
98
120
|
pm.resources.errors = c.errors
|
|
99
121
|
for (const e of c.errors) io.printErr(`WARN ${e}`)
|
|
100
122
|
// a unit file added, edited or removed in any house repo: read again,
|
|
101
|
-
// in its child process, and the unchanged processes keep their state
|
|
123
|
+
// in its child process, and the unchanged processes keep their state.
|
|
124
|
+
// Nothing moved, no child
|
|
125
|
+
const watch = new PmConfigWatch(() => readPmConfig(root))
|
|
126
|
+
watch.seed(c)
|
|
102
127
|
let reading = false
|
|
103
128
|
const reread = setInterval(() => {
|
|
104
129
|
if (reading) return
|
|
105
130
|
reading = true
|
|
106
|
-
void
|
|
107
|
-
.then((n) => pm.resources.sync(n.processes, n.errors))
|
|
131
|
+
void watch.poll()
|
|
132
|
+
.then((n) => { if (n != null) pm.resources.sync(n.processes, n.errors) })
|
|
108
133
|
.catch((e) => io.printErr(`WARN pm could not read the global processes again: ${(e as Error).message}`))
|
|
109
134
|
.finally(() => { reading = false })
|
|
110
135
|
}, PM_RESCAN_MS)
|
package/src/home/serve-web.ts
CHANGED
|
@@ -51,8 +51,8 @@ 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
|
|
55
|
-
export const readsGit = (path: string): boolean => path === '/api/home/activity.json'
|
|
54
|
+
/** the home answers that can wait on git: the activity page's first read of a window, and a repo's git page */
|
|
55
|
+
export const readsGit = (path: string): boolean => path === '/api/home/activity.json' || path === '/api/home/git.json'
|
|
56
56
|
|
|
57
57
|
/** a run or a check run streams for minutes, and a silent one is never cut */
|
|
58
58
|
export const streams = (path: string): boolean => /\/api\/(run|check\/run)$/.test(path)
|
package/src/home/supervise.ts
CHANGED
|
@@ -29,6 +29,12 @@ export type ProcSpec = {
|
|
|
29
29
|
/** never left down after CRASH_LIMIT crashes: it keeps retrying at the
|
|
30
30
|
* longest backoff, so a code fix brings it back with no hand */
|
|
31
31
|
neverGiveUp?: boolean
|
|
32
|
+
/** a pid that lives but has not answered its probe for this long is hung
|
|
33
|
+
* (a main thread stuck in one synchronous step still holds its port):
|
|
34
|
+
* killed, then started again after the crash backoff. None = never */
|
|
35
|
+
hangMs?: number
|
|
36
|
+
/** a line for every act the supervisor takes on its own */
|
|
37
|
+
say?: (line: string) => void
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
export type ProcView = {
|
|
@@ -137,6 +143,8 @@ export class Proc {
|
|
|
137
143
|
private retryAt: number | null = null
|
|
138
144
|
private killAt: number | null = null
|
|
139
145
|
private probing = false
|
|
146
|
+
/** why the kill in flight was a hang kill, for the crash note */
|
|
147
|
+
private hung: string | null = null
|
|
140
148
|
/** a restart: start again once the stop landed */
|
|
141
149
|
private again = false
|
|
142
150
|
/** it answers its probe with no pid of ours behind it */
|
|
@@ -275,6 +283,8 @@ export class Proc {
|
|
|
275
283
|
const now = this.d.now()
|
|
276
284
|
if (this.pid != null && !this.d.alive(this.pid)) {
|
|
277
285
|
this.gone()
|
|
286
|
+
const hung = this.hung
|
|
287
|
+
this.hung = null
|
|
278
288
|
if (this.again) {
|
|
279
289
|
this.again = false
|
|
280
290
|
this.want = 'up'
|
|
@@ -287,7 +297,7 @@ export class Proc {
|
|
|
287
297
|
this.set('crashed', `exited ${this.crashes.length} times in ${Math.round(CRASH_WINDOW_MS / 60_000)} min, left down: see the log`)
|
|
288
298
|
} else {
|
|
289
299
|
this.retryAt = now + BACKOFF_MS[Math.min(this.crashes.length - 1, BACKOFF_MS.length - 1)]!
|
|
290
|
-
this.set('crashed', `exited, restart in ${Math.round((this.retryAt - now) / 1000)}s`)
|
|
300
|
+
this.set('crashed', `${hung != null ? `${hung}, killed` : 'exited'}, restart in ${Math.round((this.retryAt - now) / 1000)}s`)
|
|
291
301
|
}
|
|
292
302
|
}
|
|
293
303
|
}
|
|
@@ -318,6 +328,19 @@ export class Proc {
|
|
|
318
328
|
} finally {
|
|
319
329
|
this.probing = false
|
|
320
330
|
}
|
|
331
|
+
this.killIfHung(now)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** a pid that lives and still has not answered, hangMs after it last
|
|
335
|
+
* changed status, is stopped: the crash path starts it again */
|
|
336
|
+
private killIfHung(now: number): void {
|
|
337
|
+
const hang = this.spec.hangMs
|
|
338
|
+
if (hang == null || this.status !== 'starting' || this.pid == null || this.want !== 'up' || now - this.since < hang) return
|
|
339
|
+
this.hung = `did not answer its probe for ${Math.round((now - this.since) / 1000)}s`
|
|
340
|
+
this.spec.say?.(`WARN ${this.spec.id} (pid ${this.pid}) ${this.hung}: killed, it starts again`)
|
|
341
|
+
this.d.kill(this.pid, 'SIGTERM')
|
|
342
|
+
this.killAt = now + KILL_AFTER_MS
|
|
343
|
+
this.set('stopping', `${this.hung}, killing it`)
|
|
321
344
|
}
|
|
322
345
|
|
|
323
346
|
view(): ProcView {
|
package/src/home/widgets.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// rendered by the daemon on each one's own clock and kept as html the page
|
|
3
3
|
// reads at /api/home/widgets.json. The files are read again every
|
|
4
4
|
// WIDGET_RESCAN_MS, a walk of every repo that costs a few tens of ms, and an
|
|
5
|
-
// unchanged file is never imported twice.
|
|
5
|
+
// unchanged file is never imported twice. All of it only while a reader
|
|
6
|
+
// asked within WIDGET_WATCH_MS: nobody looking, nothing scans or renders.
|
|
6
7
|
//
|
|
7
8
|
// the one constraint a reader breaks first: a render runs in the daemon, so
|
|
8
9
|
// it is bounded (WIDGET_RENDER_MS) and never awaited by the daemon's tick.
|
|
@@ -11,6 +12,8 @@ import { scanGlobalUnits, type LoadedWidget, type RepoRef } from './global-units
|
|
|
11
12
|
export const WIDGET_EVERY_MS = 5_000
|
|
12
13
|
export const WIDGET_RENDER_MS = 5_000
|
|
13
14
|
export const WIDGET_RESCAN_MS = 15_000
|
|
15
|
+
/** a read within this long keeps the clocks running */
|
|
16
|
+
export const WIDGET_WATCH_MS = 30_000
|
|
14
17
|
|
|
15
18
|
export type WidgetView = {
|
|
16
19
|
repo: string
|
|
@@ -38,6 +41,8 @@ export class Widgets {
|
|
|
38
41
|
private slots = new Map<string, Slot>()
|
|
39
42
|
private scannedAt = -Infinity
|
|
40
43
|
private scanning = false
|
|
44
|
+
private readAt: number | null = null
|
|
45
|
+
private rendering = new Set<Promise<void>>()
|
|
41
46
|
/** the unit files that would not load */
|
|
42
47
|
errors: string[] = []
|
|
43
48
|
|
|
@@ -47,9 +52,14 @@ export class Widgets {
|
|
|
47
52
|
return (this.d.now ?? Date.now)()
|
|
48
53
|
}
|
|
49
54
|
|
|
55
|
+
private get watched(): boolean {
|
|
56
|
+
return this.readAt != null && this.now() - this.readAt < WIDGET_WATCH_MS
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
/** one look: read the files again when due, render every widget due. Never
|
|
51
|
-
* waits on a render */
|
|
60
|
+
* waits on a render, and does nothing while no reader is watching */
|
|
52
61
|
async tick(repos: RepoRef[]): Promise<void> {
|
|
62
|
+
if (!this.watched) return
|
|
53
63
|
const now = this.now()
|
|
54
64
|
if (!this.scanning && now - this.scannedAt >= WIDGET_RESCAN_MS) {
|
|
55
65
|
this.scanning = true
|
|
@@ -73,7 +83,24 @@ export class Widgets {
|
|
|
73
83
|
this.scanning = false
|
|
74
84
|
}
|
|
75
85
|
}
|
|
76
|
-
for (const s of this.slots.values())
|
|
86
|
+
for (const s of this.slots.values()) {
|
|
87
|
+
if (s.busy || this.now() < s.due) continue
|
|
88
|
+
const p = this.render(s)
|
|
89
|
+
this.rendering.add(p)
|
|
90
|
+
void p.finally(() => this.rendering.delete(p))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** a reader's views: the first one in a while waits on a scan and on the
|
|
95
|
+
* renders it starts (each bounded), so it never reads hours old html */
|
|
96
|
+
async look(repos: RepoRef[]): Promise<WidgetView[]> {
|
|
97
|
+
const cold = !this.watched
|
|
98
|
+
this.readAt = this.now()
|
|
99
|
+
if (cold) {
|
|
100
|
+
await this.tick(repos)
|
|
101
|
+
await Promise.all(this.rendering)
|
|
102
|
+
}
|
|
103
|
+
return this.views()
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
/** one render, bounded, its result or its error kept */
|
|
@@ -96,7 +123,9 @@ export class Widgets {
|
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
125
|
|
|
126
|
+
/** the last html of each; a read that keeps the clocks running */
|
|
99
127
|
views(): WidgetView[] {
|
|
128
|
+
this.readAt = this.now()
|
|
100
129
|
return [...this.slots.values()].map((s) => s.view)
|
|
101
130
|
}
|
|
102
131
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// the commit-message predicate
|
|
2
|
-
//
|
|
3
|
-
// drift and the drift only shows when a repo goes public.
|
|
1
|
+
// the commit-message predicate of the commit-msg hook, which blocks at write
|
|
2
|
+
// time: the one guard against AI attribution (SK009 checks it is armed).
|
|
4
3
|
//
|
|
5
4
|
// mechanical text rules only, so this stays publishable.
|
|
6
5
|
|