@saastemly/voidcommerce 0.17.0 → 0.19.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,210 +21,37 @@ import {
21
21
  runVoid,
22
22
  version,
23
23
  voidAppsIn
24
- } from "./index-4jz1448c.js";
24
+ } from "./index-y4dk27kf.js";
25
25
  import {
26
26
  LOCAL_KEY_FILE,
27
27
  PRIVATE_KEY_VAR,
28
28
  SECRETS_FILE,
29
- cloudflareAccounts,
30
29
  committedPublicKey,
31
- committedPublicKeyInto,
32
- declaredSecretNames,
33
30
  encryptInto,
34
31
  envSummary,
35
- getVariable,
36
- ghAuth,
37
32
  ignoresKeyFile,
38
- initSecrets,
39
- keyState,
40
33
  keysCommand,
41
- localPrivateKey,
42
34
  plaintextSecretEntries,
43
35
  plaintextSecretNames,
44
- provisionKey,
45
- publicKeyFor,
46
- repoSlug,
47
36
  run,
48
- secretNames,
49
- secretsCommand,
50
- setSecret,
51
- setVariable,
52
- variableNames,
53
- verifyCloudflareToken
54
- } from "./index-negctpws.js";
37
+ secretsCommand
38
+ } from "./index-68m1eg3f.js";
55
39
  import {
56
40
  LAYOUTS,
57
41
  oneOrigin,
58
42
  readManifest,
59
43
  validate,
60
- writeManifest,
61
44
  zoneOf
62
45
  } from "./index-30y19qz5.js";
63
46
  import {
64
47
  GROUPS
65
48
  } from "./index-xyjhy6kp.js";
66
- import {
67
- __require
68
- } from "./index-0v6na3yp.js";
49
+ import"./index-0v6na3yp.js";
69
50
 
70
51
  // src/deploy/index.ts
71
- import color2 from "picocolors";
52
+ import color from "picocolors";
72
53
  import { existsSync } from "node:fs";
73
- import { join as join2 } from "node:path";
74
-
75
- // src/deploy/link.ts
76
- import { rmSync } from "node:fs";
77
54
  import { join } from "node:path";
78
- import color from "picocolors";
79
- var TOKEN_SECRET = "CLOUDFLARE_API_TOKEN";
80
- var ACCOUNT_VAR = "CLOUDFLARE_ACCOUNT_ID";
81
- var SCOPES = [
82
- ["Account · Workers Scripts: Edit", "deploy the worker"],
83
- ["Account · D1: Edit", "create the database and apply migrations"],
84
- ["Account · Queues: Edit", "create the order queue"],
85
- ["Account · Workers KV Storage: Edit", "sessions and caches"],
86
- ["Account · Workers R2 Storage: Edit", "product images"],
87
- ["Account · Account Settings: Read", "confirm which account this is"],
88
- ["Zone · Workers Routes: Edit", "answer on your domain"],
89
- ["Zone · Zone: Read", "confirm the domain's zone is on this account"],
90
- ["Zone · DNS: Read", "notice a hostname that already answers, before taking it over"],
91
- ["User · User Details: Read", "wrangler asks at startup"]
92
- ];
93
- async function linkCommand(project, args) {
94
- const p = await import("@clack/prompts");
95
- const root = project.root;
96
- const relink = args.includes("--force");
97
- p.intro(color.bgCyan(color.black(" vc link ")));
98
- const auth = await ghAuth(root);
99
- if (!auth.ok) {
100
- p.cancel(auth.reason ?? "gh is unavailable");
101
- return 1;
102
- }
103
- const slug = await repoSlug(root);
104
- if (!slug) {
105
- p.cancel(`this checkout has no GitHub repository, and everything below is stored on one.
106
-
107
- ${color.cyan("gh repo create --source=. --private --push")}
108
-
109
- then run \`vc link\` again.`);
110
- return 1;
111
- }
112
- p.log.success(`${color.green("✓")} ${slug}, as ${auth.user ?? "you"}`);
113
- const keys = await keyState(project);
114
- if (keys.inGitHub && !relink) {
115
- p.log.info(`${PRIVATE_KEY_VAR} is already set — leaving it alone (--force replaces it, which orphans ${SECRETS_FILE})`);
116
- } else if (keys.inGitHub && relink) {
117
- p.log.warn(`--force: replacing ${PRIVATE_KEY_VAR} makes every value in ${SECRETS_FILE} unreadable. Use \`vc keys --rotate\` to re-encrypt instead.`);
118
- return 1;
119
- } else {
120
- const parked = localPrivateKey(root);
121
- if (parked) {
122
- const sent = await setVariable(root, PRIVATE_KEY_VAR, parked);
123
- if (!sent.ok) {
124
- p.cancel(`GitHub refused the variable: ${sent.error ?? "unknown error"}`);
125
- return 1;
126
- }
127
- const publicKey = await publicKeyFor(parked);
128
- if (publicKey)
129
- committedPublicKeyInto(root, publicKey);
130
- const readBack = await getVariable(root, PRIVATE_KEY_VAR);
131
- if (readBack !== parked) {
132
- p.log.warn(`${color.yellow("!")} ${PRIVATE_KEY_VAR} was written to ${slug} but could not be read back,
133
- so ${LOCAL_KEY_FILE} has been LEFT IN PLACE. It is currently the only copy
134
- ` + " of the key that opens this shop — do not delete it until `vc keys --restore` works.");
135
- } else {
136
- rmSync(join(root, LOCAL_KEY_FILE), { force: true });
137
- 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`);
138
- }
139
- } else {
140
- const made = await provisionKey(project);
141
- if (!made.ok) {
142
- p.cancel(made.reason);
143
- return 1;
144
- }
145
- committedPublicKeyInto(root, made.publicKey);
146
- p.log.success(`${color.green("✓")} ${PRIVATE_KEY_VAR} generated and stored on GitHub; public half in ${SECRETS_FILE}`);
147
- }
148
- }
149
- const existing = await secretNames(root);
150
- if (existing.has(TOKEN_SECRET) && !relink) {
151
- p.log.info(`${TOKEN_SECRET} is already set — pass --force to replace it`);
152
- } else {
153
- p.log.step(`A Cloudflare API token, from ${color.cyan("https://dash.cloudflare.com/profile/api-tokens")} → Create Token → Custom token`);
154
- console.log(SCOPES.map(([scope, why]) => ` ${scope.padEnd(38)} ${color.dim(why)}`).join(`
155
- `));
156
- console.log(color.dim(`
157
- The token Cloudflare generates for its own Workers Builds will NOT do:
158
- it has no D1 and no Queues permission, so it cannot create the database.
159
- `));
160
- const token = await p.password({
161
- message: "Paste it (nothing is written to disk)",
162
- validate: (input) => input && input.length >= 20 ? undefined : "that is too short to be a Cloudflare token"
163
- });
164
- if (p.isCancel(token)) {
165
- p.cancel("nothing was stored.");
166
- return 1;
167
- }
168
- const spinner = p.spinner();
169
- spinner.start("asking Cloudflare whether that token is real");
170
- const verified = await verifyCloudflareToken(String(token));
171
- if (!verified.ok) {
172
- spinner.stop(`${color.red("✗")} Cloudflare rejected it: ${verified.detail}`);
173
- p.cancel("nothing was stored.");
174
- return 1;
175
- }
176
- const accounts = await cloudflareAccounts(String(token));
177
- spinner.stop(`${color.green("✓")} the token is active and sees ${accounts.length} account${accounts.length === 1 ? "" : "s"}`);
178
- let accountId = project.manifest.cloudflare?.accountId ?? "";
179
- if (accounts.length === 1) {
180
- accountId = accounts[0].id;
181
- p.log.info(`account "${accounts[0].name}"`);
182
- } else if (accounts.length > 1) {
183
- const picked = await p.select({
184
- message: "Which account is this shop in?",
185
- options: accounts.map((account) => ({ value: account.id, label: account.name, hint: account.id }))
186
- });
187
- if (p.isCancel(picked)) {
188
- p.cancel("nothing was stored.");
189
- return 1;
190
- }
191
- accountId = String(picked);
192
- }
193
- const stored = await setSecret(root, TOKEN_SECRET, String(token));
194
- if (!stored.ok) {
195
- p.cancel(`GitHub refused the secret: ${stored.error ?? "unknown error"}`);
196
- return 1;
197
- }
198
- p.log.success(`${color.green("✓")} ${TOKEN_SECRET} stored on ${slug}`);
199
- if (accountId) {
200
- const varred = await setVariable(root, ACCOUNT_VAR, accountId);
201
- if (varred.ok)
202
- p.log.success(`${color.green("✓")} ${ACCOUNT_VAR} set as a repository variable`);
203
- project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
204
- await writeManifest(root, project.manifest);
205
- }
206
- }
207
- if (declaredSecretNames(root).size === 0) {
208
- p.log.step(`writing ${SECRETS_FILE}`);
209
- await initSecrets(project);
210
- }
211
- const secrets = await secretNames(root);
212
- const vars = await variableNames(root);
213
- p.outro([
214
- `${color.bold("Ready.")} From here, ${color.cyan("git push")} is the deploy.`,
215
- "",
216
- ` ${secrets.has(PRIVATE_KEY_VAR) ? color.green("✓") : color.red("✗")} ${PRIVATE_KEY_VAR} ${color.dim("reads the shop's secrets")}`,
217
- ` ${secrets.has(TOKEN_SECRET) ? color.green("✓") : color.red("✗")} ${TOKEN_SECRET} ${color.dim("deploys to Cloudflare")}`,
218
- ` ${vars.has(ACCOUNT_VAR) ? color.green("✓") : color.dim("·")} ${ACCOUNT_VAR} ${color.dim("which account")}`,
219
- "",
220
- ` Set the shop's own secrets with ${color.cyan("vc secrets set KEY")} — that needs no`,
221
- ` credential at all, because encryption uses the public key in the repository.`
222
- ].join(`
223
- `));
224
- return 0;
225
- }
226
-
227
- // src/deploy/index.ts
228
55
  async function deployCommand(args) {
229
56
  const own = new Set(["--cloudflare", "--provision", "--force"]);
230
57
  const cloudflare = args.includes("--cloudflare");
@@ -247,12 +74,12 @@ async function deployCommand(args) {
247
74
  const check = await preflight(project, "void");
248
75
  printPreflight(project, check, "void");
249
76
  if (!check.ready && check.remote !== null && !args.includes("--force")) {
250
- console.error(`${color2.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
77
+ console.error(`${color.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
251
78
  `);
252
79
  return 1;
253
80
  }
254
81
  if (!check.ready && check.remote === null) {
255
- console.log(color2.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
82
+ console.log(color.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
256
83
  `));
257
84
  }
258
85
  return runVoid(["deploy", ...voids], project.appDir);
@@ -270,7 +97,7 @@ async function preflightCommand(args) {
270
97
  return result.ready ? 0 : 1;
271
98
  const pending = await uncommittedMigrations(project.root);
272
99
  if (pending.length > 0) {
273
- console.error(`${color2.red("✗")} ${pending.length} migration file${pending.length === 1 ? " is" : "s are"} not committed:
100
+ console.error(`${color.red("✗")} ${pending.length} migration file${pending.length === 1 ? " is" : "s are"} not committed:
274
101
  ` + ` ${pending.join(`
275
102
  `)}
276
103
 
@@ -295,13 +122,13 @@ async function deployHelp() {
295
122
  line("vc's preflight — every required key set, the hostnames right — then", width),
296
123
  line("void deploy, untouched. Or, with --cloudflare, your own account via wrangler.", width),
297
124
  line("", width),
298
- line(color2.bold("Usage"), width),
125
+ line(color.bold("Usage"), width),
299
126
  ...row("vc deploy [void's flags]", "preflight, then void deploy — the Void platform, void's login", width, 2),
300
127
  ...row("vc deploy --cloudflare", "wrangler, your Cloudflare account, no Void login: preflight, build, scrub baked secrets, migrate D1, deploy", width, 2),
301
128
  ...row("vc deploy --cloudflare --provision", "first time: create the D1 database and the queue, record them", width, 2),
302
129
  ...row("vc deploy --cloudflare --force", "deploy a shop preflight says is not ready — deliberately", width, 2),
303
130
  line("", width),
304
- line(color2.bold("Needs, for --cloudflare"), width),
131
+ line(color.bold("Needs, for --cloudflare"), width),
305
132
  ...row("CLOUDFLARE_API_TOKEN", "in the environment; the account is pinned for you when there is one", width, 2),
306
133
  ...row(PRIVATE_KEY_VAR, "to decrypt the shop's own secrets", width, 2),
307
134
  line("", width),
@@ -397,9 +224,9 @@ async function guardCommand() {
397
224
  if (!project)
398
225
  return 0;
399
226
  const root = project.root;
400
- if (existsSync(join2(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
227
+ if (existsSync(join(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
401
228
  console.error(`
402
- ${color2.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gitignored.
229
+ ${color.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gitignored.
403
230
 
404
231
  ` + ` It holds the private key that opens every secret in this repository.
405
232
  ` + ` Add ${LOCAL_KEY_FILE} to .gitignore before committing anything.
@@ -412,14 +239,14 @@ ${color2.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gi
412
239
  const publicKey = committedPublicKey(root);
413
240
  if (!publicKey) {
414
241
  console.error(`
415
- ${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.
242
+ ${color.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.
416
243
 
417
244
  ` + ` ${bare.map((entry) => entry.name).join(`
418
245
  `)}
419
246
 
420
- ` + ` ${color2.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
247
+ ` + ` ${color.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
421
248
 
422
- ` + color2.dim(` Committing a secret in the clear cannot be undone by a later commit;
249
+ ` + color.dim(` Committing a secret in the clear cannot be undone by a later commit;
423
250
  the value stays in the history and must be treated as burned.
424
251
  `));
425
252
  return 1;
@@ -429,7 +256,7 @@ ${color2.red("✗ refusing the commit")}: ${bare.length} value${bare.length ===
429
256
  const sealed = await encryptInto(root, publicKey, entry.name, entry.value);
430
257
  if (!sealed.ok) {
431
258
  console.error(`
432
- ${color2.red("✗ refusing the commit")}: ${sealed.error}
259
+ ${color.red("✗ refusing the commit")}: ${sealed.error}
433
260
  `);
434
261
  return 1;
435
262
  }
@@ -437,7 +264,7 @@ ${color2.red("✗ refusing the commit")}: ${sealed.error}
437
264
  const left = plaintextSecretNames(root);
438
265
  if (left.length > 0) {
439
266
  console.error(`
440
- ${color2.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
267
+ ${color.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
441
268
  `);
442
269
  return 1;
443
270
  }
@@ -445,58 +272,28 @@ ${color2.red("✗ refusing the commit")}: ${left.join(", ")} could not be encryp
445
272
  const added = await run("git", ["add", "--", SECRETS_FILE], root);
446
273
  if (added.code !== 0) {
447
274
  console.error(`
448
- ${color2.red("✗ refusing the commit")}: ${SECRETS_FILE} was encrypted but could not be re-staged,
275
+ ${color.red("✗ refusing the commit")}: ${SECRETS_FILE} was encrypted but could not be re-staged,
449
276
  ` + ` so the commit would still carry the plaintext you staged. \`git add ${SECRETS_FILE}\`.
450
277
  `);
451
278
  return 1;
452
279
  }
453
280
  }
454
281
  console.log(`
455
- ${color2.green("✓")} encrypted ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE}${staged ? " and re-staged it" : ""}: ${bare.map((entry) => entry.name).join(", ")}
456
- ` + color2.dim(` Encryption needs only the public key, so this needs no credential.
282
+ ${color.green("✓")} encrypted ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE}${staged ? " and re-staged it" : ""}: ${bare.map((entry) => entry.name).join(", ")}
283
+ ` + color.dim(` Encryption needs only the public key, so this needs no credential.
457
284
  `));
458
285
  return 0;
459
286
  }
460
- async function linkCliCommand(args) {
461
- const project = await findProject();
462
- if (!project) {
463
- console.error("vc: no voidcommerce.json here.");
464
- return 1;
465
- }
466
- return linkCommand(project, args);
467
- }
468
- async function linkHelp() {
469
- const width = 80;
470
- console.log(box("vc link", [
471
- line("Put this shop's credentials on its GitHub repository, once, so that", width),
472
- line("every deploy after this is a `git push`.", width),
473
- line("", width),
474
- ...row("vc link", "store the encryption key and the Cloudflare token on the repository", width, 2),
475
- ...row("vc link --force", "replace what is already there", width, 2),
476
- line("", width),
477
- line(color2.bold("What it stores, and where"), width),
478
- ...row(PRIVATE_KEY_VAR, "a repository SECRET. A key already waiting locally is MOVED here and the local copy deleted", width, 2),
479
- ...row("CLOUDFLARE_API_TOKEN", "a repository SECRET, checked against the Cloudflare API before it is stored", width, 2),
480
- ...row("CLOUDFLARE_ACCOUNT_ID", "a repository VARIABLE — an identifier, not a credential", width, 2),
481
- line("", width),
482
- line(color2.bold("Why one token still has to be typed"), width),
483
- line("GitHub cannot mint a Cloudflare credential. There is no OIDC federation", width),
484
- line("between them, and the Cloudflare GitHub App runs the other way: it grants", width),
485
- line("Cloudflare access to your repository, not your repository access to", width),
486
- line("Cloudflare. So it is typed once, here, and never stored on this machine.", width)
487
- ], width));
488
- return 0;
489
- }
490
287
 
491
288
  // src/init.ts
492
289
  import * as p2 from "@clack/prompts";
493
290
  import { mkdir } from "node:fs/promises";
494
- import { basename, join as join3 } from "node:path";
495
- import color4 from "picocolors";
291
+ import { basename, join as join2 } from "node:path";
292
+ import color3 from "picocolors";
496
293
 
497
294
  // src/wizard.ts
498
295
  import * as p from "@clack/prompts";
499
- import color3 from "picocolors";
296
+ import color2 from "picocolors";
500
297
  var bail = () => {
501
298
  p.cancel("Nothing was written.");
502
299
  process.exit(0);
@@ -518,8 +315,8 @@ async function askGroup(group2, previous, alsoRequired = []) {
518
315
  const required = group2.choices.filter((choice) => requiredIds.has(choice.id));
519
316
  const optional = group2.choices.filter((choice) => !requiredIds.has(choice.id));
520
317
  const message = group2.title + (group2.intro ? `
521
- ${color3.dim(group2.intro)}` : "") + (required.length ? `
522
- ${color3.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
318
+ ${color2.dim(group2.intro)}` : "") + (required.length ? `
319
+ ${color2.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
523
320
  if (optional.length === 0) {
524
321
  p.log.info(message);
525
322
  return required.map((choice) => choice.id);
@@ -550,7 +347,7 @@ async function runWizard(existing, layout) {
550
347
  validate: (value) => value.trim() ? undefined : "A shop has a name."
551
348
  }),
552
349
  domain: () => p.text({
553
- 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")}`,
350
+ message: `Domain ${color2.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")}`,
554
351
  placeholder: "northwind.com",
555
352
  initialValue: existing?.shop.domain ?? "",
556
353
  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."
@@ -561,7 +358,7 @@ async function runWizard(existing, layout) {
561
358
  if (!domain || domain === guess)
562
359
  return Promise.resolve(undefined);
563
360
  return p.text({
564
- message: `DNS zone ${color3.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
361
+ message: `DNS zone ${color2.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
565
362
  placeholder: guess,
566
363
  initialValue: existing?.shop.zone ?? guess,
567
364
  validate: (value) => {
@@ -573,13 +370,13 @@ async function runWizard(existing, layout) {
573
370
  });
574
371
  },
575
372
  country: () => p.text({
576
- message: `Country ${color3.dim("— sets the declared VAT rate and the default region")}`,
373
+ message: `Country ${color2.dim("— sets the declared VAT rate and the default region")}`,
577
374
  placeholder: "US",
578
375
  initialValue: existing?.shop.country ?? "",
579
376
  validate: (value) => /^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."
580
377
  }),
581
378
  taxRegistered: () => p.select({
582
- 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")}`,
379
+ message: `Registered to charge tax? ${color2.dim("— a business below its registration threshold must NOT add tax to a price, and one above it must")}`,
583
380
  options: [
584
381
  { value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
585
382
  { value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" }
@@ -593,14 +390,14 @@ async function runWizard(existing, layout) {
593
390
  validate: (value) => /^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."
594
391
  }),
595
392
  locale: () => p.text({
596
- message: `Locale ${color3.dim("— the language of emails and launch content")}`,
393
+ message: `Locale ${color2.dim("— the language of emails and launch content")}`,
597
394
  placeholder: "en",
598
395
  initialValue: existing?.shop.locale ?? "",
599
396
  validate: (value) => /^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."
600
397
  }),
601
398
  ...onPages ? {
602
399
  pagesHost: () => p.text({
603
- message: `GitHub Pages host ${color3.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
400
+ message: `GitHub Pages host ${color2.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
604
401
  placeholder: "northwind.github.io",
605
402
  initialValue: existing?.shop.pagesHost ?? "",
606
403
  validate: (value) => !value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank"
@@ -611,7 +408,7 @@ async function runWizard(existing, layout) {
611
408
  for (const group2 of GROUPS) {
612
409
  if (group2.id === "ui" && layout === "strict") {
613
410
  p.log.info(`${group2.title}
614
- ${color3.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
411
+ ${color2.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
615
412
  chosen[group2.id] = ["admin"];
616
413
  continue;
617
414
  }
@@ -636,14 +433,14 @@ ${color3.dim("The generated storefront and panel. Strict has no custom code to h
636
433
  }
637
434
  function summarise(manifest) {
638
435
  const lines = [
639
- `${color3.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
436
+ `${color2.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
640
437
  ];
641
438
  for (const group2 of GROUPS) {
642
439
  const ids = manifest.chosen[group2.id] ?? [];
643
440
  if (ids.length === 0)
644
441
  continue;
645
442
  const labels = ids.map((id) => group2.choices.find((choice) => choice.id === id)?.label ?? id).join(", ");
646
- lines.push(`${color3.dim(group2.title.padEnd(34))} ${labels}`);
443
+ lines.push(`${color2.dim(group2.title.padEnd(34))} ${labels}`);
647
444
  }
648
445
  return lines.join(`
649
446
  `);
@@ -684,10 +481,10 @@ async function init(args) {
684
481
  if (flags.voids.length > 0)
685
482
  return runVoid(["init", ...flags.voids]);
686
483
  let root = process.cwd();
687
- p2.intro(color4.bgCyan(color4.black(" voidcommerce ")));
484
+ p2.intro(color3.bgCyan(color3.black(" voidcommerce ")));
688
485
  const existing = await readManifest(root);
689
486
  if (existing) {
690
- p2.log.info(`Found ${color4.cyan("voidcommerce.json")} for ${color4.bold(existing.shop.name)} — answers are pre-filled; the layout (${existing.layout}) is fixed.`);
487
+ p2.log.info(`Found ${color3.cyan("voidcommerce.json")} for ${color3.bold(existing.shop.name)} — answers are pre-filled; the layout (${existing.layout}) is fixed.`);
691
488
  if (flags.layout && flags.layout !== existing.layout) {
692
489
  p2.log.error(`This shop is laid out as "${existing.layout}". The layout cannot change after init — moving files is not a regeneration.`);
693
490
  return 1;
@@ -695,10 +492,10 @@ async function init(args) {
695
492
  }
696
493
  const layout = existing?.layout ?? flags.layout ?? await askLayout();
697
494
  if (layout === "strict") {
698
- p2.log.step(`Strict: the app will be generated under ${color4.cyan(".vc/app")} — nothing there is yours to edit.`);
495
+ p2.log.step(`Strict: the app will be generated under ${color3.cyan(".vc/app")} — nothing there is yours to edit.`);
699
496
  } else if (layout === "app") {
700
497
  if (!isVoidApp(root)) {
701
- p2.log.step(`No Void app here yet — ${color4.cyan("void init")} first, then the shop.`);
498
+ p2.log.step(`No Void app here yet — ${color3.cyan("void init")} first, then the shop.`);
702
499
  const before = new Set(voidAppsIn(root));
703
500
  const code = await runVoid(["init"]);
704
501
  if (code !== 0)
@@ -711,7 +508,7 @@ async function init(args) {
711
508
  }
712
509
  root = created[0];
713
510
  process.chdir(root);
714
- p2.log.step(`Continuing in ${color4.cyan(`${basename(root)}/`)}`);
511
+ p2.log.step(`Continuing in ${color3.cyan(`${basename(root)}/`)}`);
715
512
  }
716
513
  }
717
514
  } else {
@@ -720,11 +517,11 @@ async function init(args) {
720
517
  ["frontend", "the storefront — prerendered and served from the worker's own assets"]
721
518
  ];
722
519
  for (const [dir, starter] of parts) {
723
- const target = join3(root, dir);
520
+ const target = join2(root, dir);
724
521
  if (isVoidApp(target))
725
522
  continue;
726
523
  await mkdir(target, { recursive: true });
727
- p2.log.step(`${color4.cyan("void init")} in ${color4.cyan(`${dir}/`)} — choose ${starter}.`);
524
+ p2.log.step(`${color3.cyan("void init")} in ${color3.cyan(`${dir}/`)} — choose ${starter}.`);
728
525
  const code = await runVoid(["init"], target);
729
526
  if (code !== 0)
730
527
  return code;
@@ -742,7 +539,7 @@ async function init(args) {
742
539
  if (problems.length > 0) {
743
540
  p2.log.error("These answers contradict each other:");
744
541
  for (const problem of problems)
745
- p2.log.message(` ${color4.red("✗")} ${problem}`);
542
+ p2.log.message(` ${color3.red("✗")} ${problem}`);
746
543
  p2.cancel("Nothing was written.");
747
544
  return 1;
748
545
  }
@@ -758,16 +555,16 @@ async function init(args) {
758
555
  spinner2.stop("Generated");
759
556
  const { secrets, plaintext } = envSummary(manifest);
760
557
  p2.note([
761
- ...result.written.map((file) => `${color4.green("+")} ${file}`),
762
- ...result.kept.map((file) => `${color4.dim("=")} ${file} ${color4.dim("(kept — yours)")}`),
763
- ...result.retired.map((file) => `${color4.red("-")} ${file} ${color4.dim("(retired — no longer generated)")}`)
558
+ ...result.written.map((file) => `${color3.green("+")} ${file}`),
559
+ ...result.kept.map((file) => `${color3.dim("=")} ${file} ${color3.dim("(kept — yours)")}`),
560
+ ...result.retired.map((file) => `${color3.red("-")} ${file} ${color3.dim("(retired — no longer generated)")}`)
764
561
  ].join(`
765
562
  `), "Files");
766
563
  p2.note([
767
- `${color4.bold(String(secrets.length))} secrets ${color4.dim("→ wrangler secret put <NAME>")}`,
564
+ `${color3.bold(String(secrets.length))} secrets ${color3.dim("→ wrangler secret put <NAME>")}`,
768
565
  ...secrets.map((key) => ` ${key}`),
769
566
  "",
770
- `${color4.bold(String(plaintext.length))} plaintext ${color4.dim("→ .env.production, already written")}`,
567
+ `${color3.bold(String(plaintext.length))} plaintext ${color3.dim("→ .env.production, already written")}`,
771
568
  ...plaintext.map((key) => ` ${key}`)
772
569
  ].join(`
773
570
  `), "Before going live");
@@ -777,21 +574,21 @@ async function init(args) {
777
574
  return code;
778
575
  }
779
576
  const next = layout === "app" ? [
780
- ` ${color4.cyan("bun install")}`,
781
- ` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
782
- ` ${color4.cyan("vc dev")}`,
783
- ` ${color4.cyan("vc preflight")} ${color4.dim("what is still missing, and why it matters")}`
577
+ ` ${color3.cyan("bun install")}`,
578
+ ` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
579
+ ` ${color3.cyan("vc dev")}`,
580
+ ` ${color3.cyan("vc preflight")} ${color3.dim("what is still missing, and why it matters")}`
784
581
  ] : layout === "strict" ? [
785
- ` ${color4.cyan("bun install")}`,
786
- ` ${color4.cyan("vc generate")} ${color4.dim("the app under .vc/app, void's artifacts, the migrations")}`,
787
- ` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
788
- ` ${color4.cyan("vc dev")} ${color4.dim("runs in .vc/app")}`
582
+ ` ${color3.cyan("bun install")}`,
583
+ ` ${color3.cyan("vc generate")} ${color3.dim("the app under .vc/app, void's artifacts, the migrations")}`,
584
+ ` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
585
+ ` ${color3.cyan("vc dev")} ${color3.dim("runs in .vc/app")}`
789
586
  ] : [
790
- ` ${color4.cyan("bun install")} ${color4.dim("one install for both workspaces")}`,
791
- ` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
792
- ` ${color4.cyan("bun run dev:api")} ${color4.dim("the worker, with the panel")}`,
793
- ` ${color4.cyan("bun run dev:frontend")} ${color4.dim("the storefront, against the local API")}`,
794
- ` ${color4.cyan("bun run preflight")} ${color4.dim("what is still missing, and why it matters")}`
587
+ ` ${color3.cyan("bun install")} ${color3.dim("one install for both workspaces")}`,
588
+ ` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
589
+ ` ${color3.cyan("bun run dev:api")} ${color3.dim("the worker, with the panel")}`,
590
+ ` ${color3.cyan("bun run dev:frontend")} ${color3.dim("the storefront, against the local API")}`,
591
+ ` ${color3.cyan("bun run preflight")} ${color3.dim("what is still missing, and why it matters")}`
795
592
  ];
796
593
  p2.outro(["Next:", ...next].join(`
797
594
  `));
@@ -800,7 +597,7 @@ async function init(args) {
800
597
 
801
598
  // src/regenerate.ts
802
599
  import * as p3 from "@clack/prompts";
803
- import color5 from "picocolors";
600
+ import color4 from "picocolors";
804
601
  async function generateCommand() {
805
602
  const project = await findProject();
806
603
  if (!project) {
@@ -816,9 +613,9 @@ async function generateCommand() {
816
613
  }
817
614
  const result = await generate(project.root, project.manifest);
818
615
  p3.log.step([
819
- ...result.written.map((file) => `${color5.green("+")} ${file}`),
820
- ...result.kept.map((file) => `${color5.dim("=")} ${file} ${color5.dim("(kept)")}`),
821
- ...result.retired.map((file) => `${color5.red("-")} ${file} ${color5.dim("(retired — no longer generated)")}`)
616
+ ...result.written.map((file) => `${color4.green("+")} ${file}`),
617
+ ...result.kept.map((file) => `${color4.dim("=")} ${file} ${color4.dim("(kept)")}`),
618
+ ...result.retired.map((file) => `${color4.red("-")} ${file} ${color4.dim("(retired — no longer generated)")}`)
822
619
  ].join(`
823
620
  `));
824
621
  return project.manifest.layout === "strict" ? finishStrict(project.root) : 0;
@@ -838,7 +635,7 @@ async function generateHelp() {
838
635
 
839
636
  // src/scripts.ts
840
637
  import { existsSync as existsSync2, readFileSync } from "node:fs";
841
- import { join as join4, relative } from "node:path";
638
+ import { join as join3, relative } from "node:path";
842
639
  function appScript(name) {
843
640
  return async (args) => {
844
641
  const project = await findProject();
@@ -847,7 +644,7 @@ function appScript(name) {
847
644
  const code = await ensureGenerated(project);
848
645
  if (code !== 0)
849
646
  return code;
850
- const pkgPath = join4(project.appDir, "package.json");
647
+ const pkgPath = join3(project.appDir, "package.json");
851
648
  const scripts = existsSync2(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {} : {};
852
649
  if (!scripts[name]) {
853
650
  console.error(`vc: ${relative(process.cwd(), pkgPath) || "package.json"} has no "${name}" script — void init writes one.`);
@@ -883,7 +680,6 @@ var EXTENDED = {
883
680
  preflight: { run: preflightCommand, help: preflightHelp },
884
681
  secrets: { run: secretsCliCommand, help: secretsHelp },
885
682
  keys: { run: keysCliCommand, help: keysHelp },
886
- link: { run: linkCliCommand, help: linkHelp },
887
683
  guard: { run: guardCommand, help: async () => guardCommand() },
888
684
  dev: { run: appScript("dev"), help: appScriptHelp("dev") },
889
685
  build: { run: appScript("build"), help: appScriptHelp("build") },
@@ -43,6 +43,3 @@ export declare function keysHelp(): Promise<number>;
43
43
  * Exit code is the whole interface — a hook cares about nothing else.
44
44
  */
45
45
  export declare function guardCommand(): Promise<number>;
46
- /** `vc link` — GitHub holds the credentials; this is what puts them there. */
47
- export declare function linkCliCommand(args: string[]): Promise<number>;
48
- export declare function linkHelp(): Promise<number>;
@@ -121,6 +121,16 @@ export declare function initSecrets(project: Project): Promise<number>;
121
121
  * command runs, and shells keep it in history besides.
122
122
  */
123
123
  export declare function setSecretValue(project: Project, args: string[]): Promise<number>;
124
+ /**
125
+ * One value out of `.env.secrets`, by name.
126
+ *
127
+ * How the deploy gets its Cloudflare credentials without anyone having put
128
+ * them in the environment: they are ordinary secrets in the committed file,
129
+ * so the deploy reads them out of what was pushed. Returns null rather than
130
+ * throwing — a caller that cannot decrypt has an env var to fall back on,
131
+ * and a better error to give than this one.
132
+ */
133
+ export declare function secretValue(project: Project, name: string): Promise<string | null>;
124
134
  /**
125
135
  * Encrypt one value into `.env.secrets`, in process.
126
136
  *