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,117 @@
|
|
|
1
|
+
// shipkit planned: the planned page in the console. The plan lives in the
|
|
2
|
+
// daemon (its marks, the running batch, the catch up spread all exist only
|
|
3
|
+
// there), so this asks the daemon's route, the one the page polls, and only
|
|
4
|
+
// renders it: the order and every word come from plan-timeline.ts, the page's
|
|
5
|
+
// own functions. Nothing is computed from disk: a plan rebuilt here would load
|
|
6
|
+
// every repo's checks and still not know what the daemon's plan holds.
|
|
7
|
+
import { Command, Option } from 'clipanion'
|
|
8
|
+
import { daemonUrl } from '../home/home-paths.ts'
|
|
9
|
+
import type { PlannedAnswer } from '../home/home-routes.ts'
|
|
10
|
+
import { markOf, spanShort, timeline, titleWords, waitingRows, waitingWords, whenWords, type Section } from '../plan/plan-timeline.ts'
|
|
11
|
+
import type { PlanRow } from '../plan/planner.ts'
|
|
12
|
+
|
|
13
|
+
/** the daemon's planned answer, or null when it does not answer */
|
|
14
|
+
export async function askPlanned(url = daemonUrl()): Promise<PlannedAnswer | null> {
|
|
15
|
+
try {
|
|
16
|
+
const r = await fetch(`${url}/api/home/planned.json`, { signal: AbortSignal.timeout(8_000) })
|
|
17
|
+
if (!r.ok) return null
|
|
18
|
+
return (await r.json()) as PlannedAnswer
|
|
19
|
+
} catch {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** the answer for one repo: its rows and its load error only */
|
|
25
|
+
export function onlyRepo(a: PlannedAnswer, repo: string): PlannedAnswer {
|
|
26
|
+
return { ...a, rows: a.rows.filter((r) => r.repo === repo), errors: a.errors.filter((e) => e.repo === repo) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const pad = (s: string, n: number): string => (s.length >= n ? s : s + ' '.repeat(n - s.length))
|
|
30
|
+
|
|
31
|
+
const lastWords = (r: PlanRow, now: number): string =>
|
|
32
|
+
r.last == null ? 'never ran' : `last ${r.last.status} ${spanShort(Math.max(0, now - r.last.at))} ago${r.last.streak > 1 ? ` (${r.last.streak} in a row)` : ''}`
|
|
33
|
+
|
|
34
|
+
const SECTION_WORDS: Record<Section, string> = { now: 'now', next: 'next' }
|
|
35
|
+
|
|
36
|
+
/** the page's one list as text: its three sections in its order, a line per
|
|
37
|
+
* row (when, repo, check and title, mark, last result) and the rule under
|
|
38
|
+
* it; the checks that only wait for a save are counted, or listed with `all` */
|
|
39
|
+
export function renderPlanned(a: PlannedAnswer, o: { all?: boolean } = {}): string[] {
|
|
40
|
+
const out: string[] = [...(a.held ?? [])]
|
|
41
|
+
const list = timeline(a.rows)
|
|
42
|
+
const idle = a.rows.filter((r) => r.state === 'idle')
|
|
43
|
+
const w = {
|
|
44
|
+
when: Math.max(0, ...list.map((x) => whenWords(x.row, a.now).length)),
|
|
45
|
+
what: Math.max(0, ...list.map((x) => `${x.row.repo} ${x.row.check}`.length)),
|
|
46
|
+
}
|
|
47
|
+
let prev: Section | null = null
|
|
48
|
+
for (const { section, row } of list) {
|
|
49
|
+
if (section !== prev) {
|
|
50
|
+
out.push(SECTION_WORDS[section])
|
|
51
|
+
prev = section
|
|
52
|
+
}
|
|
53
|
+
const m = markOf(row)
|
|
54
|
+
const title = titleWords(row.title, row.check)
|
|
55
|
+
const cells = [` ${pad(whenWords(row, a.now), w.when)}`, pad(`${row.repo} ${row.check}`, w.what), m.words, lastWords(row, a.now)]
|
|
56
|
+
out.push(cells.filter((c) => c !== '').join(' '))
|
|
57
|
+
out.push(` ${title == null ? '' : `${title} · `}${row.rule}`)
|
|
58
|
+
if (row.error != null) out.push(` error: ${row.error}`)
|
|
59
|
+
}
|
|
60
|
+
if (list.length === 0) out.push(a.rows.length === 0 ? 'nothing is planned' : 'nothing has a time of its own')
|
|
61
|
+
const waiting = waitingRows(a.rows)
|
|
62
|
+
if (waiting.length > 0) {
|
|
63
|
+
out.push(`${waitingWords(a.rows)}${o.all === true ? '' : ' (--all lists them)'}`)
|
|
64
|
+
if (o.all === true) for (const r of waiting) out.push(` ${r.repo} ${r.check} ${lastWords(r, a.now)} · ${r.rule}`)
|
|
65
|
+
}
|
|
66
|
+
if (o.all === true && idle.length > 0) {
|
|
67
|
+
out.push('on a save only')
|
|
68
|
+
for (const r of [...idle].sort((x, y) => x.repo.localeCompare(y.repo) || x.check.localeCompare(y.check))) out.push(` ${r.repo} ${r.check} ${lastWords(r, a.now)} · ${r.rule}`)
|
|
69
|
+
}
|
|
70
|
+
for (const e of a.errors) out.push(`WARN ${e.repo}: ${e.error}`)
|
|
71
|
+
const n = { now: list.filter((x) => x.section === 'now').length, next: list.filter((x) => x.section === 'next').length }
|
|
72
|
+
out.push(`${n.now} now, ${n.next} next, ${waiting.length} waiting, ${idle.length} on a save only${o.all === true || idle.length === 0 ? '' : ' (--all lists them)'}`)
|
|
73
|
+
return out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class PlannedCommand extends Command {
|
|
77
|
+
static override paths = [['planned']]
|
|
78
|
+
|
|
79
|
+
static override usage = Command.Usage({
|
|
80
|
+
category: 'Home',
|
|
81
|
+
description: 'the planned page in the console: every check the machine runs on its own time, running now, then next, each with its repo, check and title, when, the rule that planned it and its last result. Asks the daemon, which holds the plan. With the daemon down it says so and exits 0',
|
|
82
|
+
examples: [
|
|
83
|
+
['the plan, as the page lists it', 'shipkit planned'],
|
|
84
|
+
['one repo only', 'shipkit planned --repo my-app'],
|
|
85
|
+
['also the checks that only a save runs', 'shipkit planned --all'],
|
|
86
|
+
['the route\'s exact payload', 'shipkit planned --json'],
|
|
87
|
+
],
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
json = Option.Boolean('--json', false, { description: 'the daemon route\'s exact payload: { now, rows, errors, running, held }' })
|
|
91
|
+
repo = Option.String('--repo', { required: false, description: 'only this repo\'s rows' })
|
|
92
|
+
all = Option.Boolean('--all', false, { description: 'also list the checks that only a save runs (the page leaves them out)' })
|
|
93
|
+
|
|
94
|
+
async execute(): Promise<number> {
|
|
95
|
+
const got = await askPlanned()
|
|
96
|
+
if (got == null) {
|
|
97
|
+
const why = `the daemon does not answer on ${daemonUrl()}, and the plan lives there: no plan is known (\`shipkit pm\` starts it)`
|
|
98
|
+
if (this.json) {
|
|
99
|
+
this.context.stdout.write('null\n')
|
|
100
|
+
this.context.stderr.write(`${why}\n`)
|
|
101
|
+
} else this.context.stdout.write(`${why}\n`)
|
|
102
|
+
return 0
|
|
103
|
+
}
|
|
104
|
+
const a = this.repo == null ? got : onlyRepo(got, this.repo)
|
|
105
|
+
if (this.json) {
|
|
106
|
+
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
if (this.repo != null && a.rows.length === 0 && a.errors.length === 0) {
|
|
110
|
+
const known = [...new Set(got.rows.map((r) => r.repo))].sort()
|
|
111
|
+
this.context.stdout.write(`no planned check in ${this.repo}; the plan holds ${known.join(', ') || 'no repo'}\n`)
|
|
112
|
+
return 0
|
|
113
|
+
}
|
|
114
|
+
for (const l of renderPlanned(a, { all: this.all })) this.context.stdout.write(`${l}\n`)
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -9,10 +9,15 @@ import { AssetsAddCommand, AssetsCheckCommand, AssetsFillCommand, AssetsGenComma
|
|
|
9
9
|
import { BumpCommand } from './cli/cmd_Bump.ts'
|
|
10
10
|
import { CheckCommand } from './cli/cmd_Check.ts'
|
|
11
11
|
import { CommandsCommand } from './cli/cmd_Commands.ts'
|
|
12
|
+
import { aliasArgs, CrossRepoActivityCommand } from './cli/cmd_CrossRepoActivity.ts'
|
|
12
13
|
import { ConfigKeysCommand } from './cli/cmd_ConfigKeys.ts'
|
|
14
|
+
import { GlobalProcessesCommand } from './cli/cmd_GlobalProcesses.ts'
|
|
15
|
+
import { PlannedCommand } from './cli/cmd_Planned.ts'
|
|
16
|
+
import { OverviewCommand } from './cli/cmd_Overview.ts'
|
|
13
17
|
import { CiCommand } from './cli/cmd_Ci.ts'
|
|
14
18
|
import { ContractsCommand } from './cli/cmd_Contracts.ts'
|
|
15
19
|
import { FleetCommand } from './cli/cmd_Fleet.ts'
|
|
20
|
+
import { DaemonCommand, GuiCommand, PmCommand, PmInstallCommand, PmUninstallCommand, WebCommand } from './cli/cmd_Home.ts'
|
|
16
21
|
import { HooksCommitMsgCommand, HooksInstallCommand, HooksListCommand, HooksPreCommitCommand } from './cli/cmd_Hooks.ts'
|
|
17
22
|
import { InitCommand } from './cli/cmd_Init.ts'
|
|
18
23
|
import { KeysCommand } from './cli/cmd_Keys.ts'
|
|
@@ -59,6 +64,12 @@ export function buildCli(): Cli {
|
|
|
59
64
|
cli.register(CheckCommand)
|
|
60
65
|
cli.register(TuiCommand)
|
|
61
66
|
cli.register(ServeCommand)
|
|
67
|
+
cli.register(PmCommand)
|
|
68
|
+
cli.register(DaemonCommand)
|
|
69
|
+
cli.register(WebCommand)
|
|
70
|
+
cli.register(PmInstallCommand)
|
|
71
|
+
cli.register(PmUninstallCommand)
|
|
72
|
+
cli.register(GuiCommand)
|
|
62
73
|
cli.register(ContractsCommand)
|
|
63
74
|
cli.register(AsksCommand)
|
|
64
75
|
cli.register(AsksCheckCommand)
|
|
@@ -79,6 +90,10 @@ export function buildCli(): Cli {
|
|
|
79
90
|
cli.register(SmokeCommand)
|
|
80
91
|
cli.register(ReflectCommand)
|
|
81
92
|
cli.register(KeysCommand)
|
|
93
|
+
cli.register(GlobalProcessesCommand)
|
|
94
|
+
cli.register(CrossRepoActivityCommand)
|
|
95
|
+
cli.register(PlannedCommand)
|
|
96
|
+
cli.register(OverviewCommand)
|
|
82
97
|
cli.register(ChecksCommand)
|
|
83
98
|
cli.register(UnitsAliasCommand)
|
|
84
99
|
cli.register(CommandsCommand)
|
|
@@ -87,5 +102,18 @@ export function buildCli(): Cli {
|
|
|
87
102
|
return cli
|
|
88
103
|
}
|
|
89
104
|
|
|
90
|
-
// only when this file IS the process, so importing it costs nothing
|
|
91
|
-
|
|
105
|
+
// only when this file IS the process, so importing it costs nothing. A repo's
|
|
106
|
+
// installed copy first hands the command to the house source (handoff.ts). One copy
|
|
107
|
+
// of the lib per process: a repo config that imports its own install lands on
|
|
108
|
+
// this one (one-lib.ts), so the machine's version runs in every repo. Then pm
|
|
109
|
+
// is started when it is down (autostart.ts), before the command runs
|
|
110
|
+
if (import.meta.main) {
|
|
111
|
+
const argv = aliasArgs(process.argv.slice(2))
|
|
112
|
+
const { handoff } = await import('./home/handoff.ts')
|
|
113
|
+
handoff(argv)
|
|
114
|
+
const { useOneLib } = await import('./core/one-lib.ts')
|
|
115
|
+
useOneLib()
|
|
116
|
+
const { autostart } = await import('./home/autostart.ts')
|
|
117
|
+
await autostart(argv)
|
|
118
|
+
void buildCli().runExit(argv)
|
|
119
|
+
}
|
|
@@ -18,7 +18,7 @@ import { homedir } from 'os'
|
|
|
18
18
|
import { isAbsolute, join, relative, resolve } from 'path'
|
|
19
19
|
import { rawName, routesToShipkit } from '../core/canonical-scripts.ts'
|
|
20
20
|
import { FLOOR_IDS } from '../core/floor.ts'
|
|
21
|
-
import { formatLine,
|
|
21
|
+
import { FOREVER_MS, formatLine, ttlMsOf } from '../core/wire.ts'
|
|
22
22
|
import { gateInventory, type CheckRow } from '../ledger/gate-checks.ts'
|
|
23
23
|
import { readLastLines, readRuns, runsPath, type LastLine, type RunRow } from '../ledger/run-log.ts'
|
|
24
24
|
import { checkStatsFor, type CheckStat } from '../ledger/check-stats.ts'
|
|
@@ -166,7 +166,8 @@ function instanceOf(root: string, c: RepoConfig, row: CheckRow, stat: CheckStat
|
|
|
166
166
|
export function keepsUntil(ttl: string | undefined, stat: Pick<CheckStat, 'measuredAt' | 'measuredStatus'> | undefined): number | null {
|
|
167
167
|
if (ttl == null || ttl === '' || stat?.measuredAt == null) return null
|
|
168
168
|
let ms = 0
|
|
169
|
-
try { ms =
|
|
169
|
+
try { ms = ttlMsOf(ttl) } catch { return null }
|
|
170
|
+
if (ms >= FOREVER_MS && stat.measuredStatus === 'pass') return null
|
|
170
171
|
if (ms <= 0) return null
|
|
171
172
|
return stat.measuredAt + (stat.measuredStatus === 'pass' ? ms : Math.min(ms, NEGATIVE_TTL_CAP_MS))
|
|
172
173
|
}
|
|
@@ -31,7 +31,7 @@ import { testsCollection } from './tests.ts'
|
|
|
31
31
|
import { checksCollection } from './checks.ts'
|
|
32
32
|
import { LAYOUT } from '../core/layout.ts'
|
|
33
33
|
import { isBudgetLine } from '../core/budget.ts'
|
|
34
|
-
import { GH_LOGIN_CRED } from '../
|
|
34
|
+
import { GH_LOGIN_CRED } from '../core/gh-login.ts'
|
|
35
35
|
|
|
36
36
|
const askDirOf = (root: string, c: RepoConfig): string => join(root, c.asks?.dir ?? LAYOUT.asks)
|
|
37
37
|
// read path must not mint an asks/ dir (listAsks mkdirs on read)
|
|
@@ -127,9 +127,15 @@ export type TestCheckRow = {
|
|
|
127
127
|
shape: Shape | null
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** one file of any test check, named with the check that runs it */
|
|
131
|
+
export type TestFileRow = FileTime & { check: string }
|
|
132
|
+
|
|
130
133
|
export type TestsRootView = {
|
|
131
134
|
/** slowest wall first, the never ran ones last */
|
|
132
135
|
checks: TestCheckRow[]
|
|
136
|
+
/** every file of every test check, slowest first, the never ran ones
|
|
137
|
+
* last: each check's own list, merged */
|
|
138
|
+
files: TestFileRow[]
|
|
133
139
|
failing: FailingFile[]
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -270,8 +276,10 @@ function lastRunOf(checkId: string, at: number, files: FileTime[], sumMs: number
|
|
|
270
276
|
|
|
271
277
|
export function shapeTestsRoot(o: TestTimeInput): TestsRootView {
|
|
272
278
|
const ids = [...new Set(o.facts.filter((f) => f.standing === 'runs' && f.checkId != null).map((f) => f.checkId!))]
|
|
279
|
+
const files: TestFileRow[] = []
|
|
273
280
|
const checks = ids.map((checkId): TestCheckRow => {
|
|
274
281
|
const v = shapeTestCheck(checkId, o)
|
|
282
|
+
for (const f of v.files) files.push({ ...f, check: checkId })
|
|
275
283
|
const top = v.files.find((f) => f.ms != null)
|
|
276
284
|
return {
|
|
277
285
|
checkId,
|
|
@@ -284,5 +292,6 @@ export function shapeTestsRoot(o: TestTimeInput): TestsRootView {
|
|
|
284
292
|
shape: v.last?.shape ?? null,
|
|
285
293
|
}
|
|
286
294
|
}).sort((a, b) => (b.wallMs ?? -1) - (a.wallMs ?? -1) || a.checkId.localeCompare(b.checkId))
|
|
287
|
-
|
|
295
|
+
files.sort((a, b) => (b.ms ?? -1) - (a.ms ?? -1) || a.file.localeCompare(b.file))
|
|
296
|
+
return { checks, files, failing: failingOf(o.facts) }
|
|
288
297
|
}
|
package/src/comfy/comfy-panel.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
9
9
|
import { join, resolve } from 'path'
|
|
10
10
|
import { defaultHubPort } from '../serve/hub-port.ts'
|
|
11
|
+
import { portHolders } from '../core/port-holders.ts'
|
|
11
12
|
|
|
12
13
|
export type ComfyWorkflow = { id: string; file: string }
|
|
13
14
|
export type ComfyPanelUp = { url: string; port: number }
|
|
@@ -127,22 +128,6 @@ const KILL_STEP_MS = 25
|
|
|
127
128
|
const QUIET_TRIES = 60
|
|
128
129
|
const QUIET_STEP_MS = 50
|
|
129
130
|
|
|
130
|
-
/** the pids LISTENING on a tcp port, on darwin and linux. `-sTCP:LISTEN`
|
|
131
|
-
* carries the whole answer: `-i tcp:<port>` alone also matches every client
|
|
132
|
-
* connected to it, this process included, and those must never be killed.
|
|
133
|
-
* lsof sits in /usr/sbin on darwin, which a gui-launched shell can miss */
|
|
134
|
-
function portHolders(port: number): number[] {
|
|
135
|
-
const lsof = Bun.which('lsof') ?? ['/usr/sbin/lsof', '/usr/bin/lsof'].find((p) => existsSync(p))
|
|
136
|
-
if (lsof == null) return []
|
|
137
|
-
const r = Bun.spawnSync([lsof, '-ti', `tcp:${port}`, '-sTCP:LISTEN'], { stdout: 'pipe', stderr: 'ignore' })
|
|
138
|
-
const pids = r.stdout
|
|
139
|
-
.toString()
|
|
140
|
-
.split('\n')
|
|
141
|
-
.map((l) => Number(l.trim()))
|
|
142
|
-
.filter((n) => Number.isInteger(n) && n > 0)
|
|
143
|
-
return [...new Set(pids)]
|
|
144
|
-
}
|
|
145
|
-
|
|
146
131
|
/** a pid that died between the scan and the signal is the outcome we wanted */
|
|
147
132
|
function kill(pid: number, signal: 'SIGTERM' | 'SIGKILL'): void {
|
|
148
133
|
try {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// AGENT SPAWN, found statically: code that starts a Claude or agent process on
|
|
2
|
+
// its own (a spawn or exec of `claude`, `claude -p`, the agent sdk). No tool
|
|
3
|
+
// here starts an agent unless its owner asks in person, so the sweep tests
|
|
4
|
+
// (tests/no-agent-spawn.test.ts, and the house repo's own) fail on any hit.
|
|
5
|
+
//
|
|
6
|
+
// a string that only MENTIONS claude (a url, `~/.claude/projects`, a help
|
|
7
|
+
// line naming the product) is no spawn, and comments are skipped.
|
|
8
|
+
|
|
9
|
+
export type AgentSpawn = { line: number; what: string }
|
|
10
|
+
|
|
11
|
+
const SHAPES: { re: RegExp; what: string }[] = [
|
|
12
|
+
// the sdks that run an agent loop in process or spawn the cli
|
|
13
|
+
{ re: /(['"`])@anthropic-ai\/(?:claude-agent-sdk|claude-code)(?:\/[^'"`]*)?\1/, what: 'imports the agent sdk' },
|
|
14
|
+
// an argv whose command is claude: spawn(['claude', …]), c.exec(['claude'])
|
|
15
|
+
{ re: /\[\s*(['"`])(?:[^'"`\s]*\/)?claude\1/, what: 'an argv that runs claude' },
|
|
16
|
+
// spawn('claude', …), execSync('claude -p …'), execFile('claude')
|
|
17
|
+
{ re: /\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync|execa|execaSync|fork)\s*\(\s*(['"`])(?:[^'"`\s]*\/)?claude(?:\s[^'"`]*)?\1/, what: 'a process call that runs claude' },
|
|
18
|
+
// the bun shell: $`claude -p …`
|
|
19
|
+
{ re: /\$\s*`\s*(?:[^`\s]*\/)?claude(?:\s|`)/, what: 'a bun shell line that runs claude' },
|
|
20
|
+
// the headless form, wherever the string goes
|
|
21
|
+
{ re: /(['"`])(?:[^'"`\s]*\/)?claude\s+(?:-p|--print)\b/, what: 'a claude -p command line' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
/** the first agent spawn in one source text, 1-based line, comments skipped */
|
|
25
|
+
export function agentSpawnIn(src: string): AgentSpawn | null {
|
|
26
|
+
const lines = src.split('\n')
|
|
27
|
+
let inBlock = false
|
|
28
|
+
for (let i = 0; i < lines.length; i++) {
|
|
29
|
+
let l = lines[i]!
|
|
30
|
+
if (inBlock) {
|
|
31
|
+
const end = l.indexOf('*/')
|
|
32
|
+
if (end < 0) continue
|
|
33
|
+
l = l.slice(end + 2)
|
|
34
|
+
inBlock = false
|
|
35
|
+
}
|
|
36
|
+
l = l.replace(/\/\*.*?\*\//g, '')
|
|
37
|
+
const open = l.indexOf('/*')
|
|
38
|
+
if (open >= 0) {
|
|
39
|
+
inBlock = true
|
|
40
|
+
l = l.slice(0, open)
|
|
41
|
+
}
|
|
42
|
+
l = l.replace(/(^|\s)\/\/.*$/, '$1')
|
|
43
|
+
for (const s of SHAPES) if (s.re.test(l)) return { line: i + 1, what: s.what }
|
|
44
|
+
}
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// `archived: true` in repo.config.ts: the repo stays known and listed, and
|
|
2
|
+
// nothing runs there (no hub load, so no watch, no plan rows, no gates, no
|
|
3
|
+
// processes, no widgets, no identity fill). Read and written as TEXT, never by
|
|
4
|
+
// loading the config: the daemon and the page rows read many configs per poll,
|
|
5
|
+
// and the toggle moves that one key and no other byte.
|
|
6
|
+
import { readFileSync } from 'fs'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import { atomicWrite } from './locked-write.ts'
|
|
9
|
+
import { configDepths } from './identity-color.ts'
|
|
10
|
+
|
|
11
|
+
const KEY_RE = /(?:^|[{,])\s*(['"]?)archived\1\s*:\s*(true|false)\b/gm
|
|
12
|
+
|
|
13
|
+
/** every top level `archived` key of `defineRepo({…})` with its value and the
|
|
14
|
+
* offsets of the whole `archived: <value>` token */
|
|
15
|
+
function archivedKeys(text: string): { on: boolean; start: number; end: number }[] {
|
|
16
|
+
const depth = configDepths(text)
|
|
17
|
+
const out: { on: boolean; start: number; end: number }[] = []
|
|
18
|
+
for (const m of text.matchAll(KEY_RE)) {
|
|
19
|
+
const lead = /^[{,]/.test(m[0]) ? 1 : 0
|
|
20
|
+
const sep = lead === 1 ? m.index : m.index - 1
|
|
21
|
+
if (sep < 0 || depth[sep] !== 1) continue
|
|
22
|
+
const start = m.index + lead + /^[{,]?(\s*)/.exec(m[0])![1]!.length
|
|
23
|
+
out.push({ on: m[2] === 'true', start, end: m.index + m[0].length })
|
|
24
|
+
}
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** does the config text archive the repo */
|
|
29
|
+
export const archivedOfText = (text: string): boolean => archivedKeys(text).some((k) => k.on)
|
|
30
|
+
|
|
31
|
+
/** the config text with the repo archived or not. On: the key reads true, a
|
|
32
|
+
* missing one lands as the first key of `defineRepo({`. Off: the key goes,
|
|
33
|
+
* so a round trip gives the text back byte for byte */
|
|
34
|
+
export function setArchivedText(text: string, on: boolean): string {
|
|
35
|
+
const keys = archivedKeys(text)
|
|
36
|
+
if (keys.length > 1) throw new Error('repo.config.ts declares `archived` more than once, set it by hand')
|
|
37
|
+
const k = keys[0]
|
|
38
|
+
if (on) {
|
|
39
|
+
if (k != null) return k.on ? text : `${text.slice(0, k.start)}archived: true${text.slice(k.end)}`
|
|
40
|
+
const lines = text.split('\n')
|
|
41
|
+
const open = lines.findIndex((l) => /defineRepo\(\s*\{/.test(l))
|
|
42
|
+
if (open === -1) throw new Error('repo.config.ts has no `defineRepo({` to write `archived` into, set it by hand')
|
|
43
|
+
const line = lines[open]!
|
|
44
|
+
if (/defineRepo\(\s*\{\s*$/.test(line)) {
|
|
45
|
+
const next = lines.slice(open + 1).find((l) => l.trim() !== '' && !/^\s*}/.test(l))
|
|
46
|
+
const pad = next != null ? /^(\s*)/.exec(next)![1]! : `${/^(\s*)/.exec(line)![1]!} `
|
|
47
|
+
return [...lines.slice(0, open + 1), `${pad}archived: true,`, ...lines.slice(open + 1)].join('\n')
|
|
48
|
+
}
|
|
49
|
+
lines[open] = line.replace(/defineRepo\(\s*\{\s*/, (m) => `${m}archived: true, `)
|
|
50
|
+
return lines.join('\n')
|
|
51
|
+
}
|
|
52
|
+
if (k == null) return text
|
|
53
|
+
// a line of its own goes whole, an inline key goes with its comma
|
|
54
|
+
const lineStart = text.lastIndexOf('\n', k.start - 1) + 1
|
|
55
|
+
const lineEnd = text.indexOf('\n', k.end)
|
|
56
|
+
const rest = text.slice(k.end, lineEnd === -1 ? text.length : lineEnd)
|
|
57
|
+
if (text.slice(lineStart, k.start).trim() === '' && /^\s*,?\s*$/.test(rest)) return text.slice(0, lineStart) + text.slice(lineEnd === -1 ? text.length : lineEnd + 1)
|
|
58
|
+
const after = /^\s*,\s*/.exec(text.slice(k.end))
|
|
59
|
+
if (after != null) return text.slice(0, k.start) + text.slice(k.end + after[0].length)
|
|
60
|
+
const before = /,\s*$/.exec(text.slice(0, k.start))
|
|
61
|
+
return before != null ? text.slice(0, k.start - before[0].length) + text.slice(k.end) : text.slice(0, k.start) + text.slice(k.end)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const CONFIG_FILE = 'repo.config.ts'
|
|
65
|
+
|
|
66
|
+
/** is this repo archived; false when it has no readable config */
|
|
67
|
+
export function isArchived(root: string): boolean {
|
|
68
|
+
try {
|
|
69
|
+
return archivedOfText(readFileSync(join(root, CONFIG_FILE), 'utf8'))
|
|
70
|
+
} catch {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** archive the repo or bring it back: only that key of its config moves */
|
|
76
|
+
export function writeArchived(root: string, on: boolean): boolean {
|
|
77
|
+
const path = join(root, CONFIG_FILE)
|
|
78
|
+
const text = readFileSync(path, 'utf8')
|
|
79
|
+
const out = setArchivedText(text, on)
|
|
80
|
+
if (out !== text) atomicWrite(path, out)
|
|
81
|
+
return on
|
|
82
|
+
}
|
package/src/core/attach.ts
CHANGED
|
@@ -39,9 +39,11 @@ export type ShareOpts = {
|
|
|
39
39
|
abort?: () => boolean
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/** how long
|
|
43
|
-
*
|
|
44
|
-
|
|
42
|
+
/** how long a slot file stays on disk. Only an in flight asker can adopt an
|
|
43
|
+
* answer, so this is housekeeping, not a validity window. A live holder
|
|
44
|
+
* heartbeats its lock (locked-write.ts), so a lock this old is one a killed
|
|
45
|
+
* run left behind. */
|
|
46
|
+
export const INFLIGHT_KEEP_MS = 60 * 60 * 1000
|
|
45
47
|
|
|
46
48
|
const readShared = <T>(path: string): Shared<T> | null => {
|
|
47
49
|
try {
|
|
@@ -50,12 +52,15 @@ const readShared = <T>(path: string): Shared<T> | null => {
|
|
|
50
52
|
} catch { return null }
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
/** every slot file past the keep window goes: answers, the locks killed runs
|
|
56
|
+
* left, the graves of a steal. One level of namespace dirs below `dir` is
|
|
57
|
+
* walked too. */
|
|
58
|
+
export function sweepSlots(dir: string, now: number, keepMs = INFLIGHT_KEEP_MS): void {
|
|
54
59
|
try {
|
|
55
|
-
for (const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
try { if (statSync(p).mtimeMs < now -
|
|
60
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
61
|
+
const p = `${dir}/${e.name}`
|
|
62
|
+
if (e.isDirectory()) { sweepSlots(p, now, keepMs); continue }
|
|
63
|
+
try { if (statSync(p).mtimeMs < now - keepMs) rmSync(p, { force: true }) } catch { /* raced: gone either way */ }
|
|
59
64
|
}
|
|
60
65
|
} catch { /* housekeeping never fails a run */ }
|
|
61
66
|
}
|
|
@@ -86,7 +91,7 @@ export async function shareRun<T>(slot: string, o: ShareOpts, body: () => Promis
|
|
|
86
91
|
// an answer nobody can adopt costs the waiters a measurement, never a verdict
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
|
-
|
|
94
|
+
sweepSlots(dirname(slot), now)
|
|
90
95
|
return { kind: 'measured', value: r.value }
|
|
91
96
|
})
|
|
92
97
|
if (leased.ok) return leased.value
|
|
@@ -65,12 +65,21 @@ export function routesToShipkit(body: string): boolean {
|
|
|
65
65
|
return /\bshipkit\b\S*(?:\s+\S+)*?\s+ci(?:\s|$)/.test(body.split(/&&|\|\||;/)[0] ?? body)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
/** is this path shipkit's own cli: a file named exactly `cli.ts` under a
|
|
69
|
+
* directory whose name says shipkit. A repo's own cli (`src/cli.ts`,
|
|
70
|
+
* `src/cli/comfy-ts-cli.ts`) is not, and taking it for shipkit writes
|
|
71
|
+
* scripts that call a command that cli does not have */
|
|
72
|
+
export function isShipkitCli(path: string): boolean {
|
|
73
|
+
const parts = path.split('/')
|
|
74
|
+
return parts.at(-1) === 'cli.ts' && parts.slice(0, -1).some((d) => /shipkit/.test(d))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// recover the consumption style from existing scripts: any script already
|
|
78
|
+
// running shipkit's cli by path fixes the style for new bodies
|
|
70
79
|
export function inferBin(scripts: Record<string, string>): string | undefined {
|
|
71
80
|
for (const body of Object.values(scripts)) {
|
|
72
81
|
const m = body.match(/^bun (?:--watch )?(\S+cli\.ts) /)
|
|
73
|
-
if (m != null && m[1] !== 'node_modules/rvlib-shipkit/src/cli.ts') return m[1]
|
|
82
|
+
if (m != null && isShipkitCli(m[1]!) && m[1] !== 'node_modules/rvlib-shipkit/src/cli.ts') return m[1]
|
|
74
83
|
}
|
|
75
84
|
return undefined
|
|
76
85
|
}
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
import { basename, relative } from 'path'
|
|
6
6
|
|
|
7
7
|
export const CHECK_SUFFIX = '.check.ts'
|
|
8
|
+
/** the unit file: a check, a global process or a widget, by its default
|
|
9
|
+
* export (shipkit-files.ts). Discovery reads both suffixes, and will later
|
|
10
|
+
* read this one alone */
|
|
11
|
+
export const UNIT_SUFFIX = '.shipkit.ts'
|
|
12
|
+
|
|
13
|
+
/** the name a unit file answers to when it declares no id: its basename,
|
|
14
|
+
* `.check` dropped too, so `x.check.shipkit.ts` is `x` */
|
|
15
|
+
export const unitBase = (file: string): string => basename(file).slice(0, -UNIT_SUFFIX.length).replace(/\.check$/, '')
|
|
8
16
|
|
|
9
17
|
/** dirs discovery never walks into, beside every dot dir: installs, build
|
|
10
18
|
* output, scratch, and templates meant for other repos */
|