@saastemly/voidcommerce 0.3.0 → 0.5.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.js +2 -1
- package/dist/cli.js +300 -58
- package/dist/deploy/github.d.ts +85 -0
- package/dist/deploy/index.d.ts +12 -0
- package/dist/deploy/keys.d.ts +77 -0
- package/dist/deploy/link.d.ts +2 -0
- package/dist/deploy/secrets.d.ts +59 -10
- package/dist/generate/ci.d.ts +33 -18
- package/dist/generate/index.d.ts +2 -0
- package/dist/index-0v6na3yp.js +35 -0
- package/dist/{index-84wq4qp8.js → index-5m3t0zfc.js} +244 -503
- package/dist/{index-z4qazajn.js → index-844b3qn9.js} +1 -4
- package/dist/index-b9b4dawy.js +797 -0
- package/dist/index-mpr7gm6k.js +4597 -0
- package/dist/{index-zh3tmj08.js → index-pz6m2hkm.js} +1 -1
- package/dist/index.js +12 -9
- package/dist/keys-b91c72zp.js +19 -0
- package/dist/manifest.js +3 -2
- package/package.json +2 -1
- package/src/cli.ts +17 -1
- package/src/deploy/github.ts +198 -0
- package/src/deploy/index.ts +109 -8
- package/src/deploy/keys.ts +276 -0
- package/src/deploy/link.ts +185 -0
- package/src/deploy/secrets.ts +230 -26
- package/src/dotenvx-primitives.d.ts +14 -0
- package/src/generate/ci.ts +168 -121
- package/src/generate/index.ts +43 -3
- package/src/generate/strict.ts +2 -0
- package/src/init.ts +1 -0
- package/src/regenerate.ts +1 -0
package/dist/catalog.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
PRIVATE_KEY_VAR,
|
|
3
|
-
SECRETS_FILE,
|
|
4
2
|
box,
|
|
5
3
|
captureVoid,
|
|
6
4
|
deployCloudflare,
|
|
7
5
|
distCommand,
|
|
8
6
|
distHelp,
|
|
9
7
|
ensureGenerated,
|
|
10
|
-
envSummary,
|
|
11
8
|
findProject,
|
|
12
9
|
finishStrict,
|
|
13
10
|
fullHelp,
|
|
@@ -22,22 +19,175 @@ import {
|
|
|
22
19
|
row,
|
|
23
20
|
runInherit,
|
|
24
21
|
runVoid,
|
|
25
|
-
secretsCommand,
|
|
26
22
|
version,
|
|
27
23
|
voidAppsIn
|
|
28
|
-
} from "./index-
|
|
24
|
+
} from "./index-5m3t0zfc.js";
|
|
25
|
+
import {
|
|
26
|
+
PRIVATE_KEY_VAR,
|
|
27
|
+
SECRETS_FILE,
|
|
28
|
+
cloudflareAccounts,
|
|
29
|
+
committedPublicKeyInto,
|
|
30
|
+
declaredSecretNames,
|
|
31
|
+
envSummary,
|
|
32
|
+
ghAuth,
|
|
33
|
+
initSecrets,
|
|
34
|
+
keyState,
|
|
35
|
+
keysCommand,
|
|
36
|
+
plaintextSecretNames,
|
|
37
|
+
provisionKey,
|
|
38
|
+
repoSlug,
|
|
39
|
+
secretNames,
|
|
40
|
+
secretsCommand,
|
|
41
|
+
setSecret,
|
|
42
|
+
setVariable,
|
|
43
|
+
variableNames,
|
|
44
|
+
verifyCloudflareToken
|
|
45
|
+
} from "./index-b9b4dawy.js";
|
|
29
46
|
import {
|
|
30
47
|
LAYOUTS,
|
|
31
48
|
readManifest,
|
|
32
49
|
validate,
|
|
50
|
+
writeManifest,
|
|
33
51
|
zoneOf
|
|
34
|
-
} from "./index-
|
|
52
|
+
} from "./index-pz6m2hkm.js";
|
|
35
53
|
import {
|
|
36
54
|
GROUPS
|
|
37
|
-
} from "./index-
|
|
55
|
+
} from "./index-844b3qn9.js";
|
|
56
|
+
import {
|
|
57
|
+
__require
|
|
58
|
+
} from "./index-0v6na3yp.js";
|
|
38
59
|
|
|
39
60
|
// src/deploy/index.ts
|
|
61
|
+
import color2 from "picocolors";
|
|
62
|
+
|
|
63
|
+
// src/deploy/link.ts
|
|
40
64
|
import color from "picocolors";
|
|
65
|
+
var TOKEN_SECRET = "CLOUDFLARE_API_TOKEN";
|
|
66
|
+
var ACCOUNT_VAR = "CLOUDFLARE_ACCOUNT_ID";
|
|
67
|
+
var SCOPES = [
|
|
68
|
+
["Account · Workers Scripts: Edit", "deploy the worker"],
|
|
69
|
+
["Account · D1: Edit", "create the database and apply migrations"],
|
|
70
|
+
["Account · Queues: Edit", "create the order queue"],
|
|
71
|
+
["Account · Workers KV Storage: Edit", "sessions and caches"],
|
|
72
|
+
["Account · Workers R2 Storage: Edit", "product images"],
|
|
73
|
+
["Account · Account Settings: Read", "confirm which account this is"],
|
|
74
|
+
["Zone · Workers Routes: Edit", "answer on your domain"],
|
|
75
|
+
["User · User Details: Read", "wrangler asks at startup"]
|
|
76
|
+
];
|
|
77
|
+
async function linkCommand(project, args) {
|
|
78
|
+
const p = await import("@clack/prompts");
|
|
79
|
+
const root = project.root;
|
|
80
|
+
const relink = args.includes("--force");
|
|
81
|
+
p.intro(color.bgCyan(color.black(" vc link ")));
|
|
82
|
+
const auth = await ghAuth(root);
|
|
83
|
+
if (!auth.ok) {
|
|
84
|
+
p.cancel(auth.reason ?? "gh is unavailable");
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
const slug = await repoSlug(root);
|
|
88
|
+
if (!slug) {
|
|
89
|
+
p.cancel(`this checkout has no GitHub repository, and everything below is stored on one.
|
|
90
|
+
|
|
91
|
+
${color.cyan("gh repo create --source=. --private --push")}
|
|
92
|
+
|
|
93
|
+
then run \`vc link\` again.`);
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
p.log.success(`${color.green("✓")} ${slug}, as ${auth.user ?? "you"}`);
|
|
97
|
+
const keys = await keyState(project);
|
|
98
|
+
if (keys.inGitHub && !relink) {
|
|
99
|
+
p.log.info(`${PRIVATE_KEY_VAR} is already set — leaving it alone (--force replaces it, which orphans ${SECRETS_FILE})`);
|
|
100
|
+
} else if (keys.inGitHub && relink) {
|
|
101
|
+
p.log.warn(`--force: replacing ${PRIVATE_KEY_VAR} makes every value in ${SECRETS_FILE} unreadable. Use \`vc keys --rotate\` to re-encrypt instead.`);
|
|
102
|
+
return 1;
|
|
103
|
+
} else {
|
|
104
|
+
const made = await provisionKey(project);
|
|
105
|
+
if (!made.ok) {
|
|
106
|
+
p.cancel(made.reason);
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
committedPublicKeyInto(root, made.publicKey);
|
|
110
|
+
p.log.success(`${color.green("✓")} ${PRIVATE_KEY_VAR} generated and stored on GitHub; public half in ${SECRETS_FILE}`);
|
|
111
|
+
}
|
|
112
|
+
const existing = await secretNames(root);
|
|
113
|
+
if (existing.has(TOKEN_SECRET) && !relink) {
|
|
114
|
+
p.log.info(`${TOKEN_SECRET} is already set — pass --force to replace it`);
|
|
115
|
+
} else {
|
|
116
|
+
p.log.step(`A Cloudflare API token, from ${color.cyan("https://dash.cloudflare.com/profile/api-tokens")} → Create Token → Custom token`);
|
|
117
|
+
console.log(SCOPES.map(([scope, why]) => ` ${scope.padEnd(38)} ${color.dim(why)}`).join(`
|
|
118
|
+
`));
|
|
119
|
+
console.log(color.dim(`
|
|
120
|
+
The token Cloudflare generates for its own Workers Builds will NOT do:
|
|
121
|
+
it has no D1 and no Queues permission, so it cannot create the database.
|
|
122
|
+
`));
|
|
123
|
+
const token = await p.password({
|
|
124
|
+
message: "Paste it (nothing is written to disk)",
|
|
125
|
+
validate: (input) => input && input.length >= 20 ? undefined : "that is too short to be a Cloudflare token"
|
|
126
|
+
});
|
|
127
|
+
if (p.isCancel(token)) {
|
|
128
|
+
p.cancel("nothing was stored.");
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
const spinner = p.spinner();
|
|
132
|
+
spinner.start("asking Cloudflare whether that token is real");
|
|
133
|
+
const verified = await verifyCloudflareToken(String(token));
|
|
134
|
+
if (!verified.ok) {
|
|
135
|
+
spinner.stop(`${color.red("✗")} Cloudflare rejected it: ${verified.detail}`);
|
|
136
|
+
p.cancel("nothing was stored.");
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
const accounts = await cloudflareAccounts(String(token));
|
|
140
|
+
spinner.stop(`${color.green("✓")} the token is active and sees ${accounts.length} account${accounts.length === 1 ? "" : "s"}`);
|
|
141
|
+
let accountId = project.manifest.cloudflare?.accountId ?? "";
|
|
142
|
+
if (accounts.length === 1) {
|
|
143
|
+
accountId = accounts[0].id;
|
|
144
|
+
p.log.info(`account "${accounts[0].name}"`);
|
|
145
|
+
} else if (accounts.length > 1) {
|
|
146
|
+
const picked = await p.select({
|
|
147
|
+
message: "Which account is this shop in?",
|
|
148
|
+
options: accounts.map((account) => ({ value: account.id, label: account.name, hint: account.id }))
|
|
149
|
+
});
|
|
150
|
+
if (p.isCancel(picked)) {
|
|
151
|
+
p.cancel("nothing was stored.");
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
accountId = String(picked);
|
|
155
|
+
}
|
|
156
|
+
const stored = await setSecret(root, TOKEN_SECRET, String(token));
|
|
157
|
+
if (!stored.ok) {
|
|
158
|
+
p.cancel(`GitHub refused the secret: ${stored.error ?? "unknown error"}`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
p.log.success(`${color.green("✓")} ${TOKEN_SECRET} stored on ${slug}`);
|
|
162
|
+
if (accountId) {
|
|
163
|
+
const varred = await setVariable(root, ACCOUNT_VAR, accountId);
|
|
164
|
+
if (varred.ok)
|
|
165
|
+
p.log.success(`${color.green("✓")} ${ACCOUNT_VAR} set as a repository variable`);
|
|
166
|
+
project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
|
|
167
|
+
await writeManifest(root, project.manifest);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (declaredSecretNames(root).size === 0) {
|
|
171
|
+
p.log.step(`writing ${SECRETS_FILE}`);
|
|
172
|
+
await initSecrets(project);
|
|
173
|
+
}
|
|
174
|
+
const secrets = await secretNames(root);
|
|
175
|
+
const vars = await variableNames(root);
|
|
176
|
+
p.outro([
|
|
177
|
+
`${color.bold("Ready.")} From here, ${color.cyan("git push")} is the deploy.`,
|
|
178
|
+
"",
|
|
179
|
+
` ${secrets.has(PRIVATE_KEY_VAR) ? color.green("✓") : color.red("✗")} ${PRIVATE_KEY_VAR} ${color.dim("reads the shop's secrets")}`,
|
|
180
|
+
` ${secrets.has(TOKEN_SECRET) ? color.green("✓") : color.red("✗")} ${TOKEN_SECRET} ${color.dim("deploys to Cloudflare")}`,
|
|
181
|
+
` ${vars.has(ACCOUNT_VAR) ? color.green("✓") : color.dim("·")} ${ACCOUNT_VAR} ${color.dim("which account")}`,
|
|
182
|
+
"",
|
|
183
|
+
` Set the shop's own secrets with ${color.cyan("vc secrets set KEY")} — that needs no`,
|
|
184
|
+
` credential at all, because encryption uses the public key in the repository.`
|
|
185
|
+
].join(`
|
|
186
|
+
`));
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/deploy/index.ts
|
|
41
191
|
async function deployCommand(args) {
|
|
42
192
|
const own = new Set(["--cloudflare", "--provision", "--force"]);
|
|
43
193
|
const cloudflare = args.includes("--cloudflare");
|
|
@@ -60,12 +210,12 @@ async function deployCommand(args) {
|
|
|
60
210
|
const check = await preflight(project, "void");
|
|
61
211
|
printPreflight(project, check, "void");
|
|
62
212
|
if (!check.ready && check.remote !== null && !args.includes("--force")) {
|
|
63
|
-
console.error(`${
|
|
213
|
+
console.error(`${color2.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
|
|
64
214
|
`);
|
|
65
215
|
return 1;
|
|
66
216
|
}
|
|
67
217
|
if (!check.ready && check.remote === null) {
|
|
68
|
-
console.log(
|
|
218
|
+
console.log(color2.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
|
|
69
219
|
`));
|
|
70
220
|
}
|
|
71
221
|
return runVoid(["deploy", ...voids], project.appDir);
|
|
@@ -88,15 +238,18 @@ async function deployHelp() {
|
|
|
88
238
|
line("vc's preflight — every required key set, the hostnames right — then", width),
|
|
89
239
|
line("void deploy, untouched. Or, with --cloudflare, your own account via wrangler.", width),
|
|
90
240
|
line("", width),
|
|
91
|
-
line(
|
|
241
|
+
line(color2.bold("Usage"), width),
|
|
92
242
|
...row("vc deploy [void's flags]", "preflight, then void deploy — the Void platform, void's login", width, 2),
|
|
93
243
|
...row("vc deploy --cloudflare", "wrangler, your Cloudflare account, no Void login: preflight, build, scrub baked secrets, migrate D1, deploy", width, 2),
|
|
94
244
|
...row("vc deploy --cloudflare --provision", "first time: create the D1 database and the queue, record them", width, 2),
|
|
95
245
|
...row("vc deploy --cloudflare --force", "deploy a shop preflight says is not ready — deliberately", width, 2),
|
|
96
246
|
line("", width),
|
|
97
|
-
line(
|
|
98
|
-
...row("
|
|
99
|
-
...row(
|
|
247
|
+
line(color2.bold("Needs, for --cloudflare"), width),
|
|
248
|
+
...row("CLOUDFLARE_API_TOKEN", "in the environment; the account is pinned for you when there is one", width, 2),
|
|
249
|
+
...row(PRIVATE_KEY_VAR, "to decrypt the shop's own secrets", width, 2),
|
|
250
|
+
line("", width),
|
|
251
|
+
line("Normally neither is on your machine: `vc link` puts both on the GitHub", width),
|
|
252
|
+
line("repository and the workflow runs this command for you on every push.", width)
|
|
100
253
|
], width);
|
|
101
254
|
if (captured)
|
|
102
255
|
process.stdout.write(captured.out.replace(/\n*$/, `
|
|
@@ -130,14 +283,16 @@ async function secretsHelp() {
|
|
|
130
283
|
line(`Secrets that live in the repository, encrypted, in ${SECRETS_FILE}.`, width),
|
|
131
284
|
line("", width),
|
|
132
285
|
...row("vc secrets", "what this shop needs, and whether it is declared and encrypted", width, 2),
|
|
133
|
-
...row("vc secrets --init", `write ${SECRETS_FILE}
|
|
134
|
-
...row(
|
|
135
|
-
...row(
|
|
286
|
+
...row("vc secrets --init", `write ${SECRETS_FILE} and make this shop's key`, width, 2),
|
|
287
|
+
...row("vc secrets set KEY", "set one value — prompts, so it never reaches your shell history", width, 2),
|
|
288
|
+
...row("echo v | vc secrets set KEY", "the same, from a pipe, for scripts", width, 2),
|
|
136
289
|
line("", width),
|
|
137
290
|
line("The file is COMMITTED: values are ciphertext, key names are not, so a", width),
|
|
138
291
|
line("diff shows which secret changed without showing what it changed to.", width),
|
|
139
|
-
line(
|
|
140
|
-
line("
|
|
292
|
+
line("", width),
|
|
293
|
+
line("Setting one needs NO credential. dotenvx is asymmetric, so encryption uses", width),
|
|
294
|
+
line(`the public key already in ${SECRETS_FILE}. The private key is a GitHub`, width),
|
|
295
|
+
line(`Actions secret, ${PRIVATE_KEY_VAR}, and only the deploy uses it.`, width),
|
|
141
296
|
line("", width),
|
|
142
297
|
line("Tradeoff worth knowing: ciphertext in git is permanent, so a leaked key", width),
|
|
143
298
|
line("exposes rotated secrets too. `wrangler secret put` does not have that", width),
|
|
@@ -145,16 +300,98 @@ async function secretsHelp() {
|
|
|
145
300
|
], width));
|
|
146
301
|
return 0;
|
|
147
302
|
}
|
|
303
|
+
async function keysCliCommand(args) {
|
|
304
|
+
const project = await findProject();
|
|
305
|
+
if (!project) {
|
|
306
|
+
console.error("vc: no voidcommerce.json here.");
|
|
307
|
+
return 1;
|
|
308
|
+
}
|
|
309
|
+
return keysCommand(project, args);
|
|
310
|
+
}
|
|
311
|
+
async function keysHelp() {
|
|
312
|
+
const width = 80;
|
|
313
|
+
console.log(box("vc keys", [
|
|
314
|
+
line("Secrets are encrypted with a PUBLIC key that is committed, and read with", width),
|
|
315
|
+
line("a PRIVATE key that lives only in GitHub Actions.", width),
|
|
316
|
+
line("", width),
|
|
317
|
+
...row("vc keys", "where the key is, and what is missing", width, 2),
|
|
318
|
+
...row("vc keys --init", "generate one and store it on the GitHub repository", width, 2),
|
|
319
|
+
...row("vc keys --rotate", "re-key and re-encrypt, if you hold the current private key", width, 2),
|
|
320
|
+
line("", width),
|
|
321
|
+
line("Because the encryption is asymmetric, SETTING a secret needs no credential", width),
|
|
322
|
+
line("at all: `vc secrets set KEY` encrypts with the public key in the repository.", width),
|
|
323
|
+
line("A contributor with only a clone can rotate the Stripe key and cannot read", width),
|
|
324
|
+
line("the one already there. Only the deploy decrypts.", width),
|
|
325
|
+
line("", width),
|
|
326
|
+
line("GitHub will not hand a secret back once set, so re-keying without a local", width),
|
|
327
|
+
line("copy means entering the values again. That is the right trade: a key worth", width),
|
|
328
|
+
line("rotating is a key that may have leaked, and leaked values need replacing", width),
|
|
329
|
+
line("at the source anyway.", width)
|
|
330
|
+
], width));
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
async function guardCommand() {
|
|
334
|
+
const project = await findProject();
|
|
335
|
+
if (!project)
|
|
336
|
+
return 0;
|
|
337
|
+
const bare = plaintextSecretNames(project.root);
|
|
338
|
+
if (bare.length === 0)
|
|
339
|
+
return 0;
|
|
340
|
+
console.error(`
|
|
341
|
+
${color2.red("✗ refusing the commit")}: ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE} ${bare.length === 1 ? "is" : "are"} not encrypted.
|
|
342
|
+
|
|
343
|
+
` + ` ${bare.join(`
|
|
344
|
+
`)}
|
|
345
|
+
|
|
346
|
+
` + ` bunx dotenvx encrypt -f ${SECRETS_FILE}
|
|
347
|
+
|
|
348
|
+
` + color2.dim(` Committing a secret in the clear cannot be undone by a later commit;
|
|
349
|
+
the value stays in the history and must be treated as burned.
|
|
350
|
+
`));
|
|
351
|
+
return 1;
|
|
352
|
+
}
|
|
353
|
+
async function linkCliCommand(args) {
|
|
354
|
+
const project = await findProject();
|
|
355
|
+
if (!project) {
|
|
356
|
+
console.error("vc: no voidcommerce.json here.");
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
return linkCommand(project, args);
|
|
360
|
+
}
|
|
361
|
+
async function linkHelp() {
|
|
362
|
+
const width = 80;
|
|
363
|
+
console.log(box("vc link", [
|
|
364
|
+
line("Put this shop's credentials on its GitHub repository, once, so that", width),
|
|
365
|
+
line("every deploy after this is a `git push`.", width),
|
|
366
|
+
line("", width),
|
|
367
|
+
...row("vc link", "generate the encryption key, take the Cloudflare token, store both", width, 2),
|
|
368
|
+
...row("vc link --force", "replace what is already there", width, 2),
|
|
369
|
+
line("", width),
|
|
370
|
+
line(color2.bold("What it stores, and where"), width),
|
|
371
|
+
...row("DOTENV_PRIVATE_KEY_SECRETS", "generated here, never written to disk — a repository SECRET", width, 2),
|
|
372
|
+
...row("CLOUDFLARE_API_TOKEN", "yours, checked against the Cloudflare API first — a repository SECRET", width, 2),
|
|
373
|
+
...row("CLOUDFLARE_ACCOUNT_ID", "an identifier, not a credential — a repository VARIABLE", width, 2),
|
|
374
|
+
line("", width),
|
|
375
|
+
line(color2.bold("Why one token still has to be typed"), width),
|
|
376
|
+
line("GitHub cannot mint a Cloudflare credential. There is no OIDC federation", width),
|
|
377
|
+
line("between them, and the Cloudflare GitHub App runs the other way: it grants", width),
|
|
378
|
+
line("Cloudflare access to your repository, not your repository access to", width),
|
|
379
|
+
line("Cloudflare. Something must authorise creating a database in your account,", width),
|
|
380
|
+
line("and only Cloudflare can issue that. So it is typed once, here, and never", width),
|
|
381
|
+
line("stored on this machine.", width)
|
|
382
|
+
], width));
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
148
385
|
|
|
149
386
|
// src/init.ts
|
|
150
387
|
import * as p2 from "@clack/prompts";
|
|
151
388
|
import { mkdir } from "node:fs/promises";
|
|
152
389
|
import { basename, join } from "node:path";
|
|
153
|
-
import
|
|
390
|
+
import color4 from "picocolors";
|
|
154
391
|
|
|
155
392
|
// src/wizard.ts
|
|
156
393
|
import * as p from "@clack/prompts";
|
|
157
|
-
import
|
|
394
|
+
import color3 from "picocolors";
|
|
158
395
|
var bail = () => {
|
|
159
396
|
p.cancel("Nothing was written.");
|
|
160
397
|
process.exit(0);
|
|
@@ -176,8 +413,8 @@ async function askGroup(group2, previous, alsoRequired = []) {
|
|
|
176
413
|
const required = group2.choices.filter((choice) => requiredIds.has(choice.id));
|
|
177
414
|
const optional = group2.choices.filter((choice) => !requiredIds.has(choice.id));
|
|
178
415
|
const message = group2.title + (group2.intro ? `
|
|
179
|
-
${
|
|
180
|
-
${
|
|
416
|
+
${color3.dim(group2.intro)}` : "") + (required.length ? `
|
|
417
|
+
${color3.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
|
|
181
418
|
if (optional.length === 0) {
|
|
182
419
|
p.log.info(message);
|
|
183
420
|
return required.map((choice) => choice.id);
|
|
@@ -207,7 +444,7 @@ async function runWizard(existing, layout) {
|
|
|
207
444
|
validate: (value) => value.trim() ? undefined : "A shop has a name."
|
|
208
445
|
}),
|
|
209
446
|
domain: () => p.text({
|
|
210
|
-
message: `Domain ${
|
|
447
|
+
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")}`,
|
|
211
448
|
placeholder: "northwind.com",
|
|
212
449
|
initialValue: existing?.shop.domain ?? "",
|
|
213
450
|
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."
|
|
@@ -218,7 +455,7 @@ async function runWizard(existing, layout) {
|
|
|
218
455
|
if (!domain || domain === guess)
|
|
219
456
|
return Promise.resolve(undefined);
|
|
220
457
|
return p.text({
|
|
221
|
-
message: `DNS zone ${
|
|
458
|
+
message: `DNS zone ${color3.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
|
|
222
459
|
placeholder: guess,
|
|
223
460
|
initialValue: existing?.shop.zone ?? guess,
|
|
224
461
|
validate: (value) => {
|
|
@@ -230,13 +467,13 @@ async function runWizard(existing, layout) {
|
|
|
230
467
|
});
|
|
231
468
|
},
|
|
232
469
|
country: () => p.text({
|
|
233
|
-
message: `Country ${
|
|
470
|
+
message: `Country ${color3.dim("— sets the declared VAT rate and the default region")}`,
|
|
234
471
|
placeholder: "US",
|
|
235
472
|
initialValue: existing?.shop.country ?? "",
|
|
236
473
|
validate: (value) => /^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."
|
|
237
474
|
}),
|
|
238
475
|
taxRegistered: () => p.select({
|
|
239
|
-
message: `Registered to charge tax? ${
|
|
476
|
+
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")}`,
|
|
240
477
|
options: [
|
|
241
478
|
{ value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
|
|
242
479
|
{ value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" }
|
|
@@ -250,14 +487,14 @@ async function runWizard(existing, layout) {
|
|
|
250
487
|
validate: (value) => /^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."
|
|
251
488
|
}),
|
|
252
489
|
locale: () => p.text({
|
|
253
|
-
message: `Locale ${
|
|
490
|
+
message: `Locale ${color3.dim("— the language of emails and launch content")}`,
|
|
254
491
|
placeholder: "en",
|
|
255
492
|
initialValue: existing?.shop.locale ?? "",
|
|
256
493
|
validate: (value) => /^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."
|
|
257
494
|
}),
|
|
258
495
|
...frontend ? {
|
|
259
496
|
pagesHost: () => p.text({
|
|
260
|
-
message: `GitHub Pages host ${
|
|
497
|
+
message: `GitHub Pages host ${color3.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
|
|
261
498
|
placeholder: "northwind.github.io",
|
|
262
499
|
initialValue: existing?.shop.pagesHost ?? "",
|
|
263
500
|
validate: (value) => !value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank"
|
|
@@ -268,7 +505,7 @@ async function runWizard(existing, layout) {
|
|
|
268
505
|
for (const group2 of GROUPS) {
|
|
269
506
|
if (group2.id === "ui" && layout === "strict") {
|
|
270
507
|
p.log.info(`${group2.title}
|
|
271
|
-
${
|
|
508
|
+
${color3.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
|
|
272
509
|
chosen[group2.id] = ["admin"];
|
|
273
510
|
continue;
|
|
274
511
|
}
|
|
@@ -293,14 +530,14 @@ ${color2.dim("The generated storefront and panel. Strict has no custom code to h
|
|
|
293
530
|
}
|
|
294
531
|
function summarise(manifest) {
|
|
295
532
|
const lines = [
|
|
296
|
-
`${
|
|
533
|
+
`${color3.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
|
|
297
534
|
];
|
|
298
535
|
for (const group2 of GROUPS) {
|
|
299
536
|
const ids = manifest.chosen[group2.id] ?? [];
|
|
300
537
|
if (ids.length === 0)
|
|
301
538
|
continue;
|
|
302
539
|
const labels = ids.map((id) => group2.choices.find((choice) => choice.id === id)?.label ?? id).join(", ");
|
|
303
|
-
lines.push(`${
|
|
540
|
+
lines.push(`${color3.dim(group2.title.padEnd(34))} ${labels}`);
|
|
304
541
|
}
|
|
305
542
|
return lines.join(`
|
|
306
543
|
`);
|
|
@@ -341,10 +578,10 @@ async function init(args) {
|
|
|
341
578
|
if (flags.voids.length > 0)
|
|
342
579
|
return runVoid(["init", ...flags.voids]);
|
|
343
580
|
let root = process.cwd();
|
|
344
|
-
p2.intro(
|
|
581
|
+
p2.intro(color4.bgCyan(color4.black(" voidcommerce ")));
|
|
345
582
|
const existing = await readManifest(root);
|
|
346
583
|
if (existing) {
|
|
347
|
-
p2.log.info(`Found ${
|
|
584
|
+
p2.log.info(`Found ${color4.cyan("voidcommerce.json")} for ${color4.bold(existing.shop.name)} — answers are pre-filled; the layout (${existing.layout}) is fixed.`);
|
|
348
585
|
if (flags.layout && flags.layout !== existing.layout) {
|
|
349
586
|
p2.log.error(`This shop is laid out as "${existing.layout}". The layout cannot change after init — moving files is not a regeneration.`);
|
|
350
587
|
return 1;
|
|
@@ -352,10 +589,10 @@ async function init(args) {
|
|
|
352
589
|
}
|
|
353
590
|
const layout = existing?.layout ?? flags.layout ?? await askLayout();
|
|
354
591
|
if (layout === "strict") {
|
|
355
|
-
p2.log.step(`Strict: the app will be generated under ${
|
|
592
|
+
p2.log.step(`Strict: the app will be generated under ${color4.cyan(".vc/app")} — nothing there is yours to edit.`);
|
|
356
593
|
} else if (layout === "app") {
|
|
357
594
|
if (!isVoidApp(root)) {
|
|
358
|
-
p2.log.step(`No Void app here yet — ${
|
|
595
|
+
p2.log.step(`No Void app here yet — ${color4.cyan("void init")} first, then the shop.`);
|
|
359
596
|
const before = new Set(voidAppsIn(root));
|
|
360
597
|
const code = await runVoid(["init"]);
|
|
361
598
|
if (code !== 0)
|
|
@@ -368,7 +605,7 @@ async function init(args) {
|
|
|
368
605
|
}
|
|
369
606
|
root = created[0];
|
|
370
607
|
process.chdir(root);
|
|
371
|
-
p2.log.step(`Continuing in ${
|
|
608
|
+
p2.log.step(`Continuing in ${color4.cyan(`${basename(root)}/`)}`);
|
|
372
609
|
}
|
|
373
610
|
}
|
|
374
611
|
} else {
|
|
@@ -381,7 +618,7 @@ async function init(args) {
|
|
|
381
618
|
if (isVoidApp(target))
|
|
382
619
|
continue;
|
|
383
620
|
await mkdir(target, { recursive: true });
|
|
384
|
-
p2.log.step(`${
|
|
621
|
+
p2.log.step(`${color4.cyan("void init")} in ${color4.cyan(`${dir}/`)} — choose ${starter}.`);
|
|
385
622
|
const code = await runVoid(["init"], target);
|
|
386
623
|
if (code !== 0)
|
|
387
624
|
return code;
|
|
@@ -399,7 +636,7 @@ async function init(args) {
|
|
|
399
636
|
if (problems.length > 0) {
|
|
400
637
|
p2.log.error("These answers contradict each other:");
|
|
401
638
|
for (const problem of problems)
|
|
402
|
-
p2.log.message(` ${
|
|
639
|
+
p2.log.message(` ${color4.red("✗")} ${problem}`);
|
|
403
640
|
p2.cancel("Nothing was written.");
|
|
404
641
|
return 1;
|
|
405
642
|
}
|
|
@@ -415,15 +652,16 @@ async function init(args) {
|
|
|
415
652
|
spinner2.stop("Generated");
|
|
416
653
|
const { secrets, plaintext } = envSummary(manifest);
|
|
417
654
|
p2.note([
|
|
418
|
-
...result.written.map((file) => `${
|
|
419
|
-
...result.kept.map((file) => `${
|
|
655
|
+
...result.written.map((file) => `${color4.green("+")} ${file}`),
|
|
656
|
+
...result.kept.map((file) => `${color4.dim("=")} ${file} ${color4.dim("(kept — yours)")}`),
|
|
657
|
+
...result.retired.map((file) => `${color4.red("-")} ${file} ${color4.dim("(retired — no longer generated)")}`)
|
|
420
658
|
].join(`
|
|
421
659
|
`), "Files");
|
|
422
660
|
p2.note([
|
|
423
|
-
`${
|
|
661
|
+
`${color4.bold(String(secrets.length))} secrets ${color4.dim("→ wrangler secret put <NAME>")}`,
|
|
424
662
|
...secrets.map((key) => ` ${key}`),
|
|
425
663
|
"",
|
|
426
|
-
`${
|
|
664
|
+
`${color4.bold(String(plaintext.length))} plaintext ${color4.dim("→ .env.production, already written")}`,
|
|
427
665
|
...plaintext.map((key) => ` ${key}`)
|
|
428
666
|
].join(`
|
|
429
667
|
`), "Before going live");
|
|
@@ -433,21 +671,21 @@ async function init(args) {
|
|
|
433
671
|
return code;
|
|
434
672
|
}
|
|
435
673
|
const next = layout === "app" ? [
|
|
436
|
-
` ${
|
|
437
|
-
` ${
|
|
438
|
-
` ${
|
|
439
|
-
` ${
|
|
674
|
+
` ${color4.cyan("bun install")}`,
|
|
675
|
+
` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
|
|
676
|
+
` ${color4.cyan("vc dev")}`,
|
|
677
|
+
` ${color4.cyan("vc preflight")} ${color4.dim("what is still missing, and why it matters")}`
|
|
440
678
|
] : layout === "strict" ? [
|
|
441
|
-
` ${
|
|
442
|
-
` ${
|
|
443
|
-
` ${
|
|
444
|
-
` ${
|
|
679
|
+
` ${color4.cyan("bun install")}`,
|
|
680
|
+
` ${color4.cyan("vc generate")} ${color4.dim("the app under .vc/app, void's artifacts, the migrations")}`,
|
|
681
|
+
` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
|
|
682
|
+
` ${color4.cyan("vc dev")} ${color4.dim("runs in .vc/app")}`
|
|
445
683
|
] : [
|
|
446
|
-
` ${
|
|
447
|
-
` ${
|
|
448
|
-
` ${
|
|
449
|
-
` ${
|
|
450
|
-
` ${
|
|
684
|
+
` ${color4.cyan("bun install")} ${color4.dim("one install for both workspaces")}`,
|
|
685
|
+
` ${color4.cyan("bun run maildev")} ${color4.dim("local inbox at http://localhost:1080")}`,
|
|
686
|
+
` ${color4.cyan("bun run dev:api")} ${color4.dim("the worker, with the panel")}`,
|
|
687
|
+
` ${color4.cyan("bun run dev:frontend")} ${color4.dim("the storefront, against the local API")}`,
|
|
688
|
+
` ${color4.cyan("bun run preflight")} ${color4.dim("what is still missing, and why it matters")}`
|
|
451
689
|
];
|
|
452
690
|
p2.outro(["Next:", ...next].join(`
|
|
453
691
|
`));
|
|
@@ -456,7 +694,7 @@ async function init(args) {
|
|
|
456
694
|
|
|
457
695
|
// src/regenerate.ts
|
|
458
696
|
import * as p3 from "@clack/prompts";
|
|
459
|
-
import
|
|
697
|
+
import color5 from "picocolors";
|
|
460
698
|
async function generateCommand() {
|
|
461
699
|
const project = await findProject();
|
|
462
700
|
if (!project) {
|
|
@@ -472,8 +710,9 @@ async function generateCommand() {
|
|
|
472
710
|
}
|
|
473
711
|
const result = await generate(project.root, project.manifest);
|
|
474
712
|
p3.log.step([
|
|
475
|
-
...result.written.map((file) => `${
|
|
476
|
-
...result.kept.map((file) => `${
|
|
713
|
+
...result.written.map((file) => `${color5.green("+")} ${file}`),
|
|
714
|
+
...result.kept.map((file) => `${color5.dim("=")} ${file} ${color5.dim("(kept)")}`),
|
|
715
|
+
...result.retired.map((file) => `${color5.red("-")} ${file} ${color5.dim("(retired — no longer generated)")}`)
|
|
477
716
|
].join(`
|
|
478
717
|
`));
|
|
479
718
|
return project.manifest.layout === "strict" ? finishStrict(project.root) : 0;
|
|
@@ -537,6 +776,9 @@ var EXTENDED = {
|
|
|
537
776
|
deploy: { run: deployCommand, help: deployHelp },
|
|
538
777
|
preflight: { run: preflightCommand, help: preflightHelp },
|
|
539
778
|
secrets: { run: secretsCliCommand, help: secretsHelp },
|
|
779
|
+
keys: { run: keysCliCommand, help: keysHelp },
|
|
780
|
+
link: { run: linkCliCommand, help: linkHelp },
|
|
781
|
+
guard: { run: guardCommand, help: async () => guardCommand() },
|
|
540
782
|
dev: { run: appScript("dev"), help: appScriptHelp("dev") },
|
|
541
783
|
build: { run: appScript("build"), help: appScriptHelp("build") },
|
|
542
784
|
preview: { run: appScript("preview"), help: appScriptHelp("preview") }
|