@saastemly/voidcommerce 0.16.1 → 0.18.0

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/dist/cli.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  runVoid,
22
22
  version,
23
23
  voidAppsIn
24
- } from "./index-w0c96dxv.js";
24
+ } from "./index-3j6jtjmk.js";
25
25
  import {
26
26
  LOCAL_KEY_FILE,
27
27
  PRIVATE_KEY_VAR,
@@ -32,6 +32,8 @@ import {
32
32
  declaredSecretNames,
33
33
  encryptInto,
34
34
  envSummary,
35
+ findGh,
36
+ getVariable,
35
37
  ghAuth,
36
38
  ignoresKeyFile,
37
39
  initSecrets,
@@ -44,13 +46,14 @@ import {
44
46
  publicKeyFor,
45
47
  repoSlug,
46
48
  run,
49
+ run1 as run2,
47
50
  secretNames,
48
51
  secretsCommand,
49
52
  setSecret,
50
53
  setVariable,
51
54
  variableNames,
52
55
  verifyCloudflareToken
53
- } from "./index-s8se0x2d.js";
56
+ } from "./index-khzk6a9z.js";
54
57
  import {
55
58
  LAYOUTS,
56
59
  oneOrigin,
@@ -67,9 +70,9 @@ import {
67
70
  } from "./index-0v6na3yp.js";
68
71
 
69
72
  // src/deploy/index.ts
70
- import color2 from "picocolors";
71
- import { existsSync } from "node:fs";
72
- import { join as join2 } from "node:path";
73
+ import color3 from "picocolors";
74
+ import { existsSync as existsSync2 } from "node:fs";
75
+ import { join as join3 } from "node:path";
73
76
 
74
77
  // src/deploy/link.ts
75
78
  import { rmSync } from "node:fs";
@@ -118,16 +121,23 @@ async function linkCommand(project, args) {
118
121
  } else {
119
122
  const parked = localPrivateKey(root);
120
123
  if (parked) {
121
- const sent = await setSecret(root, PRIVATE_KEY_VAR, parked);
124
+ const sent = await setVariable(root, PRIVATE_KEY_VAR, parked);
122
125
  if (!sent.ok) {
123
- p.cancel(`GitHub refused the secret: ${sent.error ?? "unknown error"}`);
126
+ p.cancel(`GitHub refused the variable: ${sent.error ?? "unknown error"}`);
124
127
  return 1;
125
128
  }
126
129
  const publicKey = await publicKeyFor(parked);
127
130
  if (publicKey)
128
131
  committedPublicKeyInto(root, publicKey);
129
- rmSync(join(root, LOCAL_KEY_FILE), { force: true });
130
- p.log.success(`${color.green("✓")} the key waiting in ${LOCAL_KEY_FILE} moved to GitHub, and the file deleted`);
132
+ const readBack = await getVariable(root, PRIVATE_KEY_VAR);
133
+ if (readBack !== parked) {
134
+ p.log.warn(`${color.yellow("!")} ${PRIVATE_KEY_VAR} was written to ${slug} but could not be read back,
135
+ so ${LOCAL_KEY_FILE} has been LEFT IN PLACE. It is currently the only copy
136
+ ` + " of the key that opens this shop — do not delete it until `vc keys --restore` works.");
137
+ } else {
138
+ rmSync(join(root, LOCAL_KEY_FILE), { force: true });
139
+ p.log.success(`${color.green("✓")} the key moved to ${slug} and ${LOCAL_KEY_FILE} deleted — verified readable, so \`vc keys --restore\` can bring it back`);
140
+ }
131
141
  } else {
132
142
  const made = await provisionKey(project);
133
143
  if (!made.ok) {
@@ -216,6 +226,106 @@ async function linkCommand(project, args) {
216
226
  return 0;
217
227
  }
218
228
 
229
+ // src/deploy/publish.ts
230
+ import { existsSync } from "node:fs";
231
+ import { join as join2 } from "node:path";
232
+ import color2 from "picocolors";
233
+ async function publishCommand(project, args) {
234
+ const p = await import("@clack/prompts");
235
+ const root = project.root;
236
+ const gh = findGh();
237
+ p.intro(color2.bgCyan(color2.black(" vc publish ")));
238
+ const auth = await ghAuth(root);
239
+ if (!auth.ok || !gh) {
240
+ p.cancel(auth.reason ?? "the GitHub CLI is not installed. https://cli.github.com");
241
+ return 1;
242
+ }
243
+ if (!existsSync(join2(root, ".git"))) {
244
+ p.cancel(`${root} is not a git repository. \`git init\` first, and commit what you have.`);
245
+ return 1;
246
+ }
247
+ let slug = await repoSlug(root);
248
+ if (slug) {
249
+ p.log.info(`${slug} already exists — using it`);
250
+ } else {
251
+ const suggested = project.manifest.shop.domain.split(".")[0] ?? "shop";
252
+ const name = await p.text({
253
+ message: "Repository name",
254
+ initialValue: suggested,
255
+ validate: (value) => /^[A-Za-z0-9._-]+$/.test(value.trim()) ? undefined : "letters, digits, dot, dash or underscore"
256
+ });
257
+ if (p.isCancel(name)) {
258
+ p.cancel("nothing was created.");
259
+ return 1;
260
+ }
261
+ const visibility = await p.select({
262
+ message: "Visibility",
263
+ options: [
264
+ { value: "--private", label: "Private", hint: "the key that opens your secrets is readable to anyone with access" },
265
+ { value: "--public", label: "Public", hint: "only if this shop keeps no secrets at all" }
266
+ ],
267
+ initialValue: "--private"
268
+ });
269
+ if (p.isCancel(visibility)) {
270
+ p.cancel("nothing was created.");
271
+ return 1;
272
+ }
273
+ const spinner2 = p.spinner();
274
+ spinner2.start(`creating ${String(name)}`);
275
+ const created = await run(gh, ["repo", "create", String(name).trim(), "--source=.", String(visibility)], root);
276
+ if (created.code !== 0) {
277
+ spinner2.stop(`${color2.red("✗")} could not create it`);
278
+ p.cancel(created.out.trim().split(`
279
+ `).slice(-2).join(" "));
280
+ return 1;
281
+ }
282
+ slug = await repoSlug(root);
283
+ spinner2.stop(`${color2.green("✓")} ${slug ?? String(name)} created, and set as origin`);
284
+ }
285
+ p.log.step("giving the repository what the deploy needs");
286
+ const linked = await linkCommand(project, args);
287
+ if (linked !== 0) {
288
+ p.cancel("the repository exists but is not configured, so a push would not deploy. Fix the above and run `vc publish` again.");
289
+ return 1;
290
+ }
291
+ const dirty = (await run("git", ["status", "--porcelain"], root)).out.trim();
292
+ if (dirty) {
293
+ const staged = await run("git", ["add", "-A"], root);
294
+ const committed = await run("git", ["commit", "-m", "vc publish: link this shop to its repository"], root);
295
+ if (staged.code !== 0 || committed.code !== 0) {
296
+ p.cancel(`could not commit the changes vc link made:
297
+ ${committed.out.trim().split(`
298
+ `).slice(-3).join(`
299
+ `)}`);
300
+ return 1;
301
+ }
302
+ p.log.success(`${color2.green("✓")} committed what linking changed`);
303
+ }
304
+ const branch = (await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], root)).out.trim() || "main";
305
+ const spinner = p.spinner();
306
+ spinner.start(`pushing ${branch}`);
307
+ const pushed = await run("git", ["push", "-u", "origin", `${branch}:main`], root);
308
+ if (pushed.code !== 0) {
309
+ spinner.stop(`${color2.red("✗")} the push failed`);
310
+ p.cancel(pushed.out.trim().split(`
311
+ `).slice(-4).join(`
312
+ `));
313
+ return 1;
314
+ }
315
+ spinner.stop(`${color2.green("✓")} pushed to main`);
316
+ p.outro([
317
+ `${color2.bold("Published.")} The deploy is running now.`,
318
+ "",
319
+ ` ${color2.cyan(`https://github.com/${slug}/actions`)}`,
320
+ "",
321
+ ` From here ${color2.cyan("git push")} is the whole loop. \`vc\` is only needed to`,
322
+ ` change a secret (${color2.cyan("vc secrets set KEY")}) or read one back`,
323
+ ` (${color2.cyan("vc keys --restore")}).`
324
+ ].join(`
325
+ `));
326
+ return 0;
327
+ }
328
+
219
329
  // src/deploy/index.ts
220
330
  async function deployCommand(args) {
221
331
  const own = new Set(["--cloudflare", "--provision", "--force"]);
@@ -239,12 +349,12 @@ async function deployCommand(args) {
239
349
  const check = await preflight(project, "void");
240
350
  printPreflight(project, check, "void");
241
351
  if (!check.ready && check.remote !== null && !args.includes("--force")) {
242
- console.error(`${color2.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
352
+ console.error(`${color3.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
243
353
  `);
244
354
  return 1;
245
355
  }
246
356
  if (!check.ready && check.remote === null) {
247
- console.log(color2.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
357
+ console.log(color3.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
248
358
  `));
249
359
  }
250
360
  return runVoid(["deploy", ...voids], project.appDir);
@@ -262,7 +372,7 @@ async function preflightCommand(args) {
262
372
  return result.ready ? 0 : 1;
263
373
  const pending = await uncommittedMigrations(project.root);
264
374
  if (pending.length > 0) {
265
- console.error(`${color2.red("✗")} ${pending.length} migration file${pending.length === 1 ? " is" : "s are"} not committed:
375
+ console.error(`${color3.red("✗")} ${pending.length} migration file${pending.length === 1 ? " is" : "s are"} not committed:
266
376
  ` + ` ${pending.join(`
267
377
  `)}
268
378
 
@@ -274,7 +384,7 @@ async function preflightCommand(args) {
274
384
  return result.ready ? 0 : 1;
275
385
  }
276
386
  async function uncommittedMigrations(root) {
277
- const status = await run("git", ["status", "--porcelain", "--", "migrations"], root);
387
+ const status = await run2("git", ["status", "--porcelain", "--", "migrations"], root);
278
388
  if (status.code !== 0)
279
389
  return [];
280
390
  return status.out.split(`
@@ -287,13 +397,13 @@ async function deployHelp() {
287
397
  line("vc's preflight — every required key set, the hostnames right — then", width),
288
398
  line("void deploy, untouched. Or, with --cloudflare, your own account via wrangler.", width),
289
399
  line("", width),
290
- line(color2.bold("Usage"), width),
400
+ line(color3.bold("Usage"), width),
291
401
  ...row("vc deploy [void's flags]", "preflight, then void deploy — the Void platform, void's login", width, 2),
292
402
  ...row("vc deploy --cloudflare", "wrangler, your Cloudflare account, no Void login: preflight, build, scrub baked secrets, migrate D1, deploy", width, 2),
293
403
  ...row("vc deploy --cloudflare --provision", "first time: create the D1 database and the queue, record them", width, 2),
294
404
  ...row("vc deploy --cloudflare --force", "deploy a shop preflight says is not ready — deliberately", width, 2),
295
405
  line("", width),
296
- line(color2.bold("Needs, for --cloudflare"), width),
406
+ line(color3.bold("Needs, for --cloudflare"), width),
297
407
  ...row("CLOUDFLARE_API_TOKEN", "in the environment; the account is pinned for you when there is one", width, 2),
298
408
  ...row(PRIVATE_KEY_VAR, "to decrypt the shop's own secrets", width, 2),
299
409
  line("", width),
@@ -372,10 +482,15 @@ async function keysHelp() {
372
482
  line("A contributor with only a clone can rotate the Stripe key and cannot read", width),
373
483
  line("the one already there. Only the deploy decrypts.", width),
374
484
  line("", width),
375
- line("GitHub will not hand a secret back once set, so re-keying without a local", width),
376
- line("copy means entering the values again. That is the right trade: a key worth", width),
377
- line("rotating is a key that may have leaked, and leaked values need replacing", width),
378
- line("at the source anyway.", width)
485
+ line("The key is a repository VARIABLE, not a secret. GitHub secrets are", width),
486
+ line("write-only `gh secret` has no read command so a key kept there is", width),
487
+ line("gone the moment your local copy is, and every encrypted value with it.", width),
488
+ line("As a variable it can be read back, which is what makes a new machine or", width),
489
+ line("a second person possible: `vc keys --restore`.", width),
490
+ line("", width),
491
+ line("So anyone who can READ the repository can decrypt its secrets. Keep it", width),
492
+ line("private. That access list is also how you share: a collaborator can", width),
493
+ line("restore the key, and removing them removes their access.", width)
379
494
  ], width));
380
495
  return 0;
381
496
  }
@@ -384,9 +499,9 @@ async function guardCommand() {
384
499
  if (!project)
385
500
  return 0;
386
501
  const root = project.root;
387
- if (existsSync(join2(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
502
+ if (existsSync2(join3(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
388
503
  console.error(`
389
- ${color2.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gitignored.
504
+ ${color3.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gitignored.
390
505
 
391
506
  ` + ` It holds the private key that opens every secret in this repository.
392
507
  ` + ` Add ${LOCAL_KEY_FILE} to .gitignore before committing anything.
@@ -399,24 +514,24 @@ ${color2.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gi
399
514
  const publicKey = committedPublicKey(root);
400
515
  if (!publicKey) {
401
516
  console.error(`
402
- ${color2.red("✗ refusing the commit")}: ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE} ${bare.length === 1 ? "is" : "are"} in the clear, and there is no key to encrypt ${bare.length === 1 ? "it" : "them"} with.
517
+ ${color3.red("✗ refusing the commit")}: ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE} ${bare.length === 1 ? "is" : "are"} in the clear, and there is no key to encrypt ${bare.length === 1 ? "it" : "them"} with.
403
518
 
404
519
  ` + ` ${bare.map((entry) => entry.name).join(`
405
520
  `)}
406
521
 
407
- ` + ` ${color2.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
522
+ ` + ` ${color3.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
408
523
 
409
- ` + color2.dim(` Committing a secret in the clear cannot be undone by a later commit;
524
+ ` + color3.dim(` Committing a secret in the clear cannot be undone by a later commit;
410
525
  the value stays in the history and must be treated as burned.
411
526
  `));
412
527
  return 1;
413
528
  }
414
- const staged = (await run("git", ["diff", "--cached", "--name-only", "--", SECRETS_FILE], root)).out.trim().length > 0;
529
+ const staged = (await run2("git", ["diff", "--cached", "--name-only", "--", SECRETS_FILE], root)).out.trim().length > 0;
415
530
  for (const entry of bare) {
416
531
  const sealed = await encryptInto(root, publicKey, entry.name, entry.value);
417
532
  if (!sealed.ok) {
418
533
  console.error(`
419
- ${color2.red("✗ refusing the commit")}: ${sealed.error}
534
+ ${color3.red("✗ refusing the commit")}: ${sealed.error}
420
535
  `);
421
536
  return 1;
422
537
  }
@@ -424,23 +539,23 @@ ${color2.red("✗ refusing the commit")}: ${sealed.error}
424
539
  const left = plaintextSecretNames(root);
425
540
  if (left.length > 0) {
426
541
  console.error(`
427
- ${color2.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
542
+ ${color3.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
428
543
  `);
429
544
  return 1;
430
545
  }
431
546
  if (staged) {
432
- const added = await run("git", ["add", "--", SECRETS_FILE], root);
547
+ const added = await run2("git", ["add", "--", SECRETS_FILE], root);
433
548
  if (added.code !== 0) {
434
549
  console.error(`
435
- ${color2.red("✗ refusing the commit")}: ${SECRETS_FILE} was encrypted but could not be re-staged,
550
+ ${color3.red("✗ refusing the commit")}: ${SECRETS_FILE} was encrypted but could not be re-staged,
436
551
  ` + ` so the commit would still carry the plaintext you staged. \`git add ${SECRETS_FILE}\`.
437
552
  `);
438
553
  return 1;
439
554
  }
440
555
  }
441
556
  console.log(`
442
- ${color2.green("✓")} encrypted ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE}${staged ? " and re-staged it" : ""}: ${bare.map((entry) => entry.name).join(", ")}
443
- ` + color2.dim(` Encryption needs only the public key, so this needs no credential.
557
+ ${color3.green("✓")} encrypted ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE}${staged ? " and re-staged it" : ""}: ${bare.map((entry) => entry.name).join(", ")}
558
+ ` + color3.dim(` Encryption needs only the public key, so this needs no credential.
444
559
  `));
445
560
  return 0;
446
561
  }
@@ -461,12 +576,12 @@ async function linkHelp() {
461
576
  ...row("vc link", "store the encryption key and the Cloudflare token on the repository", width, 2),
462
577
  ...row("vc link --force", "replace what is already there", width, 2),
463
578
  line("", width),
464
- line(color2.bold("What it stores, and where"), width),
579
+ line(color3.bold("What it stores, and where"), width),
465
580
  ...row(PRIVATE_KEY_VAR, "a repository SECRET. A key already waiting locally is MOVED here and the local copy deleted", width, 2),
466
581
  ...row("CLOUDFLARE_API_TOKEN", "a repository SECRET, checked against the Cloudflare API before it is stored", width, 2),
467
582
  ...row("CLOUDFLARE_ACCOUNT_ID", "a repository VARIABLE — an identifier, not a credential", width, 2),
468
583
  line("", width),
469
- line(color2.bold("Why one token still has to be typed"), width),
584
+ line(color3.bold("Why one token still has to be typed"), width),
470
585
  line("GitHub cannot mint a Cloudflare credential. There is no OIDC federation", width),
471
586
  line("between them, and the Cloudflare GitHub App runs the other way: it grants", width),
472
587
  line("Cloudflare access to your repository, not your repository access to", width),
@@ -474,16 +589,49 @@ async function linkHelp() {
474
589
  ], width));
475
590
  return 0;
476
591
  }
592
+ async function publishCliCommand(args) {
593
+ const project = await findProject();
594
+ if (!project) {
595
+ console.error("vc: no voidcommerce.json here.");
596
+ return 1;
597
+ }
598
+ return publishCommand(project, args);
599
+ }
600
+ async function publishHelp() {
601
+ const width = 80;
602
+ console.log(box("vc publish", [
603
+ line("Create this shop's GitHub repository, give it what the deploy needs,", width),
604
+ line("and push. The push IS the deploy. Run once; after that, `git push`.", width),
605
+ line("", width),
606
+ ...row("vc publish", "create the repository, set the credentials, push main", width, 2),
607
+ line("", width),
608
+ line(color3.bold("What it does for you"), width),
609
+ ...row("the encryption key", "vc already holds it — set with no input from you", width, 2),
610
+ ...row("the account id", "read from voidcommerce.json", width, 2),
611
+ ...row("the Cloudflare token", "the one thing it has to ask for", width, 2),
612
+ line("", width),
613
+ line(color3.bold("Why the token cannot be automated away"), width),
614
+ line("There is no OIDC between GitHub and Cloudflare, so something must carry", width),
615
+ line("a token across, and only a person can fetch one from the dashboard. It", width),
616
+ line("is asked for once, checked against the Cloudflare API, and stored on the", width),
617
+ line("repository — never on this machine.", width),
618
+ line("", width),
619
+ line("The repository is created WITHOUT pushing, then configured, then pushed.", width),
620
+ line("Pushing first would start a deploy against a repository that has no key", width),
621
+ line("and no token: a red run, for no reason but the wrong order.", width)
622
+ ], width));
623
+ return 0;
624
+ }
477
625
 
478
626
  // src/init.ts
479
627
  import * as p2 from "@clack/prompts";
480
628
  import { mkdir } from "node:fs/promises";
481
- import { basename, join as join3 } from "node:path";
482
- import color4 from "picocolors";
629
+ import { basename, join as join4 } from "node:path";
630
+ import color5 from "picocolors";
483
631
 
484
632
  // src/wizard.ts
485
633
  import * as p from "@clack/prompts";
486
- import color3 from "picocolors";
634
+ import color4 from "picocolors";
487
635
  var bail = () => {
488
636
  p.cancel("Nothing was written.");
489
637
  process.exit(0);
@@ -505,8 +653,8 @@ async function askGroup(group2, previous, alsoRequired = []) {
505
653
  const required = group2.choices.filter((choice) => requiredIds.has(choice.id));
506
654
  const optional = group2.choices.filter((choice) => !requiredIds.has(choice.id));
507
655
  const message = group2.title + (group2.intro ? `
508
- ${color3.dim(group2.intro)}` : "") + (required.length ? `
509
- ${color3.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
656
+ ${color4.dim(group2.intro)}` : "") + (required.length ? `
657
+ ${color4.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
510
658
  if (optional.length === 0) {
511
659
  p.log.info(message);
512
660
  return required.map((choice) => choice.id);
@@ -537,7 +685,7 @@ async function runWizard(existing, layout) {
537
685
  validate: (value) => value.trim() ? undefined : "A shop has a name."
538
686
  }),
539
687
  domain: () => p.text({
540
- message: `Domain ${color3.dim(monorepo ? "— the storefront; api.<domain> is the worker; the CORS list and DNS records derive from it" : "— the shop answers here; the worker's custom domain")}`,
688
+ message: `Domain ${color4.dim(monorepo ? "— the storefront; api.<domain> is the worker; the CORS list and DNS records derive from it" : "— the shop answers here; the worker's custom domain")}`,
541
689
  placeholder: "northwind.com",
542
690
  initialValue: existing?.shop.domain ?? "",
543
691
  validate: (value) => /^[a-z0-9-]+(\.[a-z0-9-]+)+\.[a-z]{2,}$|^[a-z0-9-]+\.[a-z]{2,}$/i.test(value.trim()) ? undefined : "A hostname, like northwind.com or shop.northwind.com."
@@ -548,7 +696,7 @@ async function runWizard(existing, layout) {
548
696
  if (!domain || domain === guess)
549
697
  return Promise.resolve(undefined);
550
698
  return p.text({
551
- message: `DNS zone ${color3.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
699
+ message: `DNS zone ${color4.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
552
700
  placeholder: guess,
553
701
  initialValue: existing?.shop.zone ?? guess,
554
702
  validate: (value) => {
@@ -560,13 +708,13 @@ async function runWizard(existing, layout) {
560
708
  });
561
709
  },
562
710
  country: () => p.text({
563
- message: `Country ${color3.dim("— sets the declared VAT rate and the default region")}`,
711
+ message: `Country ${color4.dim("— sets the declared VAT rate and the default region")}`,
564
712
  placeholder: "US",
565
713
  initialValue: existing?.shop.country ?? "",
566
714
  validate: (value) => /^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."
567
715
  }),
568
716
  taxRegistered: () => p.select({
569
- message: `Registered to charge tax? ${color3.dim("— a business below its registration threshold must NOT add tax to a price, and one above it must")}`,
717
+ message: `Registered to charge tax? ${color4.dim("— a business below its registration threshold must NOT add tax to a price, and one above it must")}`,
570
718
  options: [
571
719
  { value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
572
720
  { value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" }
@@ -580,14 +728,14 @@ async function runWizard(existing, layout) {
580
728
  validate: (value) => /^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."
581
729
  }),
582
730
  locale: () => p.text({
583
- message: `Locale ${color3.dim("— the language of emails and launch content")}`,
731
+ message: `Locale ${color4.dim("— the language of emails and launch content")}`,
584
732
  placeholder: "en",
585
733
  initialValue: existing?.shop.locale ?? "",
586
734
  validate: (value) => /^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."
587
735
  }),
588
736
  ...onPages ? {
589
737
  pagesHost: () => p.text({
590
- message: `GitHub Pages host ${color3.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
738
+ message: `GitHub Pages host ${color4.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
591
739
  placeholder: "northwind.github.io",
592
740
  initialValue: existing?.shop.pagesHost ?? "",
593
741
  validate: (value) => !value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank"
@@ -598,7 +746,7 @@ async function runWizard(existing, layout) {
598
746
  for (const group2 of GROUPS) {
599
747
  if (group2.id === "ui" && layout === "strict") {
600
748
  p.log.info(`${group2.title}
601
- ${color3.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
749
+ ${color4.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
602
750
  chosen[group2.id] = ["admin"];
603
751
  continue;
604
752
  }
@@ -623,14 +771,14 @@ ${color3.dim("The generated storefront and panel. Strict has no custom code to h
623
771
  }
624
772
  function summarise(manifest) {
625
773
  const lines = [
626
- `${color3.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
774
+ `${color4.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
627
775
  ];
628
776
  for (const group2 of GROUPS) {
629
777
  const ids = manifest.chosen[group2.id] ?? [];
630
778
  if (ids.length === 0)
631
779
  continue;
632
780
  const labels = ids.map((id) => group2.choices.find((choice) => choice.id === id)?.label ?? id).join(", ");
633
- lines.push(`${color3.dim(group2.title.padEnd(34))} ${labels}`);
781
+ lines.push(`${color4.dim(group2.title.padEnd(34))} ${labels}`);
634
782
  }
635
783
  return lines.join(`
636
784
  `);
@@ -671,10 +819,10 @@ async function init(args) {
671
819
  if (flags.voids.length > 0)
672
820
  return runVoid(["init", ...flags.voids]);
673
821
  let root = process.cwd();
674
- p2.intro(color4.bgCyan(color4.black(" voidcommerce ")));
822
+ p2.intro(color5.bgCyan(color5.black(" voidcommerce ")));
675
823
  const existing = await readManifest(root);
676
824
  if (existing) {
677
- p2.log.info(`Found ${color4.cyan("voidcommerce.json")} for ${color4.bold(existing.shop.name)} — answers are pre-filled; the layout (${existing.layout}) is fixed.`);
825
+ p2.log.info(`Found ${color5.cyan("voidcommerce.json")} for ${color5.bold(existing.shop.name)} — answers are pre-filled; the layout (${existing.layout}) is fixed.`);
678
826
  if (flags.layout && flags.layout !== existing.layout) {
679
827
  p2.log.error(`This shop is laid out as "${existing.layout}". The layout cannot change after init — moving files is not a regeneration.`);
680
828
  return 1;
@@ -682,10 +830,10 @@ async function init(args) {
682
830
  }
683
831
  const layout = existing?.layout ?? flags.layout ?? await askLayout();
684
832
  if (layout === "strict") {
685
- p2.log.step(`Strict: the app will be generated under ${color4.cyan(".vc/app")} — nothing there is yours to edit.`);
833
+ p2.log.step(`Strict: the app will be generated under ${color5.cyan(".vc/app")} — nothing there is yours to edit.`);
686
834
  } else if (layout === "app") {
687
835
  if (!isVoidApp(root)) {
688
- p2.log.step(`No Void app here yet — ${color4.cyan("void init")} first, then the shop.`);
836
+ p2.log.step(`No Void app here yet — ${color5.cyan("void init")} first, then the shop.`);
689
837
  const before = new Set(voidAppsIn(root));
690
838
  const code = await runVoid(["init"]);
691
839
  if (code !== 0)
@@ -698,7 +846,7 @@ async function init(args) {
698
846
  }
699
847
  root = created[0];
700
848
  process.chdir(root);
701
- p2.log.step(`Continuing in ${color4.cyan(`${basename(root)}/`)}`);
849
+ p2.log.step(`Continuing in ${color5.cyan(`${basename(root)}/`)}`);
702
850
  }
703
851
  }
704
852
  } else {
@@ -707,11 +855,11 @@ async function init(args) {
707
855
  ["frontend", "the storefront — prerendered and served from the worker's own assets"]
708
856
  ];
709
857
  for (const [dir, starter] of parts) {
710
- const target = join3(root, dir);
858
+ const target = join4(root, dir);
711
859
  if (isVoidApp(target))
712
860
  continue;
713
861
  await mkdir(target, { recursive: true });
714
- p2.log.step(`${color4.cyan("void init")} in ${color4.cyan(`${dir}/`)} — choose ${starter}.`);
862
+ p2.log.step(`${color5.cyan("void init")} in ${color5.cyan(`${dir}/`)} — choose ${starter}.`);
715
863
  const code = await runVoid(["init"], target);
716
864
  if (code !== 0)
717
865
  return code;
@@ -729,7 +877,7 @@ async function init(args) {
729
877
  if (problems.length > 0) {
730
878
  p2.log.error("These answers contradict each other:");
731
879
  for (const problem of problems)
732
- p2.log.message(` ${color4.red("✗")} ${problem}`);
880
+ p2.log.message(` ${color5.red("✗")} ${problem}`);
733
881
  p2.cancel("Nothing was written.");
734
882
  return 1;
735
883
  }
@@ -745,16 +893,16 @@ async function init(args) {
745
893
  spinner2.stop("Generated");
746
894
  const { secrets, plaintext } = envSummary(manifest);
747
895
  p2.note([
748
- ...result.written.map((file) => `${color4.green("+")} ${file}`),
749
- ...result.kept.map((file) => `${color4.dim("=")} ${file} ${color4.dim("(kept — yours)")}`),
750
- ...result.retired.map((file) => `${color4.red("-")} ${file} ${color4.dim("(retired — no longer generated)")}`)
896
+ ...result.written.map((file) => `${color5.green("+")} ${file}`),
897
+ ...result.kept.map((file) => `${color5.dim("=")} ${file} ${color5.dim("(kept — yours)")}`),
898
+ ...result.retired.map((file) => `${color5.red("-")} ${file} ${color5.dim("(retired — no longer generated)")}`)
751
899
  ].join(`
752
900
  `), "Files");
753
901
  p2.note([
754
- `${color4.bold(String(secrets.length))} secrets ${color4.dim("→ wrangler secret put <NAME>")}`,
902
+ `${color5.bold(String(secrets.length))} secrets ${color5.dim("→ wrangler secret put <NAME>")}`,
755
903
  ...secrets.map((key) => ` ${key}`),
756
904
  "",
757
- `${color4.bold(String(plaintext.length))} plaintext ${color4.dim("→ .env.production, already written")}`,
905
+ `${color5.bold(String(plaintext.length))} plaintext ${color5.dim("→ .env.production, already written")}`,
758
906
  ...plaintext.map((key) => ` ${key}`)
759
907
  ].join(`
760
908
  `), "Before going live");
@@ -764,21 +912,21 @@ async function init(args) {
764
912
  return code;
765
913
  }
766
914
  const next = layout === "app" ? [
767
- ` ${color4.cyan("bun install")}`,
768
- ` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
769
- ` ${color4.cyan("vc dev")}`,
770
- ` ${color4.cyan("vc preflight")} ${color4.dim("what is still missing, and why it matters")}`
915
+ ` ${color5.cyan("bun install")}`,
916
+ ` ${color5.cyan("bun run maildev")} ${color5.dim("local inbox at http://localhost:1080")}`,
917
+ ` ${color5.cyan("vc dev")}`,
918
+ ` ${color5.cyan("vc preflight")} ${color5.dim("what is still missing, and why it matters")}`
771
919
  ] : layout === "strict" ? [
772
- ` ${color4.cyan("bun install")}`,
773
- ` ${color4.cyan("vc generate")} ${color4.dim("the app under .vc/app, void's artifacts, the migrations")}`,
774
- ` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
775
- ` ${color4.cyan("vc dev")} ${color4.dim("runs in .vc/app")}`
920
+ ` ${color5.cyan("bun install")}`,
921
+ ` ${color5.cyan("vc generate")} ${color5.dim("the app under .vc/app, void's artifacts, the migrations")}`,
922
+ ` ${color5.cyan("bun run maildev")} ${color5.dim("local inbox at http://localhost:1080")}`,
923
+ ` ${color5.cyan("vc dev")} ${color5.dim("runs in .vc/app")}`
776
924
  ] : [
777
- ` ${color4.cyan("bun install")} ${color4.dim("one install for both workspaces")}`,
778
- ` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
779
- ` ${color4.cyan("bun run dev:api")} ${color4.dim("the worker, with the panel")}`,
780
- ` ${color4.cyan("bun run dev:frontend")} ${color4.dim("the storefront, against the local API")}`,
781
- ` ${color4.cyan("bun run preflight")} ${color4.dim("what is still missing, and why it matters")}`
925
+ ` ${color5.cyan("bun install")} ${color5.dim("one install for both workspaces")}`,
926
+ ` ${color5.cyan("bun run maildev")} ${color5.dim("local inbox at http://localhost:1080")}`,
927
+ ` ${color5.cyan("bun run dev:api")} ${color5.dim("the worker, with the panel")}`,
928
+ ` ${color5.cyan("bun run dev:frontend")} ${color5.dim("the storefront, against the local API")}`,
929
+ ` ${color5.cyan("bun run preflight")} ${color5.dim("what is still missing, and why it matters")}`
782
930
  ];
783
931
  p2.outro(["Next:", ...next].join(`
784
932
  `));
@@ -787,7 +935,7 @@ async function init(args) {
787
935
 
788
936
  // src/regenerate.ts
789
937
  import * as p3 from "@clack/prompts";
790
- import color5 from "picocolors";
938
+ import color6 from "picocolors";
791
939
  async function generateCommand() {
792
940
  const project = await findProject();
793
941
  if (!project) {
@@ -803,9 +951,9 @@ async function generateCommand() {
803
951
  }
804
952
  const result = await generate(project.root, project.manifest);
805
953
  p3.log.step([
806
- ...result.written.map((file) => `${color5.green("+")} ${file}`),
807
- ...result.kept.map((file) => `${color5.dim("=")} ${file} ${color5.dim("(kept)")}`),
808
- ...result.retired.map((file) => `${color5.red("-")} ${file} ${color5.dim("(retired — no longer generated)")}`)
954
+ ...result.written.map((file) => `${color6.green("+")} ${file}`),
955
+ ...result.kept.map((file) => `${color6.dim("=")} ${file} ${color6.dim("(kept)")}`),
956
+ ...result.retired.map((file) => `${color6.red("-")} ${file} ${color6.dim("(retired — no longer generated)")}`)
809
957
  ].join(`
810
958
  `));
811
959
  return project.manifest.layout === "strict" ? finishStrict(project.root) : 0;
@@ -824,8 +972,8 @@ async function generateHelp() {
824
972
  }
825
973
 
826
974
  // src/scripts.ts
827
- import { existsSync as existsSync2, readFileSync } from "node:fs";
828
- import { join as join4, relative } from "node:path";
975
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
976
+ import { join as join5, relative } from "node:path";
829
977
  function appScript(name) {
830
978
  return async (args) => {
831
979
  const project = await findProject();
@@ -834,8 +982,8 @@ function appScript(name) {
834
982
  const code = await ensureGenerated(project);
835
983
  if (code !== 0)
836
984
  return code;
837
- const pkgPath = join4(project.appDir, "package.json");
838
- const scripts = existsSync2(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {} : {};
985
+ const pkgPath = join5(project.appDir, "package.json");
986
+ const scripts = existsSync3(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {} : {};
839
987
  if (!scripts[name]) {
840
988
  console.error(`vc: ${relative(process.cwd(), pkgPath) || "package.json"} has no "${name}" script — void init writes one.`);
841
989
  return 1;
@@ -871,6 +1019,7 @@ var EXTENDED = {
871
1019
  secrets: { run: secretsCliCommand, help: secretsHelp },
872
1020
  keys: { run: keysCliCommand, help: keysHelp },
873
1021
  link: { run: linkCliCommand, help: linkHelp },
1022
+ publish: { run: publishCliCommand, help: publishHelp },
874
1023
  guard: { run: guardCommand, help: async () => guardCommand() },
875
1024
  dev: { run: appScript("dev"), help: appScriptHelp("dev") },
876
1025
  build: { run: appScript("build"), help: appScriptHelp("build") },
@@ -66,6 +66,14 @@ export declare function setVariable(cwd: string, name: string, value: string): P
66
66
  ok: boolean;
67
67
  error?: string;
68
68
  }>;
69
+ /**
70
+ * A variable's VALUE — the thing a secret can never give back.
71
+ *
72
+ * `gh secret` has list, set and delete and no read, by design: a GitHub
73
+ * secret is write-only forever, even to its owner. A variable is not, and
74
+ * that single difference is why the encryption key lives in one.
75
+ */
76
+ export declare function getVariable(cwd: string, name: string): Promise<string | null>;
69
77
  export declare function variableNames(cwd: string): Promise<Set<string>>;
70
78
  /**
71
79
  * Ask Cloudflare whether a token is real, before it is stored anywhere.