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