@saastemly/voidcommerce 0.19.1 → 0.21.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 +240 -68
- package/dist/deploy/index.d.ts +9 -0
- package/dist/deploy/teardown.d.ts +62 -0
- package/dist/deploy/token-link.d.ts +15 -0
- package/dist/index-6e4eh08g.js +35 -0
- package/dist/{index-hsc97bf2.js → index-940vpkwa.js} +84 -2
- package/dist/{index-tk46w7yn.js → index-pghh8nnq.js} +18 -5
- package/dist/index.js +3 -2
- package/dist/{keys-knmqsqz1.js → keys-mewxnkwg.js} +1 -1
- package/dist/token-link-wxpp5xzp.js +11 -0
- package/package.json +1 -1
- package/src/cli.ts +3 -0
- package/src/deploy/index.ts +64 -0
- package/src/deploy/secrets.ts +97 -0
- package/src/deploy/teardown.ts +169 -0
- package/src/deploy/token-link.ts +76 -0
- package/src/generate/ci.ts +13 -2
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
|
-
|
|
26
|
+
voidAppsIn,
|
|
27
|
+
wrangler
|
|
28
|
+
} from "./index-pghh8nnq.js";
|
|
25
29
|
import {
|
|
26
30
|
LOCAL_KEY_FILE,
|
|
27
31
|
PRIVATE_KEY_VAR,
|
|
@@ -34,24 +38,138 @@ import {
|
|
|
34
38
|
plaintextSecretEntries,
|
|
35
39
|
plaintextSecretNames,
|
|
36
40
|
run,
|
|
41
|
+
secretValue,
|
|
37
42
|
secretsCommand
|
|
38
|
-
} from "./index-
|
|
43
|
+
} from "./index-940vpkwa.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 {
|
|
47
54
|
GROUPS
|
|
48
55
|
} from "./index-xyjhy6kp.js";
|
|
56
|
+
import"./index-6e4eh08g.js";
|
|
49
57
|
import"./index-0v6na3yp.js";
|
|
50
58
|
|
|
51
59
|
// src/deploy/index.ts
|
|
52
|
-
import
|
|
53
|
-
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";
|
|
54
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
|
|
55
173
|
async function deployCommand(args) {
|
|
56
174
|
const own = new Set(["--cloudflare", "--provision", "--force"]);
|
|
57
175
|
const cloudflare = args.includes("--cloudflare");
|
|
@@ -74,12 +192,12 @@ async function deployCommand(args) {
|
|
|
74
192
|
const check = await preflight(project, "void");
|
|
75
193
|
printPreflight(project, check, "void");
|
|
76
194
|
if (!check.ready && check.remote !== null && !args.includes("--force")) {
|
|
77
|
-
console.error(`${
|
|
195
|
+
console.error(`${color2.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
|
|
78
196
|
`);
|
|
79
197
|
return 1;
|
|
80
198
|
}
|
|
81
199
|
if (!check.ready && check.remote === null) {
|
|
82
|
-
console.log(
|
|
200
|
+
console.log(color2.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
|
|
83
201
|
`));
|
|
84
202
|
}
|
|
85
203
|
return runVoid(["deploy", ...voids], project.appDir);
|
|
@@ -97,7 +215,7 @@ async function preflightCommand(args) {
|
|
|
97
215
|
return result.ready ? 0 : 1;
|
|
98
216
|
const pending = await uncommittedMigrations(project.root);
|
|
99
217
|
if (pending.length > 0) {
|
|
100
|
-
console.error(`${
|
|
218
|
+
console.error(`${color2.red("✗")} ${pending.length} migration file${pending.length === 1 ? " is" : "s are"} not committed:
|
|
101
219
|
` + ` ${pending.join(`
|
|
102
220
|
`)}
|
|
103
221
|
|
|
@@ -122,13 +240,13 @@ async function deployHelp() {
|
|
|
122
240
|
line("vc's preflight — every required key set, the hostnames right — then", width),
|
|
123
241
|
line("void deploy, untouched. Or, with --cloudflare, your own account via wrangler.", width),
|
|
124
242
|
line("", width),
|
|
125
|
-
line(
|
|
243
|
+
line(color2.bold("Usage"), width),
|
|
126
244
|
...row("vc deploy [void's flags]", "preflight, then void deploy — the Void platform, void's login", width, 2),
|
|
127
245
|
...row("vc deploy --cloudflare", "wrangler, your Cloudflare account, no Void login: preflight, build, scrub baked secrets, migrate D1, deploy", width, 2),
|
|
128
246
|
...row("vc deploy --cloudflare --provision", "first time: create the D1 database and the queue, record them", width, 2),
|
|
129
247
|
...row("vc deploy --cloudflare --force", "deploy a shop preflight says is not ready — deliberately", width, 2),
|
|
130
248
|
line("", width),
|
|
131
|
-
line(
|
|
249
|
+
line(color2.bold("Needs, for --cloudflare"), width),
|
|
132
250
|
...row("CLOUDFLARE_API_TOKEN", "in the environment; the account is pinned for you when there is one", width, 2),
|
|
133
251
|
...row(PRIVATE_KEY_VAR, "to decrypt the shop's own secrets", width, 2),
|
|
134
252
|
line("", width),
|
|
@@ -224,9 +342,9 @@ async function guardCommand() {
|
|
|
224
342
|
if (!project)
|
|
225
343
|
return 0;
|
|
226
344
|
const root = project.root;
|
|
227
|
-
if (
|
|
345
|
+
if (existsSync2(join2(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
|
|
228
346
|
console.error(`
|
|
229
|
-
${
|
|
347
|
+
${color2.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gitignored.
|
|
230
348
|
|
|
231
349
|
` + ` It holds the private key that opens every secret in this repository.
|
|
232
350
|
` + ` Add ${LOCAL_KEY_FILE} to .gitignore before committing anything.
|
|
@@ -239,14 +357,14 @@ ${color.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT git
|
|
|
239
357
|
const publicKey = committedPublicKey(root);
|
|
240
358
|
if (!publicKey) {
|
|
241
359
|
console.error(`
|
|
242
|
-
${
|
|
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.
|
|
243
361
|
|
|
244
362
|
` + ` ${bare.map((entry) => entry.name).join(`
|
|
245
363
|
`)}
|
|
246
364
|
|
|
247
|
-
` + ` ${
|
|
365
|
+
` + ` ${color2.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
|
|
248
366
|
|
|
249
|
-
` +
|
|
367
|
+
` + color2.dim(` Committing a secret in the clear cannot be undone by a later commit;
|
|
250
368
|
the value stays in the history and must be treated as burned.
|
|
251
369
|
`));
|
|
252
370
|
return 1;
|
|
@@ -256,7 +374,7 @@ ${color.red("✗ refusing the commit")}: ${bare.length} value${bare.length === 1
|
|
|
256
374
|
const sealed = await encryptInto(root, publicKey, entry.name, entry.value);
|
|
257
375
|
if (!sealed.ok) {
|
|
258
376
|
console.error(`
|
|
259
|
-
${
|
|
377
|
+
${color2.red("✗ refusing the commit")}: ${sealed.error}
|
|
260
378
|
`);
|
|
261
379
|
return 1;
|
|
262
380
|
}
|
|
@@ -264,7 +382,7 @@ ${color.red("✗ refusing the commit")}: ${sealed.error}
|
|
|
264
382
|
const left = plaintextSecretNames(root);
|
|
265
383
|
if (left.length > 0) {
|
|
266
384
|
console.error(`
|
|
267
|
-
${
|
|
385
|
+
${color2.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
|
|
268
386
|
`);
|
|
269
387
|
return 1;
|
|
270
388
|
}
|
|
@@ -272,28 +390,81 @@ ${color.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypt
|
|
|
272
390
|
const added = await run("git", ["add", "--", SECRETS_FILE], root);
|
|
273
391
|
if (added.code !== 0) {
|
|
274
392
|
console.error(`
|
|
275
|
-
${
|
|
393
|
+
${color2.red("✗ refusing the commit")}: ${SECRETS_FILE} was encrypted but could not be re-staged,
|
|
276
394
|
` + ` so the commit would still carry the plaintext you staged. \`git add ${SECRETS_FILE}\`.
|
|
277
395
|
`);
|
|
278
396
|
return 1;
|
|
279
397
|
}
|
|
280
398
|
}
|
|
281
399
|
console.log(`
|
|
282
|
-
${
|
|
283
|
-
` +
|
|
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.
|
|
284
402
|
`));
|
|
285
403
|
return 0;
|
|
286
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
|
+
}
|
|
287
458
|
|
|
288
459
|
// src/init.ts
|
|
289
460
|
import * as p2 from "@clack/prompts";
|
|
290
461
|
import { mkdir } from "node:fs/promises";
|
|
291
|
-
import { basename, join as
|
|
292
|
-
import
|
|
462
|
+
import { basename, join as join3 } from "node:path";
|
|
463
|
+
import color4 from "picocolors";
|
|
293
464
|
|
|
294
465
|
// src/wizard.ts
|
|
295
466
|
import * as p from "@clack/prompts";
|
|
296
|
-
import
|
|
467
|
+
import color3 from "picocolors";
|
|
297
468
|
var bail = () => {
|
|
298
469
|
p.cancel("Nothing was written.");
|
|
299
470
|
process.exit(0);
|
|
@@ -315,8 +486,8 @@ async function askGroup(group2, previous, alsoRequired = []) {
|
|
|
315
486
|
const required = group2.choices.filter((choice) => requiredIds.has(choice.id));
|
|
316
487
|
const optional = group2.choices.filter((choice) => !requiredIds.has(choice.id));
|
|
317
488
|
const message = group2.title + (group2.intro ? `
|
|
318
|
-
${
|
|
319
|
-
${
|
|
489
|
+
${color3.dim(group2.intro)}` : "") + (required.length ? `
|
|
490
|
+
${color3.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
|
|
320
491
|
if (optional.length === 0) {
|
|
321
492
|
p.log.info(message);
|
|
322
493
|
return required.map((choice) => choice.id);
|
|
@@ -347,7 +518,7 @@ async function runWizard(existing, layout) {
|
|
|
347
518
|
validate: (value) => value.trim() ? undefined : "A shop has a name."
|
|
348
519
|
}),
|
|
349
520
|
domain: () => p.text({
|
|
350
|
-
message: `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")}`,
|
|
351
522
|
placeholder: "northwind.com",
|
|
352
523
|
initialValue: existing?.shop.domain ?? "",
|
|
353
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."
|
|
@@ -358,7 +529,7 @@ async function runWizard(existing, layout) {
|
|
|
358
529
|
if (!domain || domain === guess)
|
|
359
530
|
return Promise.resolve(undefined);
|
|
360
531
|
return p.text({
|
|
361
|
-
message: `DNS zone ${
|
|
532
|
+
message: `DNS zone ${color3.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
|
|
362
533
|
placeholder: guess,
|
|
363
534
|
initialValue: existing?.shop.zone ?? guess,
|
|
364
535
|
validate: (value) => {
|
|
@@ -370,13 +541,13 @@ async function runWizard(existing, layout) {
|
|
|
370
541
|
});
|
|
371
542
|
},
|
|
372
543
|
country: () => p.text({
|
|
373
|
-
message: `Country ${
|
|
544
|
+
message: `Country ${color3.dim("— sets the declared VAT rate and the default region")}`,
|
|
374
545
|
placeholder: "US",
|
|
375
546
|
initialValue: existing?.shop.country ?? "",
|
|
376
547
|
validate: (value) => /^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."
|
|
377
548
|
}),
|
|
378
549
|
taxRegistered: () => p.select({
|
|
379
|
-
message: `Registered to charge tax? ${
|
|
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")}`,
|
|
380
551
|
options: [
|
|
381
552
|
{ value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
|
|
382
553
|
{ value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" }
|
|
@@ -390,14 +561,14 @@ async function runWizard(existing, layout) {
|
|
|
390
561
|
validate: (value) => /^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."
|
|
391
562
|
}),
|
|
392
563
|
locale: () => p.text({
|
|
393
|
-
message: `Locale ${
|
|
564
|
+
message: `Locale ${color3.dim("— the language of emails and launch content")}`,
|
|
394
565
|
placeholder: "en",
|
|
395
566
|
initialValue: existing?.shop.locale ?? "",
|
|
396
567
|
validate: (value) => /^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."
|
|
397
568
|
}),
|
|
398
569
|
...onPages ? {
|
|
399
570
|
pagesHost: () => p.text({
|
|
400
|
-
message: `GitHub Pages host ${
|
|
571
|
+
message: `GitHub Pages host ${color3.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
|
|
401
572
|
placeholder: "northwind.github.io",
|
|
402
573
|
initialValue: existing?.shop.pagesHost ?? "",
|
|
403
574
|
validate: (value) => !value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank"
|
|
@@ -408,7 +579,7 @@ async function runWizard(existing, layout) {
|
|
|
408
579
|
for (const group2 of GROUPS) {
|
|
409
580
|
if (group2.id === "ui" && layout === "strict") {
|
|
410
581
|
p.log.info(`${group2.title}
|
|
411
|
-
${
|
|
582
|
+
${color3.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
|
|
412
583
|
chosen[group2.id] = ["admin"];
|
|
413
584
|
continue;
|
|
414
585
|
}
|
|
@@ -433,14 +604,14 @@ ${color2.dim("The generated storefront and panel. Strict has no custom code to h
|
|
|
433
604
|
}
|
|
434
605
|
function summarise(manifest) {
|
|
435
606
|
const lines = [
|
|
436
|
-
`${
|
|
607
|
+
`${color3.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
|
|
437
608
|
];
|
|
438
609
|
for (const group2 of GROUPS) {
|
|
439
610
|
const ids = manifest.chosen[group2.id] ?? [];
|
|
440
611
|
if (ids.length === 0)
|
|
441
612
|
continue;
|
|
442
613
|
const labels = ids.map((id) => group2.choices.find((choice) => choice.id === id)?.label ?? id).join(", ");
|
|
443
|
-
lines.push(`${
|
|
614
|
+
lines.push(`${color3.dim(group2.title.padEnd(34))} ${labels}`);
|
|
444
615
|
}
|
|
445
616
|
return lines.join(`
|
|
446
617
|
`);
|
|
@@ -481,10 +652,10 @@ async function init(args) {
|
|
|
481
652
|
if (flags.voids.length > 0)
|
|
482
653
|
return runVoid(["init", ...flags.voids]);
|
|
483
654
|
let root = process.cwd();
|
|
484
|
-
p2.intro(
|
|
655
|
+
p2.intro(color4.bgCyan(color4.black(" voidcommerce ")));
|
|
485
656
|
const existing = await readManifest(root);
|
|
486
657
|
if (existing) {
|
|
487
|
-
p2.log.info(`Found ${
|
|
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.`);
|
|
488
659
|
if (flags.layout && flags.layout !== existing.layout) {
|
|
489
660
|
p2.log.error(`This shop is laid out as "${existing.layout}". The layout cannot change after init — moving files is not a regeneration.`);
|
|
490
661
|
return 1;
|
|
@@ -492,10 +663,10 @@ async function init(args) {
|
|
|
492
663
|
}
|
|
493
664
|
const layout = existing?.layout ?? flags.layout ?? await askLayout();
|
|
494
665
|
if (layout === "strict") {
|
|
495
|
-
p2.log.step(`Strict: the app will be generated under ${
|
|
666
|
+
p2.log.step(`Strict: the app will be generated under ${color4.cyan(".vc/app")} — nothing there is yours to edit.`);
|
|
496
667
|
} else if (layout === "app") {
|
|
497
668
|
if (!isVoidApp(root)) {
|
|
498
|
-
p2.log.step(`No Void app here yet — ${
|
|
669
|
+
p2.log.step(`No Void app here yet — ${color4.cyan("void init")} first, then the shop.`);
|
|
499
670
|
const before = new Set(voidAppsIn(root));
|
|
500
671
|
const code = await runVoid(["init"]);
|
|
501
672
|
if (code !== 0)
|
|
@@ -508,7 +679,7 @@ async function init(args) {
|
|
|
508
679
|
}
|
|
509
680
|
root = created[0];
|
|
510
681
|
process.chdir(root);
|
|
511
|
-
p2.log.step(`Continuing in ${
|
|
682
|
+
p2.log.step(`Continuing in ${color4.cyan(`${basename(root)}/`)}`);
|
|
512
683
|
}
|
|
513
684
|
}
|
|
514
685
|
} else {
|
|
@@ -517,11 +688,11 @@ async function init(args) {
|
|
|
517
688
|
["frontend", "the storefront — prerendered and served from the worker's own assets"]
|
|
518
689
|
];
|
|
519
690
|
for (const [dir, starter] of parts) {
|
|
520
|
-
const target =
|
|
691
|
+
const target = join3(root, dir);
|
|
521
692
|
if (isVoidApp(target))
|
|
522
693
|
continue;
|
|
523
694
|
await mkdir(target, { recursive: true });
|
|
524
|
-
p2.log.step(`${
|
|
695
|
+
p2.log.step(`${color4.cyan("void init")} in ${color4.cyan(`${dir}/`)} — choose ${starter}.`);
|
|
525
696
|
const code = await runVoid(["init"], target);
|
|
526
697
|
if (code !== 0)
|
|
527
698
|
return code;
|
|
@@ -539,7 +710,7 @@ async function init(args) {
|
|
|
539
710
|
if (problems.length > 0) {
|
|
540
711
|
p2.log.error("These answers contradict each other:");
|
|
541
712
|
for (const problem of problems)
|
|
542
|
-
p2.log.message(` ${
|
|
713
|
+
p2.log.message(` ${color4.red("✗")} ${problem}`);
|
|
543
714
|
p2.cancel("Nothing was written.");
|
|
544
715
|
return 1;
|
|
545
716
|
}
|
|
@@ -555,16 +726,16 @@ async function init(args) {
|
|
|
555
726
|
spinner2.stop("Generated");
|
|
556
727
|
const { secrets, plaintext } = envSummary(manifest);
|
|
557
728
|
p2.note([
|
|
558
|
-
...result.written.map((file) => `${
|
|
559
|
-
...result.kept.map((file) => `${
|
|
560
|
-
...result.retired.map((file) => `${
|
|
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)")}`)
|
|
561
732
|
].join(`
|
|
562
733
|
`), "Files");
|
|
563
734
|
p2.note([
|
|
564
|
-
`${
|
|
735
|
+
`${color4.bold(String(secrets.length))} secrets ${color4.dim("→ wrangler secret put <NAME>")}`,
|
|
565
736
|
...secrets.map((key) => ` ${key}`),
|
|
566
737
|
"",
|
|
567
|
-
`${
|
|
738
|
+
`${color4.bold(String(plaintext.length))} plaintext ${color4.dim("→ .env.production, already written")}`,
|
|
568
739
|
...plaintext.map((key) => ` ${key}`)
|
|
569
740
|
].join(`
|
|
570
741
|
`), "Before going live");
|
|
@@ -574,21 +745,21 @@ async function init(args) {
|
|
|
574
745
|
return code;
|
|
575
746
|
}
|
|
576
747
|
const next = layout === "app" ? [
|
|
577
|
-
` ${
|
|
578
|
-
` ${
|
|
579
|
-
` ${
|
|
580
|
-
` ${
|
|
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")}`
|
|
581
752
|
] : layout === "strict" ? [
|
|
582
|
-
` ${
|
|
583
|
-
` ${
|
|
584
|
-
` ${
|
|
585
|
-
` ${
|
|
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")}`
|
|
586
757
|
] : [
|
|
587
|
-
` ${
|
|
588
|
-
` ${
|
|
589
|
-
` ${
|
|
590
|
-
` ${
|
|
591
|
-
` ${
|
|
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")}`
|
|
592
763
|
];
|
|
593
764
|
p2.outro(["Next:", ...next].join(`
|
|
594
765
|
`));
|
|
@@ -597,7 +768,7 @@ async function init(args) {
|
|
|
597
768
|
|
|
598
769
|
// src/regenerate.ts
|
|
599
770
|
import * as p3 from "@clack/prompts";
|
|
600
|
-
import
|
|
771
|
+
import color5 from "picocolors";
|
|
601
772
|
async function generateCommand() {
|
|
602
773
|
const project = await findProject();
|
|
603
774
|
if (!project) {
|
|
@@ -613,9 +784,9 @@ async function generateCommand() {
|
|
|
613
784
|
}
|
|
614
785
|
const result = await generate(project.root, project.manifest);
|
|
615
786
|
p3.log.step([
|
|
616
|
-
...result.written.map((file) => `${
|
|
617
|
-
...result.kept.map((file) => `${
|
|
618
|
-
...result.retired.map((file) => `${
|
|
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)")}`)
|
|
619
790
|
].join(`
|
|
620
791
|
`));
|
|
621
792
|
return project.manifest.layout === "strict" ? finishStrict(project.root) : 0;
|
|
@@ -634,8 +805,8 @@ async function generateHelp() {
|
|
|
634
805
|
}
|
|
635
806
|
|
|
636
807
|
// src/scripts.ts
|
|
637
|
-
import { existsSync as
|
|
638
|
-
import { join as
|
|
808
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
809
|
+
import { join as join4, relative } from "node:path";
|
|
639
810
|
function appScript(name) {
|
|
640
811
|
return async (args) => {
|
|
641
812
|
const project = await findProject();
|
|
@@ -644,8 +815,8 @@ function appScript(name) {
|
|
|
644
815
|
const code = await ensureGenerated(project);
|
|
645
816
|
if (code !== 0)
|
|
646
817
|
return code;
|
|
647
|
-
const pkgPath =
|
|
648
|
-
const scripts =
|
|
818
|
+
const pkgPath = join4(project.appDir, "package.json");
|
|
819
|
+
const scripts = existsSync3(pkgPath) ? JSON.parse(readFileSync2(pkgPath, "utf8")).scripts ?? {} : {};
|
|
649
820
|
if (!scripts[name]) {
|
|
650
821
|
console.error(`vc: ${relative(process.cwd(), pkgPath) || "package.json"} has no "${name}" script — void init writes one.`);
|
|
651
822
|
return 1;
|
|
@@ -680,6 +851,7 @@ var EXTENDED = {
|
|
|
680
851
|
preflight: { run: preflightCommand, help: preflightHelp },
|
|
681
852
|
secrets: { run: secretsCliCommand, help: secretsHelp },
|
|
682
853
|
keys: { run: keysCliCommand, help: keysHelp },
|
|
854
|
+
teardown: { run: teardownCliCommand, help: teardownHelp },
|
|
683
855
|
guard: { run: guardCommand, help: async () => guardCommand() },
|
|
684
856
|
dev: { run: appScript("dev"), help: appScriptHelp("dev") },
|
|
685
857
|
build: { run: appScript("build"), help: appScriptHelp("build") },
|
package/dist/deploy/index.d.ts
CHANGED
|
@@ -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>;
|