@saastemly/voidcommerce 0.20.0 → 0.22.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/catalog.d.ts CHANGED
@@ -24,6 +24,19 @@ export interface EnvKey {
24
24
  dev?: string | undefined;
25
25
  /** Where the real value comes from. */
26
26
  where?: string | undefined;
27
+ /**
28
+ * Needed to DEPLOY the shop, not to run it.
29
+ *
30
+ * The Cloudflare credentials are the case. They belong in `.env.secrets`
31
+ * so a push carries them, and preflight must insist on them — but the
32
+ * worker has no business seeing one. Left unmarked they ended up in three
33
+ * wrong places at once: `env.ts`, so the worker refused to boot without a
34
+ * deploy token; `.env`, where wrangler read `CLOUDFLARE_ACCOUNT_ID=unset`
35
+ * and addressed `accounts/unset/...`; and `--secrets-file`, which would
36
+ * have uploaded the deploy credential INTO the worker for any code
37
+ * running there to read.
38
+ */
39
+ deployOnly?: boolean | undefined;
27
40
  }
28
41
  export interface Choice {
29
42
  id: string;
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  distHelp,
7
7
  ensureGenerated,
8
8
  findProject,
9
+ findWrangler,
9
10
  finishStrict,
10
11
  fullHelp,
11
12
  generate,
@@ -14,14 +15,17 @@ import {
14
15
  initHelp,
15
16
  isVoidApp,
16
17
  line,
18
+ parseJsonc,
17
19
  preflight,
18
20
  printPreflight,
19
21
  row,
20
22
  runInherit,
21
23
  runVoid,
24
+ upsertJsonc,
22
25
  version,
23
- voidAppsIn
24
- } from "./index-07ppzmgt.js";
26
+ voidAppsIn,
27
+ wrangler
28
+ } from "./index-2kqz80nx.js";
25
29
  import {
26
30
  LOCAL_KEY_FILE,
27
31
  PRIVATE_KEY_VAR,
@@ -34,13 +38,16 @@ import {
34
38
  plaintextSecretEntries,
35
39
  plaintextSecretNames,
36
40
  run,
41
+ secretValue,
37
42
  secretsCommand
38
- } from "./index-vpcnasre.js";
43
+ } from "./index-t1ggktg8.js";
39
44
  import {
40
45
  LAYOUTS,
41
46
  oneOrigin,
42
47
  readManifest,
43
48
  validate,
49
+ workerHosts,
50
+ writeManifest,
44
51
  zoneOf
45
52
  } from "./index-30y19qz5.js";
46
53
  import {
@@ -50,9 +57,119 @@ import"./index-6e4eh08g.js";
50
57
  import"./index-0v6na3yp.js";
51
58
 
52
59
  // src/deploy/index.ts
53
- import color from "picocolors";
54
- import { existsSync } from "node:fs";
60
+ import color2 from "picocolors";
61
+ import { existsSync as existsSync2 } from "node:fs";
62
+ import { join as join2 } from "node:path";
63
+
64
+ // src/deploy/teardown.ts
65
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
55
66
  import { join } from "node:path";
67
+ import color from "picocolors";
68
+ var fail = (message) => {
69
+ console.error(`
70
+ ${color.red("✗")} ${message}
71
+ `);
72
+ return 1;
73
+ };
74
+ function teardownPlan(config, fallbackWorker) {
75
+ const queues = new Set;
76
+ for (const producer of config.queues?.producers ?? [])
77
+ if (producer.queue)
78
+ queues.add(producer.queue);
79
+ for (const consumer of config.queues?.consumers ?? [])
80
+ if (consumer.queue)
81
+ queues.add(consumer.queue);
82
+ const d1 = config.d1_databases?.find((db) => db.binding === "DB");
83
+ return {
84
+ worker: config.name || fallbackWorker,
85
+ d1: d1?.database_name && d1.database_id !== "local" ? d1.database_name : null,
86
+ queues: [...queues]
87
+ };
88
+ }
89
+ async function teardownCloudflare(project, opts) {
90
+ const app = project.appDir;
91
+ const configPath = join(app, "wrangler.jsonc");
92
+ if (!existsSync(configPath))
93
+ return fail("wrangler.jsonc is missing — there is nothing here to tear down.");
94
+ if (!process.env["CLOUDFLARE_API_TOKEN"]) {
95
+ const token = await secretValue(project, "CLOUDFLARE_API_TOKEN");
96
+ if (token)
97
+ process.env["CLOUDFLARE_API_TOKEN"] = token;
98
+ }
99
+ if (!process.env["CLOUDFLARE_ACCOUNT_ID"]) {
100
+ const account = await secretValue(project, "CLOUDFLARE_ACCOUNT_ID");
101
+ if (account)
102
+ process.env["CLOUDFLARE_ACCOUNT_ID"] = account;
103
+ }
104
+ const bin = findWrangler(app);
105
+ if (!bin)
106
+ return fail("wrangler is not installed. `bun add -d wrangler`.");
107
+ const config = parseJsonc(readFileSync(configPath, "utf8"));
108
+ const plan = teardownPlan(config, project.manifest.shop.domain.split(".")[0]);
109
+ const host = workerHosts(project.manifest)[0] ?? project.manifest.shop.domain;
110
+ console.log(`
111
+ ${color.bold("This will destroy, on Cloudflare:")}
112
+ `);
113
+ console.log(` worker ${plan.worker} ${color.dim(`(and its custom domain ${host})`)}`);
114
+ console.log(plan.d1 ? ` database ${plan.d1} ${opts.keepData ? color.green("KEPT — --keep-data") : color.red("DELETED, with every order in it")}` : ` database ${color.dim("none recorded")}`);
115
+ console.log(plan.queues.length ? ` queues ${plan.queues.join(", ")}` : ` queues ${color.dim("none recorded")}`);
116
+ console.log("");
117
+ if (opts.dryRun) {
118
+ console.log(color.dim(`--dry-run: nothing was touched.
119
+ `));
120
+ return 0;
121
+ }
122
+ if (!opts.confirm) {
123
+ return fail(`refusing to destroy anything without --confirm.
124
+ ` + ` This is not reversible: D1 has no undo, and the orders go with it.
125
+
126
+ ` + ` ${color.cyan(`vc teardown --cloudflare --confirm ${project.manifest.shop.domain}`)}`);
127
+ }
128
+ console.log(`▸ wrangler delete (${plan.worker})`);
129
+ const deleted = await wrangler(bin, ["delete", "--name", plan.worker, "--force"], app, true);
130
+ if (deleted.code !== 0 && !/not found|does not exist|10007|10090/i.test(deleted.out)) {
131
+ return fail(`could not delete the worker:
132
+ ${deleted.out.trim().split(`
133
+ `).slice(-3).join(`
134
+ `)}`);
135
+ }
136
+ console.log(`${color.green("✓")} worker gone${/not found|does not exist/i.test(deleted.out) ? color.dim(" (it was already)") : ""}`);
137
+ for (const queue of plan.queues) {
138
+ const dropped = await wrangler(bin, ["queues", "delete", queue], app, true);
139
+ if (dropped.code !== 0 && !/not found|does not exist|11009|11018/i.test(dropped.out)) {
140
+ console.error(`${color.yellow("!")} queue "${queue}" could not be deleted: ${dropped.out.trim().split(`
141
+ `).slice(-1)[0]}`);
142
+ } else {
143
+ console.log(`${color.green("✓")} queue "${queue}" gone`);
144
+ }
145
+ }
146
+ if (plan.d1 && !opts.keepData) {
147
+ const dropped = await wrangler(bin, ["d1", "delete", plan.d1, "--skip-confirmation"], app, true);
148
+ if (dropped.code !== 0 && !/not found|does not exist|7404/i.test(dropped.out)) {
149
+ return fail(`the worker and queues are gone but D1 "${plan.d1}" is not:
150
+ ${dropped.out.trim().split(`
151
+ `).slice(-3).join(`
152
+ `)}`);
153
+ }
154
+ console.log(`${color.green("✓")} database "${plan.d1}" gone`);
155
+ }
156
+ if (!opts.keepData) {
157
+ writeFileSync(configPath, upsertJsonc(readFileSync(configPath, "utf8"), "d1_databases", []));
158
+ if (project.manifest.cloudflare?.d1) {
159
+ const { d1: _dropped, ...rest } = project.manifest.cloudflare;
160
+ project.manifest.cloudflare = rest;
161
+ await writeManifest(project.root, project.manifest);
162
+ }
163
+ console.log(`${color.green("✓")} the database id is out of wrangler.jsonc and the manifest, so the next deploy provisions afresh`);
164
+ }
165
+ console.log(`
166
+ ${color.bold("Torn down.")} ${color.dim(`The shop's secrets are untouched in ${SECRETS_FILE} — this removed infrastructure, not configuration.`)}
167
+ ` + ` Push again, or ${color.cyan("vc deploy --cloudflare --provision")}, and it rebuilds.
168
+ `);
169
+ return 0;
170
+ }
171
+
172
+ // src/deploy/index.ts
56
173
  async function deployCommand(args) {
57
174
  const own = new Set(["--cloudflare", "--provision", "--force"]);
58
175
  const cloudflare = args.includes("--cloudflare");
@@ -75,12 +192,12 @@ async function deployCommand(args) {
75
192
  const check = await preflight(project, "void");
76
193
  printPreflight(project, check, "void");
77
194
  if (!check.ready && check.remote !== null && !args.includes("--force")) {
78
- console.error(`${color.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
195
+ console.error(`${color2.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
79
196
  `);
80
197
  return 1;
81
198
  }
82
199
  if (!check.ready && check.remote === null) {
83
- console.log(color.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
200
+ console.log(color2.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
84
201
  `));
85
202
  }
86
203
  return runVoid(["deploy", ...voids], project.appDir);
@@ -98,7 +215,7 @@ async function preflightCommand(args) {
98
215
  return result.ready ? 0 : 1;
99
216
  const pending = await uncommittedMigrations(project.root);
100
217
  if (pending.length > 0) {
101
- console.error(`${color.red("✗")} ${pending.length} migration file${pending.length === 1 ? " is" : "s are"} not committed:
218
+ console.error(`${color2.red("✗")} ${pending.length} migration file${pending.length === 1 ? " is" : "s are"} not committed:
102
219
  ` + ` ${pending.join(`
103
220
  `)}
104
221
 
@@ -123,13 +240,13 @@ async function deployHelp() {
123
240
  line("vc's preflight — every required key set, the hostnames right — then", width),
124
241
  line("void deploy, untouched. Or, with --cloudflare, your own account via wrangler.", width),
125
242
  line("", width),
126
- line(color.bold("Usage"), width),
243
+ line(color2.bold("Usage"), width),
127
244
  ...row("vc deploy [void's flags]", "preflight, then void deploy — the Void platform, void's login", width, 2),
128
245
  ...row("vc deploy --cloudflare", "wrangler, your Cloudflare account, no Void login: preflight, build, scrub baked secrets, migrate D1, deploy", width, 2),
129
246
  ...row("vc deploy --cloudflare --provision", "first time: create the D1 database and the queue, record them", width, 2),
130
247
  ...row("vc deploy --cloudflare --force", "deploy a shop preflight says is not ready — deliberately", width, 2),
131
248
  line("", width),
132
- line(color.bold("Needs, for --cloudflare"), width),
249
+ line(color2.bold("Needs, for --cloudflare"), width),
133
250
  ...row("CLOUDFLARE_API_TOKEN", "in the environment; the account is pinned for you when there is one", width, 2),
134
251
  ...row(PRIVATE_KEY_VAR, "to decrypt the shop's own secrets", width, 2),
135
252
  line("", width),
@@ -225,9 +342,9 @@ async function guardCommand() {
225
342
  if (!project)
226
343
  return 0;
227
344
  const root = project.root;
228
- if (existsSync(join(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
345
+ if (existsSync2(join2(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
229
346
  console.error(`
230
- ${color.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gitignored.
347
+ ${color2.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gitignored.
231
348
 
232
349
  ` + ` It holds the private key that opens every secret in this repository.
233
350
  ` + ` Add ${LOCAL_KEY_FILE} to .gitignore before committing anything.
@@ -240,14 +357,14 @@ ${color.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT git
240
357
  const publicKey = committedPublicKey(root);
241
358
  if (!publicKey) {
242
359
  console.error(`
243
- ${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.
360
+ ${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.
244
361
 
245
362
  ` + ` ${bare.map((entry) => entry.name).join(`
246
363
  `)}
247
364
 
248
- ` + ` ${color.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
365
+ ` + ` ${color2.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
249
366
 
250
- ` + color.dim(` Committing a secret in the clear cannot be undone by a later commit;
367
+ ` + color2.dim(` Committing a secret in the clear cannot be undone by a later commit;
251
368
  the value stays in the history and must be treated as burned.
252
369
  `));
253
370
  return 1;
@@ -257,7 +374,7 @@ ${color.red("✗ refusing the commit")}: ${bare.length} value${bare.length === 1
257
374
  const sealed = await encryptInto(root, publicKey, entry.name, entry.value);
258
375
  if (!sealed.ok) {
259
376
  console.error(`
260
- ${color.red("✗ refusing the commit")}: ${sealed.error}
377
+ ${color2.red("✗ refusing the commit")}: ${sealed.error}
261
378
  `);
262
379
  return 1;
263
380
  }
@@ -265,7 +382,7 @@ ${color.red("✗ refusing the commit")}: ${sealed.error}
265
382
  const left = plaintextSecretNames(root);
266
383
  if (left.length > 0) {
267
384
  console.error(`
268
- ${color.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
385
+ ${color2.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
269
386
  `);
270
387
  return 1;
271
388
  }
@@ -273,28 +390,81 @@ ${color.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypt
273
390
  const added = await run("git", ["add", "--", SECRETS_FILE], root);
274
391
  if (added.code !== 0) {
275
392
  console.error(`
276
- ${color.red("✗ refusing the commit")}: ${SECRETS_FILE} was encrypted but could not be re-staged,
393
+ ${color2.red("✗ refusing the commit")}: ${SECRETS_FILE} was encrypted but could not be re-staged,
277
394
  ` + ` so the commit would still carry the plaintext you staged. \`git add ${SECRETS_FILE}\`.
278
395
  `);
279
396
  return 1;
280
397
  }
281
398
  }
282
399
  console.log(`
283
- ${color.green("✓")} encrypted ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE}${staged ? " and re-staged it" : ""}: ${bare.map((entry) => entry.name).join(", ")}
284
- ` + color.dim(` Encryption needs only the public key, so this needs no credential.
400
+ ${color2.green("✓")} encrypted ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE}${staged ? " and re-staged it" : ""}: ${bare.map((entry) => entry.name).join(", ")}
401
+ ` + color2.dim(` Encryption needs only the public key, so this needs no credential.
285
402
  `));
286
403
  return 0;
287
404
  }
405
+ async function teardownCliCommand(args) {
406
+ const project = await findProject();
407
+ if (!project) {
408
+ console.error("vc: no voidcommerce.json here.");
409
+ return 1;
410
+ }
411
+ if (!args.includes("--cloudflare")) {
412
+ console.error("vc: teardown only knows how to undo a Cloudflare deploy. `vc teardown --cloudflare`.");
413
+ return 1;
414
+ }
415
+ const typed = args[args.indexOf("--confirm") + 1];
416
+ const confirmed = args.includes("--confirm") && typed === project.manifest.shop.domain;
417
+ if (args.includes("--confirm") && !confirmed) {
418
+ console.error(`
419
+ vc: --confirm needs this shop's domain, exactly:
420
+
421
+ ` + ` vc teardown --cloudflare --confirm ${project.manifest.shop.domain}
422
+
423
+ ` + ` You typed ${typed ? `"${typed}"` : "nothing"}. Naming the shop is the check — it is
424
+ ` + ` what stops this running in the wrong directory.
425
+ `);
426
+ return 1;
427
+ }
428
+ return teardownCloudflare(project, {
429
+ confirm: confirmed,
430
+ keepData: args.includes("--keep-data"),
431
+ dryRun: args.includes("--dry-run")
432
+ });
433
+ }
434
+ async function teardownHelp() {
435
+ const width = 80;
436
+ console.log(box("vc teardown", [
437
+ line("Destroy this shop's Cloudflare infrastructure, so the whole", width),
438
+ line("publish-to-live flow can be proved again from nothing.", width),
439
+ line("", width),
440
+ ...row("vc teardown --cloudflare --dry-run", "list what would be destroyed, touch nothing", width, 2),
441
+ ...row("vc teardown --cloudflare --confirm <domain>", "do it", width, 2),
442
+ ...row("--keep-data", "leave the D1 database alone", width, 2),
443
+ line("", width),
444
+ line(color2.bold("Read this first"), width),
445
+ line("D1 deletion has NO UNDO. Every order, customer and address in the", width),
446
+ line("database goes with it, and there is no snapshot unless you took one.", width),
447
+ line("", width),
448
+ line("So --confirm takes the shop's DOMAIN, not `y`: a `y` is muscle memory,", width),
449
+ line("and typing the domain is what stops this running in the wrong shop.", width),
450
+ line("", width),
451
+ line("It is idempotent — deleting what is already gone is success — and it", width),
452
+ line("clears the recorded database id, because `wrangler deploy` FAILS on a", width),
453
+ line("dangling id rather than making a new database. Secrets are untouched:", width),
454
+ line("this removes infrastructure, not configuration.", width)
455
+ ], width));
456
+ return 0;
457
+ }
288
458
 
289
459
  // src/init.ts
290
460
  import * as p2 from "@clack/prompts";
291
461
  import { mkdir } from "node:fs/promises";
292
- import { basename, join as join2 } from "node:path";
293
- import color3 from "picocolors";
462
+ import { basename, join as join3 } from "node:path";
463
+ import color4 from "picocolors";
294
464
 
295
465
  // src/wizard.ts
296
466
  import * as p from "@clack/prompts";
297
- import color2 from "picocolors";
467
+ import color3 from "picocolors";
298
468
  var bail = () => {
299
469
  p.cancel("Nothing was written.");
300
470
  process.exit(0);
@@ -316,8 +486,8 @@ async function askGroup(group2, previous, alsoRequired = []) {
316
486
  const required = group2.choices.filter((choice) => requiredIds.has(choice.id));
317
487
  const optional = group2.choices.filter((choice) => !requiredIds.has(choice.id));
318
488
  const message = group2.title + (group2.intro ? `
319
- ${color2.dim(group2.intro)}` : "") + (required.length ? `
320
- ${color2.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
489
+ ${color3.dim(group2.intro)}` : "") + (required.length ? `
490
+ ${color3.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
321
491
  if (optional.length === 0) {
322
492
  p.log.info(message);
323
493
  return required.map((choice) => choice.id);
@@ -348,7 +518,7 @@ async function runWizard(existing, layout) {
348
518
  validate: (value) => value.trim() ? undefined : "A shop has a name."
349
519
  }),
350
520
  domain: () => p.text({
351
- 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")}`,
521
+ 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")}`,
352
522
  placeholder: "northwind.com",
353
523
  initialValue: existing?.shop.domain ?? "",
354
524
  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."
@@ -359,7 +529,7 @@ async function runWizard(existing, layout) {
359
529
  if (!domain || domain === guess)
360
530
  return Promise.resolve(undefined);
361
531
  return p.text({
362
- message: `DNS zone ${color2.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
532
+ message: `DNS zone ${color3.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
363
533
  placeholder: guess,
364
534
  initialValue: existing?.shop.zone ?? guess,
365
535
  validate: (value) => {
@@ -371,13 +541,13 @@ async function runWizard(existing, layout) {
371
541
  });
372
542
  },
373
543
  country: () => p.text({
374
- message: `Country ${color2.dim("— sets the declared VAT rate and the default region")}`,
544
+ message: `Country ${color3.dim("— sets the declared VAT rate and the default region")}`,
375
545
  placeholder: "US",
376
546
  initialValue: existing?.shop.country ?? "",
377
547
  validate: (value) => /^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."
378
548
  }),
379
549
  taxRegistered: () => p.select({
380
- 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")}`,
550
+ 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")}`,
381
551
  options: [
382
552
  { value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
383
553
  { value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" }
@@ -391,14 +561,14 @@ async function runWizard(existing, layout) {
391
561
  validate: (value) => /^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."
392
562
  }),
393
563
  locale: () => p.text({
394
- message: `Locale ${color2.dim("— the language of emails and launch content")}`,
564
+ message: `Locale ${color3.dim("— the language of emails and launch content")}`,
395
565
  placeholder: "en",
396
566
  initialValue: existing?.shop.locale ?? "",
397
567
  validate: (value) => /^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."
398
568
  }),
399
569
  ...onPages ? {
400
570
  pagesHost: () => p.text({
401
- message: `GitHub Pages host ${color2.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
571
+ message: `GitHub Pages host ${color3.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
402
572
  placeholder: "northwind.github.io",
403
573
  initialValue: existing?.shop.pagesHost ?? "",
404
574
  validate: (value) => !value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank"
@@ -409,7 +579,7 @@ async function runWizard(existing, layout) {
409
579
  for (const group2 of GROUPS) {
410
580
  if (group2.id === "ui" && layout === "strict") {
411
581
  p.log.info(`${group2.title}
412
- ${color2.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
582
+ ${color3.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
413
583
  chosen[group2.id] = ["admin"];
414
584
  continue;
415
585
  }
@@ -434,14 +604,14 @@ ${color2.dim("The generated storefront and panel. Strict has no custom code to h
434
604
  }
435
605
  function summarise(manifest) {
436
606
  const lines = [
437
- `${color2.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
607
+ `${color3.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
438
608
  ];
439
609
  for (const group2 of GROUPS) {
440
610
  const ids = manifest.chosen[group2.id] ?? [];
441
611
  if (ids.length === 0)
442
612
  continue;
443
613
  const labels = ids.map((id) => group2.choices.find((choice) => choice.id === id)?.label ?? id).join(", ");
444
- lines.push(`${color2.dim(group2.title.padEnd(34))} ${labels}`);
614
+ lines.push(`${color3.dim(group2.title.padEnd(34))} ${labels}`);
445
615
  }
446
616
  return lines.join(`
447
617
  `);
@@ -482,10 +652,10 @@ async function init(args) {
482
652
  if (flags.voids.length > 0)
483
653
  return runVoid(["init", ...flags.voids]);
484
654
  let root = process.cwd();
485
- p2.intro(color3.bgCyan(color3.black(" voidcommerce ")));
655
+ p2.intro(color4.bgCyan(color4.black(" voidcommerce ")));
486
656
  const existing = await readManifest(root);
487
657
  if (existing) {
488
- p2.log.info(`Found ${color3.cyan("voidcommerce.json")} for ${color3.bold(existing.shop.name)} — answers are pre-filled; the layout (${existing.layout}) is fixed.`);
658
+ p2.log.info(`Found ${color4.cyan("voidcommerce.json")} for ${color4.bold(existing.shop.name)} — answers are pre-filled; the layout (${existing.layout}) is fixed.`);
489
659
  if (flags.layout && flags.layout !== existing.layout) {
490
660
  p2.log.error(`This shop is laid out as "${existing.layout}". The layout cannot change after init — moving files is not a regeneration.`);
491
661
  return 1;
@@ -493,10 +663,10 @@ async function init(args) {
493
663
  }
494
664
  const layout = existing?.layout ?? flags.layout ?? await askLayout();
495
665
  if (layout === "strict") {
496
- p2.log.step(`Strict: the app will be generated under ${color3.cyan(".vc/app")} — nothing there is yours to edit.`);
666
+ p2.log.step(`Strict: the app will be generated under ${color4.cyan(".vc/app")} — nothing there is yours to edit.`);
497
667
  } else if (layout === "app") {
498
668
  if (!isVoidApp(root)) {
499
- p2.log.step(`No Void app here yet — ${color3.cyan("void init")} first, then the shop.`);
669
+ p2.log.step(`No Void app here yet — ${color4.cyan("void init")} first, then the shop.`);
500
670
  const before = new Set(voidAppsIn(root));
501
671
  const code = await runVoid(["init"]);
502
672
  if (code !== 0)
@@ -509,7 +679,7 @@ async function init(args) {
509
679
  }
510
680
  root = created[0];
511
681
  process.chdir(root);
512
- p2.log.step(`Continuing in ${color3.cyan(`${basename(root)}/`)}`);
682
+ p2.log.step(`Continuing in ${color4.cyan(`${basename(root)}/`)}`);
513
683
  }
514
684
  }
515
685
  } else {
@@ -518,11 +688,11 @@ async function init(args) {
518
688
  ["frontend", "the storefront — prerendered and served from the worker's own assets"]
519
689
  ];
520
690
  for (const [dir, starter] of parts) {
521
- const target = join2(root, dir);
691
+ const target = join3(root, dir);
522
692
  if (isVoidApp(target))
523
693
  continue;
524
694
  await mkdir(target, { recursive: true });
525
- p2.log.step(`${color3.cyan("void init")} in ${color3.cyan(`${dir}/`)} — choose ${starter}.`);
695
+ p2.log.step(`${color4.cyan("void init")} in ${color4.cyan(`${dir}/`)} — choose ${starter}.`);
526
696
  const code = await runVoid(["init"], target);
527
697
  if (code !== 0)
528
698
  return code;
@@ -540,7 +710,7 @@ async function init(args) {
540
710
  if (problems.length > 0) {
541
711
  p2.log.error("These answers contradict each other:");
542
712
  for (const problem of problems)
543
- p2.log.message(` ${color3.red("✗")} ${problem}`);
713
+ p2.log.message(` ${color4.red("✗")} ${problem}`);
544
714
  p2.cancel("Nothing was written.");
545
715
  return 1;
546
716
  }
@@ -556,16 +726,16 @@ async function init(args) {
556
726
  spinner2.stop("Generated");
557
727
  const { secrets, plaintext } = envSummary(manifest);
558
728
  p2.note([
559
- ...result.written.map((file) => `${color3.green("+")} ${file}`),
560
- ...result.kept.map((file) => `${color3.dim("=")} ${file} ${color3.dim("(kept — yours)")}`),
561
- ...result.retired.map((file) => `${color3.red("-")} ${file} ${color3.dim("(retired — no longer generated)")}`)
729
+ ...result.written.map((file) => `${color4.green("+")} ${file}`),
730
+ ...result.kept.map((file) => `${color4.dim("=")} ${file} ${color4.dim("(kept — yours)")}`),
731
+ ...result.retired.map((file) => `${color4.red("-")} ${file} ${color4.dim("(retired — no longer generated)")}`)
562
732
  ].join(`
563
733
  `), "Files");
564
734
  p2.note([
565
- `${color3.bold(String(secrets.length))} secrets ${color3.dim("→ wrangler secret put <NAME>")}`,
735
+ `${color4.bold(String(secrets.length))} secrets ${color4.dim("→ wrangler secret put <NAME>")}`,
566
736
  ...secrets.map((key) => ` ${key}`),
567
737
  "",
568
- `${color3.bold(String(plaintext.length))} plaintext ${color3.dim("→ .env.production, already written")}`,
738
+ `${color4.bold(String(plaintext.length))} plaintext ${color4.dim("→ .env.production, already written")}`,
569
739
  ...plaintext.map((key) => ` ${key}`)
570
740
  ].join(`
571
741
  `), "Before going live");
@@ -575,21 +745,21 @@ async function init(args) {
575
745
  return code;
576
746
  }
577
747
  const next = layout === "app" ? [
578
- ` ${color3.cyan("bun install")}`,
579
- ` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
580
- ` ${color3.cyan("vc dev")}`,
581
- ` ${color3.cyan("vc preflight")} ${color3.dim("what is still missing, and why it matters")}`
748
+ ` ${color4.cyan("bun install")}`,
749
+ ` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
750
+ ` ${color4.cyan("vc dev")}`,
751
+ ` ${color4.cyan("vc preflight")} ${color4.dim("what is still missing, and why it matters")}`
582
752
  ] : layout === "strict" ? [
583
- ` ${color3.cyan("bun install")}`,
584
- ` ${color3.cyan("vc generate")} ${color3.dim("the app under .vc/app, void's artifacts, the migrations")}`,
585
- ` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
586
- ` ${color3.cyan("vc dev")} ${color3.dim("runs in .vc/app")}`
753
+ ` ${color4.cyan("bun install")}`,
754
+ ` ${color4.cyan("vc generate")} ${color4.dim("the app under .vc/app, void's artifacts, the migrations")}`,
755
+ ` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
756
+ ` ${color4.cyan("vc dev")} ${color4.dim("runs in .vc/app")}`
587
757
  ] : [
588
- ` ${color3.cyan("bun install")} ${color3.dim("one install for both workspaces")}`,
589
- ` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
590
- ` ${color3.cyan("bun run dev:api")} ${color3.dim("the worker, with the panel")}`,
591
- ` ${color3.cyan("bun run dev:frontend")} ${color3.dim("the storefront, against the local API")}`,
592
- ` ${color3.cyan("bun run preflight")} ${color3.dim("what is still missing, and why it matters")}`
758
+ ` ${color4.cyan("bun install")} ${color4.dim("one install for both workspaces")}`,
759
+ ` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
760
+ ` ${color4.cyan("bun run dev:api")} ${color4.dim("the worker, with the panel")}`,
761
+ ` ${color4.cyan("bun run dev:frontend")} ${color4.dim("the storefront, against the local API")}`,
762
+ ` ${color4.cyan("bun run preflight")} ${color4.dim("what is still missing, and why it matters")}`
593
763
  ];
594
764
  p2.outro(["Next:", ...next].join(`
595
765
  `));
@@ -598,7 +768,7 @@ async function init(args) {
598
768
 
599
769
  // src/regenerate.ts
600
770
  import * as p3 from "@clack/prompts";
601
- import color4 from "picocolors";
771
+ import color5 from "picocolors";
602
772
  async function generateCommand() {
603
773
  const project = await findProject();
604
774
  if (!project) {
@@ -614,9 +784,9 @@ async function generateCommand() {
614
784
  }
615
785
  const result = await generate(project.root, project.manifest);
616
786
  p3.log.step([
617
- ...result.written.map((file) => `${color4.green("+")} ${file}`),
618
- ...result.kept.map((file) => `${color4.dim("=")} ${file} ${color4.dim("(kept)")}`),
619
- ...result.retired.map((file) => `${color4.red("-")} ${file} ${color4.dim("(retired — no longer generated)")}`)
787
+ ...result.written.map((file) => `${color5.green("+")} ${file}`),
788
+ ...result.kept.map((file) => `${color5.dim("=")} ${file} ${color5.dim("(kept)")}`),
789
+ ...result.retired.map((file) => `${color5.red("-")} ${file} ${color5.dim("(retired — no longer generated)")}`)
620
790
  ].join(`
621
791
  `));
622
792
  return project.manifest.layout === "strict" ? finishStrict(project.root) : 0;
@@ -635,8 +805,8 @@ async function generateHelp() {
635
805
  }
636
806
 
637
807
  // src/scripts.ts
638
- import { existsSync as existsSync2, readFileSync } from "node:fs";
639
- import { join as join3, relative } from "node:path";
808
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
809
+ import { join as join4, relative } from "node:path";
640
810
  function appScript(name) {
641
811
  return async (args) => {
642
812
  const project = await findProject();
@@ -645,8 +815,8 @@ function appScript(name) {
645
815
  const code = await ensureGenerated(project);
646
816
  if (code !== 0)
647
817
  return code;
648
- const pkgPath = join3(project.appDir, "package.json");
649
- const scripts = existsSync2(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {} : {};
818
+ const pkgPath = join4(project.appDir, "package.json");
819
+ const scripts = existsSync3(pkgPath) ? JSON.parse(readFileSync2(pkgPath, "utf8")).scripts ?? {} : {};
650
820
  if (!scripts[name]) {
651
821
  console.error(`vc: ${relative(process.cwd(), pkgPath) || "package.json"} has no "${name}" script — void init writes one.`);
652
822
  return 1;
@@ -681,6 +851,7 @@ var EXTENDED = {
681
851
  preflight: { run: preflightCommand, help: preflightHelp },
682
852
  secrets: { run: secretsCliCommand, help: secretsHelp },
683
853
  keys: { run: keysCliCommand, help: keysHelp },
854
+ teardown: { run: teardownCliCommand, help: teardownHelp },
684
855
  guard: { run: guardCommand, help: async () => guardCommand() },
685
856
  dev: { run: appScript("dev"), help: appScriptHelp("dev") },
686
857
  build: { run: appScript("build"), help: appScriptHelp("build") },
@@ -43,3 +43,12 @@ 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
+ /**
47
+ * `vc teardown` — destroy this shop's Cloudflare infrastructure.
48
+ *
49
+ * The confirmation is the shop's DOMAIN, not `y`. A `y` is muscle memory;
50
+ * typing `devprints.saastemly.com` is not, and this deletes a database that
51
+ * has no undo.
52
+ */
53
+ export declare function teardownCliCommand(args: string[]): Promise<number>;
54
+ export declare function teardownHelp(): Promise<number>;