rvlib-shipkit 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- 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 +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -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_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -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 +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- 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 +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- 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/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- 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-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- 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/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- 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 +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- 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 +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// the daemon's api, apart from the page so a test reaches it without a bundle:
|
|
2
|
+
// its own /api/home/* and every repo's hub under /r/<repo>/. The daemon
|
|
3
|
+
// decides WHEN a hub loads and never what is true: every verdict still comes
|
|
4
|
+
// from each repo's own runner and ledger. The machine resources are pm's
|
|
5
|
+
// (run-pm.ts), and the page reaches them through the web at /api/pm/*.
|
|
6
|
+
import { existsSync } from 'fs'
|
|
7
|
+
import { dirname, join } from 'path'
|
|
8
|
+
import { fileURLToPath } from 'url'
|
|
9
|
+
import { installedVersion, isBehind } from '../fleet/scan-fleet.ts'
|
|
10
|
+
import { LIB_VERSION } from '../core/lib-version.ts'
|
|
11
|
+
import { projectGutter, type ProjectRow } from '../serve/project-gutter-route.ts'
|
|
12
|
+
import type { WiredRepo } from '../serve/project-gutter.ts'
|
|
13
|
+
import { sameOrigin } from '../serve/same-origin.ts'
|
|
14
|
+
import type { HomeState } from './home-state.ts'
|
|
15
|
+
import { tailLines } from './supervise.ts'
|
|
16
|
+
import { Hubs, type WorkerView } from './hubs.ts'
|
|
17
|
+
import type { Planner, PlanRow } from '../plan/planner.ts'
|
|
18
|
+
import type { Widgets, WidgetView } from './widgets.ts'
|
|
19
|
+
import { ActivityCache, windowMinutes } from './activity.ts'
|
|
20
|
+
import { HouseRuns } from '../serve/house-runs.ts'
|
|
21
|
+
import type { HouseView } from '../ledger/house-runs.ts'
|
|
22
|
+
|
|
23
|
+
/** how often the daemon looks at every repo */
|
|
24
|
+
export const TICK_MS = 2_000
|
|
25
|
+
|
|
26
|
+
export type ModernizeView = { status: 'running' | 'done' | 'failed'; code: number | null; tail: string[] }
|
|
27
|
+
|
|
28
|
+
export type HomeAnswer = {
|
|
29
|
+
version: string
|
|
30
|
+
workers: WorkerView[]
|
|
31
|
+
/** the repos flagged active by hand */
|
|
32
|
+
active: string[]
|
|
33
|
+
/** repo → its installed shipkit, for every repo behind this home's */
|
|
34
|
+
behind: Record<string, string>
|
|
35
|
+
modernize: Record<string, ModernizeView>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type HomeDeps = {
|
|
39
|
+
root: string
|
|
40
|
+
name: string
|
|
41
|
+
port: number
|
|
42
|
+
dir: string
|
|
43
|
+
hubs: Hubs
|
|
44
|
+
state: HomeState
|
|
45
|
+
/** the rail rows, the same measure a hub's rail reads */
|
|
46
|
+
gutter: ReturnType<typeof projectGutter>
|
|
47
|
+
/** the machine's one plan, which this tick drives. Absent = nothing runs
|
|
48
|
+
* on its own (a test of the routes alone) */
|
|
49
|
+
plan?: Planner
|
|
50
|
+
/** the repos it loads, by name; absent = every house repo. A smoke on a
|
|
51
|
+
* real machine keeps the others out, so no save there starts a gate */
|
|
52
|
+
only?: Set<string>
|
|
53
|
+
/** every house repo's widgets, rendered on the daemon's tick. Absent =
|
|
54
|
+
* none (a test of the routes alone) */
|
|
55
|
+
widgets?: Widgets
|
|
56
|
+
/** brings a repo current (modernizeCmd); resolves to its exit code */
|
|
57
|
+
modernize?: (repo: WiredRepo, log: string) => Promise<number>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const CLI = fileURLToPath(new URL('../cli.ts', import.meta.url))
|
|
61
|
+
|
|
62
|
+
/** what bringing a repo current runs: its first install when it has none,
|
|
63
|
+
* else the fleet update, which goes through the repo's own gate */
|
|
64
|
+
export function modernizeCmd(repo: WiredRepo, root: string): { cmd: string[]; cwd: string } {
|
|
65
|
+
if (!existsSync(join(repo.path, 'node_modules'))) return { cmd: ['bun', 'install'], cwd: repo.path }
|
|
66
|
+
return { cmd: ['bun', CLI, 'fleet', '--update', '--only', repo.name], cwd: root }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const runModernize = async (repo: WiredRepo, root: string, log: string): Promise<number> => {
|
|
70
|
+
const { mkdirSync, openSync, closeSync } = await import('fs')
|
|
71
|
+
mkdirSync(dirname(log), { recursive: true })
|
|
72
|
+
const fd = openSync(log, 'w')
|
|
73
|
+
try {
|
|
74
|
+
const { cmd, cwd } = modernizeCmd(repo, root)
|
|
75
|
+
const p = Bun.spawn(cmd, { cwd, stdio: ['ignore', fd, fd] })
|
|
76
|
+
return await p.exited
|
|
77
|
+
} finally {
|
|
78
|
+
closeSync(fd)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** active = an agent works in it, VS Code has it open, or it is flagged by
|
|
83
|
+
* hand. The house repo is active whenever any repo is: its checks serve
|
|
84
|
+
* every repo (a login, a shared key), so they follow the machine's work */
|
|
85
|
+
export function activeSet(rows: ProjectRow[], flagged: string[], house?: string): Set<string> {
|
|
86
|
+
const out = new Set(flagged)
|
|
87
|
+
for (const r of rows) if ((r.sessions != null && r.sessions > 0) || r.vscode === true) out.add(r.name)
|
|
88
|
+
if (house != null && out.size > 0) out.add(house)
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** the monitor area's data: every widget's last html */
|
|
93
|
+
export type WidgetsAnswer = { widgets: WidgetView[]; errors: string[] }
|
|
94
|
+
|
|
95
|
+
/** the planned page's data: every row of the machine's plan */
|
|
96
|
+
export type PlannedAnswer = {
|
|
97
|
+
now: number
|
|
98
|
+
rows: PlanRow[]
|
|
99
|
+
errors: { repo: string; error: string }[]
|
|
100
|
+
running: boolean
|
|
101
|
+
/** what holds work back, one line each (Planner.held) */
|
|
102
|
+
held: string[]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function homeRoutes(d: HomeDeps) {
|
|
106
|
+
let repos: WiredRepo[] = []
|
|
107
|
+
let rows: ProjectRow[] = []
|
|
108
|
+
const modernizing = new Map<string, ModernizeView & { log: string }>()
|
|
109
|
+
// read only while the activity page asks, never on the tick
|
|
110
|
+
const activity = new ActivityCache({ declared: (name) => d.plan?.declared(name) ?? null, titles: (name) => d.plan?.titles(name) ?? null })
|
|
111
|
+
const modernizeLog = (repo: string): string => join(d.dir, 'modernize', `${repo}.log`)
|
|
112
|
+
// the dock: every loaded repo's recent runs, read only while a page asks
|
|
113
|
+
const house = new HouseRuns(() => repos)
|
|
114
|
+
|
|
115
|
+
async function refresh(): Promise<void> {
|
|
116
|
+
const a = await d.gutter.answer()
|
|
117
|
+
rows = a.projects
|
|
118
|
+
repos = a.projects.filter((p) => d.only == null || d.only.has(p.name)).map((p) => ({ name: p.name, path: p.path, icon: null }))
|
|
119
|
+
d.hubs.sync(repos)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let active = new Set<string>()
|
|
123
|
+
if (d.plan != null) d.plan.active = (name) => active.has(name)
|
|
124
|
+
|
|
125
|
+
async function tick(): Promise<void> {
|
|
126
|
+
await refresh()
|
|
127
|
+
active = activeSet(rows, d.state.data.active, rows.find((r) => r.path === d.root)?.name ?? d.name)
|
|
128
|
+
// the plan's clock: one look per tick, and it starts at most one batch
|
|
129
|
+
d.plan?.tick()
|
|
130
|
+
// a widget render never holds the tick back
|
|
131
|
+
void d.widgets?.tick(repos.map((r) => ({ name: r.name, path: r.path }))).catch((e) => console.error(`WARN the widgets broke: ${(e as Error).message}`))
|
|
132
|
+
await d.hubs.tick(active)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function answer(): HomeAnswer {
|
|
136
|
+
const behind: Record<string, string> = {}
|
|
137
|
+
for (const r of repos) {
|
|
138
|
+
const v = installedVersion(r.path, 'rvlib-shipkit')
|
|
139
|
+
if (v != null && isBehind(v, LIB_VERSION)) behind[r.name] = v
|
|
140
|
+
}
|
|
141
|
+
const modernize: Record<string, ModernizeView> = {}
|
|
142
|
+
for (const [k, m] of modernizing) modernize[k] = { status: m.status, code: m.code, tail: tailLines(m.log, 40) }
|
|
143
|
+
return { version: LIB_VERSION, workers: d.hubs.views(repos), active: d.state.data.active, behind, modernize }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const find = (name: unknown): WiredRepo | null => repos.find((r) => r.name === name) ?? null
|
|
147
|
+
const body = async (req: Request): Promise<Record<string, unknown>> => (await req.json().catch(() => ({}))) as Record<string, unknown>
|
|
148
|
+
|
|
149
|
+
async function handle(req: Request, srv?: Parameters<Hubs['dispatch']>[2]): Promise<Response | null> {
|
|
150
|
+
const path = new URL(req.url).pathname
|
|
151
|
+
// every repo's hub, which makes its own same origin check
|
|
152
|
+
if (path.startsWith('/r/')) {
|
|
153
|
+
if (repos.length === 0) await refresh()
|
|
154
|
+
return d.hubs.dispatch(req, path, srv as Parameters<Hubs['dispatch']>[2])
|
|
155
|
+
}
|
|
156
|
+
if (path === '/hub.json')
|
|
157
|
+
// the identity a client probes (homeUp), through the web or here
|
|
158
|
+
return Response.json({ hub: 'shipkit-home', name: d.name, root: d.root, version: LIB_VERSION }, { headers: { 'access-control-allow-origin': '*' } })
|
|
159
|
+
if (!sameOrigin(req)) return new Response('refused: a request that changes something must come from the home page', { status: 403 })
|
|
160
|
+
if (path.startsWith('/api/projects')) return d.gutter.handle(req, path)
|
|
161
|
+
// the web asks which page routes to serve: one per house repo
|
|
162
|
+
if (path === '/api/home/repos.json') {
|
|
163
|
+
if (repos.length === 0) await refresh()
|
|
164
|
+
return Response.json({ repos: d.hubs.names() })
|
|
165
|
+
}
|
|
166
|
+
if (path === '/api/home/planned.json') {
|
|
167
|
+
if (repos.length === 0) await refresh()
|
|
168
|
+
const out: PlannedAnswer = { now: Date.now(), rows: d.plan?.rows() ?? [], errors: d.plan?.errors() ?? [], running: d.plan?.busy() === true, held: d.plan?.held() ?? [] }
|
|
169
|
+
return Response.json(out)
|
|
170
|
+
}
|
|
171
|
+
if (path === '/api/home/activity.json') {
|
|
172
|
+
if (repos.length === 0) await refresh()
|
|
173
|
+
const q = new URL(req.url).searchParams
|
|
174
|
+
return Response.json(await activity.get(repos, windowMinutes(q.get('m')), { fresh: q.get('fresh') === '1' }))
|
|
175
|
+
}
|
|
176
|
+
if (path === '/api/house-runs.json') {
|
|
177
|
+
if (repos.length === 0) await refresh()
|
|
178
|
+
const view: HouseView = { ...house.view(), held: d.plan?.held() ?? [] }
|
|
179
|
+
return Response.json(view, { headers: { 'cache-control': 'no-store' } })
|
|
180
|
+
}
|
|
181
|
+
if (path === '/api/home/widgets.json') {
|
|
182
|
+
const out: WidgetsAnswer = { widgets: d.widgets?.views() ?? [], errors: d.widgets?.errors ?? [] }
|
|
183
|
+
return Response.json(out)
|
|
184
|
+
}
|
|
185
|
+
if (path === '/api/home.json') {
|
|
186
|
+
if (repos.length === 0) await refresh()
|
|
187
|
+
return Response.json(answer())
|
|
188
|
+
}
|
|
189
|
+
if (req.method !== 'POST') return null
|
|
190
|
+
if (path === '/api/home/hub') {
|
|
191
|
+
const b = await body(req)
|
|
192
|
+
if (repos.length === 0) await refresh()
|
|
193
|
+
const r = find(b.name)
|
|
194
|
+
if (r == null) return new Response(`not a house repo: ${String(b.name)}`, { status: 404 })
|
|
195
|
+
if (b.act === 'ensure') return Response.json(d.hubs.ensure(r))
|
|
196
|
+
if (b.act === 'stop') return Response.json(d.hubs.stop(r))
|
|
197
|
+
if (b.act === 'restart') return Response.json(await d.hubs.restart(r))
|
|
198
|
+
return new Response(`act must be ensure, stop or restart`, { status: 400 })
|
|
199
|
+
}
|
|
200
|
+
if (path === '/api/home/planned/run') {
|
|
201
|
+
const b = await body(req)
|
|
202
|
+
if (d.plan == null || !d.plan.ask(String(b.repo), String(b.check))) return new Response(`no planned check ${String(b.check)} in ${String(b.repo)}`, { status: 404 })
|
|
203
|
+
d.plan.tick()
|
|
204
|
+
return Response.json({ ok: true })
|
|
205
|
+
}
|
|
206
|
+
if (path === '/api/home/active') {
|
|
207
|
+
const b = await body(req)
|
|
208
|
+
if (find(b.name) == null) return new Response(`not a house repo: ${String(b.name)}`, { status: 404 })
|
|
209
|
+
d.state.setActive(String(b.name), b.on === true)
|
|
210
|
+
return Response.json({ active: d.state.data.active })
|
|
211
|
+
}
|
|
212
|
+
if (path === '/api/home/modernize') {
|
|
213
|
+
const b = await body(req)
|
|
214
|
+
const r = find(b.name)
|
|
215
|
+
if (r == null) return new Response(`not a house repo: ${String(b.name)}`, { status: 404 })
|
|
216
|
+
if (modernizing.get(r.name)?.status === 'running') return new Response(`already modernizing ${r.name}`, { status: 409 })
|
|
217
|
+
const log = modernizeLog(r.name)
|
|
218
|
+
const m = { status: 'running' as ModernizeView['status'], code: null as number | null, tail: [], log }
|
|
219
|
+
modernizing.set(r.name, m)
|
|
220
|
+
void (d.modernize ?? ((repo, l) => runModernize(repo, d.root, l)))(r, log)
|
|
221
|
+
.then(async (code) => {
|
|
222
|
+
m.status = code === 0 ? 'done' : 'failed'
|
|
223
|
+
m.code = code
|
|
224
|
+
// the hub loads again on what the update installed
|
|
225
|
+
if (code === 0) await d.hubs.restart(r)
|
|
226
|
+
})
|
|
227
|
+
.catch(() => { m.status = 'failed' })
|
|
228
|
+
return Response.json({ ok: true })
|
|
229
|
+
}
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { handle, tick, answer, refresh }
|
|
234
|
+
}
|
|
235
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// getting to a running home from anywhere: is pm installed, start it, find
|
|
2
|
+
// the house repo it runs from, open a window on it. `shipkit gui` and the
|
|
3
|
+
// serve handoff both come through here.
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync } from 'fs'
|
|
5
|
+
import { homedir } from 'os'
|
|
6
|
+
import { dirname, join, resolve } from 'path'
|
|
7
|
+
import { gitRoot } from '../core/git-root.ts'
|
|
8
|
+
import { homeRepo, houseIndex } from '../serve/project-gutter.ts'
|
|
9
|
+
import { BUNDLE_ID, canBuildGui, ensureGuiApp } from './gui-app.ts'
|
|
10
|
+
import { LABEL, plistPath } from './home-launchd.ts'
|
|
11
|
+
import { childLog, daemonPort, daemonUp, homeDir, homePort, homeUp, pmLog } from './home-paths.ts'
|
|
12
|
+
|
|
13
|
+
/** the pm LaunchAgent is installed: on this machine no hub runs outside the home */
|
|
14
|
+
export const homeInstalled = (): boolean => existsSync(plistPath())
|
|
15
|
+
|
|
16
|
+
/** set, a machine with no LaunchAgent still sends its hubs and gates to the
|
|
17
|
+
* home that answers on SHIPKIT_HOME_PORT (a home started by hand, a test) */
|
|
18
|
+
export const USE_HOME_ENV = 'SHIPKIT_USE_HOME'
|
|
19
|
+
|
|
20
|
+
/** this machine runs its hubs and gates in the home */
|
|
21
|
+
export const homeWanted = (env: Record<string, string | undefined> = process.env, installed: () => boolean = homeInstalled): boolean =>
|
|
22
|
+
(env[USE_HOME_ENV] != null && env[USE_HOME_ENV] !== '') || installed()
|
|
23
|
+
|
|
24
|
+
/** how long a start waits for the home to answer */
|
|
25
|
+
export const START_WAIT_MS = 15_000
|
|
26
|
+
|
|
27
|
+
/** kick the installed pm agent and wait until the role answers: the daemon
|
|
28
|
+
* for a terminal client, the web for a window. Throws, loud, when it does
|
|
29
|
+
* not: a caller must never fall back to a hub of its own */
|
|
30
|
+
export async function startHome(o: { wait?: number; need?: 'daemon' | 'web' } = {}): Promise<void> {
|
|
31
|
+
const need = o.need ?? 'daemon'
|
|
32
|
+
const up = (): Promise<boolean> => (need === 'daemon' ? daemonUp() : homeUp(homePort()))
|
|
33
|
+
if (await up()) return
|
|
34
|
+
const p = Bun.spawn(['launchctl', 'kickstart', `gui/${process.getuid?.() ?? 501}/${LABEL}`], { stdout: 'ignore', stderr: 'pipe' })
|
|
35
|
+
const err = (await new Response(p.stderr).text()).trim()
|
|
36
|
+
if ((await p.exited) !== 0) throw new Error(`launchctl kickstart ${LABEL} failed: ${err}. Reinstall it with \`shipkit pm install\` from the house repo`)
|
|
37
|
+
const until = Date.now() + (o.wait ?? START_WAIT_MS)
|
|
38
|
+
while (Date.now() < until) {
|
|
39
|
+
if (await up()) return
|
|
40
|
+
await Bun.sleep(250)
|
|
41
|
+
}
|
|
42
|
+
const port = need === 'daemon' ? daemonPort() : homePort()
|
|
43
|
+
throw new Error(`the ${need} did not answer on :${port} within ${Math.round((o.wait ?? START_WAIT_MS) / 1000)}s: see ${childLog(homeDir(), need)} and ${pmLog()}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** the cli the home runs from its repo: the one that repo's own `hub` script
|
|
47
|
+
* runs (an installed copy, or the lib source in the repo that hosts it),
|
|
48
|
+
* else this one */
|
|
49
|
+
export function houseCli(root: string, own: string): string {
|
|
50
|
+
try {
|
|
51
|
+
const hub = (JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { scripts?: Record<string, string> }).scripts?.['hub'] ?? ''
|
|
52
|
+
const cli = /(\S*cli\.ts)\b/.exec(hub)?.[1]
|
|
53
|
+
if (cli != null) {
|
|
54
|
+
const abs = resolve(root, cli)
|
|
55
|
+
if (existsSync(abs)) return abs
|
|
56
|
+
}
|
|
57
|
+
} catch { /* no package.json: this cli */ }
|
|
58
|
+
return own
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** the repo that hosts the house wiki, seen from any house repo, or null */
|
|
62
|
+
export function houseRootOf(cwd: string): string | null {
|
|
63
|
+
const index = houseIndex(homeRepo(cwd), homedir())
|
|
64
|
+
if (index == null) return null
|
|
65
|
+
return gitRoot(dirname(index))?.top ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const CHROME = process.env.CHROME ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
|
69
|
+
/** the window's own Chrome profile: its own dock icon and cookies, apart from the everyday browser */
|
|
70
|
+
export const guiProfile = (): string => join(homeDir(), 'gui-profile')
|
|
71
|
+
const GUI_DISK_CACHE = 32 * 1024 * 1024
|
|
72
|
+
|
|
73
|
+
/** the command that opens one more home window: a Chrome app window (no tabs,
|
|
74
|
+
* no address bar) on its own profile, else the default browser */
|
|
75
|
+
export function windowCmd(url: string, o: { chrome?: string; platform?: string } = {}): string[] {
|
|
76
|
+
const chrome = o.chrome ?? CHROME
|
|
77
|
+
const platform = o.platform ?? process.platform
|
|
78
|
+
// no component downloads and a small disk cache: the profile shows one
|
|
79
|
+
// page, and those two are most of what grows it (retention.ts caps it)
|
|
80
|
+
if (existsSync(chrome)) return [chrome, `--app=${url}`, `--user-data-dir=${guiProfile()}`, '--no-first-run', '--no-default-browser-check', '--disable-component-update', `--disk-cache-size=${GUI_DISK_CACHE}`]
|
|
81
|
+
return platform === 'darwin' ? ['open', url] : ['xdg-open', url]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** one more home window: the native app where it can be built (its own
|
|
85
|
+
* dock icon, the house mark), else a Chrome app window. `warn` hears why the
|
|
86
|
+
* native one was not used */
|
|
87
|
+
export function openWindow(url: string, o: { root?: string | null; version?: string; warn?: (l: string) => void } = {}): string[] {
|
|
88
|
+
if (canBuildGui()) {
|
|
89
|
+
try {
|
|
90
|
+
const { app, rebuilt } = ensureGuiApp({ root: o.root ?? houseRootOf(process.cwd()) ?? process.cwd(), version: o.version ?? '0' })
|
|
91
|
+
// a running copy is the old build: it quits, and the new one opens the window
|
|
92
|
+
if (rebuilt) Bun.spawnSync(['osascript', '-e', `quit app id "${BUNDLE_ID}"`], { stdout: 'ignore', stderr: 'ignore' })
|
|
93
|
+
const cmd = ['open', '-a', app, url]
|
|
94
|
+
Bun.spawn(cmd, { stdio: ['ignore', 'ignore', 'ignore'] })
|
|
95
|
+
return cmd
|
|
96
|
+
} catch (e) {
|
|
97
|
+
o.warn?.(`WARN the shipkit app could not be built, so this window is a Chrome one: ${(e as Error).message}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
mkdirSync(guiProfile(), { recursive: true })
|
|
101
|
+
const cmd = windowCmd(url)
|
|
102
|
+
// detached: the window outlives the command that opened it
|
|
103
|
+
Bun.spawn(cmd, { stdio: ['ignore', 'ignore', 'ignore'], detached: true }).unref()
|
|
104
|
+
return cmd
|
|
105
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// what the home remembers across restarts, and only that: the repos flagged
|
|
2
|
+
// active by hand. One row of the machine db (core/machine-db.ts), edited by
|
|
3
|
+
// the page.
|
|
4
|
+
import { importMachineFile, machineGet, machineSet } from '../core/machine-db.ts'
|
|
5
|
+
import { stateFile } from './home-paths.ts'
|
|
6
|
+
|
|
7
|
+
export type HomeStateData = {
|
|
8
|
+
/** repos whose hub runs whether or not an agent works in them */
|
|
9
|
+
active: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const KEY = 'home.active'
|
|
13
|
+
|
|
14
|
+
export class HomeState {
|
|
15
|
+
data: HomeStateData
|
|
16
|
+
|
|
17
|
+
/** `dir` is the home dir, where the json file an older copy wrote is
|
|
18
|
+
* read once into the db */
|
|
19
|
+
constructor(dir: string) {
|
|
20
|
+
importMachineFile<string[]>(KEY, stateFile(dir), (t) => HomeState.parse(t).active)
|
|
21
|
+
this.data = { active: HomeState.clean(machineGet<unknown>(KEY)) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static parse(text: string): HomeStateData {
|
|
25
|
+
try {
|
|
26
|
+
return { active: HomeState.clean((JSON.parse(text) as Partial<HomeStateData>).active) }
|
|
27
|
+
} catch {
|
|
28
|
+
return { active: [] }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private static clean(v: unknown): string[] {
|
|
33
|
+
return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setActive(name: string, on: boolean): void {
|
|
37
|
+
const set = new Set(this.data.active)
|
|
38
|
+
if (on) set.add(name)
|
|
39
|
+
else set.delete(name)
|
|
40
|
+
this.data = { ...this.data, active: [...set].sort() }
|
|
41
|
+
if (!machineSet(KEY, this.data.active)) throw new Error('the machine db could not be written, so the active flag is not saved')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// the words the home's server and page both need, in a module with no node
|
|
2
|
+
// import, so the page bundle can take it.
|
|
3
|
+
|
|
4
|
+
/** the blocker the page answers with an install button */
|
|
5
|
+
export const NOT_INSTALLED = 'rvlib-shipkit is not installed here: run `bun install`'
|
|
6
|
+
|
|
7
|
+
/** the last line of a run a hub streams to a terminal: its exit code follows */
|
|
8
|
+
export const RUN_EXIT = '\u0000shipkit-exit '
|
|
9
|
+
|
|
10
|
+
/** a line of a run a hub streams to a terminal: the gate job it started, so a
|
|
11
|
+
* terminal whose stream is cut follows the rest from the job folder */
|
|
12
|
+
export const RUN_JOB = '\u0000shipkit-job '
|