@openparachute/hub 0.5.13-rc.11 → 0.5.13-rc.13
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 +7 -4
- package/package.json +1 -1
- package/src/__tests__/api-modules.test.ts +37 -4
- package/src/__tests__/port-assign.test.ts +6 -1
- package/src/__tests__/setup-wizard.test.ts +29 -12
- package/src/__tests__/setup.test.ts +1 -0
- package/src/__tests__/well-known.test.ts +4 -2
- package/src/api-modules.ts +7 -5
- package/src/commands/setup.ts +3 -1
- package/src/service-spec.ts +37 -2
- package/src/setup-wizard.ts +5 -1
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,
|
|
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
|
|
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 (
|
|
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
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
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/
|
|
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-
|
|
1775
|
-
version: "0.
|
|
1776
|
-
port:
|
|
1777
|
-
paths: ["/
|
|
1778
|
-
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 ?
|
|
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
|
-
|
|
1828
|
-
|
|
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&
|
|
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(
|
|
@@ -361,14 +361,16 @@ describe("buildWellKnown", () => {
|
|
|
361
361
|
test("tagline rides through from services.json (no resolver needed)", () => {
|
|
362
362
|
const notesWithTagline: ServiceEntry = {
|
|
363
363
|
...notes,
|
|
364
|
-
tagline: "Notes PWA
|
|
364
|
+
tagline: "Notes PWA — daemon deprecated 2026-05-22; install `app` for the current path.",
|
|
365
365
|
};
|
|
366
366
|
const doc = buildWellKnown({
|
|
367
367
|
services: [notesWithTagline],
|
|
368
368
|
canonicalOrigin: "https://x.example",
|
|
369
369
|
});
|
|
370
370
|
const svc = doc.services.find((s) => s.name === "parachute-notes");
|
|
371
|
-
expect(svc?.tagline).toBe(
|
|
371
|
+
expect(svc?.tagline).toBe(
|
|
372
|
+
"Notes PWA — daemon deprecated 2026-05-22; install `app` for the current path.",
|
|
373
|
+
);
|
|
372
374
|
});
|
|
373
375
|
|
|
374
376
|
test("falls back to / for empty paths", () => {
|
package/src/api-modules.ts
CHANGED
|
@@ -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
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
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 {
|
package/src/commands/setup.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/service-spec.ts
CHANGED
|
@@ -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
|
-
|
|
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" },
|
|
@@ -289,7 +293,7 @@ const NOTES_FALLBACK: FirstPartyFallback = {
|
|
|
289
293
|
name: "notes",
|
|
290
294
|
manifestName: "parachute-notes",
|
|
291
295
|
displayName: "Notes",
|
|
292
|
-
tagline: "Notes PWA
|
|
296
|
+
tagline: "Notes PWA — daemon deprecated 2026-05-22; install `app` for the current path.",
|
|
293
297
|
kind: "frontend",
|
|
294
298
|
port: 1942,
|
|
295
299
|
paths: ["/notes"],
|
|
@@ -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
|
/**
|
package/src/setup-wizard.ts
CHANGED
|
@@ -1522,7 +1522,11 @@ const INSTALL_TILE_PROPS: ReadonlyArray<{
|
|
|
1522
1522
|
displayName: string;
|
|
1523
1523
|
tagline: string;
|
|
1524
1524
|
}> = [
|
|
1525
|
-
{
|
|
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",
|