rvlib-shipkit 1.1.0 → 1.2.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 +67 -0
- package/package.json +3 -1
- package/src/check.ts +24 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
- package/src/checks/SK023-stores-under-cap.check.ts +49 -0
- package/src/checks/SK024-identity-colors.check.ts +49 -0
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +18 -5
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli.ts +22 -2
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/attach.ts +14 -9
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +40 -18
- package/src/core/discover-checks.ts +36 -18
- 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/retention.ts +213 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +3 -0
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +86 -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 +208 -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 +141 -0
- package/src/home/resources.ts +338 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +149 -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 +172 -0
- package/src/home/supervise.ts +346 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widgets.ts +102 -0
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +5 -0
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/run-log.ts +58 -23
- package/src/ledger/store.ts +228 -103
- package/src/ledger/world-cache.ts +15 -13
- package/src/plan/cadence.ts +237 -0
- package/src/plan/planner.ts +303 -0
- package/src/plan/repo-source.ts +100 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/repo-config.ts +51 -2
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +1 -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/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +1 -1
- package/src/serve/hub-app/header-bar.tsx +44 -12
- 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 +241 -0
- package/src/serve/hub-app/home-strip.css +70 -0
- package/src/serve/hub-app/home-strip.tsx +245 -0
- package/src/serve/hub-app/home.html +17 -0
- package/src/serve/hub-app/hub-app.tsx +201 -93
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +43 -23
- package/src/serve/hub-app/icons.tsx +14 -0
- package/src/serve/hub-app/index.html +3 -1
- package/src/serve/hub-app/keys.ts +25 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +12 -9
- package/src/serve/hub-app/machine-st.ts +270 -0
- 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 +88 -0
- package/src/serve/hub-app/planned-st.ts +171 -0
- package/src/serve/hub-app/planned-view.tsx +109 -0
- package/src/serve/hub-app/planned.css +39 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +97 -0
- package/src/serve/hub-app/project-list.tsx +167 -0
- package/src/serve/hub-app/projects-svc.ts +139 -17
- 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/{hub-st.ts → repo-st.ts} +155 -84
- 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/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +10 -10
- package/src/serve/hub-app/timeline-chart.tsx +24 -12
- package/src/serve/hub-app/timeline-svc.ts +5 -4
- package/src/serve/hub-app/timeline-view.tsx +13 -13
- 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 +542 -0
- package/src/serve/project-gutter-route.ts +21 -0
- package/src/serve/project-gutter.ts +3 -2
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +142 -44
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +114 -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/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
+
## 1.2.0
|
|
2
|
+
|
|
3
|
+
- **Fix: SK024 read back only an `identity` key that starts a line.** On a one line config its own fix wrote the key inline, the check kept warning, and every fix pass added the key again, so a release gate always rewrote `repo.config.ts`. The key is now found at the top level of `defineRepo({…})` wherever it sits, bare or quoted, and a nested `identity` (an SK002 exemption) or one in a string or comment is not it.
|
|
4
|
+
|
|
5
|
+
- **A filled bar for widgets.** `<span class="wbar" style="--p:51%"><i></i></span>` draws a bar in the page's state colours (`mid`, `hi`); widget rows stay on one line; the overview's shipkit section is a card.
|
|
6
|
+
|
|
7
|
+
- **The planned page is one list ordered by time.** Three small labels inside it (now, late, next) and three columns per row: when (`now`, `late 2h`, `in 4m`), project · check (the project in its identity colour opens its repo, the check opens its own page), and one mark (a spinner while it runs, an orange dot and `waits: repo inactive` when late, a hollow circle when upcoming, a red ✕ `last failed`). A check that only runs on change is not listed. The rule, the reason, the last result, the inactive policy, an error and `run now` sit in a card that floats over the row on hover. One text filter matches the project or the check, and `/planned?q=` carries it. `Tip` takes `fallbacks`, the sides it tries when its placement does not fit. `bun tests/browser/planned-check.ts` measures it against a running home.
|
|
8
|
+
- **A project list replaces the icon rail.** One row per repo, an 18px icon and the name beside it, the shown repo marked in its identity colour, in a panel whose width is dragged with a separator and kept per browser (`react-resizable-panels`). A repo's resource sidebar is resizable the same way. The rail's three sizes and their button are gone.
|
|
9
|
+
- **Everything that floats is placed by `@floating-ui/react`.** The hover card, the help `(?)`, a process log on hover and the chart tooltip open after a short delay beside their trigger, flipped and shifted into the window, in a portal (`src/serve/hub-app/floating.tsx`).
|
|
10
|
+
- **Command keys.** ⌘1 to ⌘9 select the nth repo of the list (by physical key, any layout), ⌘J the overview, ⌘P the plan, ⌘K the picker. The overview and the plan are two buttons on top of the list, each with its shortcut in its corner.
|
|
11
|
+
- **Fix: a hub serving on its own showed an empty page beside its list.**
|
|
12
|
+
- **Fix: a dark band between the list and the page in the macOS app.** A panel's own scrollbar, which WebKit draws; no panel scrolls itself now.
|
|
13
|
+
|
|
14
|
+
- **The whole home page paints at once on a refresh.** Each repo's first screen is kept per browser in IndexedDB and read before the first paint, the machine is kept for the overview, and every region holds its size from the first paint, so a refresh measures a cumulative layout shift of 0.
|
|
15
|
+
- **One quiet top strip, the places in the rail and the picker.** The strip holds the shown repo (or the page's name) and machine trouble only, no link and no button. A house at the top of the rail opens the overview, and `home` and `planned` lead the `⌘K`/`⌘J` picker.
|
|
16
|
+
- **The overview: shipkit, then one card per global process, then the monitor.** `shipkit` (pm, daemon, web; was `machine`) and the processes under headers with a line icon; each process a card with its switch, state, declaring file, url and buttons. No repo list: the project list is it.
|
|
17
|
+
- **Take over a global process started elsewhere.** One click: pm stops the process group listening on the process's local port (SIGTERM, then SIGKILL after 5 s) and starts its own supervised copy. Not offered without a local url or probe port, and the row says why. `POST /api/pm/resource` takes `act: 'takeover'`.
|
|
18
|
+
- **`open` opens a global process's url in the default browser.** `open as app` is its own button, only for `app: true` (`act: 'app'`).
|
|
19
|
+
|
|
20
|
+
- **A refresh of the home moves nothing.** The rail frame renders from the first paint at the size the browser keeps, and the last rail list (`localStorage` `hub:rail-list`) paints at once while the daemon is asked again, so the page no longer jumps right when the list arrives. `bun run home:shift` measures the cumulative layout shift of a first load and a refresh in headless Chrome.
|
|
21
|
+
|
|
22
|
+
- **`<name>.shipkit.ts` unit files: checks, global processes and widgets.** A repo drops one anywhere and default-exports `check({...})`, `globalProcess({...})` or `widget({...})` (`rvlib-shipkit/src/shipkit-files.ts`). Discovery reads a `check` there beside the `.check.ts` files. A `globalProcess` is a long running process for the whole machine, supervised by pm whichever house repo declares it: pm reads the files at boot and every 15 s, and the overview shows a switch each, its log on hover, restart, one button per declared action (argv, or a function of the file run in its own bun process), and for `app: true` with a `url` an `open` that opens a shipkit app window, or `start and open` in one go on a process that is down. A `widget` returns html the daemon renders on its own clock (`everyMs`, default 5 s, bounded at 5 s) and serves at `/api/home/widgets.json`, and the overview mounts each in its monitor area. BREAKING: the `home.resources` key is removed; `defineRepo` refuses `home` and names the unit file that replaces it. pm's `/api/pm/resource` takes `act`: start, stop, restart, open or action.
|
|
23
|
+
|
|
24
|
+
- **Switching repos on the home is instant.** Each hub keeps its rows (`/api/resources.json`), its runs page (`/api/timeline.json`) and its wiki graphs warm in memory (`WarmCell`, `src/serve/warm-cell.ts`): computed when the hub loads, one hub after another, recomputed in the background after a save burst settles, when the config or the run record stamp moves, or past `WARM_MAX_AGE_MS` (2 minutes), and served from memory, so a request never walks the repo. `?fresh=1` waits on a walk that includes a change the page just made, and the page asks it after an act, a run that ended and a config write. The page reads ahead the first screen of every other repo whose hub is up, one repo and one step at a time, once the shown repo has its own, and a repo read ahead mounts nothing until it shows. The tree, the page links, the runs page, the wiki frame and the wait line show a spinner where they will be. `HubHandler` gains `invalidate()` and `warm()`, `HubDeps` gains `stamp`. Measured on the live home: a first visit went from 80 to 380ms to 38 to 80ms, the runs page from 1.6 to 3.0s to under 110ms.
|
|
25
|
+
|
|
26
|
+
- **Every repo has identity colours, and the home strip holds only what it should.** `repo.config.ts` declares `identity: { colorLight, colorDark }`, one `#rrggbb` each. New rule SK024 (`identity colors`, non-blocking) warns when they are missing or malformed; `shipkit check --fix SK024` derives them from the dominant hue of the repo icon, else from a hue hashed from the name, and writes that one key as one line, every other byte kept. The daemon fills each repo it loads the same way and never commits. A repo whose installed copy does not know the key is never written into. The home strip now holds `home` and `planned` (always there, the current one marked), the shown repo's icon and its name in its colour with two swatches that write the colours back (`POST /api/projects/identity`), and on its right only machine trouble: pm or the daemon down, a child or a resource crashed. Restart, the active flag, VS Code and modernize moved to the repo's own header, the resource switches and `new window` to the overview. The rail frames the shown repo's tile in its colour. `⌘K` opens the same picker as `⌘J`, which now lists the shown repo's targets, then `home`, `planned` and every other house repo.
|
|
27
|
+
|
|
28
|
+
- **The home page is one app over one state tree.** A rail click shows another repo in place: no page load, the rail and the strip stay mounted, and a repo you come back to shows the page, filter, search and scroll it had. `HomeSt` is the root (`window.app`) for `/`, `/planned` and `/r/<repo>/`, with one `RepoSt` per repo visited, and only the shown repo polls. Every request of a repo goes through `RepoSt.url()`. The web serves one page at all three routes (`serveWeb({ page })`), and a hub of its own renders the same app with one repo and no machine.
|
|
29
|
+
|
|
30
|
+
- **`shipkit gui` is its own macOS app.** Built once with swiftc into `~/Applications/Shipkit.app`, with the house repo's mark as its icon, so the dock and cmd-tab show shipkit instead of a second Chrome. Chrome's app mode stays the fallback.
|
|
31
|
+
|
|
32
|
+
- **A boot never fires everything at once.** Work overdue when the plan starts is spread over the next 2 hours, and a check with no cadence runs at a stable point of 20h to 28h.
|
|
33
|
+
|
|
34
|
+
- **The json answer import proves every file against its own hash.** Two check ids that slug alike can no longer share one row, and the import never overwrites a fresher row.
|
|
35
|
+
|
|
36
|
+
- **Machine state lives in one machine database.** The repos flagged active and the pid records pm, the resources and a repo's dev servers are adopted from now live in `~/.config/shipkit/shipkit.sqlite` (beside `SHIPKIT_HOME_DIR` when it is set, or at `SHIPKIT_MACHINE_DB`). The json and pid files an older version wrote are read once and deleted, so a running pm adopts its children across the update. `ProcSpec.pidFile` is renamed `pidKey`.
|
|
37
|
+
- **Nothing shipkit writes grows unbound.** Every store has a declared cap: the answers and the run record in the repo db, the attach slots, the run logs (40 runs, 6 hours), the job folders (100, 7 days), the scratch git indexes, the dev server logs, and on a machine with a home its logs and the gui window's Chrome profile. The daemon sweeps them all on boot and hourly, and a gate still sweeps what it writes. New rule SK023 (`stores under cap`, health) fails when a store is past its cap plus one sweep's drift; `shipkit check --fix SK023` runs the sweep. The old json answers are imported once (the last day's) and deleted.
|
|
38
|
+
- **Cached answers live in the repo's database.** The one json file per cached answer under `.git/shipkit/store` is replaced by an `answers` table in the same `shipkit.sqlite` as the run record. Anything that makes the table unreadable (a corrupt or locked file, a missing row) means the check runs, never a replay and never a red. Answers nobody used for 14 days are dropped, and a repo keeps at most 5,000. The run record keeps 30 days and at most 200,000 rows per table.
|
|
39
|
+
- **A repo's installed copy hands the command to the house source.** `node_modules/.bin/shipkit` and the `.shipkit/hooks` run a repo's own `rvlib-shipkit` cli directly, past the machine launcher. That cli now replaces itself (same pid, same argv) with the house cli the launcher runs, so every repo runs one version. A house checkout runs its own source, and CI, `SHIPKIT_NO_HOME` or a machine with no house run the installed copy. The page no longer says "daily pass": a health check runs each time the plan runs it.
|
|
40
|
+
- **One shipkit version per machine, and any command starts pm.** The cli routes every `rvlib-shipkit` import a repo config makes to the copy that runs, so a repo's own install is never loaded beside it, and new rule SK022 warns when a repo pins or installed a version newer than the running one: the pin is a minimum. Before any command the cli probes pm on its port and, when it is down, kicks its LaunchAgent, or installs it from the house repo the first time. Skipped under CI, `SHIPKIT_NO_HOME=1`, inside a process pm started, off macOS, and after `shipkit pm uninstall`, which stays in force until `shipkit pm install`. `SHIPKIT_LAUNCH_AGENTS_DIR` moves where the agent is written.
|
|
41
|
+
- **The plan replaces the daily pass and the save trigger.** A check declares when it runs beside what it reads: `every: '24h'`, a range `every: ['10h', '12h']` (each repo and check lands on a stable point inside it, so checks spread out), `cron: '0 9 * * 1'`, null for on change only, or a pure function of its last result (`status`, `at`, `streak`) and whether the repo is active, returning any of those. `whenInactive: 'run' | 'wait'` (default wait, also a lambda) says what a due check does while its repo is inactive: wait turns it late and runs it once an agent works in the repo, VS Code has it open, or it is flagged active. With nothing declared, a `ttl` reads as its `every`, a build step runs on change only, and any other check runs every 24h. A bad value dies when the check file loads; a lambda that throws is an error on its row, and the check runs on change only. The daemon keeps ONE plan for the machine: every repo and check pair with its next due time and why (`changed`, `scheduled`, `late`, `asked`), and runs what is due as one background batch per repo through the gate jobs. A save makes due only the checks whose cached answer would not replay. The page `/planned` (the `planned` link in the header strip) shows the whole plan, filterable by repo: running, due, late, next with its time, each row with its reason and the rule that planned it (for a lambda, the value it returned and the state that chose it); a click runs it now. The check page's `next run` reads the same row. `HealthSchedule`, `GateTrigger` and `lastHealthPassAt` are deleted; the save watch lives in `serve/tracked-watch.ts`.
|
|
42
|
+
- **Every gate in the daemon is a detached job, so a daemon reload never kills one.** A terminal `shipkit ci` or `shipkit check`, a save trigger, the daily pass and a button on the page each start `shipkit ci --job <id>` (or `check --job <id>`) in the background, the same mechanism as `--detach`, with its options in the job folder. The daemon runs one job at a time per repo, a background one takes the machine's one background turn inside the job, and it follows the job's `out.log` and `checks.ndjson` into the terminal, the page and the problems pane. A new daemon image follows the jobs still running when it boots, and queues behind them. A terminal whose stream the daemon cut (a reload, `kill -9`) reads the rest from the job folder and exits with the gate's own code; a cut before the job started sends the run again once the daemon is back. The daemon now reloads on its own code at once, gates or not. `SHIPKIT_DAEMON_ONLY=a,b` loads only those house repos.
|
|
43
|
+
- **A hub that crashed on a broken `repo.config.ts` loads again once the file is edited**, as a working hub reloads on an edit. It stayed crashed until a restart by hand.
|
|
44
|
+
- **`shipkit pm`, the machine's process manager.** It runs `shipkit daemon` and `shipkit web` as detached children with pid files, starts one again when it dies (backoff, left down after 5 crashes in 5 minutes), and owns the machine resources (`home.resources`), which moved out of the daemon. Its api answers on port 4797 (`SHIPKIT_PM_PORT`): the status of each child and resource, start, stop and restart of a child, the resource switches. The web proxies `/api/pm/*` to it, and the overview shows pm, the daemon and the web with a restart button each. pm killed or restarted leaves its children running, and the next pm adopts them; a stop asked of pm (SIGTERM, `shipkit pm uninstall`) stops them. `shipkit pm install` writes the LaunchAgent `com.shipkit.pm`, which runs `bun <cli> pm` and replaces the `com.shipkit.home` agent. `shipkit home`, `home install` and `home uninstall` stay as names of the pm commands. `shipkit gui` starts pm when the web does not answer and waits for the web.
|
|
45
|
+
- **pm, the daemon and the web each reload alone.** None runs under a bun watch, which follows every command through `cli.ts`. Each watches the lib files its own runner imports and restarts in place, same pid, on an edit of one of them: a page edit restarts the web, a runner edit the daemon (a gate in flight keeps running, see gate jobs), a supervisor edit pm, which adopts its children.
|
|
46
|
+
- **pm stays small.** It reads the house repo's `home.resources` in a child process and never loads the repo config, and `shipkit pm` replaces its own image with pm's entry file, so pm runs in about 35 MB and reloads only on its own few files. Its boot lines say `adopted` for a child it took from a pid file. A stop or restart of the web asked from the page answers before the web goes down, so the page no longer shows a fetch error for a restart that worked.
|
|
47
|
+
- **A supervised process is started through a shell that exits at once**, so it belongs to init from its start, in its own process group. A process started before its supervisor restarted in place stayed a zombie of the new image and read alive forever, so a crash was never restarted. A pid file whose pid a process started after the record now holds is no longer adopted.
|
|
48
|
+
- **A daemon that hangs reads as down on the page: the web waits 5s for the home api and 30s for a repo request, runs excepted, and the page's poll gives up at 8s. The web learns the repo pages once the daemon first answers, and follows the list after that. A failed poll with the daemon up shows its error and no longer reloads the page.
|
|
49
|
+
- **One app for every repo on the machine: `shipkit daemon` and `shipkit web`.** Run from the repo that hosts the house wiki. `shipkit daemon` loads every repo's hub and answers every api on an internal port (4798, `SHIPKIT_DAEMON_PORT`); `shipkit web` serves the page on port 4799 (`SHIPKIT_HOME_PORT`) and proxies every `/api/*` and `/r/<repo>/…` request to the daemon, run streams included, so a browser only ever sees one origin. `shipkit pm` runs both. A daemon that is down reads as a clear error on the page, which reloads itself once the daemon answers again. A terminal gate talks to the daemon directly, so it never depends on the web. The daemon loads every house repo's hub at boot, one after the other, and serves each under `/r/<repo>/` of its one server: the page, `/api/*`, `/graph/*`, `/media/*` and `/hub.json`. A boot runs no gate: a repo's background gate starts on a save in it, and the daily pass and the RED pings run for ACTIVE repos only (an agent works in it, or it is flagged active). A repo whose config fails to load reads crashed with its error while the others serve. The page has no frames: `/` is the overview of every resource and repo with its state, and `/r/<repo>/` is that repo's own hub page carrying the rail (read from the machine wide `/api/projects.json`) and a home strip over its header, with the resource switches and, for the shown repo, restart, the active flag, VS Code and modernize. A rail click goes to that repo's page, a double click opens VS Code. A repo whose hub cannot load shows why and the fix in place of its page. The home repo declares the machine resources in `home.resources`. `shipkit pm install` adds a macOS LaunchAgent that keeps it alive. Resources and repo processes run detached with a pid file, so the next pm or daemon adopts them.
|
|
50
|
+
- **`shipkit serve` attaches to a running home.** It hands the hub to the home and streams its log, the last problem frame first, so a VS Code folder-open task keeps its problems pane with no change to `tasks.json`. A worktree, `--port` and `SHIPKIT_NO_HOME=1` serve in process as before.
|
|
51
|
+
- **`serve.processes`: a repo's dev servers, supervised by its hub.** One switch each in the hub header, crash restart with backoff, adopted across a hub restart. **`serve.uses`** names the machine resources a repo needs, and the home rail marks it while one is off.
|
|
52
|
+
- **`shipkit gui [repo|.]`: the home in its own window.** A Chrome app window on its own profile (the default browser when Chrome is absent), one more per call and per `new window` in the header. It starts the home when none runs, installing its LaunchAgent the first time from the house repo.
|
|
53
|
+
- **On a machine with the home, no hub runs outside it.** `shipkit serve` hands off, starting an installed home that is down, and never serves in process there. A process the home started, linked worktrees, machines without a home and `SHIPKIT_NO_HOME=1` serve as before. `SHIPKIT_USE_HOME=1` sends hubs and gates to the home on `SHIPKIT_HOME_PORT` on a machine with no LaunchAgent.
|
|
54
|
+
- **The daemon reloads one repo at a time.** An edit of a repo's `repo.config.ts` reloads that hub alone, once its gate is done, its processes kept. The daemon warns while it runs under a bun watch.
|
|
55
|
+
- **A hub stopped by hand stays down** until a restart: its page no longer loads it again, and a stop during a load wins over the load. `/r/<name>/` answers 404 for a name that is not a house repo, never a page whose every call fails. The home's view of a hub drops its dead `external` and `restarts` fields.
|
|
56
|
+
- **One copy of the lib in the home.** Before any repo config loads, a Bun resolver hook routes every `rvlib-shipkit` import (the bare name, an export alias, a subpath) to the home's own copy, so a repo's own install is never loaded and each lib module has one instance.
|
|
57
|
+
- **The hub page works under a base path.** Every request it makes goes through one helper that reads `/r/<repo>` from its own address, so the same page serves standalone at `/` and in the home.
|
|
58
|
+
- **On a machine with the home, `shipkit ci` and `shipkit check` run in the repo's hub, in the home.** They POST to `/r/<repo>/api/run`, and the terminal streams the hub's lines and takes its exit code, so every gate of a repo runs in one process and feeds one problems pane. What runs in process: a process the home started (a check's child, a script calling shipkit), CI, a linked worktree, `SHIPKIT_NO_HOME=1`, a machine without the home, a repo the home cannot load, and `check --fix`, `bump`, `release`. An unreachable home is a loud failure, never a quiet local run.
|
|
59
|
+
- **Background gates take turns on the whole machine.** The save trigger and the daily pass of every hub wait for one shared turn, so several hubs never gate at once. A gate someone asks for never waits for it. A hub start feeds the same quiet window as a save, so a burst of restarts is one run.
|
|
60
|
+
- **The rail: a single click selects, a double click opens VS Code.** On a hub serving on its own, a click opens that repo's page on the home when one serves.
|
|
61
|
+
|
|
62
|
+
- **The project rail has three sizes.** A button pinned to the foot of the rail shows three dots, the current size lit, and a click cycles small (44px rail, 24px icons), medium (76px, 56px) and large (132px, 112px). The browser keeps the choice in `localStorage` `hub:rail`. Every size is a literal of the rail's class in `project-gutter.css`, never an inline value, and each icon carries its size as `width` and `height` attributes with `object-fit: contain`, so a 512px icon file never draws at its natural size, even when the sheet does not apply. `tests/browser/hub-layout-check.ts` measures all of it in headless Chrome.
|
|
63
|
+
|
|
64
|
+
## 1.1.1
|
|
65
|
+
|
|
66
|
+
- **The project rail reads `.shipkit/identity/icon.png` first.** A repo with no public surface has no favicon, so the rail showed its initial. `ICON_FILES` now starts with `.shipkit/identity/icon.png`, ahead of `public/favicon.png`, so a repo can carry a rail icon without shipping a public folder.
|
|
67
|
+
|
|
1
68
|
## 1.1.0
|
|
2
69
|
|
|
3
70
|
- **`shipkit ask` scaffolds a walkthrough, not a one line body.** A new ask used to carry `**Ask**: <title>` and nothing a human could follow. A step ask (every kind but `decision`) now starts with numbered `## N.` sections, each opening with a bold link slot and a `field | value` table, and ends with a `Tell the agent "done"` section. A `decision` ask starts with numbered options, the recommended one first. An HTML comment, invisible in the hub, states the shape, and `shipkit ask` reminds the author to fill every `{{placeholder}}` (braces, since the hub markdown hides an unknown `<tag>`). The `**Why**` line and `--why` are unchanged.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"funding": "https://github.com/sponsors/rvion",
|
|
5
5
|
"description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"test:raw": "bun test"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@floating-ui/react": "^0.27.20",
|
|
36
37
|
"@napi-rs/canvas": "^1.0.2",
|
|
37
38
|
"clipanion": "^4.0.0-rc.4",
|
|
38
39
|
"comfy-ts": "^2.9.0",
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
"mobx-react-lite": "^5.0.0",
|
|
43
44
|
"react": "^19.2.7",
|
|
44
45
|
"react-dom": "^19.2.7",
|
|
46
|
+
"react-resizable-panels": "^4.13.3",
|
|
45
47
|
"sanitize-html": "^2.17.6",
|
|
46
48
|
"sharp": "^0.35.3"
|
|
47
49
|
}
|
package/src/check.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// its cache key automatically, so nothing declares its own code.
|
|
21
21
|
import type { CheckCtx } from './core/check-ctx.ts'
|
|
22
22
|
import { parseWhen } from './core/when.ts'
|
|
23
|
+
import { planDeclProblem, type Every, type WhenInactiveDecl } from './plan/cadence.ts'
|
|
23
24
|
import type { Line } from './core/wire.ts'
|
|
24
25
|
import type { RepoConfig } from './repo-config.ts'
|
|
25
26
|
|
|
@@ -108,6 +109,27 @@ export type CheckDef = {
|
|
|
108
109
|
* key, and every other check shares one answer between a frozen run and
|
|
109
110
|
* a normal one. Declared, never guessed from the body. */
|
|
110
111
|
writes?: boolean
|
|
112
|
+
/** WHEN the planner runs it on its own, beside the save that moved what
|
|
113
|
+
* it reads: a span since its last run (`'24h'`), a range (`['10h',
|
|
114
|
+
* '12h']`, each repo and check on a stable point inside it, so checks
|
|
115
|
+
* spread out), a cron expression (`'0 9 * * 1'`), or null for on change
|
|
116
|
+
* only. Or a PURE function of its last result (`status`, `at`, `streak`
|
|
117
|
+
* of that same status) and whether the repo is active, returning any of
|
|
118
|
+
* those: `(last) => (last?.status === 'fail' ? '1h' : '7d')`. A lambda
|
|
119
|
+
* that throws is an error row on the planned page, and the check runs on
|
|
120
|
+
* change only. Absent: its `ttl`, else a build step waits for a save and
|
|
121
|
+
* any other check runs every 24h. */
|
|
122
|
+
every?: Every
|
|
123
|
+
/** a time of day or week for the rare check that needs one, as five cron
|
|
124
|
+
* fields in local time (`'0 9 * * 1'`, Mondays at 9). Never with `every`. */
|
|
125
|
+
cron?: string
|
|
126
|
+
/** what a due check does while its repo is inactive (no agent session in
|
|
127
|
+
* it, VS Code does not have it open, it is not flagged active): `'wait'`
|
|
128
|
+
* turns it late and runs it once the repo is active again, `'run'` runs
|
|
129
|
+
* it on time anyway (a credential probe, an api key that must keep
|
|
130
|
+
* working). A pure function of the last result may choose. Default
|
|
131
|
+
* `'wait'`. */
|
|
132
|
+
whenInactive?: WhenInactiveDecl
|
|
111
133
|
/** cache-identity contribution for a `ttl` check whose verdict depends on
|
|
112
134
|
* config: a changed key busts the answer instantly instead of serving
|
|
113
135
|
* a stale cached assertion for the rest of the TTL. */
|
|
@@ -179,6 +201,8 @@ export function check(d: CheckDef): CheckDef {
|
|
|
179
201
|
const populations = (['inputs', 'ttl', 'never'] as const).filter((k) => has(k))
|
|
180
202
|
if (populations.length > 1)
|
|
181
203
|
throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: ${populations.join(' and ')} contradict each other — a check reads files, or the world, or nothing can describe it`)
|
|
204
|
+
const planBad = planDeclProblem({ every: d.every, cron: d.cron, whenInactive: d.whenInactive })
|
|
205
|
+
if (planBad != null) throw new Error(`check${name}: ${planBad}`)
|
|
182
206
|
if (d.when != null) {
|
|
183
207
|
const w = parseWhen(d.when)
|
|
184
208
|
if (typeof w === 'string') throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: when ${w}`)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SK022 machine runs the pin (build/shipkit.md): on a machine with a house
|
|
2
|
+
// every shipkit runs the house copy, whatever a repo installed, so a repo's
|
|
3
|
+
// rvlib-shipkit pin is a minimum. It warns when the repo asks for a version
|
|
4
|
+
// newer than the one this process runs: a rule or an api that pin relies on
|
|
5
|
+
// is absent here until the house copy catches up.
|
|
6
|
+
import { LIB_VERSION } from '../core/lib-version.ts'
|
|
7
|
+
import { check } from '../check.ts'
|
|
8
|
+
|
|
9
|
+
type Pkg = { version?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
10
|
+
|
|
11
|
+
const NAME = 'rvlib-shipkit'
|
|
12
|
+
|
|
13
|
+
/** the lowest version a semver range admits (`^0.30.1` → 0.30.1), null when
|
|
14
|
+
* it names no version (link:, workspace:*, a tag) */
|
|
15
|
+
export function pinFloor(spec: string): string | null {
|
|
16
|
+
const m = /^\s*(?:\^|~|>=|=|v)?\s*(\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(spec)
|
|
17
|
+
return m?.[1] ?? null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const machineRunsThePin = check({
|
|
21
|
+
id: 'SK022',
|
|
22
|
+
blocking: false,
|
|
23
|
+
title: 'machine runs the pin',
|
|
24
|
+
description: 'the shipkit this machine runs is at least the version this repo pins',
|
|
25
|
+
owner: 'build/shipkit.md',
|
|
26
|
+
// the running version is no file of the repo
|
|
27
|
+
cacheKey: () => LIB_VERSION,
|
|
28
|
+
run: (c) => {
|
|
29
|
+
const pkg = c.json<Pkg>('package.json')
|
|
30
|
+
const spec = pkg?.devDependencies?.[NAME] ?? pkg?.dependencies?.[NAME]
|
|
31
|
+
if (spec == null) return c.ok(`no ${NAME} pin, this machine runs ${LIB_VERSION}`)
|
|
32
|
+
const wants: Array<{ v: string; from: string }> = []
|
|
33
|
+
const floor = pinFloor(spec)
|
|
34
|
+
if (floor != null) wants.push({ v: floor, from: `package.json pins ${spec}` })
|
|
35
|
+
const installed = c.json<Pkg>(`node_modules/${NAME}/package.json`)?.version
|
|
36
|
+
if (installed != null) wants.push({ v: installed, from: `node_modules has ${installed}` })
|
|
37
|
+
const ahead = wants.filter((w) => Bun.semver.order(w.v, LIB_VERSION) > 0)
|
|
38
|
+
if (ahead.length === 0) return c.ok(`${NAME} ${spec}: this machine runs ${LIB_VERSION}, at least that`)
|
|
39
|
+
c.warn(`${NAME}: ${ahead.map((w) => w.from).join(', ')}, newer than the ${LIB_VERSION} this machine runs. Update the house copy (git pull in the repo that hosts the house wiki), or run this repo's own install: bun node_modules/${NAME}/src/cli.ts`)
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export default machineRunsThePin
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SK023 stores under cap (core/retention.ts owns the caps): every store shipkit
|
|
2
|
+
// writes for this repo, and the machine's own when a home dir exists, is
|
|
3
|
+
// inside its declared cap plus the drift one sweep interval allows. Red means
|
|
4
|
+
// the daemon's hourly sweep stopped running, or a store grows faster than its
|
|
5
|
+
// cap assumes. The fix runs the sweep.
|
|
6
|
+
import { existsSync } from 'fs'
|
|
7
|
+
import { check } from '../check.ts'
|
|
8
|
+
import { fmtAge, fmtBytes, measureStores, repoStores, sweepStores, type Store } from '../core/retention.ts'
|
|
9
|
+
import { homeDir } from '../home/home-paths.ts'
|
|
10
|
+
import { machineStores } from '../home/machine-stores.ts'
|
|
11
|
+
|
|
12
|
+
const storesOf = (root: string): Store[] => [...repoStores(root), ...(existsSync(homeDir()) ? machineStores(homeDir()) : [])]
|
|
13
|
+
|
|
14
|
+
const capWords = (s: Store): string =>
|
|
15
|
+
[
|
|
16
|
+
s.cap.maxCount != null ? `${s.cap.maxCount} entries` : null,
|
|
17
|
+
s.cap.maxAgeMs != null ? fmtAge(s.cap.maxAgeMs) : null,
|
|
18
|
+
s.cap.maxFileBytes != null ? `${fmtBytes(s.cap.maxFileBytes)} a file` : null,
|
|
19
|
+
s.cap.maxBytes != null ? fmtBytes(s.cap.maxBytes) : null,
|
|
20
|
+
].filter((x) => x != null).join(', ')
|
|
21
|
+
|
|
22
|
+
export const storesUnderCap = check({
|
|
23
|
+
id: 'SK023',
|
|
24
|
+
blocking: false,
|
|
25
|
+
title: 'stores under cap',
|
|
26
|
+
description: 'every store shipkit writes (answers, run record, logs, jobs) is inside its cap',
|
|
27
|
+
owner: 'build/shipkit/built-in-checks.md',
|
|
28
|
+
when: 'health',
|
|
29
|
+
run: (c) => {
|
|
30
|
+
const all = measureStores(storesOf(c.root))
|
|
31
|
+
const over = all.filter((m) => m.over != null)
|
|
32
|
+
const unread = all.filter((m) => m.use == null)
|
|
33
|
+
for (const m of over) c.fail(`${m.store.id} over its cap: ${m.over} (${m.store.where})`, 'shipkit check --fix SK023 runs the sweep the daemon runs hourly')
|
|
34
|
+
for (const m of unread) c.warn(`${m.store.id} could not be read (${m.store.where})`)
|
|
35
|
+
if (over.length === 0) c.ok(`${all.length} stores inside their caps: ${all.map((m) => `${m.store.id} ${m.use?.count ?? '?'} (cap ${capWords(m.store)})`).join(', ')}`)
|
|
36
|
+
},
|
|
37
|
+
fix: async (c) => {
|
|
38
|
+
const stores = storesOf(c.root)
|
|
39
|
+
const before = measureStores(stores).filter((m) => m.over != null)
|
|
40
|
+
const r = await sweepStores(stores)
|
|
41
|
+
const broke = r.filter((x) => x.error != null)
|
|
42
|
+
return {
|
|
43
|
+
changed: before.length > 0,
|
|
44
|
+
summary: `swept ${stores.length} stores${before.length > 0 ? `, ${before.map((m) => m.store.id).join(', ')} were over` : ''}${broke.length > 0 ? `; failed: ${broke.map((x) => `${x.id} (${x.error})`).join(', ')}` : ''}`,
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export default storesUnderCap
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SK024 identity colors (core/identity.ts owns the derivation): repo.config.ts
|
|
2
|
+
// declares `identity: { colorLight, colorDark }`, two #rrggbb hex, so every
|
|
3
|
+
// surface that shows the repo (the home page header, its rail tile) draws it
|
|
4
|
+
// in its own colour. The fix derives them from the repo icon, or from a hue
|
|
5
|
+
// hashed from the name, and writes that one key, every other byte kept.
|
|
6
|
+
import { check } from '../check.ts'
|
|
7
|
+
import { configName, CONFIG_FILE, deriveIdentity, identityRefusal, INSTALLED_CONFIG_SRC } from '../core/identity.ts'
|
|
8
|
+
import { declaresIdentity, identityOfText, identityProblem, setIdentityText } from '../core/identity-color.ts'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const FIX = 'shipkit check --fix SK024 derives them from the repo icon, or from its name'
|
|
12
|
+
|
|
13
|
+
export const identityColors = check({
|
|
14
|
+
id: 'SK024',
|
|
15
|
+
blocking: false,
|
|
16
|
+
title: 'identity colors',
|
|
17
|
+
description: 'repo.config.ts declares identity colours: colorLight and colorDark, two hex',
|
|
18
|
+
owner: 'build/shipkit/built-in-checks.md',
|
|
19
|
+
run: (c) => {
|
|
20
|
+
const text = c.read(CONFIG_FILE)
|
|
21
|
+
if (text == null) return c.note(`no ${CONFIG_FILE}`)
|
|
22
|
+
const declared = declaresIdentity(text)
|
|
23
|
+
const id = identityOfText(text)
|
|
24
|
+
const why = id == null ? (declared ? 'identity is declared but its two colours cannot be read as #rrggbb hex' : 'no identity colours') : identityProblem(id)
|
|
25
|
+
const refused = why == null ? null : identityRefusal(c.read(INSTALLED_CONFIG_SRC))
|
|
26
|
+
if (refused != null) return c.note(`${CONFIG_FILE}: ${why}, and ${refused}`)
|
|
27
|
+
if (why != null) return c.warn(`${CONFIG_FILE}: ${why}`, FIX)
|
|
28
|
+
c.ok(`identity ${id!.colorLight} on light, ${id!.colorDark} on dark`)
|
|
29
|
+
},
|
|
30
|
+
fix: async (c) => {
|
|
31
|
+
const text = c.read(CONFIG_FILE)
|
|
32
|
+
if (text == null) return { changed: false, summary: `no ${CONFIG_FILE}` }
|
|
33
|
+
const have = identityOfText(text)
|
|
34
|
+
if (have != null && identityProblem(have) == null) return { changed: false, summary: 'identity colours already declared' }
|
|
35
|
+
const refused = identityRefusal(c.read(INSTALLED_CONFIG_SRC))
|
|
36
|
+
if (refused != null) return { changed: false, summary: refused }
|
|
37
|
+
const { id, from } = await deriveIdentity(c.root, configName(text, c.root), (rel) => c.bytes(rel))
|
|
38
|
+
let next: string
|
|
39
|
+
try {
|
|
40
|
+
next = setIdentityText(text, id)
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { changed: false, summary: (e as Error).message }
|
|
43
|
+
}
|
|
44
|
+
c.write(CONFIG_FILE, next)
|
|
45
|
+
return { changed: true, summary: `identity ${id.colorLight} / ${id.colorDark}, from ${from}` }
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export default identityColors
|
package/src/cli/cmd_Check.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// autofixes then re-lints. Thin clipanion surface; logic in
|
|
3
3
|
// core/run-check.ts and core/run-fix.ts. ci NEVER fixes.
|
|
4
4
|
import { Command, Option } from 'clipanion'
|
|
5
|
+
import { runViaHub } from '../home/via-hub.ts'
|
|
6
|
+
import { readJobOpts, recordExit } from '../core/jobs.ts'
|
|
7
|
+
import { runJob } from '../core/job-run.ts'
|
|
5
8
|
import { runCheck } from '../core/run-check.ts'
|
|
6
9
|
import { maybeSelfUpdateAndRestart } from '../core/self-update.ts'
|
|
7
10
|
import { runFix } from '../core/run-fix.ts'
|
|
@@ -27,8 +30,25 @@ export class CheckCommand extends Command {
|
|
|
27
30
|
fix = Option.Boolean('--fix', false, { description: 'apply check autofixes (every fixable check, or only the listed ids), then re-run them' })
|
|
28
31
|
frozen = Option.Boolean('--frozen', false, { description: 'report-only mode for the self-maintaining checks (SK005 never touches package.json)' })
|
|
29
32
|
fixIds = Option.Rest()
|
|
33
|
+
job = Option.String('--job', { hidden: true })
|
|
30
34
|
|
|
31
35
|
async execute(): Promise<number> {
|
|
36
|
+
// a gate job the daemon started: its options are in the job folder
|
|
37
|
+
if (this.job != null) {
|
|
38
|
+
const root = process.cwd()
|
|
39
|
+
const job = readJobOpts(root, this.job)
|
|
40
|
+
let code = 1
|
|
41
|
+
if (job == null) this.context.stderr.write(`FAIL: gate job ${this.job} has no options in its folder\n`)
|
|
42
|
+
else {
|
|
43
|
+
try {
|
|
44
|
+
code = await runJob(root, this.job, 'check', job, (l) => { this.context.stdout.write(`${l}\n`) })
|
|
45
|
+
} catch (e) {
|
|
46
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
recordExit(root, this.job, code)
|
|
50
|
+
return code
|
|
51
|
+
}
|
|
32
52
|
// a stale toolkit updates itself and restarts this exact invocation
|
|
33
53
|
// (loop-guarded, skipped under CI/frozen/source consumption)
|
|
34
54
|
await maybeSelfUpdateAndRestart(process.cwd(), { fresh: this.fresh })
|
|
@@ -43,7 +63,12 @@ export class CheckCommand extends Command {
|
|
|
43
63
|
}
|
|
44
64
|
if (this.fix)
|
|
45
65
|
return await runFix(process.cwd(), { ids: this.fixIds, print: (l) => this.context.stdout.write(`${l}\n`) })
|
|
46
|
-
|
|
66
|
+
const opts = { json: this.json, fresh: this.fresh, frozen: this.frozen }
|
|
67
|
+
const print = (l: string): void => { this.context.stdout.write(`${l}\n`) }
|
|
68
|
+
// on a machine with the home, the repo's hub runs it (home/via-hub.ts)
|
|
69
|
+
const via = await runViaHub(process.cwd(), 'check', opts, { print, printErr: (l) => this.context.stderr.write(`${l}\n`) })
|
|
70
|
+
if (via != null) return via
|
|
71
|
+
return await runCheck(process.cwd(), { ...opts, print })
|
|
47
72
|
} catch (e) {
|
|
48
73
|
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
49
74
|
return 1
|
package/src/cli/cmd_Ci.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// core/run-ci.ts.
|
|
3
3
|
import { Command, Option } from 'clipanion'
|
|
4
4
|
import { join } from 'path'
|
|
5
|
-
import { recordExit, startDetached } from '../core/jobs.ts'
|
|
5
|
+
import { readJobOpts, recordExit, startDetached } from '../core/jobs.ts'
|
|
6
|
+
import { runJob } from '../core/job-run.ts'
|
|
6
7
|
import { runCi } from '../core/run-ci.ts'
|
|
8
|
+
import { runViaHub } from '../home/via-hub.ts'
|
|
7
9
|
import { maybeSelfUpdateAndRestart } from '../core/self-update.ts'
|
|
8
10
|
|
|
9
11
|
export class CiCommand extends Command {
|
|
@@ -46,7 +48,13 @@ export class CiCommand extends Command {
|
|
|
46
48
|
for (const l of d.lines) this.context.stdout.write(`${l}\n`)
|
|
47
49
|
return 0
|
|
48
50
|
}
|
|
49
|
-
const
|
|
51
|
+
const job = this.job != null ? readJobOpts(root, this.job) : null
|
|
52
|
+
let code = 1
|
|
53
|
+
try {
|
|
54
|
+
code = this.job != null && job != null ? await runJob(root, this.job, 'ci', job, (l) => { this.context.stdout.write(`${l}\n`) }) : await this.gate(root)
|
|
55
|
+
} catch (e) {
|
|
56
|
+
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
57
|
+
}
|
|
50
58
|
if (this.job != null) recordExit(root, this.job, code)
|
|
51
59
|
return code
|
|
52
60
|
}
|
|
@@ -72,7 +80,7 @@ export class CiCommand extends Command {
|
|
|
72
80
|
// (loop-guarded, skipped under CI/frozen/source consumption)
|
|
73
81
|
await maybeSelfUpdateAndRestart(root, { fresh: this.fresh })
|
|
74
82
|
try {
|
|
75
|
-
|
|
83
|
+
const opts = {
|
|
76
84
|
bail: this.bail,
|
|
77
85
|
json: this.json,
|
|
78
86
|
fix: this.fix,
|
|
@@ -84,8 +92,13 @@ export class CiCommand extends Command {
|
|
|
84
92
|
onlyIds: this.only,
|
|
85
93
|
fresh: this.fresh,
|
|
86
94
|
...(this.full ? { tests: 'full' as const } : {}),
|
|
87
|
-
|
|
88
|
-
})
|
|
95
|
+
}
|
|
96
|
+
const print = (l: string): void => { this.context.stdout.write(`${l}\n`) }
|
|
97
|
+
// on a machine with the home, the repo's hub runs it (home/via-hub.ts)
|
|
98
|
+
// a job is already the run, wherever it was started from
|
|
99
|
+
const via = this.job != null ? null : await runViaHub(root, 'ci', opts, { print, printErr: (l) => this.context.stderr.write(`${l}\n`) })
|
|
100
|
+
if (via != null) return via
|
|
101
|
+
return await runCi(root, { ...opts, print })
|
|
89
102
|
} catch (e) {
|
|
90
103
|
this.context.stderr.write(`FAIL: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
91
104
|
return 1
|
|
@@ -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.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { ConfigKeysCommand } from './cli/cmd_ConfigKeys.ts'
|
|
|
13
13
|
import { CiCommand } from './cli/cmd_Ci.ts'
|
|
14
14
|
import { ContractsCommand } from './cli/cmd_Contracts.ts'
|
|
15
15
|
import { FleetCommand } from './cli/cmd_Fleet.ts'
|
|
16
|
+
import { DaemonCommand, GuiCommand, PmCommand, PmInstallCommand, PmUninstallCommand, WebCommand } from './cli/cmd_Home.ts'
|
|
16
17
|
import { HooksCommitMsgCommand, HooksInstallCommand, HooksListCommand, HooksPreCommitCommand } from './cli/cmd_Hooks.ts'
|
|
17
18
|
import { InitCommand } from './cli/cmd_Init.ts'
|
|
18
19
|
import { KeysCommand } from './cli/cmd_Keys.ts'
|
|
@@ -59,6 +60,12 @@ export function buildCli(): Cli {
|
|
|
59
60
|
cli.register(CheckCommand)
|
|
60
61
|
cli.register(TuiCommand)
|
|
61
62
|
cli.register(ServeCommand)
|
|
63
|
+
cli.register(PmCommand)
|
|
64
|
+
cli.register(DaemonCommand)
|
|
65
|
+
cli.register(WebCommand)
|
|
66
|
+
cli.register(PmInstallCommand)
|
|
67
|
+
cli.register(PmUninstallCommand)
|
|
68
|
+
cli.register(GuiCommand)
|
|
62
69
|
cli.register(ContractsCommand)
|
|
63
70
|
cli.register(AsksCommand)
|
|
64
71
|
cli.register(AsksCheckCommand)
|
|
@@ -87,5 +94,18 @@ export function buildCli(): Cli {
|
|
|
87
94
|
return cli
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
// only when this file IS the process, so importing it costs nothing
|
|
91
|
-
|
|
97
|
+
// only when this file IS the process, so importing it costs nothing. A repo's
|
|
98
|
+
// installed copy first hands the command to the house source (handoff.ts). One copy
|
|
99
|
+
// of the lib per process: a repo config that imports its own install lands on
|
|
100
|
+
// this one (one-lib.ts), so the machine's version runs in every repo. Then pm
|
|
101
|
+
// is started when it is down (autostart.ts), before the command runs
|
|
102
|
+
if (import.meta.main) {
|
|
103
|
+
const argv = process.argv.slice(2)
|
|
104
|
+
const { handoff } = await import('./home/handoff.ts')
|
|
105
|
+
handoff(argv)
|
|
106
|
+
const { useOneLib } = await import('./core/one-lib.ts')
|
|
107
|
+
useOneLib()
|
|
108
|
+
const { autostart } = await import('./home/autostart.ts')
|
|
109
|
+
await autostart(argv)
|
|
110
|
+
void buildCli().runExit(argv)
|
|
111
|
+
}
|