@openparachute/hub 0.7.4-rc.6 → 0.7.4-rc.8
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 +2 -2
- package/src/__tests__/init.test.ts +153 -0
- package/src/__tests__/setup-wizard.test.ts +7 -7
- package/src/cli.ts +27 -5
- package/src/commands/init.ts +108 -0
- package/src/help.ts +12 -1
- package/src/setup-wizard.ts +13 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openparachute/hub",
|
|
3
|
-
"version": "0.7.4-rc.
|
|
3
|
+
"version": "0.7.4-rc.8",
|
|
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": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@node-rs/argon2": "^2.0.2",
|
|
43
|
-
"@openparachute/depcheck": "0.1.
|
|
43
|
+
"@openparachute/depcheck": "0.1.1",
|
|
44
44
|
"jose": "^6.2.2",
|
|
45
45
|
"otpauth": "^9.5.0",
|
|
46
46
|
"qrcode": "^1.5.4"
|
|
@@ -2218,5 +2218,158 @@ describe("resolveInitChannel (hub#694 bug 2)", () => {
|
|
|
2218
2218
|
});
|
|
2219
2219
|
});
|
|
2220
2220
|
|
|
2221
|
+
// ---------------------------------------------------------------------------
|
|
2222
|
+
// #478 Part 2 — `parachute init --vault-name <name>` creates the first vault
|
|
2223
|
+
// ---------------------------------------------------------------------------
|
|
2224
|
+
|
|
2225
|
+
describe("init --vault-name (#478 Part 2)", () => {
|
|
2226
|
+
/** Minimal stub that satisfies every init seam except the ones under test. */
|
|
2227
|
+
function baseOpts(
|
|
2228
|
+
h: Harness,
|
|
2229
|
+
overrides: Parameters<typeof init>[0] = {},
|
|
2230
|
+
): Parameters<typeof init>[0] {
|
|
2231
|
+
return {
|
|
2232
|
+
configDir: h.configDir,
|
|
2233
|
+
manifestPath: h.manifestPath,
|
|
2234
|
+
log: () => {},
|
|
2235
|
+
alive: () => false,
|
|
2236
|
+
ensureHubVersion: async () => ({
|
|
2237
|
+
outcome: "match" as const,
|
|
2238
|
+
installedVersion: "test",
|
|
2239
|
+
messages: [],
|
|
2240
|
+
}),
|
|
2241
|
+
ensureHub: async () => {
|
|
2242
|
+
writeHubPort(1939, h.configDir);
|
|
2243
|
+
return { pid: 0, port: 1939, started: true };
|
|
2244
|
+
},
|
|
2245
|
+
readExposeStateFn: () => undefined,
|
|
2246
|
+
isTty: false,
|
|
2247
|
+
platform: "linux" as const,
|
|
2248
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
2249
|
+
noBrowser: true,
|
|
2250
|
+
noExposePrompt: true,
|
|
2251
|
+
noWizardPrompt: true,
|
|
2252
|
+
...overrides,
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
test("(a) with vaultName set: invokes createFirstVaultImpl with the name", async () => {
|
|
2257
|
+
const h = makeHarness();
|
|
2258
|
+
try {
|
|
2259
|
+
const createCalls: string[] = [];
|
|
2260
|
+
const logs: string[] = [];
|
|
2261
|
+
const code = await init(
|
|
2262
|
+
baseOpts(h, {
|
|
2263
|
+
vaultName: "myvault",
|
|
2264
|
+
log: (l) => logs.push(l),
|
|
2265
|
+
createFirstVaultImpl: async (name) => {
|
|
2266
|
+
createCalls.push(name);
|
|
2267
|
+
return 0;
|
|
2268
|
+
},
|
|
2269
|
+
}),
|
|
2270
|
+
);
|
|
2271
|
+
expect(code).toBe(0);
|
|
2272
|
+
expect(createCalls).toEqual(["myvault"]);
|
|
2273
|
+
expect(logs.join("\n")).toContain('Creating vault "myvault"');
|
|
2274
|
+
expect(logs.join("\n")).toContain('Vault "myvault" created');
|
|
2275
|
+
} finally {
|
|
2276
|
+
h.cleanup();
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
test("(b) without vaultName: does NOT invoke createFirstVaultImpl", async () => {
|
|
2281
|
+
const h = makeHarness();
|
|
2282
|
+
try {
|
|
2283
|
+
let createCalled = false;
|
|
2284
|
+
const code = await init(
|
|
2285
|
+
baseOpts(h, {
|
|
2286
|
+
// no vaultName set
|
|
2287
|
+
createFirstVaultImpl: async () => {
|
|
2288
|
+
createCalled = true;
|
|
2289
|
+
return 0;
|
|
2290
|
+
},
|
|
2291
|
+
}),
|
|
2292
|
+
);
|
|
2293
|
+
expect(code).toBe(0);
|
|
2294
|
+
expect(createCalled).toBe(false);
|
|
2295
|
+
} finally {
|
|
2296
|
+
h.cleanup();
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
|
|
2300
|
+
test("(c) invalid vault name via the CLI seam: validateVaultName rejects it", () => {
|
|
2301
|
+
// The CLI validates before calling init; test the validator directly
|
|
2302
|
+
// so the unit test doesn't need to drive argv parsing. The validator
|
|
2303
|
+
// is the same one the CLI uses (imported in cli.ts).
|
|
2304
|
+
const { validateVaultName } = require("../vault-name.ts");
|
|
2305
|
+
const result = validateVaultName("My Vault!");
|
|
2306
|
+
expect(result.ok).toBe(false);
|
|
2307
|
+
expect(result.error).toMatch(/lowercase alphanumeric/);
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2310
|
+
test("(d) a seeded services.json vault MODULE row does NOT suppress the create", async () => {
|
|
2311
|
+
// REGRESSION (the rc.7 verification bug): Step 0.5's `install("vault",
|
|
2312
|
+
// { noCreate: true })` seeds a `parachute-vault` services.json row via
|
|
2313
|
+
// `spec.seedEntry` on EVERY fresh install. The OLD Step 1.6 keyed
|
|
2314
|
+
// idempotency off that row, so on the exact fresh-box path this feature
|
|
2315
|
+
// targets it saw the row + silently no-op'd the create — the headline
|
|
2316
|
+
// feature never fired. The row marks "module installed", not "instance
|
|
2317
|
+
// exists". Idempotency must live in `parachute-vault create`'s own exit
|
|
2318
|
+
// (which errors "already exists" on a real re-run), NOT a row precheck.
|
|
2319
|
+
// So: a seeded module row must NOT prevent the create from being attempted.
|
|
2320
|
+
const h = makeHarness();
|
|
2321
|
+
try {
|
|
2322
|
+
// Seed services.json with the module row (as Step 0.5 always does).
|
|
2323
|
+
seedVault(h.manifestPath);
|
|
2324
|
+
const createCalls: string[] = [];
|
|
2325
|
+
const logs: string[] = [];
|
|
2326
|
+
const code = await init(
|
|
2327
|
+
baseOpts(h, {
|
|
2328
|
+
vaultName: "myvault",
|
|
2329
|
+
log: (l) => logs.push(l),
|
|
2330
|
+
createFirstVaultImpl: async (name) => {
|
|
2331
|
+
createCalls.push(name);
|
|
2332
|
+
return 0;
|
|
2333
|
+
},
|
|
2334
|
+
}),
|
|
2335
|
+
);
|
|
2336
|
+
expect(code).toBe(0);
|
|
2337
|
+
// The create WAS attempted despite the seeded module row.
|
|
2338
|
+
expect(createCalls).toEqual(["myvault"]);
|
|
2339
|
+
expect(logs.join("\n")).toContain('Creating vault "myvault"');
|
|
2340
|
+
expect(logs.join("\n")).toContain('Vault "myvault" created');
|
|
2341
|
+
// No "already configured / ignored" no-op message.
|
|
2342
|
+
expect(logs.join("\n")).not.toContain("already configured");
|
|
2343
|
+
} finally {
|
|
2344
|
+
h.cleanup();
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
test("non-zero exit from create (e.g. vault already exists): warns but init still exits 0", async () => {
|
|
2349
|
+
// A non-zero exit covers both "vault already exists" (a benign re-run, where
|
|
2350
|
+
// `parachute-vault create` errors + exits 1) and a genuine creation failure.
|
|
2351
|
+
// Either way init is non-fatal — the operator can re-run / check status.
|
|
2352
|
+
const h = makeHarness();
|
|
2353
|
+
try {
|
|
2354
|
+
const logs: string[] = [];
|
|
2355
|
+
const code = await init(
|
|
2356
|
+
baseOpts(h, {
|
|
2357
|
+
vaultName: "myvault",
|
|
2358
|
+
log: (l) => logs.push(l),
|
|
2359
|
+
createFirstVaultImpl: async () => 1,
|
|
2360
|
+
}),
|
|
2361
|
+
);
|
|
2362
|
+
// Init is non-fatal on create failure — operator can retry.
|
|
2363
|
+
expect(code).toBe(0);
|
|
2364
|
+
const joined = logs.join("\n");
|
|
2365
|
+
expect(joined).toContain("exited 1");
|
|
2366
|
+
expect(joined).toContain("may already exist, or creation failed");
|
|
2367
|
+
expect(joined).toContain("parachute vault create myvault");
|
|
2368
|
+
} finally {
|
|
2369
|
+
h.cleanup();
|
|
2370
|
+
}
|
|
2371
|
+
});
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2221
2374
|
// Type alias used only inside this test file for the heuristic test.
|
|
2222
2375
|
type ExposeChoice = "none" | "tailnet" | "cloudflare";
|
|
@@ -3267,7 +3267,7 @@ describe("typed vault name (hub#267)", () => {
|
|
|
3267
3267
|
}
|
|
3268
3268
|
});
|
|
3269
3269
|
|
|
3270
|
-
test("vault POST with empty name falls back to 'default' +
|
|
3270
|
+
test("vault POST with empty name falls back to 'default' + ALWAYS passes PARACHUTE_VAULT_NAME (#478 Part 2)", async () => {
|
|
3271
3271
|
const db = openHubDb(hubDbPath(h.dir));
|
|
3272
3272
|
try {
|
|
3273
3273
|
const user = await createUser(db, "owner", "pw");
|
|
@@ -3330,12 +3330,12 @@ describe("typed vault name (hub#267)", () => {
|
|
|
3330
3330
|
await new Promise((r) => setTimeout(r, 50));
|
|
3331
3331
|
const vaultSpawn = spawnRequests.find((s) => s.short === "vault");
|
|
3332
3332
|
expect(vaultSpawn).toBeDefined();
|
|
3333
|
-
//
|
|
3334
|
-
//
|
|
3335
|
-
//
|
|
3336
|
-
//
|
|
3337
|
-
//
|
|
3338
|
-
expect(vaultSpawn?.env?.PARACHUTE_VAULT_NAME).
|
|
3333
|
+
// #478 Part 2: wizard ALWAYS passes PARACHUTE_VAULT_NAME so vault's
|
|
3334
|
+
// first-boot knows which vault to create once silent auto-create is
|
|
3335
|
+
// removed. Even for the default name, the env var must be set to
|
|
3336
|
+
// "default" — not omitted. PORT is set by the supervisor (hub#356)
|
|
3337
|
+
// for every supervised child regardless.
|
|
3338
|
+
expect(vaultSpawn?.env?.PARACHUTE_VAULT_NAME).toBe("default");
|
|
3339
3339
|
expect(vaultSpawn?.env?.PORT).toBe("1940");
|
|
3340
3340
|
} finally {
|
|
3341
3341
|
db.close();
|
package/src/cli.ts
CHANGED
|
@@ -420,17 +420,37 @@ async function main(argv: string[]): Promise<number> {
|
|
|
420
420
|
);
|
|
421
421
|
return 1;
|
|
422
422
|
}
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
423
|
+
// #478 Part 2: --vault-name <name> creates the first vault in one shot.
|
|
424
|
+
const vaultNameExtract = extractNamedFlag(channelExtract.rest, "--vault-name");
|
|
425
|
+
if (vaultNameExtract.error) {
|
|
426
|
+
console.error(`parachute init: ${vaultNameExtract.error}`);
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
let validatedVaultName: string | undefined;
|
|
430
|
+
if (vaultNameExtract.value !== undefined) {
|
|
431
|
+
if (vaultNameExtract.value.trim() === "") {
|
|
432
|
+
console.error("parachute init: --vault-name must not be empty.");
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
const { validateVaultName: vvn } = await import("./vault-name.ts");
|
|
436
|
+
const vr = vvn(vaultNameExtract.value);
|
|
437
|
+
if (!vr.ok) {
|
|
438
|
+
console.error(`parachute init: invalid --vault-name: ${vr.error}`);
|
|
439
|
+
return 1;
|
|
440
|
+
}
|
|
441
|
+
validatedVaultName = vr.name;
|
|
442
|
+
}
|
|
443
|
+
const noBrowser = vaultNameExtract.rest.includes("--no-browser");
|
|
444
|
+
const noExposePrompt = vaultNameExtract.rest.includes("--no-expose-prompt");
|
|
445
|
+
const cliWizard = vaultNameExtract.rest.includes("--cli-wizard");
|
|
446
|
+
const browserWizard = vaultNameExtract.rest.includes("--browser-wizard");
|
|
427
447
|
const known = new Set([
|
|
428
448
|
"--no-browser",
|
|
429
449
|
"--no-expose-prompt",
|
|
430
450
|
"--cli-wizard",
|
|
431
451
|
"--browser-wizard",
|
|
432
452
|
]);
|
|
433
|
-
const unknown =
|
|
453
|
+
const unknown = vaultNameExtract.rest.find((a) => !known.has(a));
|
|
434
454
|
if (unknown !== undefined) {
|
|
435
455
|
console.error(`parachute init: unknown argument "${unknown}"`);
|
|
436
456
|
console.error(
|
|
@@ -438,6 +458,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
438
458
|
" [--expose none|tailnet|cloudflare]\n" +
|
|
439
459
|
" [--channel rc|latest]\n" +
|
|
440
460
|
" [--hub-origin <url>]\n" +
|
|
461
|
+
" [--vault-name <name>]\n" +
|
|
441
462
|
" [--cli-wizard | --browser-wizard]",
|
|
442
463
|
);
|
|
443
464
|
return 1;
|
|
@@ -456,6 +477,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
456
477
|
if (channelExtract.value === "rc" || channelExtract.value === "latest") {
|
|
457
478
|
initOpts.channel = channelExtract.value;
|
|
458
479
|
}
|
|
480
|
+
if (validatedVaultName !== undefined) initOpts.vaultName = validatedVaultName;
|
|
459
481
|
if (cliWizard) initOpts.wizardChoice = "cli";
|
|
460
482
|
else if (browserWizard) initOpts.wizardChoice = "browser";
|
|
461
483
|
const mod = await loadCommand("init", () => import("./commands/init.ts"));
|
package/src/commands/init.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { issueOperatorToken, readOperatorTokenFile } from "../operator-token.ts"
|
|
|
52
52
|
import { type AliveFn, defaultAlive, processState } from "../process-state.ts";
|
|
53
53
|
import { findService, readManifestLenient } from "../services-manifest.ts";
|
|
54
54
|
import { listUsers } from "../users.ts";
|
|
55
|
+
import { validateVaultName } from "../vault-name.ts";
|
|
55
56
|
import { type InstallOpts, install as defaultInstall } from "./install.ts";
|
|
56
57
|
|
|
57
58
|
/** The three options the exposure prompt offers — also the `--expose` flag's domain. */
|
|
@@ -202,6 +203,44 @@ export interface InitOpts {
|
|
|
202
203
|
* already known so there's no question to ask).
|
|
203
204
|
*/
|
|
204
205
|
noWizardPrompt?: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Vault name to create as part of `parachute init --vault-name <name>`
|
|
208
|
+
* (#478 Part 2). When set, init creates the first vault via
|
|
209
|
+
* `createFirstVaultImpl` immediately after Step 1.5 (operator-token
|
|
210
|
+
* guarantee), before the admin-URL resolution. Validated with
|
|
211
|
+
* `validateVaultName` in the CLI before reaching here.
|
|
212
|
+
*
|
|
213
|
+
* Idempotency lives in `parachute-vault create`, NOT in a services.json
|
|
214
|
+
* precheck: `create <name>` exits 0 + creates when `<name>` is new, and
|
|
215
|
+
* exits non-zero ("Vault \"<name>\" already exists.") on a re-run. We
|
|
216
|
+
* therefore ALWAYS attempt the create when this field is set and treat a
|
|
217
|
+
* non-zero exit as non-fatal (warn + continue). A services.json
|
|
218
|
+
* `parachute-vault` row is the MODULE-installed marker (Step 0.5 seeds it
|
|
219
|
+
* via `spec.seedEntry` on EVERY fresh install), not an instance marker —
|
|
220
|
+
* keying idempotency off it would silently no-op the create on the exact
|
|
221
|
+
* fresh-box path this feature targets.
|
|
222
|
+
*
|
|
223
|
+
* Without this field (the default), init makes NO vault — the wizard
|
|
224
|
+
* owns Create/Import/Skip as before. The --no-browser / scripted path
|
|
225
|
+
* remains vault-free unless --vault-name is explicitly passed.
|
|
226
|
+
*/
|
|
227
|
+
vaultName?: string;
|
|
228
|
+
/**
|
|
229
|
+
* Test seam: injectable impl for the `--vault-name` create step (#478
|
|
230
|
+
* Part 2). This IS the whole create implementation — tests swap the
|
|
231
|
+
* entire function for a stub that records the call without touching a
|
|
232
|
+
* live vault binary. It takes the vault name plus a ctx carrying a
|
|
233
|
+
* runner shim, and returns an exit code (0 = success).
|
|
234
|
+
*
|
|
235
|
+
* The production default (`defaultCreateFirstVault`) uses that runner to
|
|
236
|
+
* shell out `["parachute-vault", "create", name]`, following the `Runner`
|
|
237
|
+
* type pattern established across every command that shells out:
|
|
238
|
+
* `readonly string[] => Promise<number>`.
|
|
239
|
+
*/
|
|
240
|
+
createFirstVaultImpl?: (
|
|
241
|
+
name: string,
|
|
242
|
+
ctx: { runner: (cmd: readonly string[]) => Promise<number> },
|
|
243
|
+
) => Promise<number>;
|
|
205
244
|
/**
|
|
206
245
|
* Canonical public hub origin (the OAuth issuer / `iss` claim). Persisted to
|
|
207
246
|
* `hub_settings.hub_origin` BEFORE the hub unit starts + modules spawn, so
|
|
@@ -599,6 +638,38 @@ async function defaultFetchBootstrapToken(loopbackUrl: string): Promise<string |
|
|
|
599
638
|
}
|
|
600
639
|
}
|
|
601
640
|
|
|
641
|
+
/**
|
|
642
|
+
* Default runner shim for the `--vault-name` create step (#478 Part 2).
|
|
643
|
+
* Shells out `parachute-vault create <name>` via Bun.spawn, inheriting
|
|
644
|
+
* the operator's full env (so PARACHUTE_HOME, PATH, etc. pass through).
|
|
645
|
+
*
|
|
646
|
+
* This is the same pattern every other command that shells out uses — a
|
|
647
|
+
* `Runner` (`readonly string[] => Promise<number>`) so tests can inject a
|
|
648
|
+
* stub without touching the real vault binary.
|
|
649
|
+
*/
|
|
650
|
+
async function defaultRunner(cmd: readonly string[]): Promise<number> {
|
|
651
|
+
const proc = Bun.spawn([...cmd], {
|
|
652
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
653
|
+
env: process.env,
|
|
654
|
+
});
|
|
655
|
+
return await proc.exited;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Default impl for the `--vault-name` first-vault create step (#478 Part 2).
|
|
660
|
+
* Invokes `parachute-vault create <name>` via the injectable runner. The
|
|
661
|
+
* `parachute-vault` binary must already be on PATH (guaranteed by Step 0.5's
|
|
662
|
+
* vault-module install). Exit code is forwarded directly — callers log the
|
|
663
|
+
* outcome and continue (a non-zero create doesn't abort init; the operator
|
|
664
|
+
* can re-run `parachute vault create <name>` or use the wizard to retry).
|
|
665
|
+
*/
|
|
666
|
+
async function defaultCreateFirstVault(
|
|
667
|
+
name: string,
|
|
668
|
+
ctx: { runner: (cmd: readonly string[]) => Promise<number> },
|
|
669
|
+
): Promise<number> {
|
|
670
|
+
return await ctx.runner(["parachute-vault", "create", name]);
|
|
671
|
+
}
|
|
672
|
+
|
|
602
673
|
/**
|
|
603
674
|
* Prompt for the wizard-choice question (hub#168 Cut 4). Returns the
|
|
604
675
|
* picked option, or `undefined` if the operator quit. Default is
|
|
@@ -735,6 +806,7 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
735
806
|
const runCliWizardImpl = opts.runCliWizardImpl ?? defaultRunCliWizard;
|
|
736
807
|
const fetchBootstrapTokenImpl = opts.fetchBootstrapTokenImpl ?? defaultFetchBootstrapToken;
|
|
737
808
|
const setHubOriginImpl = opts.setHubOriginImpl ?? defaultSetHubOrigin;
|
|
809
|
+
const createFirstVaultImpl = opts.createFirstVaultImpl ?? defaultCreateFirstVault;
|
|
738
810
|
|
|
739
811
|
log("Parachute init — getting your hub set up.");
|
|
740
812
|
log("");
|
|
@@ -885,6 +957,42 @@ export async function init(opts: InitOpts = {}): Promise<number> {
|
|
|
885
957
|
// to the wizard regardless.
|
|
886
958
|
await guaranteeOperatorToken({ configDir, hubPort, log });
|
|
887
959
|
|
|
960
|
+
// Step 1.6 (#478 Part 2): if `--vault-name <name>` was given, create the
|
|
961
|
+
// first vault now — after the hub is up (Step 1) and the operator token is
|
|
962
|
+
// guaranteed (Step 1.5). The vault module was installed at Step 0.5, so
|
|
963
|
+
// `parachute-vault` is on PATH.
|
|
964
|
+
//
|
|
965
|
+
// We ALWAYS attempt the create when `--vault-name` is set. Idempotency lives
|
|
966
|
+
// in `parachute-vault create` itself, NOT in a services.json precheck:
|
|
967
|
+
// - `create <name>` exits 0 + creates the vault when `<name>` is new.
|
|
968
|
+
// - `create <name>` exits non-zero ("Vault \"<name>\" already exists.") when
|
|
969
|
+
// that exact name already exists (a benign re-run).
|
|
970
|
+
//
|
|
971
|
+
// We DON'T precheck the services.json `parachute-vault` row: Step 0.5's
|
|
972
|
+
// `install("vault", { noCreate: true })` seeds that row via `spec.seedEntry`
|
|
973
|
+
// on EVERY fresh install (the module-installed marker — see install.ts's
|
|
974
|
+
// InstallOpts doc), so on the exact fresh-box path this feature targets the
|
|
975
|
+
// row is ALWAYS present and a row-keyed precheck would silently no-op the
|
|
976
|
+
// create. The row marks "module installed", not "instance exists" — only the
|
|
977
|
+
// create command's own exit reliably distinguishes the two.
|
|
978
|
+
//
|
|
979
|
+
// A non-zero exit is non-fatal: warn + continue. It could mean the vault
|
|
980
|
+
// already exists (a fine re-run) OR a genuine creation failure — init's
|
|
981
|
+
// contract is hub up → wizard regardless, so we never abort here. The
|
|
982
|
+
// operator can check `parachute status` / re-run `parachute vault create`.
|
|
983
|
+
if (opts.vaultName !== undefined) {
|
|
984
|
+
log(`Creating vault "${opts.vaultName}"…`);
|
|
985
|
+
const createCode = await createFirstVaultImpl(opts.vaultName, { runner: defaultRunner });
|
|
986
|
+
if (createCode === 0) {
|
|
987
|
+
log(`✓ Vault "${opts.vaultName}" created.`);
|
|
988
|
+
} else {
|
|
989
|
+
log(
|
|
990
|
+
`⚠ \`parachute-vault create ${opts.vaultName}\` exited ${createCode} — the vault may already exist, or creation failed. Check \`parachute status\` / re-run \`parachute vault create ${opts.vaultName}\`.`,
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
log("");
|
|
994
|
+
}
|
|
995
|
+
|
|
888
996
|
// Step 2: exposure chain. Skipped when already exposed, in non-TTY,
|
|
889
997
|
// or when --no-expose-prompt was passed. `--expose <choice>` jumps
|
|
890
998
|
// straight to the corresponding chain without asking.
|
package/src/help.ts
CHANGED
|
@@ -134,6 +134,7 @@ Usage:
|
|
|
134
134
|
[--expose none|tailnet|cloudflare]
|
|
135
135
|
[--channel rc|latest]
|
|
136
136
|
[--hub-origin <url>]
|
|
137
|
+
[--vault-name <name>]
|
|
137
138
|
[--cli-wizard | --browser-wizard]
|
|
138
139
|
|
|
139
140
|
What it does:
|
|
@@ -178,6 +179,14 @@ Flags:
|
|
|
178
179
|
accepting it in one pass. For reverse-proxy /
|
|
179
180
|
Caddy-direct boxes that bind loopback but are reached
|
|
180
181
|
over a public HTTPS URL (e.g. https://<ip>.sslip.io).
|
|
182
|
+
--vault-name <name> create the first vault in one shot (#478 Part 2).
|
|
183
|
+
Runs \`parachute-vault create <name>\` after the hub
|
|
184
|
+
is up. Non-fatal on re-run — \`create\` exits
|
|
185
|
+
non-zero if the vault already exists, and that's
|
|
186
|
+
tolerated. Must be a valid vault name: lowercase
|
|
187
|
+
alphanumeric + hyphens/underscores, 2–32 chars.
|
|
188
|
+
Without this flag, the wizard owns vault creation
|
|
189
|
+
(the default experience is unchanged).
|
|
181
190
|
--cli-wizard skip the "browser or CLI?" prompt and walk the wizard
|
|
182
191
|
in this terminal (hub#168 Cut 4)
|
|
183
192
|
--browser-wizard skip the prompt and open the browser wizard directly
|
|
@@ -190,7 +199,9 @@ Examples:
|
|
|
190
199
|
parachute init --expose tailnet # CI/scripted: chain straight into Tailscale
|
|
191
200
|
parachute init --no-browser # don't shell out to open / xdg-open
|
|
192
201
|
parachute init --cli-wizard # walk the wizard in this terminal (hub#168)
|
|
193
|
-
parachute init --channel rc
|
|
202
|
+
parachute init --channel rc # rc box: install the vault module from @rc
|
|
203
|
+
parachute init --vault-name default --no-browser
|
|
204
|
+
# CI/scripted: hub + first vault in one pass
|
|
194
205
|
`;
|
|
195
206
|
}
|
|
196
207
|
|
package/src/setup-wizard.ts
CHANGED
|
@@ -2344,19 +2344,19 @@ export async function handleSetupVaultPost(req: Request, deps: SetupWizardDeps):
|
|
|
2344
2344
|
if (registry) {
|
|
2345
2345
|
// hub#267: thread the typed name through `PARACHUTE_VAULT_NAME` so
|
|
2346
2346
|
// vault's first-boot path (vault#342) names the created vault
|
|
2347
|
-
// accordingly.
|
|
2348
|
-
// field blank — vault's `resolveFirstBootVaultName` defaults to
|
|
2349
|
-
// `default` on absent env vars, so this preserves the prior
|
|
2350
|
-
// behaviour for the empty-input case.
|
|
2347
|
+
// accordingly.
|
|
2351
2348
|
//
|
|
2352
|
-
//
|
|
2353
|
-
//
|
|
2354
|
-
//
|
|
2355
|
-
//
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2349
|
+
// #478 Part 2: ALWAYS set `PARACHUTE_VAULT_NAME`, even when the name
|
|
2350
|
+
// is "default". Once vault removes its silent auto-create-on-missing-env
|
|
2351
|
+
// behavior, vault's first-boot will require the env var to know which
|
|
2352
|
+
// vault to create — a missing `PARACHUTE_VAULT_NAME` will mean "no vault
|
|
2353
|
+
// to create" rather than "create one named default". Passing it
|
|
2354
|
+
// explicitly for every path (including the default) is correct and safe:
|
|
2355
|
+
// vault's `resolveFirstBootVaultName` accepts "default" as a valid name
|
|
2356
|
+
// and behaves identically to the prior implicit default.
|
|
2357
|
+
const spawnEnv: Record<string, string> = {
|
|
2358
|
+
PARACHUTE_VAULT_NAME: vaultName,
|
|
2359
|
+
};
|
|
2360
2360
|
// Capture importParams + deps in the runInstall promise chain — when
|
|
2361
2361
|
// mode === "import", run the vault-side `/.parachute/mirror/import`
|
|
2362
2362
|
// POST as a follow-up step once the supervised vault has come up
|
|
@@ -2377,7 +2377,7 @@ export async function handleSetupVaultPost(req: Request, deps: SetupWizardDeps):
|
|
|
2377
2377
|
registry,
|
|
2378
2378
|
...(deps.run ? { run: deps.run } : {}),
|
|
2379
2379
|
...(deps.isLinked ? { isLinked: deps.isLinked } : {}),
|
|
2380
|
-
|
|
2380
|
+
spawnEnv,
|
|
2381
2381
|
})
|
|
2382
2382
|
.then(async () => {
|
|
2383
2383
|
if (!importToRun) return;
|