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