@openparachute/hub 0.6.3-rc.1 → 0.6.3-rc.2

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.
Files changed (40) hide show
  1. package/README.md +87 -35
  2. package/package.json +1 -1
  3. package/src/__tests__/api-hub-upgrade.test.ts +690 -0
  4. package/src/__tests__/expose-cloudflare.test.ts +163 -72
  5. package/src/__tests__/expose-off-auto.test.ts +26 -1
  6. package/src/__tests__/expose.test.ts +260 -240
  7. package/src/__tests__/hub-control.test.ts +1 -242
  8. package/src/__tests__/hub-server.test.ts +64 -0
  9. package/src/__tests__/lifecycle.test.ts +431 -1886
  10. package/src/__tests__/migrate-cutover.test.ts +840 -0
  11. package/src/__tests__/migrate-offer.test.ts +240 -0
  12. package/src/__tests__/migrate.test.ts +132 -0
  13. package/src/__tests__/status-supervisor.test.ts +12 -77
  14. package/src/__tests__/status.test.ts +157 -708
  15. package/src/__tests__/upgrade.test.ts +351 -5
  16. package/src/api-hub-upgrade.ts +384 -0
  17. package/src/api-hub.ts +2 -1
  18. package/src/cli.ts +85 -10
  19. package/src/commands/expose-cloudflare.ts +63 -71
  20. package/src/commands/expose-supervisor.ts +247 -0
  21. package/src/commands/expose.ts +59 -48
  22. package/src/commands/lifecycle.ts +184 -873
  23. package/src/commands/migrate-cutover.ts +837 -0
  24. package/src/commands/migrate.ts +71 -2
  25. package/src/commands/status.ts +35 -282
  26. package/src/commands/upgrade.ts +100 -2
  27. package/src/help.ts +128 -68
  28. package/src/hub-control.ts +23 -162
  29. package/src/hub-server.ts +28 -0
  30. package/src/hub-upgrade-helper.ts +306 -0
  31. package/src/hub-upgrade-mode.ts +209 -0
  32. package/src/hub-upgrade-status.ts +150 -0
  33. package/src/managed-unit.ts +20 -2
  34. package/src/migrate-offer.ts +186 -0
  35. package/src/process-state.ts +19 -3
  36. package/src/supervisor.ts +29 -24
  37. package/web/ui/dist/assets/index-D_6AFvZy.js +61 -0
  38. package/web/ui/dist/assets/{index-BiBlvEaj.css → index-mz8XcVPP.css} +1 -1
  39. package/web/ui/dist/index.html +2 -2
  40. package/web/ui/dist/assets/index-CIN3mnmf.js +0 -61
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@openparachute/hub` — the local hub for the [Parachute](https://parachute.computer) ecosystem. The `parachute` binary is one of its surfaces.
4
4
 
5
- The hub coordinates the modules running on your machine: it installs them, runs them as background processes, exposes them over Tailscale, serves the discovery document at `/.well-known/parachute.json`, and (soon) issues OAuth tokens. Each module (vault, app, scribe, …) stays a standalone package; the hub stitches them together.
5
+ The hub coordinates the modules running on your machine: it installs them, supervises them as child processes (the hub itself runs under your platform's process manager — launchd / systemd / the container runtime), exposes them over Tailscale, serves the discovery document at `/.well-known/parachute.json`, and (soon) issues OAuth tokens. Each module (vault, app, scribe, …) stays a standalone package; the hub stitches them together.
6
6
 
7
7
  > Previously published as `@openparachute/cli`. Renamed 2026-04-26 to better reflect the role — see [parachute-patterns/hub-as-issuer](https://github.com/ParachuteComputer/parachute-patterns/blob/main/patterns/hub-as-issuer.md). The `parachute` binary name is unchanged.
8
8
 
@@ -75,7 +75,9 @@ parachute init
75
75
 
76
76
  `parachute init` is idempotent — every re-run is safe. End to end it:
77
77
 
78
- 1. **Starts the hub** if it isn't already running (port `1939`).
78
+ 1. **Installs and starts the hub** as a managed unit (launchd on a Mac,
79
+ systemd on a Linux VM) on port `1939`, so it survives reboots. Re-runs are
80
+ no-ops if the unit is already up.
79
81
  2. **Offers to expose it** so you can reach the wizard from other devices. In a
80
82
  terminal you pick: stay loopback-only, your **tailnet** (`tailscale serve` —
81
83
  private to your own Tailscale devices), or a **Cloudflare Tunnel** (public
@@ -108,9 +110,9 @@ UI. Verify the stack any time:
108
110
 
109
111
  ```sh
110
112
  parachute status
111
- # SERVICE PORT VERSION PROCESS PID UPTIME HEALTH LATENCY
112
- # parachute-hub 1939 0.5.14 running 12344 20s ok 1ms
113
- # parachute-vault 1940 0.4.5 running 12345 12s ok 2ms
113
+ # SERVICE PORT VERSION STATE PID UPTIME LATENCY SOURCE
114
+ # parachute-hub 1939 0.5.14 active 12344 20s 1ms managed unit (launchd)
115
+ # parachute-vault 1940 0.4.5 active 12345 12s 2ms npm (0.4.5)
114
116
  ```
115
117
 
116
118
  Vault is up on `127.0.0.1:1940`; Claude Code picks up the MCP on your next
@@ -144,7 +146,7 @@ still work and are additive:
144
146
  ```sh
145
147
  parachute install vault # install + register + create first vault + start one module
146
148
  parachute setup # older interactive multi-pick: survey + install vault/notes/scribe
147
- parachute start vault # PID + logs tracked under ~/.parachute/vault/
149
+ parachute start vault # start one module via the running hub's supervisor
148
150
  ```
149
151
 
150
152
  ### Expose across your tailnet
@@ -180,35 +182,77 @@ deleted or reset from the Users page — it changes its own password at
180
182
 
181
183
  ## Service lifecycle
182
184
 
183
- `parachute start`, `stop`, `restart`, and `logs` manage services as background processes no launchd, no manual `bun serve`, no hunting for PIDs.
185
+ Parachute runs **one runtime everywhere**: `parachute serve` — the hub in the
186
+ foreground with an in-process supervisor that runs each module as an attached
187
+ child, multiplexes their logs, and restarts crashed ones. That `serve` process
188
+ runs under your platform's own process manager — **launchd** on a Mac,
189
+ **systemd** on a Linux VM, the **container runtime** on Render / Fly — so the
190
+ hub (and every module under it) survives reboots and crashes without you
191
+ SSHing back in.
192
+
193
+ `parachute init` installs and starts that managed hub unit for you; the
194
+ lifecycle verbs then drive the running supervisor:
184
195
 
185
196
  ```sh
186
- parachute start # start every installed service
187
- parachute start vault # just one
188
- parachute stop # SIGTERM, then SIGKILL after 10s if stuck
189
- parachute restart vault # stop + start
197
+ parachute start # ensure the hub unit is up (boots every module)
198
+ parachute start vault # start one module via the running supervisor
199
+ parachute stop vault # stop one module via the supervisor
200
+ parachute stop # stop the hub unit (children stop with it)
201
+ parachute restart vault # restart one module via the supervisor
202
+ parachute restart # restart the hub unit (re-boots every module)
190
203
  parachute logs vault # last 200 lines
191
204
  parachute logs vault -f # tail (like `tail -f`)
192
205
  ```
193
206
 
194
- State lives under `~/.parachute/<service>/`:
195
-
196
- - `run/<service>.pid` child PID; `parachute status` uses this to report running/stopped + uptime
197
- - `logs/<service>.log` stdout + stderr (appended)
198
-
199
- `parachute start` is idempotent: if the service is already running, it's a no-op. Stale PID files (process died without cleanup) are cleared on the next start. Services whose PID file is absent are treated as *unknown* — status still probes their port, so externally-managed services (e.g. you ran `parachute-vault serve` directly) aren't misreported as stopped.
200
-
201
- ### Migrating from launchd (pre-launch beta)
202
-
203
- If you previously ran vault under launchd, switch to `parachute start`:
207
+ How the verbs map to the model:
208
+
209
+ - **`start` / `stop` / `restart <svc>`** are clients of the running hub. They
210
+ ensure the hub unit is up, then call its supervisor over a loopback
211
+ module-ops API (authenticated with your operator token) to start / stop /
212
+ restart that one module. There's no per-module daemon to track the
213
+ supervisor owns module processes.
214
+ - **`start` (no service)** ensures the hub unit is up; the hub boots every
215
+ installed module on start, so this brings the whole stack up.
216
+ - **`stop` (no service)** stops the **hub unit** through the platform manager
217
+ (`launchctl bootout` / `systemctl stop`); the modules are attached children
218
+ and stop with it.
219
+ - **`restart` (no service)** restarts the **hub unit** (`launchctl kickstart
220
+ -k` / `systemctl restart`), which re-boots every module — it is *not* a
221
+ fan-out of per-module restarts.
222
+
223
+ `parachute status` shows a hub row even with zero modules installed — that row
224
+ is derived from the platform manager (`launchctl print` / `systemctl
225
+ is-active`, or "container runtime (managed)" on Render / Fly), since the
226
+ supervisor runs the modules but the manager runs the hub.
227
+
228
+ ### No process manager installed yet?
229
+
230
+ If you've never run `parachute init` (or you're on a legacy install that used
231
+ the old detached-daemon model), the lifecycle verbs will prompt you to run
232
+ `parachute migrate --to-supervised`, which installs the hub unit and moves you
233
+ onto the supervised model. See **Migrating a legacy install** below.
234
+
235
+ On a host with no process manager at all (a bare container, an init-less
236
+ image), there's no unit to install — run `parachute serve` in the foreground
237
+ instead (this is exactly what the container `CMD` does).
238
+
239
+ ### Migrating a legacy install
240
+
241
+ Earlier Parachute releases ran each service as an independent detached daemon
242
+ (its own pidfile + log file under `~/.parachute/<service>/`), with no
243
+ supervisor and no reboot survival. To move an existing box onto the supervised
244
+ model:
204
245
 
205
246
  ```sh
206
- launchctl unload ~/Library/LaunchAgents/computer.parachute.vault.plist
207
- rm ~/Library/LaunchAgents/computer.parachute.vault.plist
208
- parachute start vault
247
+ parachute migrate --to-supervised # install + start the hub unit, cut over
209
248
  ```
210
249
 
211
- An at-login auto-start mode (`parachute start --boot`) is on the post-launch roadmap.
250
+ The cutover is idempotent and re-runnable. It writes the platform unit, stops
251
+ the old detached processes, verifies the canonical ports are free, then starts
252
+ the hub unit and confirms the hub is healthy. To roll it back, `parachute
253
+ migrate --teardown` removes the unit (run that *before* `bun remove -g
254
+ @openparachute/hub` so a removed package doesn't leave a unit pointing at a
255
+ deleted binary).
212
256
 
213
257
  ### Migrating from pre-CLI installs
214
258
 
@@ -225,17 +269,21 @@ Anything swept goes to `~/.parachute/.archive-<YYYY-MM-DD>/` with its original n
225
269
  ## Two supported layers (plus an exploratory third)
226
270
 
227
271
  Each additive; each can be turned off without affecting the layer below.
272
+ Exposure is decoupled from the hub's own lifecycle: `expose` makes the
273
+ already-running hub reachable on a network layer, and `expose <layer> off`
274
+ tears down only that exposure — the hub keeps running as its managed unit
275
+ either way.
228
276
 
229
- - **Local** — services on loopback. Zero config. Browsers treat `localhost` as a secure context, so OAuth, PKCE, and Web Crypto all just work out of the box.
277
+ - **Local** — the hub on loopback. Zero config. Browsers treat `localhost` as a secure context, so OAuth, PKCE, and Web Crypto all just work out of the box.
230
278
  - **Tailnet** — `parachute expose tailnet` wraps `tailscale serve` for every registered service. HTTPS via Tailscale's MagicDNS cert. Only machines on your tailnet can reach the URL. **This is the documented shape for the hub today.** Tailnet is already authenticated at the network layer, every user's tailnet is their own, and the OAuth + module access work happening in the hub is being designed against this shape first.
231
279
 
232
280
  ### Public exposure (exploratory)
233
281
 
234
282
  `parachute expose public` exists for early testers. It routes each handler through `tailscale funnel` (or, with `--cloudflare`, a named Cloudflare tunnel) so the same URLs become reachable from the public internet. The code path is live and the flag still works, but the public-internet posture (DNS, cross-internet OAuth, Funnel quirks) hasn't been hardened the way tailnet has — expect rough edges.
235
283
 
236
- When the hub's OAuth issuer + per-module scope enforcement land, public will re-enter the documented narrative as "now safe." Until then, prefer tailnet.
284
+ When the hub's OAuth issuer + per-module scope enforcement land, public will re-enter the documented narrative as "now safe." Until then, prefer tailnet. (If you route a public layer through Cloudflare, note their bot-protection / Browser Integrity Check can interfere with OAuth and MCP clients — see the caveat on [parachute.computer](https://parachute.computer).)
237
285
 
238
- Under the hood, tailnet mode uses `tailscale serve` and public mode uses `tailscale funnel`; both write into the same node-level serve config. The CLI records which layer is live so that `expose <other-layer> off` is a no-op rather than a surprise teardown of the active layer.
286
+ Under the hood, tailnet mode uses `tailscale serve` and public mode uses `tailscale funnel`; both write into the same node-level serve config. The CLI records which layer is live so that `expose <other-layer> off` is a no-op rather than a surprise teardown of the active layer. `expose off` only ever tears down exposure — it never stops the hub (the platform manager owns the hub's lifecycle now).
239
287
 
240
288
  ## Path-routing (and why)
241
289
 
@@ -251,7 +299,9 @@ https://parachute.<tailnet>.ts.net/.well-known/parachute.json ← discovery
251
299
 
252
300
  The hub page fetches the discovery doc at load, then each service's `/.parachute/info` endpoint for display name, tagline, and icon. Adding a new service is zero CLI code — drop in its manifest entry and the hub picks it up.
253
301
 
254
- Under the hood, `/` and `/.well-known/parachute.json` are proxied by a tiny internal HTTP server (`parachute-hub`) that `parachute expose` spawns on the loopback interface. Tailscale's file-serve mode is sandbox-restricted on macOS, so a localhost proxy is the portable shape. The hub process is stopped automatically when the last exposure layer is torn down; `parachute status` lists it under `(internal)`.
302
+ Under the hood, `/` and `/.well-known/parachute.json` are served by the hub
303
+ process on the loopback interface — the same `parachute serve` process the
304
+ platform manager keeps running. Tailscale's file-serve mode is sandbox-restricted on macOS, so a localhost proxy is the portable shape. The hub is a persistent managed unit: it runs whether or not any exposure layer is up, so `expose tailnet off` / `expose public off` tears down only the *exposure*, leaving the hub serving on loopback. `parachute status` lists the hub row (manager-derived) at the top.
255
305
 
256
306
  The `/.well-known/parachute.json` document is an always-present descriptor — flat `services[]` array that the hub iterates, plus top-level keys for legacy clients:
257
307
 
@@ -402,19 +452,21 @@ Public-internet exposure (`parachute expose public`) is exploratory — see "Pub
402
452
  Run `parachute --help` for the top-level list, and `parachute <subcommand> --help` for details on any individual command.
403
453
 
404
454
  ```
405
- parachute init fresh-install front door: start hub, offer expose,
406
- install vault module, open the setup wizard
455
+ parachute init fresh-install front door: install + start the
456
+ managed hub, offer expose, install vault, wizard
407
457
  parachute setup-wizard --hub-url <url>
408
458
  in-terminal mirror of /admin/setup (Account/Vault/Expose)
409
459
  parachute setup older interactive multi-pick service installer
410
460
  parachute install <service> install and register a service
411
- parachute status show installed services, process state, health
412
- parachute start [service] start services in the background
413
- parachute stop [service] stop services (SIGTERM 10s → SIGKILL)
414
- parachute restart [service] stop + start
461
+ parachute status show installed services, run state, health
462
+ parachute start [service] start a module via the supervisor (or ensure the hub is up)
463
+ parachute stop [service] stop a module via the supervisor (or the hub unit)
464
+ parachute restart [service] restart a module via the supervisor (or the hub unit)
465
+ parachute serve run the hub + supervisor foregrounded (the runtime)
415
466
  parachute logs <service> [-f] print/tail service logs
416
467
  parachute expose tailnet [off] HTTPS across your tailnet (supported)
417
468
  parachute expose public [off] HTTPS on the public internet (exploratory)
469
+ parachute migrate --to-supervised move a legacy detached install to the managed hub
418
470
  parachute migrate [--dry-run] archive legacy files at ecosystem root
419
471
  parachute vault <args...> dispatch to parachute-vault
420
472
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/hub",
3
- "version": "0.6.3-rc.1",
3
+ "version": "0.6.3-rc.2",
4
4
  "description": "parachute — the local hub for the Parachute ecosystem (discovery, ports, lifecycle, soon OAuth).",
5
5
  "license": "AGPL-3.0",
6
6
  "publishConfig": {