@openparachute/hub 0.6.2 → 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.
- package/README.md +87 -35
- package/package.json +1 -1
- package/src/__tests__/api-hub-upgrade.test.ts +690 -0
- package/src/__tests__/api-modules-ops.test.ts +359 -3
- package/src/__tests__/api-modules.test.ts +54 -0
- package/src/__tests__/expose-cloudflare.test.ts +163 -72
- package/src/__tests__/expose-off-auto.test.ts +26 -1
- package/src/__tests__/expose.test.ts +260 -240
- package/src/__tests__/hub-control.test.ts +1 -242
- package/src/__tests__/hub-server.test.ts +64 -0
- package/src/__tests__/hub-unit.test.ts +574 -0
- package/src/__tests__/init.test.ts +219 -2
- package/src/__tests__/lifecycle.test.ts +416 -1448
- package/src/__tests__/managed-unit.test.ts +575 -0
- package/src/__tests__/migrate-cutover.test.ts +840 -0
- package/src/__tests__/migrate-offer.test.ts +240 -0
- package/src/__tests__/migrate.test.ts +132 -0
- package/src/__tests__/module-ops-client.test.ts +556 -0
- package/src/__tests__/port-probe.test.ts +23 -0
- package/src/__tests__/setup-wizard.test.ts +130 -0
- package/src/__tests__/status-supervisor.test.ts +504 -0
- package/src/__tests__/status.test.ts +157 -708
- package/src/__tests__/supervisor.test.ts +471 -6
- package/src/__tests__/upgrade.test.ts +351 -5
- package/src/api-hub-upgrade.ts +384 -0
- package/src/api-hub.ts +2 -1
- package/src/api-modules-ops.ts +221 -0
- package/src/api-modules.ts +18 -2
- package/src/cli.ts +97 -12
- package/src/cloudflare/connector-service.ts +117 -322
- package/src/commands/expose-cloudflare.ts +63 -71
- package/src/commands/expose-supervisor.ts +247 -0
- package/src/commands/expose.ts +59 -48
- package/src/commands/init.ts +225 -12
- package/src/commands/lifecycle.ts +455 -816
- package/src/commands/migrate-cutover.ts +837 -0
- package/src/commands/migrate.ts +71 -2
- package/src/commands/serve-boot.ts +71 -25
- package/src/commands/status.ts +535 -235
- package/src/commands/upgrade.ts +100 -2
- package/src/help.ts +128 -68
- package/src/hub-control.ts +23 -162
- package/src/hub-server.ts +39 -0
- package/src/hub-unit.ts +735 -0
- package/src/hub-upgrade-helper.ts +306 -0
- package/src/hub-upgrade-mode.ts +209 -0
- package/src/hub-upgrade-status.ts +150 -0
- package/src/managed-unit.ts +692 -0
- package/src/migrate-offer.ts +186 -0
- package/src/module-ops-client.ts +457 -0
- package/src/port-probe.ts +50 -0
- package/src/process-state.ts +19 -3
- package/src/setup-wizard.ts +80 -1
- package/src/supervisor.ts +389 -38
- package/web/ui/dist/assets/index-D_6AFvZy.js +61 -0
- package/web/ui/dist/assets/{index-BiBlvEaj.css → index-mz8XcVPP.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-CIN3mnmf.js +0 -61
package/src/api-modules.ts
CHANGED
|
@@ -42,8 +42,13 @@ import { FIRST_PARTY_FALLBACKS, KNOWN_MODULES } from "./service-spec.ts";
|
|
|
42
42
|
// still required) and the latter for vault/scribe/runner (post-FALLBACK
|
|
43
43
|
// retirement, hub#310). The local helper hides the split from the rest of
|
|
44
44
|
// this file.
|
|
45
|
-
import {
|
|
46
|
-
|
|
45
|
+
import {
|
|
46
|
+
type UiSubUnit,
|
|
47
|
+
type UiSubUnitStatus,
|
|
48
|
+
readManifest,
|
|
49
|
+
readManifestLenient,
|
|
50
|
+
} from "./services-manifest.ts";
|
|
51
|
+
import type { ModuleStartError, ModuleState, Supervisor } from "./supervisor.ts";
|
|
47
52
|
|
|
48
53
|
/**
|
|
49
54
|
* Resolve a curated module to the display + install bootstrap data the
|
|
@@ -174,6 +179,16 @@ interface ModuleWireShape {
|
|
|
174
179
|
latest_version: string | null;
|
|
175
180
|
supervisor_status: ModuleState["status"] | null;
|
|
176
181
|
pid: number | null;
|
|
182
|
+
/**
|
|
183
|
+
* Structured supervisor start-failure detail (§6.5 / §6.4), when the
|
|
184
|
+
* supervisor recorded one for this module — a preflight `MissingDependencyError`
|
|
185
|
+
* or the alive-but-never-bound shape (hub#487). Mirrors the services.json
|
|
186
|
+
* `lastStartError` the detached path persists, so `parachute status` and the
|
|
187
|
+
* SPA keep the SAME friendly missing-dependency surface (#188) whether a
|
|
188
|
+
* module was started via the supervisor or the detached path. Null when the
|
|
189
|
+
* module started cleanly (or the hub is in pidfile/CLI mode with no supervisor).
|
|
190
|
+
*/
|
|
191
|
+
supervisor_start_error: ModuleStartError | null;
|
|
177
192
|
/**
|
|
178
193
|
* The path on disk where the module is installed, if known. Surfaces
|
|
179
194
|
* the BUN_INSTALL or bun-link install location for operator debug —
|
|
@@ -477,6 +492,7 @@ export async function handleApiModules(req: Request, deps: ApiModulesDeps): Prom
|
|
|
477
492
|
latest_version: latestByShort.get(short) ?? null,
|
|
478
493
|
supervisor_status: state?.status ?? null,
|
|
479
494
|
pid: state?.pid ?? null,
|
|
495
|
+
supervisor_start_error: state?.startError ?? null,
|
|
480
496
|
install_dir: installed?.installDir ?? null,
|
|
481
497
|
uis: toUisWireShape(installed?.uis),
|
|
482
498
|
management_url: managementUrlByShort.get(short) ?? null,
|
package/src/cli.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { exposePublic, exposeTailnet } from "./commands/expose.ts";
|
|
|
14
14
|
import { init } from "./commands/init.ts";
|
|
15
15
|
import { install } from "./commands/install.ts";
|
|
16
16
|
import { logs, restart, start, stop } from "./commands/lifecycle.ts";
|
|
17
|
+
import { cutoverToSupervised, teardownHubUnit } from "./commands/migrate-cutover.ts";
|
|
17
18
|
import { migrate } from "./commands/migrate.ts";
|
|
18
19
|
import { serve } from "./commands/serve.ts";
|
|
19
20
|
import { setup } from "./commands/setup.ts";
|
|
@@ -454,7 +455,11 @@ async function main(argv: string[]): Promise<number> {
|
|
|
454
455
|
console.log(statusHelp());
|
|
455
456
|
return 0;
|
|
456
457
|
}
|
|
457
|
-
|
|
458
|
+
// Pass an empty `supervisor` block so `status` takes the Phase 3c
|
|
459
|
+
// dual-dispatch: on a box with a hub unit installed it reads the platform
|
|
460
|
+
// manager + the running supervisor; on a legacy detached box it falls back
|
|
461
|
+
// to the pidfile readout (design §6.4). Tests drive the seams directly.
|
|
462
|
+
return await status({ supervisor: {} });
|
|
458
463
|
|
|
459
464
|
case "expose": {
|
|
460
465
|
const hubExtract = extractHubOrigin(rest);
|
|
@@ -528,7 +533,14 @@ async function main(argv: string[]): Promise<number> {
|
|
|
528
533
|
const { exposeCloudflareUp, exposeCloudflareOff } = await import(
|
|
529
534
|
"./commands/expose-cloudflare.ts"
|
|
530
535
|
);
|
|
531
|
-
|
|
536
|
+
// `supervisor: {}` opts into the Phase 4 dual-dispatch (design §4.3): on
|
|
537
|
+
// a box with a hub unit installed, ensure the unit + drive the running
|
|
538
|
+
// supervisor for the post-expose vault restart, and leave the hub running
|
|
539
|
+
// on `off`; on a legacy detached box (no unit), the unchanged path.
|
|
540
|
+
const cfOpts = {
|
|
541
|
+
supervisor: {},
|
|
542
|
+
...(flagExtract.tunnelName ? { tunnelName: flagExtract.tunnelName } : {}),
|
|
543
|
+
};
|
|
532
544
|
if (action === "off") {
|
|
533
545
|
return await exposeCloudflareOff(cfOpts);
|
|
534
546
|
}
|
|
@@ -540,7 +552,14 @@ async function main(argv: string[]): Promise<number> {
|
|
|
540
552
|
// doesn't block on an invisible prompt.
|
|
541
553
|
if (isTtyInteractive()) {
|
|
542
554
|
const { exposePublicInteractive } = await import("./commands/expose-interactive.ts");
|
|
543
|
-
|
|
555
|
+
// Thread `supervisor: {}` into BOTH provider opts so the interactive
|
|
556
|
+
// path takes the Phase 4 unit-arm on a unit-managed box regardless of
|
|
557
|
+
// which provider the operator picks (design §4.3).
|
|
558
|
+
return await exposePublicInteractive({
|
|
559
|
+
preselect: "cloudflare",
|
|
560
|
+
exposeOpts: { supervisor: {} },
|
|
561
|
+
cloudflareOpts: { supervisor: {} },
|
|
562
|
+
});
|
|
544
563
|
}
|
|
545
564
|
console.error("parachute expose public --cloudflare: --domain <hostname> is required.");
|
|
546
565
|
console.error("Example: parachute expose public --cloudflare --domain vault.example.com");
|
|
@@ -557,7 +576,14 @@ async function main(argv: string[]): Promise<number> {
|
|
|
557
576
|
return await exposeCloudflareUp(flagExtract.domain, cfOpts);
|
|
558
577
|
}
|
|
559
578
|
|
|
560
|
-
|
|
579
|
+
// `supervisor: {}` opts into the Phase 4 dual-dispatch (design §4.3): on a
|
|
580
|
+
// box with a hub unit installed, ensure the unit (not a detached spawn) +
|
|
581
|
+
// drive the running supervisor for the post-expose vault restart, and leave
|
|
582
|
+
// the hub running on `off`; on a legacy detached box, the unchanged path.
|
|
583
|
+
const exposeOpts = {
|
|
584
|
+
supervisor: {},
|
|
585
|
+
...(hubExtract.hubOrigin ? { hubOrigin: hubExtract.hubOrigin } : {}),
|
|
586
|
+
};
|
|
561
587
|
|
|
562
588
|
// `--tailnet` is the explicit Tailscale Funnel pin — bypass both the
|
|
563
589
|
// interactive picker and the non-TTY auto-pick. Goes straight to
|
|
@@ -572,7 +598,9 @@ async function main(argv: string[]): Promise<number> {
|
|
|
572
598
|
// hands back to the flag-driven entry points.
|
|
573
599
|
if (layer === "public" && action === "up" && isTtyInteractive()) {
|
|
574
600
|
const { exposePublicInteractive } = await import("./commands/expose-interactive.ts");
|
|
575
|
-
|
|
601
|
+
// `exposeOpts` already carries `supervisor: {}`; thread it into the
|
|
602
|
+
// Cloudflare branch too so the unit-arm applies regardless of pick.
|
|
603
|
+
return await exposePublicInteractive({ exposeOpts, cloudflareOpts: { supervisor: {} } });
|
|
576
604
|
}
|
|
577
605
|
|
|
578
606
|
// Non-TTY auto-pick: detect which provider is configured and run it.
|
|
@@ -591,7 +619,12 @@ async function main(argv: string[]): Promise<number> {
|
|
|
591
619
|
// guessing intent.
|
|
592
620
|
if (layer === "public" && action === "up" && !flagExtract.skipProviderCheck) {
|
|
593
621
|
const { exposePublicAutoPick } = await import("./commands/expose-public-auto.ts");
|
|
594
|
-
|
|
622
|
+
// `tailscaleOpts` carries `supervisor: {}`; thread it into the Cloudflare
|
|
623
|
+
// branch too so the Phase 4 unit-arm applies regardless of auto-pick.
|
|
624
|
+
return await exposePublicAutoPick({
|
|
625
|
+
tailscaleOpts: exposeOpts,
|
|
626
|
+
cloudflareOpts: { supervisor: {} },
|
|
627
|
+
});
|
|
595
628
|
}
|
|
596
629
|
|
|
597
630
|
// `expose public off` (no `--cloudflare`) auto-detects which provider is
|
|
@@ -600,7 +633,16 @@ async function main(argv: string[]): Promise<number> {
|
|
|
600
633
|
// remember which provider they brought up last.
|
|
601
634
|
if (layer === "public" && action === "off") {
|
|
602
635
|
const { runExposePublicOffAutoDetect } = await import("./commands/expose-off-auto.ts");
|
|
603
|
-
|
|
636
|
+
// `tailscaleOffOpts` carries `supervisor: {}`; thread it into the
|
|
637
|
+
// Cloudflare teardown leg too so the Phase 4 supervisor resolution is
|
|
638
|
+
// consistent across both providers on the auto-detect path (matching
|
|
639
|
+
// the explicit `--cloudflare off` branch above). Harmless today
|
|
640
|
+
// (exposeCloudflareOff has no stopHub call) but keeps `supervisor: {}`
|
|
641
|
+
// threaded everywhere.
|
|
642
|
+
return await runExposePublicOffAutoDetect({
|
|
643
|
+
tailscaleOffOpts: exposeOpts,
|
|
644
|
+
cloudflareOffOpts: { supervisor: {} },
|
|
645
|
+
});
|
|
604
646
|
}
|
|
605
647
|
|
|
606
648
|
// `--skip-provider-check` fallthrough: pin to today's Tailscale-Funnel
|
|
@@ -624,7 +666,17 @@ async function main(argv: string[]): Promise<number> {
|
|
|
624
666
|
console.error(`parachute start: ${hubExtract.error}`);
|
|
625
667
|
return 1;
|
|
626
668
|
}
|
|
627
|
-
|
|
669
|
+
// `supervisor: {}` opts into the Phase 3b dual-dispatch: on a box with a
|
|
670
|
+
// hub unit installed, drive the running supervisor; on a legacy detached
|
|
671
|
+
// box (no unit), fall through to the unchanged detached path (design §3.3).
|
|
672
|
+
// `migrateOffer: { enabled: true }` arms the §7.5 detect-and-offer on the
|
|
673
|
+
// detached arm (offers the supervised cutover when a prior detached
|
|
674
|
+
// install is found; never auto-migrates).
|
|
675
|
+
const startOpts = {
|
|
676
|
+
supervisor: {},
|
|
677
|
+
migrateOffer: { enabled: true },
|
|
678
|
+
...(hubExtract.hubOrigin ? { hubOrigin: hubExtract.hubOrigin } : {}),
|
|
679
|
+
};
|
|
628
680
|
return await start(hubExtract.rest[0], startOpts);
|
|
629
681
|
}
|
|
630
682
|
|
|
@@ -633,7 +685,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
633
685
|
console.log(stopHelp());
|
|
634
686
|
return 0;
|
|
635
687
|
}
|
|
636
|
-
return await stop(rest[0]);
|
|
688
|
+
return await stop(rest[0], { supervisor: {}, migrateOffer: { enabled: true } });
|
|
637
689
|
}
|
|
638
690
|
|
|
639
691
|
case "restart": {
|
|
@@ -641,7 +693,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
641
693
|
console.log(restartHelp());
|
|
642
694
|
return 0;
|
|
643
695
|
}
|
|
644
|
-
return await restart(rest[0]);
|
|
696
|
+
return await restart(rest[0], { supervisor: {}, migrateOffer: { enabled: true } });
|
|
645
697
|
}
|
|
646
698
|
|
|
647
699
|
case "upgrade": {
|
|
@@ -682,7 +734,11 @@ async function main(argv: string[]): Promise<number> {
|
|
|
682
734
|
);
|
|
683
735
|
return 1;
|
|
684
736
|
}
|
|
685
|
-
|
|
737
|
+
// `supervisor: {}` opts into the Phase 4 dual-dispatch (design §5): on a
|
|
738
|
+
// box with a hub unit installed, `upgrade hub` rewrites the binary then
|
|
739
|
+
// restarts the UNIT via the platform manager (children re-boot from
|
|
740
|
+
// services.json); on a legacy detached box, the unchanged restart path.
|
|
741
|
+
const upgradeOpts: Parameters<typeof upgrade>[1] = { supervisor: {} };
|
|
686
742
|
if (tagExtract.tag) upgradeOpts.tag = tagExtract.tag;
|
|
687
743
|
if (channelExtract.value === "rc" || channelExtract.value === "latest") {
|
|
688
744
|
upgradeOpts.channel = channelExtract.value;
|
|
@@ -711,6 +767,33 @@ async function main(argv: string[]): Promise<number> {
|
|
|
711
767
|
console.log(migrateHelp());
|
|
712
768
|
return 0;
|
|
713
769
|
}
|
|
770
|
+
// §7.4 teardown — remove the hub unit (the cutover rollback path).
|
|
771
|
+
if (rest.includes("--teardown")) {
|
|
772
|
+
const teardownUnknown = rest.find((a) => a !== "--teardown");
|
|
773
|
+
if (teardownUnknown !== undefined) {
|
|
774
|
+
console.error(`parachute migrate: unknown argument "${teardownUnknown}"`);
|
|
775
|
+
console.error("usage: parachute migrate --teardown");
|
|
776
|
+
return 1;
|
|
777
|
+
}
|
|
778
|
+
teardownHubUnit();
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
781
|
+
// §7.1 detached→supervised cutover. Opt-in surface (the archive sweep
|
|
782
|
+
// below stays the bare `migrate` default — the cutover is destructive-
|
|
783
|
+
// adjacent and must be asked for, not implicit).
|
|
784
|
+
if (rest.includes("--to-supervised")) {
|
|
785
|
+
const cutoverUnknown = rest.find((a) => a !== "--to-supervised");
|
|
786
|
+
if (cutoverUnknown !== undefined) {
|
|
787
|
+
console.error(`parachute migrate: unknown argument "${cutoverUnknown}"`);
|
|
788
|
+
console.error("usage: parachute migrate --to-supervised");
|
|
789
|
+
return 1;
|
|
790
|
+
}
|
|
791
|
+
const result = await cutoverToSupervised();
|
|
792
|
+
for (const line of result.messages) console.log(line);
|
|
793
|
+
// "already-migrated" / "migrated" are success; every other outcome is a
|
|
794
|
+
// recoverable failure that should exit non-zero so scripts can retry.
|
|
795
|
+
return result.outcome === "migrated" || result.outcome === "already-migrated" ? 0 : 1;
|
|
796
|
+
}
|
|
714
797
|
const dryRun = rest.includes("--dry-run");
|
|
715
798
|
const list = rest.includes("--list");
|
|
716
799
|
const yes = rest.includes("--yes") || rest.includes("-y");
|
|
@@ -719,7 +802,9 @@ async function main(argv: string[]): Promise<number> {
|
|
|
719
802
|
);
|
|
720
803
|
if (unknown !== undefined) {
|
|
721
804
|
console.error(`parachute migrate: unknown argument "${unknown}"`);
|
|
722
|
-
console.error(
|
|
805
|
+
console.error(
|
|
806
|
+
"usage: parachute migrate [--list] [--dry-run] [--yes] [--to-supervised] [--teardown]",
|
|
807
|
+
);
|
|
723
808
|
return 1;
|
|
724
809
|
}
|
|
725
810
|
return await migrate({ dryRun, list, yes });
|