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,116 @@
|
|
|
1
|
+
// shipkit cross-repo-activity: the activity page in the console. The rows come
|
|
2
|
+
// from activityOf, the one computation the daemon's route serves the page, so
|
|
3
|
+
// the two can never disagree; this file only renders them as text. It reads
|
|
4
|
+
// local git and each repo's run record directly and needs no daemon: when one
|
|
5
|
+
// answers, its plan says which checks each repo declares today, as the page's
|
|
6
|
+
// status dot counts them.
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
import { Command, Option } from 'clipanion'
|
|
9
|
+
import { activityOf, type Declared } from '../home/activity.ts'
|
|
10
|
+
import { ageShort, clampCap, DEFAULT_CAP, DEFAULT_MINUTES, dirtWords, idleRow, upstreamWords, type ActivityAnswer, type ActivityCommit, type ActivityRow } from '../home/activity-shape.ts'
|
|
11
|
+
import { railRepos } from '../serve/project-gutter-route.ts'
|
|
12
|
+
|
|
13
|
+
/** a span as typed: `10m`, `1h`, `3d`, `90` (minutes); null when it is none */
|
|
14
|
+
export function spanMinutes(v: string): number | null {
|
|
15
|
+
const m = /^(\d+(?:\.\d+)?)\s*(m|min|h|d)?$/.exec(v.trim())
|
|
16
|
+
if (m == null) return null
|
|
17
|
+
const n = Number(m[1]) * (m[2] === 'h' ? 60 : m[2] === 'd' ? 1440 : 1)
|
|
18
|
+
return n > 0 ? Math.round(n) : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** single dash spellings of the long options, as typed: `-max-commit 3` */
|
|
22
|
+
export function aliasArgs(argv: string[]): string[] {
|
|
23
|
+
return argv.map((a) => (/^-max-(commit|age)(=|$)/.test(a) ? `-${a}` : a))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** the declared check ids from a running daemon's plan; null when none answers */
|
|
27
|
+
export async function daemonDeclared(port = Number(process.env.SHIPKIT_DAEMON_PORT ?? 4798)): Promise<Declared | null> {
|
|
28
|
+
try {
|
|
29
|
+
const r = await fetch(`http://127.0.0.1:${port}/api/home/planned.json`, { signal: AbortSignal.timeout(1_500) })
|
|
30
|
+
if (!r.ok) return null
|
|
31
|
+
const j = (await r.json()) as { rows: { repo: string; check: string }[] }
|
|
32
|
+
const by = new Map<string, Set<string>>()
|
|
33
|
+
for (const row of j.rows) {
|
|
34
|
+
if (!by.has(row.repo)) by.set(row.repo, new Set())
|
|
35
|
+
by.get(row.repo)!.add(row.check)
|
|
36
|
+
}
|
|
37
|
+
return (name) => by.get(name) ?? null
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dot = (r: ActivityRow): string => {
|
|
44
|
+
const c = r.status
|
|
45
|
+
if (c == null) return '·'
|
|
46
|
+
if (c.state === 'red') return `● ${c.reds.length} red`
|
|
47
|
+
if (c.state === 'orange') return `● ${c.warns.length} warn`
|
|
48
|
+
return '● green'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const commitLine = (c: ActivityCommit, now: number, old: boolean): string =>
|
|
52
|
+
` ${ageShort(c.at, now).padStart(3)} ${c.sha} ${c.pushed ? '✓' : '↑'} ${c.branch == null ? '' : `[${c.branch}] `}${c.title}${old ? ' (last commit, before the window)' : ''}`
|
|
53
|
+
|
|
54
|
+
/** the page's rows as text: busy repos first, then the quiet ones, each with
|
|
55
|
+
* its status, branch, upstream, uncommitted line and commits */
|
|
56
|
+
export function renderActivity(a: ActivityAnswer, cap: number): string[] {
|
|
57
|
+
const out: string[] = []
|
|
58
|
+
const busy = a.rows.filter((r) => !idleRow(r))
|
|
59
|
+
const quiet = a.rows.filter(idleRow)
|
|
60
|
+
for (const r of [...busy, ...quiet]) {
|
|
61
|
+
const branch = r.branch ?? (r.detached == null ? '' : `detached ${r.detached}`)
|
|
62
|
+
out.push(`${r.name} ${dot(r)} ${branch} (${upstreamWords(r)})`)
|
|
63
|
+
if (r.status != null && r.status.reds.length > 0) out.push(` failing: ${r.status.reds.map((id) => (r.status!.since[id] == null ? id : `${id} (${ageShort(r.status!.since[id]!, a.now)})`)).join(', ')}`)
|
|
64
|
+
if (r.error != null) out.push(` git failed: ${r.error}`)
|
|
65
|
+
const dirt = r.error == null ? dirtWords(r) : ''
|
|
66
|
+
if (dirt !== '') {
|
|
67
|
+
out.push(` uncommitted: ${dirt}`)
|
|
68
|
+
const mark = { staged: 'S', modified: 'M', untracked: '?' } as const
|
|
69
|
+
for (const f of r.files) out.push(` ${mark[f.kind]} ${f.path}`)
|
|
70
|
+
}
|
|
71
|
+
for (const c of r.commits.slice(0, cap)) out.push(commitLine(c, a.now, false))
|
|
72
|
+
if (r.commits.length > cap) out.push(` … ${r.commits.length - cap} more`)
|
|
73
|
+
if (r.last != null) out.push(commitLine(r.last, a.now, true))
|
|
74
|
+
if (r.commits.length === 0 && r.last == null && r.error == null) out.push(' no commit yet')
|
|
75
|
+
}
|
|
76
|
+
out.push(`${busy.length} busy, ${quiet.length} quiet, read in ${a.ms}ms`)
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class CrossRepoActivityCommand extends Command {
|
|
81
|
+
static override paths = [['cross-repo-activity'], ['activity']]
|
|
82
|
+
|
|
83
|
+
static override usage = Command.Usage({
|
|
84
|
+
category: 'Home',
|
|
85
|
+
description: 'the activity page in the console: every house repo (archived ones left out), its checks, branch, upstream, uncommitted work and commits in a window. The same computation the page reads, local git only, no daemon needed',
|
|
86
|
+
examples: [
|
|
87
|
+
['the last 30 minutes, 3 commits per repo, as the page opens', 'shipkit cross-repo-activity'],
|
|
88
|
+
['the same, by the page name', 'shipkit activity'],
|
|
89
|
+
['the last 3 hours, up to 10 commits per repo', 'shipkit cross-repo-activity --max-age 3h --max-commit 10'],
|
|
90
|
+
['the route\'s exact payload', 'shipkit cross-repo-activity --max-age 30m --json'],
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
maxAge = Option.String('--max-age', { required: false, description: `how far back: 10m, 1h, 3d, or minutes (default ${DEFAULT_MINUTES}m)` })
|
|
95
|
+
maxCommit = Option.String('--max-commit', { required: false, description: `commits shown per repo, 1 to 15 (default ${DEFAULT_CAP}). -max-commit works too` })
|
|
96
|
+
json = Option.Boolean('--json', false, { description: 'the daemon route\'s exact payload: { now, minutes, rows, ms }' })
|
|
97
|
+
|
|
98
|
+
async execute(): Promise<number> {
|
|
99
|
+
const minutes = this.maxAge == null ? DEFAULT_MINUTES : spanMinutes(this.maxAge)
|
|
100
|
+
if (minutes == null) {
|
|
101
|
+
this.context.stderr.write(`--max-age must be a span like 10m, 1h or 3d, got ${JSON.stringify(this.maxAge)}\n`)
|
|
102
|
+
return 2
|
|
103
|
+
}
|
|
104
|
+
const cap = clampCap(this.maxCommit ?? DEFAULT_CAP)
|
|
105
|
+
const declared = await daemonDeclared()
|
|
106
|
+
// HOME as the shell has it now: the house include names `~`
|
|
107
|
+
const a = await activityOf(railRepos(process.cwd(), process.env.HOME || homedir()), minutes, Date.now(), declared)
|
|
108
|
+
if (this.json) {
|
|
109
|
+
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
for (const l of renderActivity(a, cap)) this.context.stdout.write(`${l}\n`)
|
|
113
|
+
if (declared == null) this.context.stdout.write('note: no daemon answered, so every check a run record holds counts, declared today or not\n')
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// shipkit global-processes: the overview's processes table in the console,
|
|
2
|
+
// shipkit's own (pm, the daemon, the web) first. The rows come from pm's own
|
|
3
|
+
// answer through process-rows.ts, the one model the page draws, so the two can
|
|
4
|
+
// never disagree; this file only asks pm and the daemon and renders them as
|
|
5
|
+
// text. With pm down it reads the declared units the way pm does
|
|
6
|
+
// (pm-config.ts) and marks each one unknown.
|
|
7
|
+
import { homedir } from 'os'
|
|
8
|
+
import { Command, Option } from 'clipanion'
|
|
9
|
+
import { daemonPort, daemonUrl, homePort, pmPort, pmUrl } from '../home/home-paths.ts'
|
|
10
|
+
import { houseRootOf } from '../home/home-start.ts'
|
|
11
|
+
import type { PmAnswer } from '../home/pm.ts'
|
|
12
|
+
import { coreGroup, declaredRow, groupRows, processGroups, stateWords, type ProcessGroup } from '../home/process-rows.ts'
|
|
13
|
+
import type { PmConfig } from '../home/pm-config.ts'
|
|
14
|
+
import { readPmConfig } from '../home/run-pm.ts'
|
|
15
|
+
import { railRepos } from '../serve/project-gutter-route.ts'
|
|
16
|
+
|
|
17
|
+
export type GlobalProcessesAnswer = {
|
|
18
|
+
/** pm as it answered, null when it did not */
|
|
19
|
+
pm: { pid: number; version: string } | null
|
|
20
|
+
now: number
|
|
21
|
+
groups: ProcessGroup[]
|
|
22
|
+
/** the unit files that would not load */
|
|
23
|
+
errors: string[]
|
|
24
|
+
/** with pm down: whether a house was found to read the declared units from */
|
|
25
|
+
house: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type GlobalProcessesDeps = {
|
|
29
|
+
pm: () => Promise<PmAnswer | null>
|
|
30
|
+
/** the daemon's identity, for its version; null when it does not answer */
|
|
31
|
+
daemon?: () => Promise<{ version: string } | null>
|
|
32
|
+
/** the declared units, read like pm reads them; null when no house is found */
|
|
33
|
+
declared: () => Promise<Pick<PmConfig, 'processes' | 'errors'> | null>
|
|
34
|
+
/** the project list's order */
|
|
35
|
+
order: string[]
|
|
36
|
+
now: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** the ports this shell would find pm and its children on */
|
|
40
|
+
const localPorts = () => ({ pm: pmPort(), daemon: daemonPort(), web: homePort() })
|
|
41
|
+
|
|
42
|
+
/** pm's answer, shipkit's own rows first; the declared units as unknown when
|
|
43
|
+
* pm does not answer */
|
|
44
|
+
export async function globalProcessesAnswer(d: GlobalProcessesDeps): Promise<GlobalProcessesAnswer> {
|
|
45
|
+
const [pm, daemon] = await Promise.all([d.pm(), d.daemon?.() ?? null])
|
|
46
|
+
const core = { daemonVersion: daemon?.version ?? null, ports: localPorts() }
|
|
47
|
+
if (pm != null) return { pm: { pid: pm.pid, version: pm.version }, now: d.now, groups: processGroups(pm, d.order, core), errors: pm.unitErrors, house: true }
|
|
48
|
+
const c = await d.declared()
|
|
49
|
+
return { pm: null, now: d.now, groups: [coreGroup(null, core), ...groupRows((c?.processes ?? []).map(declaredRow), d.order)], errors: c?.errors ?? [], house: c != null }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** the daemon's identity at /hub.json, or null */
|
|
53
|
+
export async function askDaemon(url = daemonUrl()): Promise<{ version: string } | null> {
|
|
54
|
+
try {
|
|
55
|
+
const r = await fetch(`${url}/hub.json`, { signal: AbortSignal.timeout(3_000) })
|
|
56
|
+
const j = r.ok ? ((await r.json()) as { hub?: string; version?: string }) : null
|
|
57
|
+
return j?.hub === 'shipkit-home' && typeof j.version === 'string' ? { version: j.version } : null
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** pm's status answer, or null when it does not answer */
|
|
64
|
+
export async function askPm(url = pmUrl()): Promise<PmAnswer | null> {
|
|
65
|
+
try {
|
|
66
|
+
const r = await fetch(`${url}/api/pm/status.json`, { signal: AbortSignal.timeout(1_500) })
|
|
67
|
+
if (!r.ok) return null
|
|
68
|
+
const j = (await r.json()) as PmAnswer
|
|
69
|
+
return j.pm === 'shipkit-pm' ? j : null
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** the project list's order and the declared units, read from disk the way
|
|
76
|
+
* pm reads them: what the table falls back on while pm does not answer */
|
|
77
|
+
export function localProcessDeps(cwd: string): Pick<GlobalProcessesDeps, 'declared' | 'order'> {
|
|
78
|
+
// HOME as the shell has it now: the house include names `~`
|
|
79
|
+
const home = process.env.HOME || homedir()
|
|
80
|
+
let order: string[] = []
|
|
81
|
+
try {
|
|
82
|
+
order = railRepos(cwd, home).map((r) => r.name)
|
|
83
|
+
} catch {
|
|
84
|
+
// no rail from here: the groups sort by name
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
order,
|
|
88
|
+
declared: async () => {
|
|
89
|
+
const root = houseRootOf(cwd)
|
|
90
|
+
if (root == null) return null
|
|
91
|
+
try {
|
|
92
|
+
return await readPmConfig(root)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return { processes: [], errors: [(e as Error).message] }
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pad = (s: string, n: number): string => (s.length >= n ? s : s + ' '.repeat(n - s.length))
|
|
101
|
+
|
|
102
|
+
/** the table as text: shipkit's own, then per repo, one line per process,
|
|
103
|
+
* its note and last log line under it */
|
|
104
|
+
export function renderGlobalProcesses(a: GlobalProcessesAnswer): string[] {
|
|
105
|
+
const out: string[] = []
|
|
106
|
+
const n = a.groups.reduce((k, g) => k + (g.core ? 0 : g.rows.length), 0)
|
|
107
|
+
if (a.pm != null) out.push(`pm pid ${a.pm.pid}, shipkit ${a.pm.version}: ${n} global process${n === 1 ? '' : 'es'}`)
|
|
108
|
+
else if (!a.house) out.push(`pm does not answer on ${pmUrl()}, and no house wiki is found from ${process.cwd()}, so no declared process is known`)
|
|
109
|
+
else out.push(`pm does not answer on ${pmUrl()}: the ${n} declared process${n === 1 ? '' : 'es'} below, each state unknown`)
|
|
110
|
+
const rows = a.groups.flatMap((g) => g.rows)
|
|
111
|
+
const w = {
|
|
112
|
+
state: Math.max(0, ...rows.map((r) => stateWords(r, a.now).length)),
|
|
113
|
+
label: Math.max(0, ...rows.map((r) => r.label.length)),
|
|
114
|
+
host: Math.max(0, ...rows.map((r) => (r.host ?? '').length)),
|
|
115
|
+
}
|
|
116
|
+
for (const g of a.groups) {
|
|
117
|
+
out.push(g.repo)
|
|
118
|
+
for (const r of g.rows) {
|
|
119
|
+
const cells = [` ● ${pad(stateWords(r, a.now), w.state)}`, pad(r.label, w.label), pad(r.host ?? '', w.host), pad(r.pid == null ? '' : `pid ${r.pid}`, 10), r.version == null ? '' : `shipkit ${r.version}`]
|
|
120
|
+
out.push(cells.join(' ').trimEnd())
|
|
121
|
+
if (r.note != null) out.push(` ${r.note}`)
|
|
122
|
+
// shipkit's own logs only matter while one is not up
|
|
123
|
+
if (r.lastLog != null && !(r.core && r.status === 'up')) out.push(` log: ${r.lastLog}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (a.pm != null && n === 0) out.push('none declared: a repo declares one in a <name>.shipkit.ts file, as export default globalProcess({...})')
|
|
127
|
+
for (const e of a.errors) out.push(`WARN ${e}`)
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class GlobalProcessesCommand extends Command {
|
|
132
|
+
static override paths = [['global-processes'], ['gp']]
|
|
133
|
+
|
|
134
|
+
static override usage = Command.Usage({
|
|
135
|
+
category: 'Home',
|
|
136
|
+
description: 'every process on the machine and its state, as the overview shows them: shipkit\'s own (pm, the daemon, the web, with their version) first, then per repo, one line per global process with its state, url, pid and last log line. When pm is down, the declared ones with their state unknown. Always exits 0',
|
|
137
|
+
examples: [
|
|
138
|
+
['the table', 'shipkit global-processes'],
|
|
139
|
+
['the same, short', 'shipkit gp'],
|
|
140
|
+
['the rows the overview draws, as json', 'shipkit global-processes --json'],
|
|
141
|
+
],
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
json = Option.Boolean('--json', false, { description: 'the rows as json: { pm, now, groups, errors, house }, groups exactly as the overview draws them' })
|
|
145
|
+
|
|
146
|
+
async execute(): Promise<number> {
|
|
147
|
+
const a = await globalProcessesAnswer({ ...localProcessDeps(process.cwd()), pm: () => askPm(), daemon: () => askDaemon(), now: Date.now() })
|
|
148
|
+
if (this.json) {
|
|
149
|
+
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
150
|
+
return 0
|
|
151
|
+
}
|
|
152
|
+
for (const l of renderGlobalProcesses(a)) this.context.stdout.write(`${l}\n`)
|
|
153
|
+
return 0
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// shipkit pm, daemon, web and gui: the machine's one app. Thin clipanion surface; logic in home/.
|
|
2
|
+
import { Command, Option } from 'clipanion'
|
|
3
|
+
import { LIB_VERSION } from '../core/lib-version.ts'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { basename } from 'path'
|
|
6
|
+
import { markAutostart } from '../home/autostart.ts'
|
|
7
|
+
import { installHome, uninstallHome } from '../home/home-launchd.ts'
|
|
8
|
+
import { homeInstalled, houseCli, houseRootOf, openWindow, startHome } from '../home/home-start.ts'
|
|
9
|
+
import { homeUp, homeUrl, hubBase } from '../home/home-paths.ts'
|
|
10
|
+
import { homeRepo, houseIndex } from '../serve/project-gutter.ts'
|
|
11
|
+
import { homedir } from 'os'
|
|
12
|
+
import { execPm, runPm } from '../home/run-pm.ts'
|
|
13
|
+
import { runDaemon } from '../home/run-daemon.ts'
|
|
14
|
+
import { runWeb } from '../home/run-web.ts'
|
|
15
|
+
|
|
16
|
+
const OWN_CLI = fileURLToPath(new URL('../cli.ts', import.meta.url))
|
|
17
|
+
|
|
18
|
+
const io = (c: Command) => ({
|
|
19
|
+
print: (l: string) => c.context.stdout.write(`${l}\n`),
|
|
20
|
+
printErr: (l: string) => c.context.stderr.write(`${l}\n`),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
/** the --port value, undefined when absent, null when it is not a port */
|
|
24
|
+
function portOf(c: Command, raw: string | undefined): number | undefined | null {
|
|
25
|
+
if (raw == null) return undefined
|
|
26
|
+
const port = Number(raw)
|
|
27
|
+
if (Number.isInteger(port) && port > 0) return port
|
|
28
|
+
c.context.stderr.write(`FAIL --port '${raw}' is not a port number\n`)
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class PmCommand extends Command {
|
|
33
|
+
// `home` is the name agents installed before pm still run
|
|
34
|
+
static override paths = [['pm'], ['home']]
|
|
35
|
+
|
|
36
|
+
static override usage = Command.Usage({
|
|
37
|
+
category: 'Serve',
|
|
38
|
+
description: 'the machine\'s process manager: runs `shipkit daemon` and `shipkit web` as detached children, starts one again when it dies, owns the global processes every house repo declares in its .shipkit.ts files, and serves its api on port 4797 (SHIPKIT_PM_PORT). A pm restarted or killed leaves its children running and the next one adopts them. Run from the repo that hosts the house wiki',
|
|
39
|
+
examples: [
|
|
40
|
+
['run pm in this terminal', 'shipkit pm'],
|
|
41
|
+
['on test ports', 'SHIPKIT_PM_PORT=4887 SHIPKIT_HOME_PORT=4889 SHIPKIT_DAEMON_PORT=4888 SHIPKIT_HOME_DIR=/tmp/home shipkit pm'],
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
port = Option.String('--port', { required: false, description: 'pm port (default 4797, or SHIPKIT_PM_PORT)' })
|
|
46
|
+
|
|
47
|
+
async execute(): Promise<number> {
|
|
48
|
+
const port = portOf(this, this.port)
|
|
49
|
+
if (port === null) return 1
|
|
50
|
+
if (execPm({ port })) return 0
|
|
51
|
+
return runPm(process.cwd(), io(this), { port })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class DaemonCommand extends Command {
|
|
56
|
+
static override paths = [['daemon']]
|
|
57
|
+
|
|
58
|
+
static override usage = Command.Usage({
|
|
59
|
+
category: 'Serve',
|
|
60
|
+
description: 'the home\'s daemon: loads every house repo hub in this one process and serves their api, their run streams and the home api under /r/<repo>/ on an internal port. No page: `shipkit web` serves it. Run from the repo that hosts the house wiki',
|
|
61
|
+
examples: [
|
|
62
|
+
['the daemon alone', 'shipkit daemon'],
|
|
63
|
+
['on another port', 'shipkit daemon --port 4888'],
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
port = Option.String('--port', { required: false, description: 'daemon port (default 4798, or SHIPKIT_DAEMON_PORT)' })
|
|
68
|
+
|
|
69
|
+
async execute(): Promise<number> {
|
|
70
|
+
const port = portOf(this, this.port)
|
|
71
|
+
if (port === null) return 1
|
|
72
|
+
return runDaemon(process.cwd(), io(this), { port })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class WebCommand extends Command {
|
|
77
|
+
static override paths = [['web']]
|
|
78
|
+
|
|
79
|
+
static override usage = Command.Usage({
|
|
80
|
+
category: 'Serve',
|
|
81
|
+
description: 'the home\'s page: one server on the home port that serves the page and proxies every api request to the daemon, so a browser only sees this origin. It loads no repo',
|
|
82
|
+
examples: [
|
|
83
|
+
['the page alone', 'shipkit web'],
|
|
84
|
+
['on another port', 'shipkit web --port 4889'],
|
|
85
|
+
],
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
port = Option.String('--port', { required: false, description: 'home port (default 4799, or SHIPKIT_HOME_PORT)' })
|
|
89
|
+
|
|
90
|
+
async execute(): Promise<number> {
|
|
91
|
+
const port = portOf(this, this.port)
|
|
92
|
+
if (port === null) return 1
|
|
93
|
+
return runWeb(io(this), { port })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class PmInstallCommand extends Command {
|
|
98
|
+
static override paths = [['pm', 'install'], ['home', 'install']]
|
|
99
|
+
|
|
100
|
+
static override usage = Command.Usage({
|
|
101
|
+
category: 'Serve',
|
|
102
|
+
description: 'keep pm running, and through it the daemon and the web: a macOS LaunchAgent starts it now and at every login, and restarts it when it dies. Idempotent',
|
|
103
|
+
examples: [['install from the house repo', 'shipkit pm install']],
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
async execute(): Promise<number> {
|
|
107
|
+
markAutostart(true)
|
|
108
|
+
return installHome({ root: process.cwd(), cli: houseCli(process.cwd(), OWN_CLI), print: io(this).print })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class PmUninstallCommand extends Command {
|
|
113
|
+
static override paths = [['pm', 'uninstall'], ['home', 'uninstall']]
|
|
114
|
+
|
|
115
|
+
static override usage = Command.Usage({
|
|
116
|
+
category: 'Serve',
|
|
117
|
+
description: 'stop pm, the daemon and the web, and remove the LaunchAgent. The resources and repo processes keep running. No later command starts pm on its own until `shipkit pm install`',
|
|
118
|
+
examples: [['remove the agent', 'shipkit pm uninstall']],
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
async execute(): Promise<number> {
|
|
122
|
+
// a machine that said no: no later command starts pm again on its own
|
|
123
|
+
markAutostart(false)
|
|
124
|
+
return uninstallHome({ print: io(this).print })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class GuiCommand extends Command {
|
|
129
|
+
static override paths = [['gui']]
|
|
130
|
+
|
|
131
|
+
static override usage = Command.Usage({
|
|
132
|
+
category: 'Serve',
|
|
133
|
+
description: 'open the home in its own window, outside VS Code and the browser, starting pm first when the web does not answer (and installing its LaunchAgent the first time). Every call opens one more window',
|
|
134
|
+
examples: [
|
|
135
|
+
['a window on the home', 'shipkit gui'],
|
|
136
|
+
['a window on one repo', 'shipkit gui my-app'],
|
|
137
|
+
['a window on the repo you stand in', 'shipkit gui .'],
|
|
138
|
+
],
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
repo = Option.String({ required: false, name: 'repo' })
|
|
142
|
+
|
|
143
|
+
async execute(): Promise<number> {
|
|
144
|
+
const out = io(this)
|
|
145
|
+
if (!(await homeUp())) {
|
|
146
|
+
// pm runs the web: ensure pm, then wait for the web
|
|
147
|
+
try {
|
|
148
|
+
if (!homeInstalled()) {
|
|
149
|
+
const root = houseRootOf(process.cwd())
|
|
150
|
+
if (root == null) {
|
|
151
|
+
out.printErr(`FAIL: no pm runs, and ${process.cwd()} is not a house repo, so there is none to install it from. Run \`shipkit gui\` once from any repo whose CLAUDE.md includes the house wiki`)
|
|
152
|
+
return 1
|
|
153
|
+
}
|
|
154
|
+
const code = await installHome({ root, cli: houseCli(root, OWN_CLI), print: out.print })
|
|
155
|
+
if (code !== 0) return code
|
|
156
|
+
}
|
|
157
|
+
await startHome({ need: 'web' })
|
|
158
|
+
} catch (e) {
|
|
159
|
+
out.printErr(`FAIL: ${(e as Error).message}`)
|
|
160
|
+
return 1
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// `.` = the repo you stand in, named the way the rail names it
|
|
164
|
+
const name = this.repo === '.' && houseIndex(homeRepo(process.cwd()), homedir()) != null ? basename(homeRepo(process.cwd())) : this.repo
|
|
165
|
+
const url = `${homeUrl()}${name != null && name !== '.' ? `${hubBase(name)}/` : '/'}`
|
|
166
|
+
const cmd = openWindow(url, { version: LIB_VERSION, warn: out.printErr })
|
|
167
|
+
out.print(`ok: a home window on ${url}${cmd[0] === 'open' ? ' (the shipkit app)' : ''}`)
|
|
168
|
+
return 0
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/cli/cmd_Keys.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
import { createHash } from 'crypto'
|
|
9
9
|
import { Command, Option } from 'clipanion'
|
|
10
10
|
import { loadRepoConfig } from '../repo-config.ts'
|
|
11
|
-
import { envTier, FULL_TESTS_ENV,
|
|
11
|
+
import { envTier, FULL_TESTS_ENV, gateInventory } from '../ledger/gate-checks.ts'
|
|
12
|
+
import { planGap, plannable } from '../plan/repo-source.ts'
|
|
12
13
|
import { KeyLayer } from '../ledger/git-key.ts'
|
|
13
14
|
import { Ledger } from '../ledger/ledger.ts'
|
|
14
15
|
import { gateBinary } from '../ledger/toolchain-identity.ts'
|
|
@@ -42,14 +43,22 @@ export class KeysCommand extends Command {
|
|
|
42
43
|
// every task's env, which is in the key, so a surface that renders keys
|
|
43
44
|
// without it renders a tier nobody asked for
|
|
44
45
|
const tier = this.full ? 'full' : envTier()
|
|
45
|
-
const
|
|
46
|
+
const rows = await gateInventory(root, c, { tier })
|
|
47
|
+
const decls = rows.map((r) => r.decl)
|
|
48
|
+
// the plan's side of the burn-down: a check nothing keys and that
|
|
49
|
+
// declares no cadence is re-asked on a guess. A built-in there is a
|
|
50
|
+
// toolkit bug, a repo check rides the default range until it declares
|
|
51
|
+
const planGaps = rows.filter(plannable).flatMap((r) => {
|
|
52
|
+
const gap = planGap(r)
|
|
53
|
+
return gap == null ? [] : [{ id: r.decl.id, from: r.from, gap }]
|
|
54
|
+
})
|
|
46
55
|
const layer = KeyLayer.open(root, decls, { cliEntry: gateBinary() })
|
|
47
56
|
const keys = layer.keys()
|
|
48
57
|
const out = (l: string): void => { this.context.stdout.write(`${l}\n`) }
|
|
49
58
|
|
|
50
59
|
if (this.json) {
|
|
51
60
|
const drift = this.rescan ? layer.rescan() : null
|
|
52
|
-
out(JSON.stringify({ tier, checks: keys, burnDown: layer.registration.burnDown, errors: layer.registration.errors, layerMs: Math.round(layer.openMs), scanMs: Math.round(layer.scanMs), failure: layer.failure, drift }))
|
|
61
|
+
out(JSON.stringify({ tier, checks: keys, burnDown: layer.registration.burnDown, planGaps, errors: layer.registration.errors, layerMs: Math.round(layer.openMs), scanMs: Math.round(layer.scanMs), failure: layer.failure, drift }))
|
|
53
62
|
return layer.registration.errors.length > 0 || layer.failure != null ? 1 : 0
|
|
54
63
|
}
|
|
55
64
|
|
|
@@ -115,6 +124,11 @@ export class KeysCommand extends Command {
|
|
|
115
124
|
const decided = new Set(layer.decls.filter((d) => d.never != null && d.never !== '').map((d) => d.id))
|
|
116
125
|
const shown = this.burnDown ? keys.filter((k) => k.population === 'none' && !decided.has(k.id)) : keys
|
|
117
126
|
for (const k of shown) out(`${k.population.padEnd(7)} ${k.id.padEnd(28)} ${k.key ?? `— ${k.reason ?? ''}`}`)
|
|
127
|
+
if (planGaps.length > 0) out('')
|
|
128
|
+
for (const g of planGaps)
|
|
129
|
+
out(g.from === 'repo'
|
|
130
|
+
? `WARN ${g.id}: ${g.gap}, so the plan runs it every 20h..28h. Declare inputs, a cacheKey, ttl or every`
|
|
131
|
+
: `FAIL: built-in ${g.id}: ${g.gap}. A toolkit bug: the plan waits for a save until it declares one`)
|
|
118
132
|
|
|
119
133
|
const content = keys.filter((k) => k.population === 'content').length
|
|
120
134
|
const reads = keys.filter((k) => k.population === 'reads').length
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// shipkit overview: the overview page in the console, top to bottom as the
|
|
2
|
+
// page: the monitor widgets as the daemon last rendered them, their html read
|
|
3
|
+
// as text by widget-text.ts, then the processes through
|
|
4
|
+
// globalProcessesAnswer (the rows `shipkit gp` prints: pm, the daemon and the
|
|
5
|
+
// web first, then the global processes). Nothing renders a widget here: the
|
|
6
|
+
// daemon holds the render, and while it is down the monitor is unknown.
|
|
7
|
+
import { Command, Option } from 'clipanion'
|
|
8
|
+
import { daemonUrl } from '../home/home-paths.ts'
|
|
9
|
+
import type { WidgetsAnswer } from '../home/home-routes.ts'
|
|
10
|
+
import type { ChildView, PmAnswer } from '../home/pm.ts'
|
|
11
|
+
import { splitWidgetHead, widgetText } from '../home/widget-text.ts'
|
|
12
|
+
import { askDaemon, askPm, globalProcessesAnswer, localProcessDeps, renderGlobalProcesses, type GlobalProcessesAnswer, type GlobalProcessesDeps } from './cmd_GlobalProcesses.ts'
|
|
13
|
+
|
|
14
|
+
export type OverviewAnswer = {
|
|
15
|
+
now: number
|
|
16
|
+
/** pm as it answered: its own pid and version, its children; null when it did not */
|
|
17
|
+
pm: { pid: number; since: number | null; version: string; children: ChildView[] } | null
|
|
18
|
+
/** the daemon's identity, asked of the daemon itself; null when it does not answer */
|
|
19
|
+
daemon: { version: string } | null
|
|
20
|
+
/** the global processes, exactly as `shipkit global-processes --json` */
|
|
21
|
+
processes: GlobalProcessesAnswer
|
|
22
|
+
/** the daemon's widgets route payload; null when the daemon does not answer */
|
|
23
|
+
widgets: WidgetsAnswer | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type OverviewDeps = Pick<GlobalProcessesDeps, 'declared' | 'order'> & {
|
|
27
|
+
pm: () => Promise<PmAnswer | null>
|
|
28
|
+
daemon: () => Promise<{ version: string } | null>
|
|
29
|
+
widgets: () => Promise<WidgetsAnswer | null>
|
|
30
|
+
now: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function getJson<T>(url: string): Promise<T | null> {
|
|
34
|
+
try {
|
|
35
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(3_000) })
|
|
36
|
+
return r.ok ? ((await r.json()) as T) : null
|
|
37
|
+
} catch {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** the daemon's widgets route payload, or null */
|
|
43
|
+
export const askWidgets = (url = daemonUrl()): Promise<WidgetsAnswer | null> => getJson<WidgetsAnswer>(`${url}/api/home/widgets.json`)
|
|
44
|
+
|
|
45
|
+
export async function overviewAnswer(d: OverviewDeps): Promise<OverviewAnswer> {
|
|
46
|
+
const [pm, daemon, widgets] = await Promise.all([d.pm(), d.daemon(), d.widgets()])
|
|
47
|
+
const processes = await globalProcessesAnswer({ pm: async () => pm, daemon: async () => daemon, declared: d.declared, order: d.order, now: d.now })
|
|
48
|
+
return { now: d.now, pm: pm == null ? null : { pid: pm.pid, since: pm.since ?? null, version: pm.version, children: pm.children }, daemon, processes, widgets }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const indent = (lines: string[], by = ' '): string[] => lines.map((l) => by + l)
|
|
52
|
+
|
|
53
|
+
/** the page as text: the monitor, then the processes (shipkit's own first),
|
|
54
|
+
* each section saying what is unknown and why when pm or the daemon does not
|
|
55
|
+
* answer */
|
|
56
|
+
export function renderOverview(a: OverviewAnswer): string[] {
|
|
57
|
+
const out: string[] = ['monitor']
|
|
58
|
+
if (a.widgets == null) out.push(` the daemon does not answer on ${daemonUrl()}, and it renders the widgets: the monitor is unknown`)
|
|
59
|
+
else {
|
|
60
|
+
if (a.widgets.widgets.length === 0 && a.widgets.errors.length === 0) out.push(' no widget declared: a repo declares one in a <name>.shipkit.ts file, as export default widget({...})')
|
|
61
|
+
for (const x of a.widgets.widgets) {
|
|
62
|
+
const { head, body } = splitWidgetHead(x.html ?? '')
|
|
63
|
+
out.push(` ${x.title}${head == null ? '' : ` · ${widgetText(head).join(' ')}`}`)
|
|
64
|
+
if (x.error != null) out.push(` WARN ${x.error}`)
|
|
65
|
+
if (x.html == null) out.push(' not rendered yet')
|
|
66
|
+
else out.push(...indent(widgetText(body), ' '))
|
|
67
|
+
}
|
|
68
|
+
for (const e of a.widgets.errors) out.push(` WARN ${e}`)
|
|
69
|
+
}
|
|
70
|
+
out.push('processes', ...indent(renderGlobalProcesses(a.processes)))
|
|
71
|
+
if (a.daemon == null) out.push(` the daemon does not answer on ${daemonUrl()}`)
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class OverviewCommand extends Command {
|
|
76
|
+
static override paths = [['overview']]
|
|
77
|
+
|
|
78
|
+
static override usage = Command.Usage({
|
|
79
|
+
category: 'Home',
|
|
80
|
+
description: 'the overview page in the console: every monitor widget as text, from the html the daemon last rendered, then the processes as `shipkit gp` prints them (pm, the daemon and the web with pid, uptime and version, then every global process). When pm or the daemon does not answer, it says which and prints what is still known. Always exits 0',
|
|
81
|
+
examples: [
|
|
82
|
+
['the overview', 'shipkit overview'],
|
|
83
|
+
['the answers the page reads, as json', 'shipkit overview --json'],
|
|
84
|
+
],
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
json = Option.Boolean('--json', false, { description: 'the answers as json: { now, pm, daemon, processes, widgets }, processes as `shipkit gp --json`, widgets the daemon route\'s payload' })
|
|
88
|
+
|
|
89
|
+
async execute(): Promise<number> {
|
|
90
|
+
const a = await overviewAnswer({ ...localProcessDeps(process.cwd()), pm: () => askPm(), daemon: () => askDaemon(), widgets: () => askWidgets(), now: Date.now() })
|
|
91
|
+
if (this.json) {
|
|
92
|
+
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
for (const l of renderOverview(a)) this.context.stdout.write(`${l}\n`)
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
}
|