@openparachute/hub 0.6.3 → 0.6.4-rc.10
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/package.json +1 -2
- package/src/__tests__/account-home-ui.test.ts +344 -110
- package/src/__tests__/account-mirror.test.ts +156 -0
- package/src/__tests__/account-setup.test.ts +880 -0
- package/src/__tests__/account-usage.test.ts +137 -0
- package/src/__tests__/account-vault-admin-token.test.ts +301 -0
- package/src/__tests__/account-vault-token.test.ts +53 -1
- package/src/__tests__/admin-vault-admin-token.test.ts +17 -0
- package/src/__tests__/admin-vaults.test.ts +20 -0
- package/src/__tests__/api-account.test.ts +236 -4
- package/src/__tests__/api-invites.test.ts +217 -0
- package/src/__tests__/api-mint-token.test.ts +259 -10
- package/src/__tests__/api-modules-ops.test.ts +195 -3
- package/src/__tests__/api-modules.test.ts +40 -4
- package/src/__tests__/api-settings-hub-origin.test.ts +13 -8
- package/src/__tests__/auto-wire.test.ts +101 -1
- package/src/__tests__/cli.test.ts +188 -2
- package/src/__tests__/cloudflare-state.test.ts +104 -0
- package/src/__tests__/expose-2fa-warning.test.ts +11 -8
- package/src/__tests__/expose-cloudflare.test.ts +135 -9
- package/src/__tests__/expose-interactive.test.ts +234 -7
- package/src/__tests__/expose-supervisor-version.test.ts +104 -0
- package/src/__tests__/expose.test.ts +10 -5
- package/src/__tests__/grants.test.ts +197 -8
- package/src/__tests__/hub-origin-resolution.test.ts +179 -25
- package/src/__tests__/hub-server.test.ts +761 -13
- package/src/__tests__/hub-unit.test.ts +185 -0
- package/src/__tests__/init.test.ts +579 -3
- package/src/__tests__/install.test.ts +448 -2
- package/src/__tests__/invites.test.ts +220 -0
- package/src/__tests__/launchctl-guard.test.ts +185 -0
- package/src/__tests__/migrate-cutover.test.ts +33 -0
- package/src/__tests__/module-ops-client.test.ts +68 -0
- package/src/__tests__/scope-explanations.test.ts +16 -0
- package/src/__tests__/serve-boot.test.ts +74 -1
- package/src/__tests__/serve.test.ts +121 -7
- package/src/__tests__/setup-wizard.test.ts +110 -0
- package/src/__tests__/spawn-path.test.ts +191 -0
- package/src/__tests__/status.test.ts +64 -0
- package/src/__tests__/supervisor.test.ts +374 -0
- package/src/__tests__/users.test.ts +66 -0
- package/src/__tests__/well-known.test.ts +25 -0
- package/src/__tests__/wizard.test.ts +72 -1
- package/src/account-home-ui.ts +481 -235
- package/src/account-mirror.ts +126 -0
- package/src/account-setup.ts +381 -0
- package/src/account-usage.ts +118 -0
- package/src/account-vault-admin-token.ts +242 -0
- package/src/account-vault-token.ts +36 -2
- package/src/admin-login-ui.ts +121 -0
- package/src/admin-vault-admin-token.ts +8 -2
- package/src/admin-vaults.ts +137 -29
- package/src/api-account.ts +118 -1
- package/src/api-invites.ts +345 -0
- package/src/api-mint-token.ts +81 -0
- package/src/api-modules-ops.ts +168 -53
- package/src/api-modules.ts +36 -0
- package/src/auto-wire.ts +87 -0
- package/src/cli.ts +128 -34
- package/src/cloudflare/detect.ts +1 -1
- package/src/cloudflare/state.ts +104 -8
- package/src/commands/expose-2fa-warning.ts +17 -13
- package/src/commands/expose-cloudflare.ts +103 -36
- package/src/commands/expose-interactive.ts +163 -17
- package/src/commands/expose-supervisor.ts +45 -0
- package/src/commands/init.ts +183 -4
- package/src/commands/install.ts +321 -3
- package/src/commands/migrate-cutover.ts +12 -5
- package/src/commands/serve-boot.ts +33 -3
- package/src/commands/serve.ts +158 -37
- package/src/commands/status.ts +9 -1
- package/src/commands/wizard.ts +36 -2
- package/src/grants.ts +113 -0
- package/src/help.ts +18 -5
- package/src/hub-db.ts +70 -2
- package/src/hub-server.ts +438 -41
- package/src/hub-settings.ts +3 -3
- package/src/hub-unit.ts +259 -9
- package/src/invites.ts +291 -0
- package/src/launchctl-guard.ts +131 -0
- package/src/managed-unit.ts +13 -3
- package/src/migrate-offer.ts +15 -6
- package/src/module-ops-client.ts +47 -22
- package/src/scope-attenuation.ts +19 -0
- package/src/scope-explanations.ts +9 -1
- package/src/service-spec.ts +17 -4
- package/src/setup-wizard.ts +34 -2
- package/src/spawn-path.ts +148 -0
- package/src/supervisor.ts +232 -7
- package/src/users.ts +54 -8
- package/src/vault-hub-origin-env.ts +28 -0
- package/src/vault-name.ts +13 -1
- package/src/well-known.ts +13 -0
- package/web/ui/dist/assets/{index-mz8XcVPP.css → index-BYYUeLGA.css} +1 -1
- package/web/ui/dist/assets/index-D3cDUOOj.js +61 -0
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-D_0TRjeo.js +0 -61
package/src/commands/init.ts
CHANGED
|
@@ -35,12 +35,18 @@
|
|
|
35
35
|
import { spawnSync } from "node:child_process";
|
|
36
36
|
import { join } from "node:path";
|
|
37
37
|
import { fileURLToPath } from "node:url";
|
|
38
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
38
39
|
import { CONFIG_DIR, SERVICES_MANIFEST_PATH } from "../config.ts";
|
|
39
40
|
import { type ExposeState, readExposeState } from "../expose-state.ts";
|
|
40
41
|
import { type EnsureHubOpts, HUB_DEFAULT_PORT, HUB_SVC, readHubPort } from "../hub-control.ts";
|
|
41
42
|
import { hubDbPath, openHubDb } from "../hub-db.ts";
|
|
42
43
|
import { deriveHubOrigin } from "../hub-origin.ts";
|
|
43
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
type EnsureHubVersionMatchesResult,
|
|
46
|
+
ensureHubUnit,
|
|
47
|
+
ensureHubVersionMatches,
|
|
48
|
+
installAndStartHubUnit,
|
|
49
|
+
} from "../hub-unit.ts";
|
|
44
50
|
import { issueOperatorToken, readOperatorTokenFile } from "../operator-token.ts";
|
|
45
51
|
import { type AliveFn, defaultAlive, processState } from "../process-state.ts";
|
|
46
52
|
import { findService, readManifestLenient } from "../services-manifest.ts";
|
|
@@ -81,6 +87,18 @@ export interface InitOpts {
|
|
|
81
87
|
* Design §3.3 (init row), §4.1/§4.2, appendix (c).
|
|
82
88
|
*/
|
|
83
89
|
ensureHub?: (opts: EnsureHubOpts) => Promise<{ pid: number; port: number; started: boolean }>;
|
|
90
|
+
/**
|
|
91
|
+
* Test seam: version-check-and-restart at the hub adoption point (#590).
|
|
92
|
+
* After init confirms a hub is answering on the canonical port, it compares
|
|
93
|
+
* the RUNNING hub's `/health` version against this installed package version;
|
|
94
|
+
* on mismatch it restarts the managed unit (once) so a freshly-installed hub
|
|
95
|
+
* never adopts a stale zombie. Production wires `ensureHubVersionMatches`;
|
|
96
|
+
* tests stub it to assert the call without touching launchctl / the live hub.
|
|
97
|
+
*/
|
|
98
|
+
ensureHubVersion?: (ctx: {
|
|
99
|
+
port: number;
|
|
100
|
+
log: (line: string) => void;
|
|
101
|
+
}) => Promise<EnsureHubVersionMatchesResult>;
|
|
84
102
|
/**
|
|
85
103
|
* Test seam: guarantee an operator token exists once the hub is up (design
|
|
86
104
|
* §3.1 / §3.3). Production reads `operator.token`; if absent AND a hub user
|
|
@@ -162,6 +180,17 @@ export interface InitOpts {
|
|
|
162
180
|
* already known so there's no question to ask).
|
|
163
181
|
*/
|
|
164
182
|
noWizardPrompt?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Test seam: probe the running hub for its first-claim bootstrap token
|
|
185
|
+
* (hub#576). Production hits `GET http://127.0.0.1:<port>/admin/setup` with
|
|
186
|
+
* `accept: application/json` and reads `bootstrapToken` (the hub returns it
|
|
187
|
+
* only to loopback callers). Returns the token string when the hub is in
|
|
188
|
+
* wizard mode (no admin yet), or `undefined` when there's no token to surface
|
|
189
|
+
* (admin already exists, or the probe failed). Init uses it to print the
|
|
190
|
+
* token next to the admin URL when the hub is publicly exposed, so a browser
|
|
191
|
+
* operator can claim the box without digging through the hub logs.
|
|
192
|
+
*/
|
|
193
|
+
fetchBootstrapTokenImpl?: (loopbackUrl: string) => Promise<string | undefined>;
|
|
165
194
|
}
|
|
166
195
|
|
|
167
196
|
/**
|
|
@@ -461,6 +490,41 @@ async function defaultRunCliWizard(opts: {
|
|
|
461
490
|
return await runCliWizard(opts);
|
|
462
491
|
}
|
|
463
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Default impl for the bootstrap-token probe (hub#576). GETs the loopback hub's
|
|
495
|
+
* `/admin/setup` with `accept: application/json` and returns the `bootstrapToken`
|
|
496
|
+
* the hub hands to loopback callers. Returns `undefined` on any failure (hub
|
|
497
|
+
* not answering, no token because an admin already exists, malformed body) —
|
|
498
|
+
* surfacing the token is a convenience, never a hard dependency of init.
|
|
499
|
+
*/
|
|
500
|
+
async function defaultFetchBootstrapToken(loopbackUrl: string): Promise<string | undefined> {
|
|
501
|
+
// Debug breadcrumb (gated on PARACHUTE_DEBUG so it never clutters the normal
|
|
502
|
+
// operator output). When the token doesn't print in the field, this tells a
|
|
503
|
+
// troubleshooter WHY — hub didn't answer, returned non-200, or the body
|
|
504
|
+
// carried no token (already-claimed / no-gate) — instead of a silent nothing.
|
|
505
|
+
const debug = (msg: string): void => {
|
|
506
|
+
if (process.env.PARACHUTE_DEBUG) console.error(`[init][bootstrap-token] ${msg}`);
|
|
507
|
+
};
|
|
508
|
+
try {
|
|
509
|
+
const res = await fetch(`${loopbackUrl.replace(/\/+$/, "")}/admin/setup`, {
|
|
510
|
+
headers: { accept: "application/json" },
|
|
511
|
+
});
|
|
512
|
+
if (!res.ok) {
|
|
513
|
+
debug(`probe returned ${res.status}; not printing a token`);
|
|
514
|
+
return undefined;
|
|
515
|
+
}
|
|
516
|
+
const body = (await res.json()) as { bootstrapToken?: unknown };
|
|
517
|
+
if (typeof body.bootstrapToken === "string" && body.bootstrapToken.length > 0) {
|
|
518
|
+
return body.bootstrapToken;
|
|
519
|
+
}
|
|
520
|
+
debug("probe ok but no bootstrapToken in body (already-claimed or no gate active)");
|
|
521
|
+
return undefined;
|
|
522
|
+
} catch (err) {
|
|
523
|
+
debug(`probe failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
464
528
|
/**
|
|
465
529
|
* Prompt for the wizard-choice question (hub#168 Cut 4). Returns the
|
|
466
530
|
* picked option, or `undefined` if the operator quit. Default is
|
|
@@ -536,6 +600,18 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
536
600
|
// spawn). The `ensureHub` seam is preserved for tests (and the return shape is
|
|
537
601
|
// unchanged); only the production default flipped.
|
|
538
602
|
const ensureHub = opts.ensureHub ?? defaultEnsureHubViaUnit;
|
|
603
|
+
// #590: after the hub is confirmed up, compare its RUNNING version to the
|
|
604
|
+
// installed package version and restart the managed unit on mismatch, so a
|
|
605
|
+
// freshly-installed hub never adopts a stale zombie that merely answers
|
|
606
|
+
// /health. Injectable for tests.
|
|
607
|
+
const ensureHubVersion =
|
|
608
|
+
opts.ensureHubVersion ??
|
|
609
|
+
((ctx) =>
|
|
610
|
+
ensureHubVersionMatches({
|
|
611
|
+
installedVersion: pkg.version,
|
|
612
|
+
port: ctx.port,
|
|
613
|
+
log: ctx.log,
|
|
614
|
+
}));
|
|
539
615
|
const guaranteeOperatorToken = opts.guaranteeOperatorToken ?? defaultGuaranteeOperatorToken;
|
|
540
616
|
const readExposeStateFn = opts.readExposeStateFn ?? (() => readExposeState());
|
|
541
617
|
const isTty = opts.isTty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -547,6 +623,7 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
547
623
|
const exposeCloudflareImpl = opts.exposeCloudflareImpl ?? defaultExposeCloudflare;
|
|
548
624
|
const installVaultModuleImpl = opts.installVaultModuleImpl ?? defaultInstallVaultModule;
|
|
549
625
|
const runCliWizardImpl = opts.runCliWizardImpl ?? defaultRunCliWizard;
|
|
626
|
+
const fetchBootstrapTokenImpl = opts.fetchBootstrapTokenImpl ?? defaultFetchBootstrapToken;
|
|
550
627
|
|
|
551
628
|
log("Parachute init — getting your hub set up.");
|
|
552
629
|
log("");
|
|
@@ -593,6 +670,38 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
593
670
|
// overridden, so the fallback is almost always correct.
|
|
594
671
|
if (hubPort === undefined) hubPort = HUB_DEFAULT_PORT;
|
|
595
672
|
|
|
673
|
+
// Step 1.25 (#590): the hub answered /health, but is it the version we just
|
|
674
|
+
// installed? A zombie LaunchAgent survives `rm -rf ~/.parachute`, so a brand-
|
|
675
|
+
// new install can adopt month-old code that merely keeps the port. Compare the
|
|
676
|
+
// RUNNING version to the installed package version; on mismatch, restart the
|
|
677
|
+
// managed unit (once) so the tunnel/wizard/vault-install downstream bind to the
|
|
678
|
+
// NEW code. A non-unit-managed hub (legacy detached pid / dev `bun run serve`)
|
|
679
|
+
// is NOT killed — we surface the mismatch + an actionable message and bail so
|
|
680
|
+
// the operator decides. A still-mismatched-after-restart (bun-linked branch)
|
|
681
|
+
// warns + continues rather than looping.
|
|
682
|
+
try {
|
|
683
|
+
const versionResult = await ensureHubVersion({ port: hubPort, log });
|
|
684
|
+
for (const m of versionResult.messages) log(m);
|
|
685
|
+
if (versionResult.outcome === "not-unit-managed") {
|
|
686
|
+
// We can't safely take over a hub we don't own. Stop here so init doesn't
|
|
687
|
+
// wire a fresh tunnel + credentials to a stale runtime (the #590 field bug).
|
|
688
|
+
log("");
|
|
689
|
+
log("Resolve the version mismatch above, then re-run `parachute init`.");
|
|
690
|
+
return 1;
|
|
691
|
+
}
|
|
692
|
+
if (versionResult.outcome === "restart-failed") {
|
|
693
|
+
log("");
|
|
694
|
+
log("The hub service manager rejected the restart command.");
|
|
695
|
+
log("Try checking the logs:");
|
|
696
|
+
log(" parachute logs hub");
|
|
697
|
+
return 1;
|
|
698
|
+
}
|
|
699
|
+
// `match` / `not-running` / `restarted` / `still-mismatched` → continue.
|
|
700
|
+
} catch (err) {
|
|
701
|
+
// A version-check failure must never block init — degrade to a note.
|
|
702
|
+
log(`note: hub version check skipped (${err instanceof Error ? err.message : String(err)})`);
|
|
703
|
+
}
|
|
704
|
+
|
|
596
705
|
// Step 1.5: guarantee an operator token exists (design §3.1 / §3.3). Under
|
|
597
706
|
// the unified model every per-module verb is an authenticated module-ops
|
|
598
707
|
// call, so the steady-state operator needs an `operator.token` on disk — the
|
|
@@ -619,7 +728,12 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
619
728
|
exposeTailnetImpl,
|
|
620
729
|
exposeCloudflareImpl,
|
|
621
730
|
});
|
|
622
|
-
|
|
731
|
+
// hub#565: exposure is an ENHANCEMENT, not a prerequisite. A failed
|
|
732
|
+
// expose chain must NOT abort init — warn + print the exact retry
|
|
733
|
+
// command, then fall through to vault install + the admin-URL/wizard
|
|
734
|
+
// handoff on the loopback URL. Init's contract is hub up → vault module
|
|
735
|
+
// installed → admin URL → wizard, ALWAYS.
|
|
736
|
+
if (code !== 0) warnExposeFailedContinue(opts.exposeChoice, log);
|
|
623
737
|
// Refresh state — the chain may have brought up an FQDN.
|
|
624
738
|
exposeState = readExposeStateFn();
|
|
625
739
|
} else if (opts.noExposePrompt) {
|
|
@@ -642,7 +756,9 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
642
756
|
exposeTailnetImpl,
|
|
643
757
|
exposeCloudflareImpl,
|
|
644
758
|
});
|
|
645
|
-
|
|
759
|
+
// hub#565: warn + continue on a failed expose chain rather than
|
|
760
|
+
// aborting init (same contract as the non-interactive branch above).
|
|
761
|
+
if (code !== 0) warnExposeFailedContinue(picked, log);
|
|
646
762
|
exposeState = readExposeStateFn();
|
|
647
763
|
}
|
|
648
764
|
}
|
|
@@ -704,6 +820,19 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
704
820
|
return 1;
|
|
705
821
|
}
|
|
706
822
|
|
|
823
|
+
// hub#576: when the hub is publicly exposed AND still in wizard mode (no
|
|
824
|
+
// admin yet), the admin URL above is a public FQDN — whoever opens it first
|
|
825
|
+
// claims the box. Surface the first-claim bootstrap token in the operator's
|
|
826
|
+
// OWN terminal so the wizard's account step demands proof of box access. We
|
|
827
|
+
// only probe + print on the public-FQDN path: a loopback-only install needs
|
|
828
|
+
// no token (reaching 127.0.0.1 already proves access), and the CLI-wizard
|
|
829
|
+
// path picks the token up transparently over loopback (above). The probe is
|
|
830
|
+
// best-effort — a failure (or an already-claimed hub) just prints nothing.
|
|
831
|
+
let bootstrapToken: string | undefined;
|
|
832
|
+
if (exposeState?.canonicalFqdn) {
|
|
833
|
+
bootstrapToken = await fetchBootstrapTokenImpl(`http://127.0.0.1:${hubPort}`);
|
|
834
|
+
}
|
|
835
|
+
|
|
707
836
|
log("");
|
|
708
837
|
if (hasVault) {
|
|
709
838
|
log("Looks good — your hub is up and a vault is configured.");
|
|
@@ -713,6 +842,24 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
713
842
|
log("");
|
|
714
843
|
log(` ${adminUrl}`);
|
|
715
844
|
log("");
|
|
845
|
+
if (bootstrapToken) {
|
|
846
|
+
log("Because this hub is reachable on the public internet, the wizard asks for a");
|
|
847
|
+
log("one-time bootstrap token before it lets anyone create the admin account —");
|
|
848
|
+
log("so whoever opens the URL first can't claim your hub. Paste this when asked:");
|
|
849
|
+
log("");
|
|
850
|
+
log(` ${bootstrapToken}`);
|
|
851
|
+
log("");
|
|
852
|
+
log("(Valid until the admin is created or the hub restarts. Re-run `parachute init`");
|
|
853
|
+
log(" to mint a fresh one.)");
|
|
854
|
+
log("");
|
|
855
|
+
}
|
|
856
|
+
// hub#565: when we're on the loopback URL (no public exposure active),
|
|
857
|
+
// remind the operator they can expose later. Skipped once an FQDN is up.
|
|
858
|
+
if (!exposeState?.canonicalFqdn) {
|
|
859
|
+
log("(Reachable on this machine. To expose it publicly later, run");
|
|
860
|
+
log(" `parachute expose public --cloudflare` or `parachute expose public --tailnet`.)");
|
|
861
|
+
log("");
|
|
862
|
+
}
|
|
716
863
|
|
|
717
864
|
// Step 4.5: offer the operator the CLI wizard vs. the browser wizard
|
|
718
865
|
// (hub#168 Cut 4). Aaron's 2026-05-28 directive: "we should be able to
|
|
@@ -742,7 +889,13 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
742
889
|
if (choice === "cli") {
|
|
743
890
|
log("");
|
|
744
891
|
log("Launching the CLI wizard. (You can also visit the URL above in a browser any time.)");
|
|
745
|
-
|
|
892
|
+
// hub#576: drive the CLI wizard against the LOOPBACK hub, not the public
|
|
893
|
+
// FQDN in `adminUrl`. The wizard runs on this box, so loopback is both
|
|
894
|
+
// correct and what lets the hub hand it the bootstrap token transparently
|
|
895
|
+
// (the loopback-gated GET /admin/setup probe) — the operator never has to
|
|
896
|
+
// copy the token out of the startup logs.
|
|
897
|
+
const cliWizardUrl = `http://127.0.0.1:${hubPort}`;
|
|
898
|
+
return await runCliWizardImpl({ hubUrl: cliWizardUrl, log });
|
|
746
899
|
}
|
|
747
900
|
|
|
748
901
|
// Step 5: offer to open the browser. Skip in non-TTY shells (CI),
|
|
@@ -784,6 +937,32 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
784
937
|
return 0;
|
|
785
938
|
}
|
|
786
939
|
|
|
940
|
+
/** The exact retry command for a given exposure choice (hub#565 / #566). */
|
|
941
|
+
export function exposeRetryCommand(choice: ExposeChoice): string {
|
|
942
|
+
if (choice === "tailnet") return "parachute expose public --tailnet";
|
|
943
|
+
// `none` never reaches here in practice — `runExposureChoice("none")` always
|
|
944
|
+
// returns 0, so `warnExposeFailedContinue` (the only caller) is never invoked
|
|
945
|
+
// for it. It falls through to the `--cloudflare` branch below; harmless, and
|
|
946
|
+
// spelled out so the fallthrough isn't read as a bug.
|
|
947
|
+
// Cloudflare (and the unreachable `none`): default the bare command to
|
|
948
|
+
// `--cloudflare` so the operator who picked Cloudflare lands in the right
|
|
949
|
+
// provider on retry (bare `parachute expose public` defaults to Tailscale
|
|
950
|
+
// Funnel — hub#566).
|
|
951
|
+
return "parachute expose public --cloudflare";
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* hub#565: warn that the exposure chain failed but init is continuing anyway,
|
|
956
|
+
* and print the exact retry command. Exposure is an enhancement, not a
|
|
957
|
+
* prerequisite — init still installs the vault module and hands off to the
|
|
958
|
+
* wizard on the loopback URL.
|
|
959
|
+
*/
|
|
960
|
+
function warnExposeFailedContinue(choice: ExposeChoice, log: (line: string) => void): void {
|
|
961
|
+
log("");
|
|
962
|
+
log("⚠ Couldn't finish setting up public access — continuing without it.");
|
|
963
|
+
log(` To expose publicly later, run: ${exposeRetryCommand(choice)}`);
|
|
964
|
+
}
|
|
965
|
+
|
|
787
966
|
/**
|
|
788
967
|
* Dispatch the chosen exposure path. Returns the exit code of the
|
|
789
968
|
* downstream chain. `none` is a no-op (success).
|