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
package/src/serve/other-repos.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
// The OTHER house repos on this disk, as
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
// most every LIST_TTL_MS, and a repo's record is opened only when its file or
|
|
8
|
-
// its write ahead log moved since the last read.
|
|
9
|
-
import { existsSync, realpathSync } from 'fs'
|
|
1
|
+
// The OTHER house repos on this disk, as a hub serving on its own lists them
|
|
2
|
+
// for the dock (house-runs.ts). Which repos count is houseRepos
|
|
3
|
+
// (project-gutter.ts), the same discovery the project gutter lists, minus
|
|
4
|
+
// this repo. The list is read at most every LIST_TTL_MS, since the dock asks
|
|
5
|
+
// every few seconds.
|
|
6
|
+
import { realpathSync } from 'fs'
|
|
10
7
|
import { homedir } from 'os'
|
|
11
|
-
import type
|
|
12
|
-
import { readLiveReadonly } from '../ledger/run-log.ts'
|
|
13
|
-
import { houseRepos, hubPortOf, recordOf, recordStamp, type WiredRepo } from './project-gutter.ts'
|
|
8
|
+
import { houseRepos, type WiredRepo } from './project-gutter.ts'
|
|
14
9
|
|
|
15
10
|
/** how long the list of repos is trusted before the dev root is read again */
|
|
16
11
|
export const LIST_TTL_MS = 30_000
|
|
@@ -30,11 +25,10 @@ export function otherHouseRepos(root: string, userHome: string = homedir()): Wir
|
|
|
30
25
|
return repos.filter((r) => real(r.path) !== me)
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
/** the other repos
|
|
28
|
+
/** the other repos, the list re-read at most every LIST_TTL_MS */
|
|
34
29
|
export class OtherRepos {
|
|
35
30
|
private list: WiredRepo[] = []
|
|
36
31
|
private listedAt = -Infinity
|
|
37
|
-
private rows = new Map<string, { stamp: string; rows: LiveRow[] }>()
|
|
38
32
|
|
|
39
33
|
constructor(private root: string, private now: () => number = Date.now, private userHome: string = homedir()) {}
|
|
40
34
|
|
|
@@ -45,35 +39,4 @@ export class OtherRepos {
|
|
|
45
39
|
}
|
|
46
40
|
return this.list
|
|
47
41
|
}
|
|
48
|
-
|
|
49
|
-
/** every repo's live rows, as its record holds them */
|
|
50
|
-
read(): { repo: string; rows: LiveRow[] }[] {
|
|
51
|
-
const out: { repo: string; rows: LiveRow[] }[] = []
|
|
52
|
-
for (const r of this.repos()) {
|
|
53
|
-
const path = recordOf(r.path)
|
|
54
|
-
if (path == null || !existsSync(path)) continue
|
|
55
|
-
const stamp = recordStamp(path)
|
|
56
|
-
let hit = this.rows.get(r.path)
|
|
57
|
-
if (hit == null || hit.stamp !== stamp) {
|
|
58
|
-
hit = { stamp, rows: readLiveReadonly(path) }
|
|
59
|
-
this.rows.set(r.path, hit)
|
|
60
|
-
}
|
|
61
|
-
out.push({ repo: r.name, rows: hit.rows })
|
|
62
|
-
}
|
|
63
|
-
return out
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** a repo's hub url when it serves, checked by asking it who it is */
|
|
67
|
-
async hubOf(repo: string): Promise<string | null> {
|
|
68
|
-
const r = this.repos().find((x) => x.name === repo)
|
|
69
|
-
if (r == null) return null
|
|
70
|
-
const url = `http://localhost:${hubPortOf(r.path)}`
|
|
71
|
-
try {
|
|
72
|
-
const res = await fetch(`${url}/hub.json`, { signal: AbortSignal.timeout(500) })
|
|
73
|
-
const j = (await res.json()) as { hub?: string; root?: string }
|
|
74
|
-
return j.hub === 'shipkit' && j.root != null && real(j.root) === real(r.path) ? url : null
|
|
75
|
-
} catch {
|
|
76
|
-
return null
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
42
|
}
|
|
@@ -5,20 +5,23 @@
|
|
|
5
5
|
// the index moved, a few repos per poll, and a repo's run record is opened
|
|
6
6
|
// only when it moved, so a steady poll spawns ps and lsof and nothing else. The icon route
|
|
7
7
|
// serves only the icon files a discovered repo carries, and POST
|
|
8
|
-
// /api/projects/open opens a discovered repo in VS Code
|
|
9
|
-
// repo, never a path. The rail ORDER is one text file for the whole machine
|
|
8
|
+
// /api/projects/open opens a discovered repo in VS Code, POST
|
|
9
|
+
// /api/projects/fork in Fork: a request names a repo, never a path. The rail ORDER is one text file for the whole machine
|
|
10
10
|
// (ORDER_FILE), so every hub shows the same order and a hand edit is seen
|
|
11
11
|
// within one poll; POST /api/projects/order writes it.
|
|
12
|
-
import { mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'fs'
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'fs'
|
|
13
13
|
import { readFile, stat } from 'fs/promises'
|
|
14
14
|
import { homedir } from 'os'
|
|
15
15
|
import { dirname, join } from 'path'
|
|
16
16
|
import {
|
|
17
|
-
asksDirOf, branchOf, countSessions, descriptionOf, dirtyCount,
|
|
17
|
+
asksDirOf, branchOf, countSessions, descriptionOf, dirtyCount, gateFrom, checkStateOf, gitStamp, houseRepos, iconStamp, ICON_TYPES, lastActivity,
|
|
18
18
|
lastRunAt, lastSessionAt, liveUrlOf, mergeOrder, openAsks, openFolders, parseClaudePids, parseDescribe, parseLsofCwd, parseOrder,
|
|
19
|
-
recordOf, recordStamp, RELEASE_TAGS, renderOrder, versionOf, vscodeStoragePath, type Gate, type WiredRepo,
|
|
19
|
+
recordOf, recordStamp, RELEASE_TAGS, renderOrder, versionOf, vscodeStoragePath, type CheckState, type Gate, type LastRow, type WiredRepo,
|
|
20
20
|
} from './project-gutter.ts'
|
|
21
21
|
import { readLastStatusReadonly } from '../ledger/run-log.ts'
|
|
22
|
+
import { identityOfText, identityProblem, type Identity } from '../core/identity-color.ts'
|
|
23
|
+
import { writeIdentity } from '../core/identity.ts'
|
|
24
|
+
import { writeArchived } from '../core/archived.ts'
|
|
22
25
|
|
|
23
26
|
export type { Gate }
|
|
24
27
|
|
|
@@ -60,14 +63,23 @@ export type ProjectRow = {
|
|
|
60
63
|
dirty: number | null
|
|
61
64
|
/** the last gate its own run record holds */
|
|
62
65
|
gate: Gate
|
|
66
|
+
/** every check's last outcome as one colour, warnings included */
|
|
67
|
+
checks: CheckState
|
|
63
68
|
release: Release
|
|
64
69
|
/** the deployed url `smoke.base` declares, or null */
|
|
65
70
|
live: string | null
|
|
71
|
+
/** the colours its repo.config.ts declares (SK024), null when it declares none */
|
|
72
|
+
identity: Identity | null
|
|
66
73
|
/** epoch ms of the newest of: the last commit, the last shipkit run, the
|
|
67
74
|
* newest Claude Code transcript. null when none of them exists */
|
|
68
75
|
activity: number | null
|
|
76
|
+
/** epoch ms of the last commit on HEAD, null when git cannot say */
|
|
77
|
+
commit: number | null
|
|
69
78
|
}
|
|
70
|
-
|
|
79
|
+
/** a repo whose repo.config.ts says `archived: true`: only the archived page lists it */
|
|
80
|
+
export type ArchivedRow = { name: string; icon: string | null }
|
|
81
|
+
|
|
82
|
+
export type ProjectsAnswer = { projects: ProjectRow[]; archived: ArchivedRow[]; ms: number; at: number }
|
|
71
83
|
|
|
72
84
|
/** a command's exit code and stdout, or null when its binary is missing */
|
|
73
85
|
export type Run = (cmd: string[], cwd?: string) => Promise<{ code: number; out: string } | null>
|
|
@@ -79,12 +91,18 @@ export type GutterDeps = {
|
|
|
79
91
|
run?: Run
|
|
80
92
|
/** opens a folder in VS Code; throws with the reason it could not */
|
|
81
93
|
open?: (path: string) => void
|
|
94
|
+
/** opens a folder in Fork, the git client; throws with the reason it could not */
|
|
95
|
+
fork?: (path: string) => void
|
|
82
96
|
warn?: (line: string) => void
|
|
83
97
|
now?: () => number
|
|
84
98
|
/** where the rail order lives, default ~/ORDER_FILE */
|
|
85
99
|
orderFile?: string
|
|
86
100
|
/** the Claude Code `projects` folders, default the `projects` folder of every ~/.claude config root */
|
|
87
101
|
transcriptRoots?: string[]
|
|
102
|
+
/** the check ids a repo declares today (the daemon's plan), so a check
|
|
103
|
+
* it no longer declares never colours it. Absent or null = not known,
|
|
104
|
+
* every recorded check counts */
|
|
105
|
+
declared?: (name: string) => ReadonlySet<string> | null
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
const runCmd: Run = async (cmd, cwd) => {
|
|
@@ -107,6 +125,21 @@ function openInCode(path: string): void {
|
|
|
107
125
|
throw new Error('the `code` command is not on PATH: in VS Code, run "Shell Command: Install \'code\' command in PATH"')
|
|
108
126
|
}
|
|
109
127
|
|
|
128
|
+
/** where Fork is installed, the places macOS keeps apps */
|
|
129
|
+
export const FORK_APPS = ['/Applications/Fork.app', join(homedir(), 'Applications', 'Fork.app')]
|
|
130
|
+
|
|
131
|
+
/** the command that opens a folder in Fork; throws when Fork is not installed */
|
|
132
|
+
export function forkCommand(path: string, installed: (app: string) => boolean = existsSync, platform: string = process.platform): string[] {
|
|
133
|
+
if (platform !== 'darwin') throw new Error('opening in Fork is macOS only here')
|
|
134
|
+
const app = FORK_APPS.find(installed)
|
|
135
|
+
if (app == null) throw new Error(`Fork is not installed (looked in ${FORK_APPS.join(', ')})`)
|
|
136
|
+
return ['open', '-a', app, path]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function openInFork(path: string): void {
|
|
140
|
+
Bun.spawn(forkCommand(path), { stdio: ['ignore', 'ignore', 'ignore'] }).unref()
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
const epochMs = (out: string | undefined): number | null => {
|
|
111
144
|
const secs = Number(out?.trim())
|
|
112
145
|
return Number.isFinite(secs) && secs > 0 ? secs * 1000 : null
|
|
@@ -126,6 +159,15 @@ const readText = (file: string): string => {
|
|
|
126
159
|
}
|
|
127
160
|
}
|
|
128
161
|
|
|
162
|
+
/** the house repos in the rail's order, archived ones left out: the rows of
|
|
163
|
+
* the project list without the facts the list measures, for a caller that
|
|
164
|
+
* needs only names and paths (`shipkit cross-repo-activity`) */
|
|
165
|
+
export function railRepos(root: string, userHome: string = homedir(), orderFile: string = join(userHome, ORDER_FILE)): WiredRepo[] {
|
|
166
|
+
const repos = houseRepos(root, userHome).repos
|
|
167
|
+
const by = new Map(repos.map((r) => [r.name, r]))
|
|
168
|
+
return mergeOrder(parseOrder(readText(orderFile)), repos.map((r) => r.name)).map((n) => by.get(n)!)
|
|
169
|
+
}
|
|
170
|
+
|
|
129
171
|
export function projectGutter(d: GutterDeps) {
|
|
130
172
|
let home = d.root
|
|
131
173
|
const userHome = d.home ?? homedir()
|
|
@@ -141,21 +183,25 @@ export function projectGutter(d: GutterDeps) {
|
|
|
141
183
|
}
|
|
142
184
|
|
|
143
185
|
let repos: WiredRepo[] = []
|
|
186
|
+
let archivedRepos: WiredRepo[] = []
|
|
144
187
|
let last: ProjectsAnswer | null = null
|
|
145
188
|
let inFlight: Promise<ProjectsAnswer> | null = null
|
|
146
189
|
let folders: { mtime: number; list: string[] } | null = null
|
|
147
190
|
const releases = new Map<string, { stamp: string | null; at: number; release: Release; commitAt: number | null; dirty: number | null }>()
|
|
148
|
-
const
|
|
149
|
-
/** a repo's last gate, its record opened only when it
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
191
|
+
const records = new Map<string, { stamp: string; rows: LastRow[] }>()
|
|
192
|
+
/** a repo's last gate and check colour, its record opened only when it
|
|
193
|
+
* moved, counted over the checks it declares today */
|
|
194
|
+
const gateOfRepo = (r: WiredRepo): { gate: Gate; checks: CheckState } => {
|
|
195
|
+
const record = recordOf(r.path)
|
|
196
|
+
if (record == null) return { gate: null, checks: null }
|
|
153
197
|
const stamp = recordStamp(record)
|
|
154
|
-
|
|
155
|
-
if (hit
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
198
|
+
let hit = records.get(r.path)
|
|
199
|
+
if (hit == null || hit.stamp !== stamp) {
|
|
200
|
+
hit = { stamp, rows: readLastStatusReadonly(record) }
|
|
201
|
+
records.set(r.path, hit)
|
|
202
|
+
}
|
|
203
|
+
const checks = checkStateOf(hit.rows, d.declared?.(r.name) ?? null)
|
|
204
|
+
return { gate: gateFrom(checks), checks }
|
|
159
205
|
}
|
|
160
206
|
const orderFile = d.orderFile ?? join(userHome, ORDER_FILE)
|
|
161
207
|
const transcriptRoots = (): string[] => {
|
|
@@ -247,12 +293,15 @@ export function projectGutter(d: GutterDeps) {
|
|
|
247
293
|
return paths.map((p) => ({ release: releases.get(p)?.release ?? null, commitAt: releases.get(p)?.commitAt ?? null, dirty: releases.get(p)?.dirty ?? null }))
|
|
248
294
|
}
|
|
249
295
|
|
|
296
|
+
const iconUrl = (r: WiredRepo): string | null => (r.icon == null ? null : `/api/projects/icon/${encodeURIComponent(r.name)}?v=${iconStamp(join(r.path, r.icon))}`)
|
|
297
|
+
|
|
250
298
|
async function measure(): Promise<ProjectsAnswer> {
|
|
251
299
|
const t0 = performance.now()
|
|
252
300
|
const house = houseRepos(d.root, userHome)
|
|
253
301
|
home = house.home
|
|
254
302
|
if (house.index == null) warnOnce('house', `WARN project gutter: ${home} includes no house wiki index, so no sibling shares one and the gutter is empty`)
|
|
255
303
|
repos = house.repos
|
|
304
|
+
archivedRepos = house.archived
|
|
256
305
|
const [open, cwds, facts] = await Promise.all([vscodeFolders(), sessionCwds(), gitFacts(repos.map((r) => r.path))])
|
|
257
306
|
const transcripts = lastSessionAt(transcriptRoots(), repos.map((r) => r.path))
|
|
258
307
|
const counts = cwds == null ? null : countSessions(cwds, repos.map((r) => r.path))
|
|
@@ -262,10 +311,11 @@ export function projectGutter(d: GutterDeps) {
|
|
|
262
311
|
const asks = openAsks(join(r.path, asksDirOf(config, r.path)))
|
|
263
312
|
// the README is read only when package.json says nothing
|
|
264
313
|
const description = descriptionOf(pkg, '') ?? descriptionOf('', readText(join(r.path, 'README.md')))
|
|
314
|
+
const state = gateOfRepo(r)
|
|
265
315
|
return {
|
|
266
316
|
name: r.name,
|
|
267
317
|
path: r.path,
|
|
268
|
-
icon:
|
|
318
|
+
icon: iconUrl(r),
|
|
269
319
|
current: r.path === home,
|
|
270
320
|
vscode: open == null ? null : open.some((f) => f === r.path),
|
|
271
321
|
sessions: counts?.get(r.path) ?? null,
|
|
@@ -275,13 +325,17 @@ export function projectGutter(d: GutterDeps) {
|
|
|
275
325
|
version: versionOf(pkg),
|
|
276
326
|
branch: branchOf(readText(join(r.path, '.git', 'HEAD'))),
|
|
277
327
|
dirty: facts[i]!.dirty,
|
|
278
|
-
gate:
|
|
328
|
+
gate: state.gate,
|
|
329
|
+
checks: state.checks,
|
|
279
330
|
release: facts[i]!.release,
|
|
280
331
|
live: liveUrlOf(config),
|
|
332
|
+
identity: identityOfText(config),
|
|
281
333
|
activity: lastActivity(facts[i]!.commitAt, lastRunAt(r.path), transcripts.get(r.path)),
|
|
334
|
+
commit: facts[i]!.commitAt,
|
|
282
335
|
}
|
|
283
336
|
})
|
|
284
|
-
|
|
337
|
+
const archived = archivedRepos.map((r) => ({ name: r.name, icon: iconUrl(r) }))
|
|
338
|
+
return { projects: inOrder(projects, parseOrder(readText(orderFile))), archived, ms: Math.round(performance.now() - t0), at: now() }
|
|
285
339
|
}
|
|
286
340
|
|
|
287
341
|
/** the last answer while it is younger than POLL_MS, one measure otherwise,
|
|
@@ -296,6 +350,8 @@ export function projectGutter(d: GutterDeps) {
|
|
|
296
350
|
}
|
|
297
351
|
|
|
298
352
|
const find = (name: string): WiredRepo | undefined => repos.find((r) => r.name === name)
|
|
353
|
+
/** a live repo or an archived one: the icon and the archive toggle reach both */
|
|
354
|
+
const findAny = (name: string): WiredRepo | undefined => find(name) ?? archivedRepos.find((r) => r.name === name)
|
|
299
355
|
|
|
300
356
|
/** saves the rail order: the names given, then every discovered repo they
|
|
301
357
|
* leave out. Written whole through a temp file, so a reader never sees
|
|
@@ -315,12 +371,43 @@ export function projectGutter(d: GutterDeps) {
|
|
|
315
371
|
if (path === '/api/projects.json') return Response.json(await answer())
|
|
316
372
|
if (path.startsWith('/api/projects/icon/')) {
|
|
317
373
|
if (repos.length === 0) await answer()
|
|
318
|
-
const r =
|
|
374
|
+
const r = findAny(decodeURIComponent(path.slice('/api/projects/icon/'.length)))
|
|
319
375
|
if (r?.icon == null) return new Response('no icon', { status: 404 })
|
|
320
376
|
return new Response(Bun.file(join(r.path, r.icon)), {
|
|
321
377
|
headers: { 'content-type': ICON_TYPES[r.icon.split('.').pop()!] ?? 'application/octet-stream', 'cache-control': 'max-age=86400' },
|
|
322
378
|
})
|
|
323
379
|
}
|
|
380
|
+
if (path === '/api/projects/identity' && req.method === 'POST') {
|
|
381
|
+
if (repos.length === 0) await answer()
|
|
382
|
+
const b = (await req.json().catch(() => ({}))) as { name?: unknown; colorLight?: unknown; colorDark?: unknown }
|
|
383
|
+
const r = find(String(b.name))
|
|
384
|
+
if (r == null) return new Response(`not a house repo: ${String(b.name)}`, { status: 404 })
|
|
385
|
+
// a colour left out keeps the config's own, so only the sent ones are checked
|
|
386
|
+
const bad = identityProblem({ colorLight: b.colorLight ?? '#000000', colorDark: b.colorDark ?? '#000000' })
|
|
387
|
+
if (bad != null) return new Response(bad, { status: 400 })
|
|
388
|
+
try {
|
|
389
|
+
const id = writeIdentity(r.path, { colorLight: b.colorLight as string | undefined, colorDark: b.colorDark as string | undefined })
|
|
390
|
+
if (last != null) last = { ...last, projects: last.projects.map((p) => (p.name === r.name ? { ...p, identity: id } : p)) }
|
|
391
|
+
return Response.json({ identity: id })
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return new Response((e as Error).message, { status: 409 })
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (path === '/api/projects/archived' && req.method === 'POST') {
|
|
397
|
+
if (repos.length === 0) await answer()
|
|
398
|
+
const b = (await req.json().catch(() => ({}))) as { name?: unknown; archived?: unknown }
|
|
399
|
+
const r = findAny(String(b.name))
|
|
400
|
+
if (r == null) return new Response(`not a house repo: ${String(b.name)}`, { status: 404 })
|
|
401
|
+
if (typeof b.archived !== 'boolean') return new Response('archived must be true or false', { status: 400 })
|
|
402
|
+
try {
|
|
403
|
+
const on = writeArchived(r.path, b.archived)
|
|
404
|
+
// the repo moves between the two lists: the next answer measures again
|
|
405
|
+
last = null
|
|
406
|
+
return Response.json({ archived: on })
|
|
407
|
+
} catch (e) {
|
|
408
|
+
return new Response((e as Error).message, { status: 409 })
|
|
409
|
+
}
|
|
410
|
+
}
|
|
324
411
|
if (path === '/api/projects/order' && req.method === 'POST') {
|
|
325
412
|
if (repos.length === 0) await answer()
|
|
326
413
|
const b = (await req.json().catch(() => ({}))) as { names?: unknown }
|
|
@@ -331,6 +418,20 @@ export function projectGutter(d: GutterDeps) {
|
|
|
331
418
|
return new Response(`the order could not be saved to ${orderFile}: ${(e as Error).message}`, { status: 500 })
|
|
332
419
|
}
|
|
333
420
|
}
|
|
421
|
+
if (path === '/api/projects/fork' && req.method === 'POST') {
|
|
422
|
+
if (repos.length === 0) await answer()
|
|
423
|
+
const b = (await req.json().catch(() => ({}))) as { name?: unknown }
|
|
424
|
+
const name = typeof b.name === 'string' ? b.name : ''
|
|
425
|
+
if (archivedRepos.some((r) => r.name === name)) return new Response(`${name} is archived: unarchive it first`, { status: 409 })
|
|
426
|
+
const r = find(name)
|
|
427
|
+
if (r == null) return new Response(`not a house repo: ${JSON.stringify(b.name ?? '')}`, { status: 404 })
|
|
428
|
+
try {
|
|
429
|
+
;(d.fork ?? openInFork)(r.path)
|
|
430
|
+
} catch (e) {
|
|
431
|
+
return new Response((e as Error).message, { status: 400 })
|
|
432
|
+
}
|
|
433
|
+
return Response.json({ ok: true })
|
|
434
|
+
}
|
|
334
435
|
if (path === '/api/projects/open' && req.method === 'POST') {
|
|
335
436
|
if (repos.length === 0) await answer()
|
|
336
437
|
const b = (await req.json().catch(() => ({}))) as { name?: string }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// read of each one: which folders VS Code has open, which Claude Code
|
|
3
3
|
// sessions run in which repo, its open asks, its distance to the last
|
|
4
4
|
// release, its declared live url, its run record and its hub port. The
|
|
5
|
-
// project gutter and the
|
|
5
|
+
// project gutter and the dock both list repos through houseRepos, so
|
|
6
6
|
// the two surfaces show the same repos by construction. The parsers read
|
|
7
7
|
// text they are handed, so sample output pins them; project-gutter-route.ts
|
|
8
8
|
// and other-repos.ts own the spawns, the reads and the caches.
|
|
@@ -13,6 +13,7 @@ import { gitRoot } from '../core/git-root.ts'
|
|
|
13
13
|
import { DB_FILE } from '../ledger/db.ts'
|
|
14
14
|
import { defaultHubPort } from './hub-port.ts'
|
|
15
15
|
import { LAYOUT } from '../core/layout.ts'
|
|
16
|
+
import { isArchived } from '../core/archived.ts'
|
|
16
17
|
|
|
17
18
|
export type WiredRepo = { name: string; path: string; icon: string | null }
|
|
18
19
|
|
|
@@ -72,11 +73,16 @@ export function wiredRepos(parent: string, index: string, home: string): WiredRe
|
|
|
72
73
|
|
|
73
74
|
/** the house repos around the repo at `root`: its main checkout (`home`),
|
|
74
75
|
* the house index its own includes name, and every sibling that includes the
|
|
75
|
-
* same index, `home` among them. No index = no repos
|
|
76
|
-
|
|
76
|
+
* same index, `home` among them. No index = no repos. An archived repo
|
|
77
|
+
* (`archived: true`, core/archived.ts) is never in `repos`, so nothing that
|
|
78
|
+
* lists through here processes it: it is in `archived`, which only the
|
|
79
|
+
* archived page reads */
|
|
80
|
+
export function houseRepos(root: string, userHome: string): { home: string; index: string | null; repos: WiredRepo[]; archived: WiredRepo[] } {
|
|
77
81
|
const home = homeRepo(root)
|
|
78
82
|
const index = houseIndex(home, userHome)
|
|
79
|
-
|
|
83
|
+
const all = index == null ? [] : wiredRepos(dirname(home), index, userHome)
|
|
84
|
+
const archived = all.filter((r) => isArchived(r.path))
|
|
85
|
+
return { home, index, repos: all.filter((r) => !archived.includes(r)), archived }
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
/** the checkout a linked worktree belongs to, so a hub started in a worktree
|
|
@@ -289,15 +295,31 @@ export function branchOf(head: string): string | null {
|
|
|
289
295
|
/** `git status --porcelain`: one line per changed or untracked path */
|
|
290
296
|
export const dirtyCount = (porcelain: string): number => porcelain.split('\n').filter((l) => l.trim() !== '').length
|
|
291
297
|
|
|
292
|
-
/**
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
/** a check's last outcome as the run record keeps it */
|
|
299
|
+
export type LastRow = { id: string; status: string; at?: number }
|
|
300
|
+
|
|
301
|
+
/** every check's last outcome in one colour: red when one failed, orange
|
|
302
|
+
* when one only warned, green when all passed or skipped, with when each
|
|
303
|
+
* red or warning was recorded. Only the ids the repo declares today count
|
|
304
|
+
* (`declared`; null = not known, every row counts), so a renamed or deleted
|
|
305
|
+
* check never holds a repo red. No row left = unknown */
|
|
306
|
+
export type CheckState = { state: 'red' | 'orange' | 'green'; reds: string[]; warns: string[]; since: Record<string, number> } | null
|
|
307
|
+
export function checkStateOf(rows: LastRow[], declared: ReadonlySet<string> | null = null): CheckState {
|
|
308
|
+
const live = declared == null ? rows : rows.filter((r) => declared.has(r.id))
|
|
309
|
+
if (live.length === 0) return null
|
|
310
|
+
const reds = live.filter((r) => r.status === 'fail').map((r) => r.id).sort()
|
|
311
|
+
const warns = live.filter((r) => r.status === 'warn').map((r) => r.id).sort()
|
|
312
|
+
const since: Record<string, number> = {}
|
|
313
|
+
for (const r of live) if ((r.status === 'fail' || r.status === 'warn') && typeof r.at === 'number') since[r.id] = r.at
|
|
314
|
+
return { state: reds.length > 0 ? 'red' : warns.length > 0 ? 'orange' : 'green', reds, warns, since }
|
|
299
315
|
}
|
|
300
316
|
|
|
317
|
+
/** the last gate: green, or the checks whose last outcome failed, read off
|
|
318
|
+
* the same state. Unknown when no declared row is left */
|
|
319
|
+
export type Gate = 'green' | { reds: string[] } | null
|
|
320
|
+
export const gateFrom = (c: CheckState): Gate => (c == null ? null : c.reds.length === 0 ? 'green' : { reds: c.reds })
|
|
321
|
+
export const gateOf = (rows: LastRow[], declared: ReadonlySet<string> | null = null): Gate => gateFrom(checkStateOf(rows, declared))
|
|
322
|
+
|
|
301
323
|
/** the tag shapes a release leaves: `v1.2.3`, `name@1.2.3` (the shipkit
|
|
302
324
|
* defaults) and a bare `1.2.3`. A date or a merge marker is no release */
|
|
303
325
|
export const RELEASE_TAGS = ['v[0-9]*', '*@[0-9]*', '[0-9]*.[0-9]*']
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// the repo's README for the hub page's empty state, and the images it points
|
|
2
|
+
// at. A file answers only when it is an image AND sits inside the repo root
|
|
3
|
+
// once symlinks resolve: the route must never become a way to read the disk
|
|
4
|
+
import { existsSync, realpathSync, statSync } from 'fs'
|
|
5
|
+
import { extname, isAbsolute, join, relative, resolve } from 'path'
|
|
6
|
+
|
|
7
|
+
const README_NAMES = ['README.md', 'readme.md', 'Readme.md', 'README.markdown', 'README']
|
|
8
|
+
|
|
9
|
+
const IMAGE_TYPES: Record<string, string> = {
|
|
10
|
+
'.png': 'image/png',
|
|
11
|
+
'.jpg': 'image/jpeg',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.gif': 'image/gif',
|
|
14
|
+
'.webp': 'image/webp',
|
|
15
|
+
'.svg': 'image/svg+xml',
|
|
16
|
+
'.avif': 'image/avif',
|
|
17
|
+
'.ico': 'image/x-icon',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** the README file name at the root, null when the repo has none */
|
|
21
|
+
export function findReadme(root: string): string | null {
|
|
22
|
+
return README_NAMES.find((n) => existsSync(join(root, n)) && statSync(join(root, n)).isFile()) ?? null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** an image under the root, or null for anything else: another type, a
|
|
26
|
+
* missing file, `..`, an absolute path, a symlink that leaves the root */
|
|
27
|
+
export function readmeAsset(root: string, rel: string): { path: string; type: string } | null {
|
|
28
|
+
if (rel === '' || isAbsolute(rel) || rel.includes('\0')) return null
|
|
29
|
+
const type = IMAGE_TYPES[extname(rel).toLowerCase()]
|
|
30
|
+
if (type == null) return null
|
|
31
|
+
const abs = resolve(root, rel)
|
|
32
|
+
if (!existsSync(abs)) return null
|
|
33
|
+
const real = realpathSync(abs)
|
|
34
|
+
const inside = relative(realpathSync(root), real)
|
|
35
|
+
if (inside === '' || inside.startsWith('..') || isAbsolute(inside)) return null
|
|
36
|
+
if (!statSync(real).isFile()) return null
|
|
37
|
+
return { path: real, type }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `/api/readme.json` and `/api/readme-file/<path>`, null for any other path */
|
|
41
|
+
export async function readmeRoutes(root: string, path: string): Promise<Response | null> {
|
|
42
|
+
if (path === '/api/readme.json') {
|
|
43
|
+
const file = findReadme(root)
|
|
44
|
+
return Response.json({ file, md: file == null ? null : await Bun.file(join(root, file)).text() })
|
|
45
|
+
}
|
|
46
|
+
const prefix = '/api/readme-file/'
|
|
47
|
+
if (!path.startsWith(prefix)) return null
|
|
48
|
+
let rel: string
|
|
49
|
+
try {
|
|
50
|
+
rel = decodeURIComponent(path.slice(prefix.length))
|
|
51
|
+
} catch {
|
|
52
|
+
return new Response('bad path encoding', { status: 400 })
|
|
53
|
+
}
|
|
54
|
+
const asset = readmeAsset(root, rel)
|
|
55
|
+
if (asset == null) return new Response(`not an image inside this repo: ${rel}`, { status: 404 })
|
|
56
|
+
return new Response(Bun.file(asset.path), { headers: { 'content-type': asset.type, 'cache-control': 'no-cache' } })
|
|
57
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// the long running processes a repo declares in `serve.processes` (a dev
|
|
2
|
+
// server, a watcher), supervised by its hub with the same supervisor the home
|
|
3
|
+
// uses for resources. Each runs detached with a pid record in the machine db
|
|
4
|
+
// (named by its path under .shipkit/tmp, where an older copy kept a pid file),
|
|
5
|
+
// so a hub restart in place adopts it, and only a real stop of the hub stops it.
|
|
6
|
+
import { join, resolve } from 'path'
|
|
7
|
+
import { Proc, realDeps, tailLines, type ProcView, type SuperviseDeps } from '../home/supervise.ts'
|
|
8
|
+
import type { ProcessDecl } from '../repo-config.ts'
|
|
9
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
10
|
+
|
|
11
|
+
export type ProcessView = ProcView & { url: string | null; auto: boolean; cmd: string; tail: string[] }
|
|
12
|
+
|
|
13
|
+
export const processDir = (root: string): string => join(root, LAYOUT.tmp, 'processes')
|
|
14
|
+
|
|
15
|
+
const probeUrl = async (url: string): Promise<boolean> => {
|
|
16
|
+
try {
|
|
17
|
+
// any answer means it serves: a dev server may 404 its root
|
|
18
|
+
await fetch(url, { signal: AbortSignal.timeout(1_500) })
|
|
19
|
+
return true
|
|
20
|
+
} catch {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class RepoProcesses {
|
|
26
|
+
private procs: { decl: ProcessDecl; proc: Proc }[]
|
|
27
|
+
|
|
28
|
+
constructor(root: string, decls: ProcessDecl[], o: { deps?: SuperviseDeps; probe?: (url: string) => Promise<boolean> } = {}) {
|
|
29
|
+
const dir = processDir(root)
|
|
30
|
+
this.procs = decls.map((decl) => ({
|
|
31
|
+
decl,
|
|
32
|
+
proc: new Proc({
|
|
33
|
+
id: decl.id,
|
|
34
|
+
cmd: decl.cmd,
|
|
35
|
+
cwd: resolve(root, decl.cwd ?? '.'),
|
|
36
|
+
log: join(dir, `${decl.id}.log`),
|
|
37
|
+
pidKey: join(dir, `${decl.id}.pid`),
|
|
38
|
+
probe: decl.url == null ? undefined : () => (o.probe ?? probeUrl)(decl.url!),
|
|
39
|
+
}, o.deps ?? realDeps),
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** the auto ones start with the hub; an adopted one is left as it runs */
|
|
44
|
+
async startAuto(): Promise<void> {
|
|
45
|
+
await Promise.all(this.procs.filter((p) => p.decl.auto === true && p.proc.pid == null).map((p) => p.proc.ensure()))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
act(id: string, act: 'start' | 'stop' | 'restart'): ProcessView | null {
|
|
49
|
+
const p = this.procs.find((x) => x.decl.id === id)
|
|
50
|
+
if (p == null) return null
|
|
51
|
+
if (act === 'stop' || act === 'restart') p.proc.stop()
|
|
52
|
+
if (act === 'start') void p.proc.ensure()
|
|
53
|
+
if (act === 'restart') this.restartWhenGone(p.proc)
|
|
54
|
+
return this.view(p)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private restartWhenGone(proc: Proc): void {
|
|
58
|
+
const wait = async (): Promise<void> => {
|
|
59
|
+
for (let i = 0; i < 40 && proc.status === 'stopping'; i++) {
|
|
60
|
+
await Bun.sleep(250)
|
|
61
|
+
await proc.tick()
|
|
62
|
+
}
|
|
63
|
+
proc.start()
|
|
64
|
+
}
|
|
65
|
+
void wait()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async tick(): Promise<void> {
|
|
69
|
+
await Promise.all(this.procs.map((p) => p.proc.tick()))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** a real stop of the hub: every process it started stops with it */
|
|
73
|
+
stopAll(): void {
|
|
74
|
+
for (const p of this.procs) if (p.proc.pid != null) p.proc.stop()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private view(p: { decl: ProcessDecl; proc: Proc }): ProcessView {
|
|
78
|
+
const v = p.proc.view()
|
|
79
|
+
return { ...v, url: p.decl.url ?? null, auto: p.decl.auto === true, cmd: p.decl.cmd.join(' '), tail: v.status === 'up' ? [] : tailLines(p.proc.spec.log, 20) }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
views(): ProcessView[] {
|
|
83
|
+
return this.procs.map((p) => this.view(p))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get size(): number {
|
|
87
|
+
return this.procs.length
|
|
88
|
+
}
|
|
89
|
+
}
|