@openparachute/hub 0.5.13-rc.11 → 0.5.13-rc.12

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/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, notes, scribe, channel, …) stays a standalone package; the hub stitches them together.
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.
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
 
@@ -160,16 +160,19 @@ Parachute services reserve a block of loopback ports in the canonical range **19
160
160
  | 1939 | parachute-hub (internal proxy + static) |
161
161
  | 1940 | parachute-vault |
162
162
  | 1941 | parachute-channel |
163
- | 1942 | parachute-notes |
163
+ | 1942 | parachute-notes *(deprecating — see [notes#154](https://github.com/ParachuteComputer/parachute-notes/issues/154); folds into parachute-app at 1946)* |
164
164
  | 1943 | parachute-scribe |
165
- | 1944–1949 | *unassigned (CLI fallback range)* |
165
+ | 1944 | *parachute-agent (retired 2026-05-20; slot held — see [`parachute-agent/DEPRECATED.md`](https://github.com/ParachuteComputer/parachute-agent/blob/main/DEPRECATED.md))* |
166
+ | 1945 | parachute-runner *(shipped; exploration-tier, not committed-core)* |
167
+ | 1946 | parachute-app *(committed core; UI host, ships Notes as canonical first app)* |
168
+ | 1947–1949 | *unassigned (CLI fallback range)* |
166
169
 
167
170
  The hub pins 1939 — no fallback. If something else is on 1939 when you run `parachute expose`, the command fails with a pointer to `lsof -iTCP:1939` rather than walking up into another service's slot.
168
171
 
169
172
  **The CLI is the port authority.** `parachute install <svc>` picks the port at install time and writes `PORT=<port>` into `~/.parachute/<svc>/.env`; lifecycle.start merges that .env into the spawn env so the next daemon boot binds the port the CLI assigned. The algorithm:
170
173
 
171
174
  1. Prefer the canonical slot (e.g. vault → 1940).
172
- 2. On collision, walk the unassigned range (1944–1949).
175
+ 2. On collision, walk the unassigned range (1947–1949).
173
176
  3. Range exhausted: assign past 1949 with a warning.
174
177
 
175
178
  Idempotent: an existing `PORT=` in `~/.parachute/<svc>/.env` wins, so re-installs and operator-edited ports survive across upgrades. Services keep their compiled-in fallbacks (vault → 1940 etc.) so a stand-alone `bun run` still works without a CLI-managed .env.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/hub",
3
- "version": "0.5.13-rc.11",
3
+ "version": "0.5.13-rc.12",
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": {
@@ -149,8 +149,10 @@ describe("GET /api/modules", () => {
149
149
 
150
150
  test("200 + curated list on fresh container (empty services.json)", async () => {
151
151
  // The v0.6 hot path: brand-new Render container, no services.json
152
- // yet. UI must render "install vault / notes / scribe / runner"
153
- // cards even though nothing's installed.
152
+ // yet. UI must render "install vault / app / notes / scribe / runner"
153
+ // cards even though nothing's installed. hub#323 inserted `app` between
154
+ // `vault` and `notes` — app auto-bootstraps notes-ui as a sub-unit;
155
+ // `notes` (notes-daemon) stays curated for back-compat install paths.
154
156
  const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
155
157
  const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
156
158
  db: h.db,
@@ -168,8 +170,8 @@ describe("GET /api/modules", () => {
168
170
  }>;
169
171
  supervisor_available: boolean;
170
172
  };
171
- // Curated order is preserved: vault → notes → scribe → runner.
172
- expect(body.modules.map((m) => m.short)).toEqual(["vault", "notes", "scribe", "runner"]);
173
+ // Curated order is preserved: vault → app → notes → scribe → runner.
174
+ expect(body.modules.map((m) => m.short)).toEqual(["vault", "app", "notes", "scribe", "runner"]);
173
175
  expect(body.modules.every((m) => m.available)).toBe(true);
174
176
  expect(body.modules.every((m) => !m.installed)).toBe(true);
175
177
  expect(body.modules.every((m) => m.latest_version === "0.9.9")).toBe(true);
@@ -177,6 +179,37 @@ describe("GET /api/modules", () => {
177
179
  expect(body.supervisor_available).toBe(false);
178
180
  });
179
181
 
182
+ test("app row carries package + display props from KNOWN_MODULES (#323)", async () => {
183
+ // hub#323 added app to CURATED_MODULES + KNOWN_MODULES so the admin SPA
184
+ // install catalog + setup-wizard install tile surface it. Spot-check the
185
+ // wire shape resolves app-specific fields (package, displayName, tagline)
186
+ // from KNOWN_MODULES rather than a stale default — same shape as the
187
+ // runner row test below.
188
+ const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
189
+ const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
190
+ db: h.db,
191
+ issuer: ISSUER,
192
+ manifestPath: h.manifestPath,
193
+ fetchLatestVersion: async () => "0.2.0",
194
+ });
195
+ expect(res.status).toBe(200);
196
+ const body = (await res.json()) as {
197
+ modules: Array<{
198
+ short: string;
199
+ package: string;
200
+ display_name: string;
201
+ tagline: string;
202
+ available: boolean;
203
+ }>;
204
+ };
205
+ const app = body.modules.find((m) => m.short === "app");
206
+ expect(app).toBeDefined();
207
+ expect(app?.package).toBe("@openparachute/app");
208
+ expect(app?.display_name).toBe("App");
209
+ expect(app?.tagline).toContain("auto-installs Notes");
210
+ expect(app?.available).toBe(true);
211
+ });
212
+
180
213
  test("runner row carries package + display props from FIRST_PARTY_FALLBACKS (#305)", async () => {
181
214
  // hub#305 added runner to CURATED_MODULES + FIRST_PARTY_FALLBACKS so
182
215
  // the admin SPA install catalog surfaces it. Spot-check the wire
@@ -68,8 +68,13 @@ describe("assignPort (pure)", () => {
68
68
  });
69
69
 
70
70
  test("third-party with reservations occupied walks further in the range", () => {
71
+ // PORT_RESERVATIONS post-hub#323: 1944 reserved, 1945 reserved, 1946
72
+ // assigned (parachute-app — KNOWN_MODULES carries the canonical slot
73
+ // so the fallback walker doesn't hand it to a third party), 1947
74
+ // reserved. With 1944 + 1945 occupied, the walker skips the assigned
75
+ // 1946 slot and lands on the next reserved-and-free port — 1947.
71
76
  const result = assignPort(undefined, [1944, 1945]);
72
- expect(result.port).toBe(1946);
77
+ expect(result.port).toBe(1947);
73
78
  expect(result.source).toBe("fallback-in-range");
74
79
  });
75
80
  });
@@ -1712,7 +1712,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
1712
1712
  });
1713
1713
  afterEach(() => h.cleanup());
1714
1714
 
1715
- test("done screen renders Install Notes + Install Scribe tiles when neither is installed", async () => {
1715
+ test("done screen renders Install App + Install Scribe tiles when neither is installed", async () => {
1716
1716
  const db = openHubDb(hubDbPath(h.dir));
1717
1717
  try {
1718
1718
  const user = await createUser(db, "owner", "pw");
@@ -1747,10 +1747,21 @@ describe("done screen install tiles (hub#272 Item B)", () => {
1747
1747
  );
1748
1748
  const html = await res.text();
1749
1749
  expect(html).toContain("What's next?");
1750
- expect(html).toContain("Install Notes");
1750
+ // hub#323: App replaces Notes as the first install tile. App auto-bootstraps
1751
+ // Notes (parachute-app §17 Phase 2.1) so operators don't need to install
1752
+ // notes-daemon directly; the tagline telegraphs that Notes comes with App.
1753
+ expect(html).toContain("Install App");
1751
1754
  expect(html).toContain("Install Scribe");
1752
- expect(html).toContain('action="/admin/setup/install/notes"');
1755
+ expect(html).toContain('action="/admin/setup/install/app"');
1753
1756
  expect(html).toContain('action="/admin/setup/install/scribe"');
1757
+ // App tile sits first in the render order — verified by both tiles
1758
+ // appearing AND app's index in the rendered HTML preceding scribe's.
1759
+ expect(html.indexOf("Install App")).toBeLessThan(html.indexOf("Install Scribe"));
1760
+ // Notes is no longer a wizard tile; notes-daemon still installable
1761
+ // via /api/modules/notes/install for back-compat, but the wizard
1762
+ // doesn't surface it.
1763
+ expect(html).not.toContain("Install Notes");
1764
+ expect(html).not.toContain('action="/admin/setup/install/notes"');
1754
1765
  } finally {
1755
1766
  db.close();
1756
1767
  }
@@ -1770,12 +1781,15 @@ describe("done screen install tiles (hub#272 Item B)", () => {
1770
1781
  paths: ["/vault/default"],
1771
1782
  health: "/health",
1772
1783
  },
1784
+ // hub#323: app replaces notes as the wizard's first install tile.
1785
+ // Seeding services.json with `parachute-app` exercises the
1786
+ // already-installed render path on the wizard's first tile.
1773
1787
  {
1774
- name: "parachute-notes",
1775
- version: "0.1.0",
1776
- port: 1942,
1777
- paths: ["/notes"],
1778
- health: "/notes/health",
1788
+ name: "parachute-app",
1789
+ version: "0.2.0",
1790
+ port: 1946,
1791
+ paths: ["/app", "/.parachute"],
1792
+ health: "/app/healthz",
1779
1793
  },
1780
1794
  ],
1781
1795
  },
@@ -1804,7 +1818,7 @@ describe("done screen install tiles (hub#272 Item B)", () => {
1804
1818
  }
1805
1819
  });
1806
1820
 
1807
- test("done screen renders op-poll panel when ?op_notes=<id> matches a registry op", async () => {
1821
+ test("done screen renders op-poll panel when ?op_app=<id> matches a registry op", async () => {
1808
1822
  const db = openHubDb(hubDbPath(h.dir));
1809
1823
  try {
1810
1824
  const user = await createUser(db, "owner", "pw");
@@ -1824,12 +1838,15 @@ describe("done screen install tiles (hub#272 Item B)", () => {
1824
1838
  );
1825
1839
  setSetting(db, "setup_expose_mode", "localhost");
1826
1840
  const reg = getDefaultOperationsRegistry();
1827
- const op = reg.create("install", "notes");
1828
- reg.update(op.id, { status: "running" }, "running bun add -g @openparachute/notes@latest");
1841
+ // hub#323: op-poll panel rides on the `app` tile now (app is the wizard's
1842
+ // first install tile post-Notes-as-app-migration). Same shape as the
1843
+ // pre-#324 `op_notes=<id>` flow.
1844
+ const op = reg.create("install", "app");
1845
+ reg.update(op.id, { status: "running" }, "running bun add -g @openparachute/app@latest");
1829
1846
  const { createSession } = await import("../sessions.ts");
1830
1847
  const session = createSession(db, { userId: user.id });
1831
1848
  const res = handleSetupGet(
1832
- req(`/admin/setup?just_finished=1&op_notes=${op.id}`, {
1849
+ req(`/admin/setup?just_finished=1&op_app=${op.id}`, {
1833
1850
  headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
1834
1851
  }),
1835
1852
  {
@@ -123,6 +123,7 @@ describe("setup", () => {
123
123
  { name: "parachute-scribe", port: 1943 },
124
124
  { name: "parachute-channel", port: 1941 },
125
125
  { name: "parachute-runner", port: 1945 },
126
+ { name: "parachute-app", port: 1946 },
126
127
  ];
127
128
  for (const s of seeds) {
128
129
  upsertService(
@@ -77,12 +77,14 @@ export const API_MODULES_REQUIRED_SCOPE = "parachute:host:auth";
77
77
 
78
78
  /**
79
79
  * Curated module short-names for v0.6 Render self-host. Marketplace is
80
- * Phase 2 — until then, the admin UI offers exactly these four. Order
81
- * is the recommended install order (vault → notes → scribe → runner;
82
- * runner comes last because it depends on a working vault + scribe to
83
- * be useful, and operators usually wire those up first).
80
+ * Phase 2 — until then, the admin UI offers exactly these. Order is the
81
+ * recommended install order (vault → app → notes → scribe → runner;
82
+ * app auto-bootstraps notes-ui on first boot `notes` here is the
83
+ * notes-daemon back-compat install path retained for operators still on
84
+ * the pre-app architecture; scribe + runner come last because they
85
+ * depend on a working vault + app to be useful).
84
86
  */
85
- export const CURATED_MODULES = ["vault", "notes", "scribe", "runner"] as const;
87
+ export const CURATED_MODULES = ["vault", "app", "notes", "scribe", "runner"] as const;
86
88
  export type CuratedModuleShort = (typeof CURATED_MODULES)[number];
87
89
 
88
90
  export interface ApiModulesDeps {
@@ -151,8 +151,10 @@ function surveyServices(manifestPath: string): ServiceChoice[] {
151
151
 
152
152
  const BLURBS: Record<string, string> = {
153
153
  vault: "knowledge graph (MCP) — your owner-authenticated note + tag store",
154
- notes: "Notes PWAweb/mobile UI on top of vault",
154
+ app: "Parachute UI host auto-installs Notes on first boot (recommended over notes-daemon)",
155
+ notes: "Notes PWA — web/mobile UI on top of vault (notes-daemon; superseded by `app`)",
155
156
  scribe: "audio transcription for dictation + recordings",
157
+ runner: "vault-as-job-substrate — scheduled claude -p against vault job notes",
156
158
  channel: "(exploratory — may retire) notification fan-out across modules",
157
159
  };
158
160
 
@@ -64,7 +64,11 @@ export const PORT_RESERVATIONS: readonly PortReservation[] = [
64
64
  { port: 1943, name: "parachute-scribe", status: "assigned" },
65
65
  { port: 1944, name: "unassigned", status: "reserved" },
66
66
  { port: 1945, name: "unassigned", status: "reserved" },
67
- { port: 1946, name: "unassigned", status: "reserved" },
67
+ // hub#323: parachute-app's canonical slot. Status `assigned` keeps the
68
+ // fallback-port walker (`assignPort` in port-assign.ts) from handing this
69
+ // port out to a colliding third-party module. The matching KNOWN_MODULES
70
+ // row carries the canonicalPort + paths for status/expose surfaces.
71
+ { port: 1946, name: "parachute-app", status: "assigned" },
68
72
  { port: 1947, name: "unassigned", status: "reserved" },
69
73
  { port: 1948, name: "unassigned", status: "reserved" },
70
74
  { port: 1949, name: "unassigned", status: "reserved" },
@@ -484,6 +488,37 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
484
488
  hasAuth: true,
485
489
  },
486
490
  },
491
+ app: {
492
+ short: "app",
493
+ package: "@openparachute/app",
494
+ manifestName: "parachute-app",
495
+ canonicalPort: 1946,
496
+ displayName: "App",
497
+ // Tagline telegraphs the auto-bootstrap so wizard + admin-SPA copy explain
498
+ // the architecture: installing `app` brings Notes (and other UIs) along
499
+ // via the Phase 2.1 bootstrap-default-apps step. The notes-daemon path
500
+ // still exists as a back-compat install (CURATED_MODULES still lists
501
+ // `notes`) but `app` is the recommended first install post-vault.
502
+ tagline: "Host module for Parachute UIs — auto-installs Notes on first boot.",
503
+ // Frontend posture: app's primary surface is serving sub-app UIs under
504
+ // `/app/<name>/` mounted under one origin (design doc §12). Hub's
505
+ // exposure-default + supervisor-defaults follow the frontend lane.
506
+ kind: "frontend",
507
+ canonicalPaths: ["/app", "/.parachute"],
508
+ canonicalHealth: "/app/healthz",
509
+ canonicalStripPrefix: false,
510
+ extras: {
511
+ // Backward-compat startCmd — same rationale as scribe / vault / runner
512
+ // above. Post-self-register, lifecycle reads module.json's startCmd via
513
+ // `composeKnownModuleSpec` and that path wins.
514
+ startCmd: () => ["parachute-app", "serve"],
515
+ // App's admin + per-UI surfaces gate behind hub-issued JWTs (design
516
+ // doc §6 same-hub auto-trust + scope `app:admin`). Surfaces in
517
+ // `parachute status` as auth-required by default, same posture as vault
518
+ // + runner.
519
+ hasAuth: true,
520
+ },
521
+ },
487
522
  };
488
523
 
489
524
  /**
@@ -1522,7 +1522,11 @@ const INSTALL_TILE_PROPS: ReadonlyArray<{
1522
1522
  displayName: string;
1523
1523
  tagline: string;
1524
1524
  }> = [
1525
- { short: "notes", displayName: "Notes", tagline: "Notes PWA backed by your vault." },
1525
+ {
1526
+ short: "app",
1527
+ displayName: "App",
1528
+ tagline: "Host module for Parachute UIs — auto-installs Notes on first boot.",
1529
+ },
1526
1530
  {
1527
1531
  short: "scribe",
1528
1532
  displayName: "Scribe",