@saastemly/voidcommerce 0.18.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,312 +21,37 @@ import {
21
21
  runVoid,
22
22
  version,
23
23
  voidAppsIn
24
- } from "./index-3j6jtjmk.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
- findGh,
36
- getVariable,
37
- ghAuth,
38
32
  ignoresKeyFile,
39
- initSecrets,
40
- keyState,
41
33
  keysCommand,
42
- localPrivateKey,
43
34
  plaintextSecretEntries,
44
35
  plaintextSecretNames,
45
- provisionKey,
46
- publicKeyFor,
47
- repoSlug,
48
36
  run,
49
- run1 as run2,
50
- secretNames,
51
- secretsCommand,
52
- setSecret,
53
- setVariable,
54
- variableNames,
55
- verifyCloudflareToken
56
- } from "./index-khzk6a9z.js";
37
+ secretsCommand
38
+ } from "./index-68m1eg3f.js";
57
39
  import {
58
40
  LAYOUTS,
59
41
  oneOrigin,
60
42
  readManifest,
61
43
  validate,
62
- writeManifest,
63
44
  zoneOf
64
45
  } from "./index-30y19qz5.js";
65
46
  import {
66
47
  GROUPS
67
48
  } from "./index-xyjhy6kp.js";
68
- import {
69
- __require
70
- } from "./index-0v6na3yp.js";
49
+ import"./index-0v6na3yp.js";
71
50
 
72
51
  // src/deploy/index.ts
73
- import color3 from "picocolors";
74
- import { existsSync as existsSync2 } from "node:fs";
75
- import { join as join3 } from "node:path";
76
-
77
- // src/deploy/link.ts
78
- import { rmSync } from "node:fs";
79
- import { join } from "node:path";
80
52
  import color from "picocolors";
81
- var TOKEN_SECRET = "CLOUDFLARE_API_TOKEN";
82
- var ACCOUNT_VAR = "CLOUDFLARE_ACCOUNT_ID";
83
- var SCOPES = [
84
- ["Account · Workers Scripts: Edit", "deploy the worker"],
85
- ["Account · D1: Edit", "create the database and apply migrations"],
86
- ["Account · Queues: Edit", "create the order queue"],
87
- ["Account · Workers KV Storage: Edit", "sessions and caches"],
88
- ["Account · Workers R2 Storage: Edit", "product images"],
89
- ["Account · Account Settings: Read", "confirm which account this is"],
90
- ["Zone · Workers Routes: Edit", "answer on your domain"],
91
- ["Zone · Zone: Read", "confirm the domain's zone is on this account"],
92
- ["Zone · DNS: Read", "notice a hostname that already answers, before taking it over"],
93
- ["User · User Details: Read", "wrangler asks at startup"]
94
- ];
95
- async function linkCommand(project, args) {
96
- const p = await import("@clack/prompts");
97
- const root = project.root;
98
- const relink = args.includes("--force");
99
- p.intro(color.bgCyan(color.black(" vc link ")));
100
- const auth = await ghAuth(root);
101
- if (!auth.ok) {
102
- p.cancel(auth.reason ?? "gh is unavailable");
103
- return 1;
104
- }
105
- const slug = await repoSlug(root);
106
- if (!slug) {
107
- p.cancel(`this checkout has no GitHub repository, and everything below is stored on one.
108
-
109
- ${color.cyan("gh repo create --source=. --private --push")}
110
-
111
- then run \`vc link\` again.`);
112
- return 1;
113
- }
114
- p.log.success(`${color.green("✓")} ${slug}, as ${auth.user ?? "you"}`);
115
- const keys = await keyState(project);
116
- if (keys.inGitHub && !relink) {
117
- p.log.info(`${PRIVATE_KEY_VAR} is already set — leaving it alone (--force replaces it, which orphans ${SECRETS_FILE})`);
118
- } else if (keys.inGitHub && relink) {
119
- p.log.warn(`--force: replacing ${PRIVATE_KEY_VAR} makes every value in ${SECRETS_FILE} unreadable. Use \`vc keys --rotate\` to re-encrypt instead.`);
120
- return 1;
121
- } else {
122
- const parked = localPrivateKey(root);
123
- if (parked) {
124
- const sent = await setVariable(root, PRIVATE_KEY_VAR, parked);
125
- if (!sent.ok) {
126
- p.cancel(`GitHub refused the variable: ${sent.error ?? "unknown error"}`);
127
- return 1;
128
- }
129
- const publicKey = await publicKeyFor(parked);
130
- if (publicKey)
131
- committedPublicKeyInto(root, publicKey);
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
- }
141
- } else {
142
- const made = await provisionKey(project);
143
- if (!made.ok) {
144
- p.cancel(made.reason);
145
- return 1;
146
- }
147
- committedPublicKeyInto(root, made.publicKey);
148
- p.log.success(`${color.green("✓")} ${PRIVATE_KEY_VAR} generated and stored on GitHub; public half in ${SECRETS_FILE}`);
149
- }
150
- }
151
- const existing = await secretNames(root);
152
- if (existing.has(TOKEN_SECRET) && !relink) {
153
- p.log.info(`${TOKEN_SECRET} is already set — pass --force to replace it`);
154
- } else {
155
- p.log.step(`A Cloudflare API token, from ${color.cyan("https://dash.cloudflare.com/profile/api-tokens")} → Create Token → Custom token`);
156
- console.log(SCOPES.map(([scope, why]) => ` ${scope.padEnd(38)} ${color.dim(why)}`).join(`
157
- `));
158
- console.log(color.dim(`
159
- The token Cloudflare generates for its own Workers Builds will NOT do:
160
- it has no D1 and no Queues permission, so it cannot create the database.
161
- `));
162
- const token = await p.password({
163
- message: "Paste it (nothing is written to disk)",
164
- validate: (input) => input && input.length >= 20 ? undefined : "that is too short to be a Cloudflare token"
165
- });
166
- if (p.isCancel(token)) {
167
- p.cancel("nothing was stored.");
168
- return 1;
169
- }
170
- const spinner = p.spinner();
171
- spinner.start("asking Cloudflare whether that token is real");
172
- const verified = await verifyCloudflareToken(String(token));
173
- if (!verified.ok) {
174
- spinner.stop(`${color.red("✗")} Cloudflare rejected it: ${verified.detail}`);
175
- p.cancel("nothing was stored.");
176
- return 1;
177
- }
178
- const accounts = await cloudflareAccounts(String(token));
179
- spinner.stop(`${color.green("✓")} the token is active and sees ${accounts.length} account${accounts.length === 1 ? "" : "s"}`);
180
- let accountId = project.manifest.cloudflare?.accountId ?? "";
181
- if (accounts.length === 1) {
182
- accountId = accounts[0].id;
183
- p.log.info(`account "${accounts[0].name}"`);
184
- } else if (accounts.length > 1) {
185
- const picked = await p.select({
186
- message: "Which account is this shop in?",
187
- options: accounts.map((account) => ({ value: account.id, label: account.name, hint: account.id }))
188
- });
189
- if (p.isCancel(picked)) {
190
- p.cancel("nothing was stored.");
191
- return 1;
192
- }
193
- accountId = String(picked);
194
- }
195
- const stored = await setSecret(root, TOKEN_SECRET, String(token));
196
- if (!stored.ok) {
197
- p.cancel(`GitHub refused the secret: ${stored.error ?? "unknown error"}`);
198
- return 1;
199
- }
200
- p.log.success(`${color.green("✓")} ${TOKEN_SECRET} stored on ${slug}`);
201
- if (accountId) {
202
- const varred = await setVariable(root, ACCOUNT_VAR, accountId);
203
- if (varred.ok)
204
- p.log.success(`${color.green("✓")} ${ACCOUNT_VAR} set as a repository variable`);
205
- project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
206
- await writeManifest(root, project.manifest);
207
- }
208
- }
209
- if (declaredSecretNames(root).size === 0) {
210
- p.log.step(`writing ${SECRETS_FILE}`);
211
- await initSecrets(project);
212
- }
213
- const secrets = await secretNames(root);
214
- const vars = await variableNames(root);
215
- p.outro([
216
- `${color.bold("Ready.")} From here, ${color.cyan("git push")} is the deploy.`,
217
- "",
218
- ` ${secrets.has(PRIVATE_KEY_VAR) ? color.green("✓") : color.red("✗")} ${PRIVATE_KEY_VAR} ${color.dim("reads the shop's secrets")}`,
219
- ` ${secrets.has(TOKEN_SECRET) ? color.green("✓") : color.red("✗")} ${TOKEN_SECRET} ${color.dim("deploys to Cloudflare")}`,
220
- ` ${vars.has(ACCOUNT_VAR) ? color.green("✓") : color.dim("·")} ${ACCOUNT_VAR} ${color.dim("which account")}`,
221
- "",
222
- ` Set the shop's own secrets with ${color.cyan("vc secrets set KEY")} — that needs no`,
223
- ` credential at all, because encryption uses the public key in the repository.`
224
- ].join(`
225
- `));
226
- return 0;
227
- }
228
-
229
- // src/deploy/publish.ts
230
53
  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
-
329
- // src/deploy/index.ts
54
+ import { join } from "node:path";
330
55
  async function deployCommand(args) {
331
56
  const own = new Set(["--cloudflare", "--provision", "--force"]);
332
57
  const cloudflare = args.includes("--cloudflare");
@@ -349,12 +74,12 @@ async function deployCommand(args) {
349
74
  const check = await preflight(project, "void");
350
75
  printPreflight(project, check, "void");
351
76
  if (!check.ready && check.remote !== null && !args.includes("--force")) {
352
- console.error(`${color3.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.
353
78
  `);
354
79
  return 1;
355
80
  }
356
81
  if (!check.ready && check.remote === null) {
357
- console.log(color3.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.
358
83
  `));
359
84
  }
360
85
  return runVoid(["deploy", ...voids], project.appDir);
@@ -372,7 +97,7 @@ async function preflightCommand(args) {
372
97
  return result.ready ? 0 : 1;
373
98
  const pending = await uncommittedMigrations(project.root);
374
99
  if (pending.length > 0) {
375
- console.error(`${color3.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:
376
101
  ` + ` ${pending.join(`
377
102
  `)}
378
103
 
@@ -384,7 +109,7 @@ async function preflightCommand(args) {
384
109
  return result.ready ? 0 : 1;
385
110
  }
386
111
  async function uncommittedMigrations(root) {
387
- const status = await run2("git", ["status", "--porcelain", "--", "migrations"], root);
112
+ const status = await run("git", ["status", "--porcelain", "--", "migrations"], root);
388
113
  if (status.code !== 0)
389
114
  return [];
390
115
  return status.out.split(`
@@ -397,13 +122,13 @@ async function deployHelp() {
397
122
  line("vc's preflight — every required key set, the hostnames right — then", width),
398
123
  line("void deploy, untouched. Or, with --cloudflare, your own account via wrangler.", width),
399
124
  line("", width),
400
- line(color3.bold("Usage"), width),
125
+ line(color.bold("Usage"), width),
401
126
  ...row("vc deploy [void's flags]", "preflight, then void deploy — the Void platform, void's login", width, 2),
402
127
  ...row("vc deploy --cloudflare", "wrangler, your Cloudflare account, no Void login: preflight, build, scrub baked secrets, migrate D1, deploy", width, 2),
403
128
  ...row("vc deploy --cloudflare --provision", "first time: create the D1 database and the queue, record them", width, 2),
404
129
  ...row("vc deploy --cloudflare --force", "deploy a shop preflight says is not ready — deliberately", width, 2),
405
130
  line("", width),
406
- line(color3.bold("Needs, for --cloudflare"), width),
131
+ line(color.bold("Needs, for --cloudflare"), width),
407
132
  ...row("CLOUDFLARE_API_TOKEN", "in the environment; the account is pinned for you when there is one", width, 2),
408
133
  ...row(PRIVATE_KEY_VAR, "to decrypt the shop's own secrets", width, 2),
409
134
  line("", width),
@@ -499,9 +224,9 @@ async function guardCommand() {
499
224
  if (!project)
500
225
  return 0;
501
226
  const root = project.root;
502
- if (existsSync2(join3(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
227
+ if (existsSync(join(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
503
228
  console.error(`
504
- ${color3.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.
505
230
 
506
231
  ` + ` It holds the private key that opens every secret in this repository.
507
232
  ` + ` Add ${LOCAL_KEY_FILE} to .gitignore before committing anything.
@@ -514,24 +239,24 @@ ${color3.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gi
514
239
  const publicKey = committedPublicKey(root);
515
240
  if (!publicKey) {
516
241
  console.error(`
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.
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.
518
243
 
519
244
  ` + ` ${bare.map((entry) => entry.name).join(`
520
245
  `)}
521
246
 
522
- ` + ` ${color3.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
247
+ ` + ` ${color.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
523
248
 
524
- ` + color3.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;
525
250
  the value stays in the history and must be treated as burned.
526
251
  `));
527
252
  return 1;
528
253
  }
529
- const staged = (await run2("git", ["diff", "--cached", "--name-only", "--", SECRETS_FILE], root)).out.trim().length > 0;
254
+ const staged = (await run("git", ["diff", "--cached", "--name-only", "--", SECRETS_FILE], root)).out.trim().length > 0;
530
255
  for (const entry of bare) {
531
256
  const sealed = await encryptInto(root, publicKey, entry.name, entry.value);
532
257
  if (!sealed.ok) {
533
258
  console.error(`
534
- ${color3.red("✗ refusing the commit")}: ${sealed.error}
259
+ ${color.red("✗ refusing the commit")}: ${sealed.error}
535
260
  `);
536
261
  return 1;
537
262
  }
@@ -539,99 +264,36 @@ ${color3.red("✗ refusing the commit")}: ${sealed.error}
539
264
  const left = plaintextSecretNames(root);
540
265
  if (left.length > 0) {
541
266
  console.error(`
542
- ${color3.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
267
+ ${color.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
543
268
  `);
544
269
  return 1;
545
270
  }
546
271
  if (staged) {
547
- const added = await run2("git", ["add", "--", SECRETS_FILE], root);
272
+ const added = await run("git", ["add", "--", SECRETS_FILE], root);
548
273
  if (added.code !== 0) {
549
274
  console.error(`
550
- ${color3.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,
551
276
  ` + ` so the commit would still carry the plaintext you staged. \`git add ${SECRETS_FILE}\`.
552
277
  `);
553
278
  return 1;
554
279
  }
555
280
  }
556
281
  console.log(`
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.
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.
559
284
  `));
560
285
  return 0;
561
286
  }
562
- async function linkCliCommand(args) {
563
- const project = await findProject();
564
- if (!project) {
565
- console.error("vc: no voidcommerce.json here.");
566
- return 1;
567
- }
568
- return linkCommand(project, args);
569
- }
570
- async function linkHelp() {
571
- const width = 80;
572
- console.log(box("vc link", [
573
- line("Put this shop's credentials on its GitHub repository, once, so that", width),
574
- line("every deploy after this is a `git push`.", width),
575
- line("", width),
576
- ...row("vc link", "store the encryption key and the Cloudflare token on the repository", width, 2),
577
- ...row("vc link --force", "replace what is already there", width, 2),
578
- line("", width),
579
- line(color3.bold("What it stores, and where"), width),
580
- ...row(PRIVATE_KEY_VAR, "a repository SECRET. A key already waiting locally is MOVED here and the local copy deleted", width, 2),
581
- ...row("CLOUDFLARE_API_TOKEN", "a repository SECRET, checked against the Cloudflare API before it is stored", width, 2),
582
- ...row("CLOUDFLARE_ACCOUNT_ID", "a repository VARIABLE — an identifier, not a credential", width, 2),
583
- line("", width),
584
- line(color3.bold("Why one token still has to be typed"), width),
585
- line("GitHub cannot mint a Cloudflare credential. There is no OIDC federation", width),
586
- line("between them, and the Cloudflare GitHub App runs the other way: it grants", width),
587
- line("Cloudflare access to your repository, not your repository access to", width),
588
- line("Cloudflare. So it is typed once, here, and never stored on this machine.", width)
589
- ], width));
590
- return 0;
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
- }
625
287
 
626
288
  // src/init.ts
627
289
  import * as p2 from "@clack/prompts";
628
290
  import { mkdir } from "node:fs/promises";
629
- import { basename, join as join4 } from "node:path";
630
- import color5 from "picocolors";
291
+ import { basename, join as join2 } from "node:path";
292
+ import color3 from "picocolors";
631
293
 
632
294
  // src/wizard.ts
633
295
  import * as p from "@clack/prompts";
634
- import color4 from "picocolors";
296
+ import color2 from "picocolors";
635
297
  var bail = () => {
636
298
  p.cancel("Nothing was written.");
637
299
  process.exit(0);
@@ -653,8 +315,8 @@ async function askGroup(group2, previous, alsoRequired = []) {
653
315
  const required = group2.choices.filter((choice) => requiredIds.has(choice.id));
654
316
  const optional = group2.choices.filter((choice) => !requiredIds.has(choice.id));
655
317
  const message = group2.title + (group2.intro ? `
656
- ${color4.dim(group2.intro)}` : "") + (required.length ? `
657
- ${color4.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(", ")}`)}` : "");
658
320
  if (optional.length === 0) {
659
321
  p.log.info(message);
660
322
  return required.map((choice) => choice.id);
@@ -685,7 +347,7 @@ async function runWizard(existing, layout) {
685
347
  validate: (value) => value.trim() ? undefined : "A shop has a name."
686
348
  }),
687
349
  domain: () => p.text({
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")}`,
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")}`,
689
351
  placeholder: "northwind.com",
690
352
  initialValue: existing?.shop.domain ?? "",
691
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."
@@ -696,7 +358,7 @@ async function runWizard(existing, layout) {
696
358
  if (!domain || domain === guess)
697
359
  return Promise.resolve(undefined);
698
360
  return p.text({
699
- message: `DNS zone ${color4.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`)}`,
700
362
  placeholder: guess,
701
363
  initialValue: existing?.shop.zone ?? guess,
702
364
  validate: (value) => {
@@ -708,13 +370,13 @@ async function runWizard(existing, layout) {
708
370
  });
709
371
  },
710
372
  country: () => p.text({
711
- message: `Country ${color4.dim("— sets the declared VAT rate and the default region")}`,
373
+ message: `Country ${color2.dim("— sets the declared VAT rate and the default region")}`,
712
374
  placeholder: "US",
713
375
  initialValue: existing?.shop.country ?? "",
714
376
  validate: (value) => /^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."
715
377
  }),
716
378
  taxRegistered: () => p.select({
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")}`,
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")}`,
718
380
  options: [
719
381
  { value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
720
382
  { value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" }
@@ -728,14 +390,14 @@ async function runWizard(existing, layout) {
728
390
  validate: (value) => /^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."
729
391
  }),
730
392
  locale: () => p.text({
731
- message: `Locale ${color4.dim("— the language of emails and launch content")}`,
393
+ message: `Locale ${color2.dim("— the language of emails and launch content")}`,
732
394
  placeholder: "en",
733
395
  initialValue: existing?.shop.locale ?? "",
734
396
  validate: (value) => /^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."
735
397
  }),
736
398
  ...onPages ? {
737
399
  pagesHost: () => p.text({
738
- message: `GitHub Pages host ${color4.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")}`,
739
401
  placeholder: "northwind.github.io",
740
402
  initialValue: existing?.shop.pagesHost ?? "",
741
403
  validate: (value) => !value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank"
@@ -746,7 +408,7 @@ async function runWizard(existing, layout) {
746
408
  for (const group2 of GROUPS) {
747
409
  if (group2.id === "ui" && layout === "strict") {
748
410
  p.log.info(`${group2.title}
749
- ${color4.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.")}`);
750
412
  chosen[group2.id] = ["admin"];
751
413
  continue;
752
414
  }
@@ -771,14 +433,14 @@ ${color4.dim("The generated storefront and panel. Strict has no custom code to h
771
433
  }
772
434
  function summarise(manifest) {
773
435
  const lines = [
774
- `${color4.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}`
775
437
  ];
776
438
  for (const group2 of GROUPS) {
777
439
  const ids = manifest.chosen[group2.id] ?? [];
778
440
  if (ids.length === 0)
779
441
  continue;
780
442
  const labels = ids.map((id) => group2.choices.find((choice) => choice.id === id)?.label ?? id).join(", ");
781
- lines.push(`${color4.dim(group2.title.padEnd(34))} ${labels}`);
443
+ lines.push(`${color2.dim(group2.title.padEnd(34))} ${labels}`);
782
444
  }
783
445
  return lines.join(`
784
446
  `);
@@ -819,10 +481,10 @@ async function init(args) {
819
481
  if (flags.voids.length > 0)
820
482
  return runVoid(["init", ...flags.voids]);
821
483
  let root = process.cwd();
822
- p2.intro(color5.bgCyan(color5.black(" voidcommerce ")));
484
+ p2.intro(color3.bgCyan(color3.black(" voidcommerce ")));
823
485
  const existing = await readManifest(root);
824
486
  if (existing) {
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.`);
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.`);
826
488
  if (flags.layout && flags.layout !== existing.layout) {
827
489
  p2.log.error(`This shop is laid out as "${existing.layout}". The layout cannot change after init — moving files is not a regeneration.`);
828
490
  return 1;
@@ -830,10 +492,10 @@ async function init(args) {
830
492
  }
831
493
  const layout = existing?.layout ?? flags.layout ?? await askLayout();
832
494
  if (layout === "strict") {
833
- p2.log.step(`Strict: the app will be generated under ${color5.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.`);
834
496
  } else if (layout === "app") {
835
497
  if (!isVoidApp(root)) {
836
- p2.log.step(`No Void app here yet — ${color5.cyan("void init")} first, then the shop.`);
498
+ p2.log.step(`No Void app here yet — ${color3.cyan("void init")} first, then the shop.`);
837
499
  const before = new Set(voidAppsIn(root));
838
500
  const code = await runVoid(["init"]);
839
501
  if (code !== 0)
@@ -846,7 +508,7 @@ async function init(args) {
846
508
  }
847
509
  root = created[0];
848
510
  process.chdir(root);
849
- p2.log.step(`Continuing in ${color5.cyan(`${basename(root)}/`)}`);
511
+ p2.log.step(`Continuing in ${color3.cyan(`${basename(root)}/`)}`);
850
512
  }
851
513
  }
852
514
  } else {
@@ -855,11 +517,11 @@ async function init(args) {
855
517
  ["frontend", "the storefront — prerendered and served from the worker's own assets"]
856
518
  ];
857
519
  for (const [dir, starter] of parts) {
858
- const target = join4(root, dir);
520
+ const target = join2(root, dir);
859
521
  if (isVoidApp(target))
860
522
  continue;
861
523
  await mkdir(target, { recursive: true });
862
- p2.log.step(`${color5.cyan("void init")} in ${color5.cyan(`${dir}/`)} — choose ${starter}.`);
524
+ p2.log.step(`${color3.cyan("void init")} in ${color3.cyan(`${dir}/`)} — choose ${starter}.`);
863
525
  const code = await runVoid(["init"], target);
864
526
  if (code !== 0)
865
527
  return code;
@@ -877,7 +539,7 @@ async function init(args) {
877
539
  if (problems.length > 0) {
878
540
  p2.log.error("These answers contradict each other:");
879
541
  for (const problem of problems)
880
- p2.log.message(` ${color5.red("✗")} ${problem}`);
542
+ p2.log.message(` ${color3.red("✗")} ${problem}`);
881
543
  p2.cancel("Nothing was written.");
882
544
  return 1;
883
545
  }
@@ -893,16 +555,16 @@ async function init(args) {
893
555
  spinner2.stop("Generated");
894
556
  const { secrets, plaintext } = envSummary(manifest);
895
557
  p2.note([
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)")}`)
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)")}`)
899
561
  ].join(`
900
562
  `), "Files");
901
563
  p2.note([
902
- `${color5.bold(String(secrets.length))} secrets ${color5.dim("→ wrangler secret put <NAME>")}`,
564
+ `${color3.bold(String(secrets.length))} secrets ${color3.dim("→ wrangler secret put <NAME>")}`,
903
565
  ...secrets.map((key) => ` ${key}`),
904
566
  "",
905
- `${color5.bold(String(plaintext.length))} plaintext ${color5.dim("→ .env.production, already written")}`,
567
+ `${color3.bold(String(plaintext.length))} plaintext ${color3.dim("→ .env.production, already written")}`,
906
568
  ...plaintext.map((key) => ` ${key}`)
907
569
  ].join(`
908
570
  `), "Before going live");
@@ -912,21 +574,21 @@ async function init(args) {
912
574
  return code;
913
575
  }
914
576
  const next = layout === "app" ? [
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")}`
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")}`
919
581
  ] : layout === "strict" ? [
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")}`
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")}`
924
586
  ] : [
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")}`
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")}`
930
592
  ];
931
593
  p2.outro(["Next:", ...next].join(`
932
594
  `));
@@ -935,7 +597,7 @@ async function init(args) {
935
597
 
936
598
  // src/regenerate.ts
937
599
  import * as p3 from "@clack/prompts";
938
- import color6 from "picocolors";
600
+ import color4 from "picocolors";
939
601
  async function generateCommand() {
940
602
  const project = await findProject();
941
603
  if (!project) {
@@ -951,9 +613,9 @@ async function generateCommand() {
951
613
  }
952
614
  const result = await generate(project.root, project.manifest);
953
615
  p3.log.step([
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)")}`)
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)")}`)
957
619
  ].join(`
958
620
  `));
959
621
  return project.manifest.layout === "strict" ? finishStrict(project.root) : 0;
@@ -972,8 +634,8 @@ async function generateHelp() {
972
634
  }
973
635
 
974
636
  // src/scripts.ts
975
- import { existsSync as existsSync3, readFileSync } from "node:fs";
976
- import { join as join5, relative } from "node:path";
637
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
638
+ import { join as join3, relative } from "node:path";
977
639
  function appScript(name) {
978
640
  return async (args) => {
979
641
  const project = await findProject();
@@ -982,8 +644,8 @@ function appScript(name) {
982
644
  const code = await ensureGenerated(project);
983
645
  if (code !== 0)
984
646
  return code;
985
- const pkgPath = join5(project.appDir, "package.json");
986
- const scripts = existsSync3(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {} : {};
647
+ const pkgPath = join3(project.appDir, "package.json");
648
+ const scripts = existsSync2(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {} : {};
987
649
  if (!scripts[name]) {
988
650
  console.error(`vc: ${relative(process.cwd(), pkgPath) || "package.json"} has no "${name}" script — void init writes one.`);
989
651
  return 1;
@@ -1018,8 +680,6 @@ var EXTENDED = {
1018
680
  preflight: { run: preflightCommand, help: preflightHelp },
1019
681
  secrets: { run: secretsCliCommand, help: secretsHelp },
1020
682
  keys: { run: keysCliCommand, help: keysHelp },
1021
- link: { run: linkCliCommand, help: linkHelp },
1022
- publish: { run: publishCliCommand, help: publishHelp },
1023
683
  guard: { run: guardCommand, help: async () => guardCommand() },
1024
684
  dev: { run: appScript("dev"), help: appScriptHelp("dev") },
1025
685
  build: { run: appScript("build"), help: appScriptHelp("build") },