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