@saastemly/voidcommerce 0.17.0 → 0.19.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 +66 -270
- package/dist/deploy/index.d.ts +0 -3
- package/dist/deploy/secrets.d.ts +10 -0
- package/dist/{index-negctpws.js → index-68m1eg3f.js} +29 -49
- package/dist/{index-4jz1448c.js → index-y4dk27kf.js} +46 -58
- package/dist/index.js +2 -2
- package/dist/{keys-w65kbdp6.js → keys-e6w5dcxx.js} +1 -1
- package/package.json +1 -1
- package/src/cli.ts +0 -3
- package/src/deploy/cloudflare.ts +23 -1
- package/src/deploy/index.ts +3 -34
- package/src/deploy/secrets.ts +27 -1
- package/src/generate/ci.ts +31 -56
- package/src/generate/env.ts +22 -0
- package/dist/deploy/link.d.ts +0 -2
- package/src/deploy/link.ts +0 -220
package/dist/cli.js
CHANGED
|
@@ -21,210 +21,37 @@ import {
|
|
|
21
21
|
runVoid,
|
|
22
22
|
version,
|
|
23
23
|
voidAppsIn
|
|
24
|
-
} from "./index-
|
|
24
|
+
} from "./index-y4dk27kf.js";
|
|
25
25
|
import {
|
|
26
26
|
LOCAL_KEY_FILE,
|
|
27
27
|
PRIVATE_KEY_VAR,
|
|
28
28
|
SECRETS_FILE,
|
|
29
|
-
cloudflareAccounts,
|
|
30
29
|
committedPublicKey,
|
|
31
|
-
committedPublicKeyInto,
|
|
32
|
-
declaredSecretNames,
|
|
33
30
|
encryptInto,
|
|
34
31
|
envSummary,
|
|
35
|
-
getVariable,
|
|
36
|
-
ghAuth,
|
|
37
32
|
ignoresKeyFile,
|
|
38
|
-
initSecrets,
|
|
39
|
-
keyState,
|
|
40
33
|
keysCommand,
|
|
41
|
-
localPrivateKey,
|
|
42
34
|
plaintextSecretEntries,
|
|
43
35
|
plaintextSecretNames,
|
|
44
|
-
provisionKey,
|
|
45
|
-
publicKeyFor,
|
|
46
|
-
repoSlug,
|
|
47
36
|
run,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
setSecret,
|
|
51
|
-
setVariable,
|
|
52
|
-
variableNames,
|
|
53
|
-
verifyCloudflareToken
|
|
54
|
-
} from "./index-negctpws.js";
|
|
37
|
+
secretsCommand
|
|
38
|
+
} from "./index-68m1eg3f.js";
|
|
55
39
|
import {
|
|
56
40
|
LAYOUTS,
|
|
57
41
|
oneOrigin,
|
|
58
42
|
readManifest,
|
|
59
43
|
validate,
|
|
60
|
-
writeManifest,
|
|
61
44
|
zoneOf
|
|
62
45
|
} from "./index-30y19qz5.js";
|
|
63
46
|
import {
|
|
64
47
|
GROUPS
|
|
65
48
|
} from "./index-xyjhy6kp.js";
|
|
66
|
-
import
|
|
67
|
-
__require
|
|
68
|
-
} from "./index-0v6na3yp.js";
|
|
49
|
+
import"./index-0v6na3yp.js";
|
|
69
50
|
|
|
70
51
|
// src/deploy/index.ts
|
|
71
|
-
import
|
|
52
|
+
import color from "picocolors";
|
|
72
53
|
import { existsSync } from "node:fs";
|
|
73
|
-
import { join as join2 } from "node:path";
|
|
74
|
-
|
|
75
|
-
// src/deploy/link.ts
|
|
76
|
-
import { rmSync } from "node:fs";
|
|
77
54
|
import { join } from "node:path";
|
|
78
|
-
import color from "picocolors";
|
|
79
|
-
var TOKEN_SECRET = "CLOUDFLARE_API_TOKEN";
|
|
80
|
-
var ACCOUNT_VAR = "CLOUDFLARE_ACCOUNT_ID";
|
|
81
|
-
var SCOPES = [
|
|
82
|
-
["Account · Workers Scripts: Edit", "deploy the worker"],
|
|
83
|
-
["Account · D1: Edit", "create the database and apply migrations"],
|
|
84
|
-
["Account · Queues: Edit", "create the order queue"],
|
|
85
|
-
["Account · Workers KV Storage: Edit", "sessions and caches"],
|
|
86
|
-
["Account · Workers R2 Storage: Edit", "product images"],
|
|
87
|
-
["Account · Account Settings: Read", "confirm which account this is"],
|
|
88
|
-
["Zone · Workers Routes: Edit", "answer on your domain"],
|
|
89
|
-
["Zone · Zone: Read", "confirm the domain's zone is on this account"],
|
|
90
|
-
["Zone · DNS: Read", "notice a hostname that already answers, before taking it over"],
|
|
91
|
-
["User · User Details: Read", "wrangler asks at startup"]
|
|
92
|
-
];
|
|
93
|
-
async function linkCommand(project, args) {
|
|
94
|
-
const p = await import("@clack/prompts");
|
|
95
|
-
const root = project.root;
|
|
96
|
-
const relink = args.includes("--force");
|
|
97
|
-
p.intro(color.bgCyan(color.black(" vc link ")));
|
|
98
|
-
const auth = await ghAuth(root);
|
|
99
|
-
if (!auth.ok) {
|
|
100
|
-
p.cancel(auth.reason ?? "gh is unavailable");
|
|
101
|
-
return 1;
|
|
102
|
-
}
|
|
103
|
-
const slug = await repoSlug(root);
|
|
104
|
-
if (!slug) {
|
|
105
|
-
p.cancel(`this checkout has no GitHub repository, and everything below is stored on one.
|
|
106
|
-
|
|
107
|
-
${color.cyan("gh repo create --source=. --private --push")}
|
|
108
|
-
|
|
109
|
-
then run \`vc link\` again.`);
|
|
110
|
-
return 1;
|
|
111
|
-
}
|
|
112
|
-
p.log.success(`${color.green("✓")} ${slug}, as ${auth.user ?? "you"}`);
|
|
113
|
-
const keys = await keyState(project);
|
|
114
|
-
if (keys.inGitHub && !relink) {
|
|
115
|
-
p.log.info(`${PRIVATE_KEY_VAR} is already set — leaving it alone (--force replaces it, which orphans ${SECRETS_FILE})`);
|
|
116
|
-
} else if (keys.inGitHub && relink) {
|
|
117
|
-
p.log.warn(`--force: replacing ${PRIVATE_KEY_VAR} makes every value in ${SECRETS_FILE} unreadable. Use \`vc keys --rotate\` to re-encrypt instead.`);
|
|
118
|
-
return 1;
|
|
119
|
-
} else {
|
|
120
|
-
const parked = localPrivateKey(root);
|
|
121
|
-
if (parked) {
|
|
122
|
-
const sent = await setVariable(root, PRIVATE_KEY_VAR, parked);
|
|
123
|
-
if (!sent.ok) {
|
|
124
|
-
p.cancel(`GitHub refused the variable: ${sent.error ?? "unknown error"}`);
|
|
125
|
-
return 1;
|
|
126
|
-
}
|
|
127
|
-
const publicKey = await publicKeyFor(parked);
|
|
128
|
-
if (publicKey)
|
|
129
|
-
committedPublicKeyInto(root, publicKey);
|
|
130
|
-
const readBack = await getVariable(root, PRIVATE_KEY_VAR);
|
|
131
|
-
if (readBack !== parked) {
|
|
132
|
-
p.log.warn(`${color.yellow("!")} ${PRIVATE_KEY_VAR} was written to ${slug} but could not be read back,
|
|
133
|
-
so ${LOCAL_KEY_FILE} has been LEFT IN PLACE. It is currently the only copy
|
|
134
|
-
` + " of the key that opens this shop — do not delete it until `vc keys --restore` works.");
|
|
135
|
-
} else {
|
|
136
|
-
rmSync(join(root, LOCAL_KEY_FILE), { force: true });
|
|
137
|
-
p.log.success(`${color.green("✓")} the key moved to ${slug} and ${LOCAL_KEY_FILE} deleted — verified readable, so \`vc keys --restore\` can bring it back`);
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
const made = await provisionKey(project);
|
|
141
|
-
if (!made.ok) {
|
|
142
|
-
p.cancel(made.reason);
|
|
143
|
-
return 1;
|
|
144
|
-
}
|
|
145
|
-
committedPublicKeyInto(root, made.publicKey);
|
|
146
|
-
p.log.success(`${color.green("✓")} ${PRIVATE_KEY_VAR} generated and stored on GitHub; public half in ${SECRETS_FILE}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const existing = await secretNames(root);
|
|
150
|
-
if (existing.has(TOKEN_SECRET) && !relink) {
|
|
151
|
-
p.log.info(`${TOKEN_SECRET} is already set — pass --force to replace it`);
|
|
152
|
-
} else {
|
|
153
|
-
p.log.step(`A Cloudflare API token, from ${color.cyan("https://dash.cloudflare.com/profile/api-tokens")} → Create Token → Custom token`);
|
|
154
|
-
console.log(SCOPES.map(([scope, why]) => ` ${scope.padEnd(38)} ${color.dim(why)}`).join(`
|
|
155
|
-
`));
|
|
156
|
-
console.log(color.dim(`
|
|
157
|
-
The token Cloudflare generates for its own Workers Builds will NOT do:
|
|
158
|
-
it has no D1 and no Queues permission, so it cannot create the database.
|
|
159
|
-
`));
|
|
160
|
-
const token = await p.password({
|
|
161
|
-
message: "Paste it (nothing is written to disk)",
|
|
162
|
-
validate: (input) => input && input.length >= 20 ? undefined : "that is too short to be a Cloudflare token"
|
|
163
|
-
});
|
|
164
|
-
if (p.isCancel(token)) {
|
|
165
|
-
p.cancel("nothing was stored.");
|
|
166
|
-
return 1;
|
|
167
|
-
}
|
|
168
|
-
const spinner = p.spinner();
|
|
169
|
-
spinner.start("asking Cloudflare whether that token is real");
|
|
170
|
-
const verified = await verifyCloudflareToken(String(token));
|
|
171
|
-
if (!verified.ok) {
|
|
172
|
-
spinner.stop(`${color.red("✗")} Cloudflare rejected it: ${verified.detail}`);
|
|
173
|
-
p.cancel("nothing was stored.");
|
|
174
|
-
return 1;
|
|
175
|
-
}
|
|
176
|
-
const accounts = await cloudflareAccounts(String(token));
|
|
177
|
-
spinner.stop(`${color.green("✓")} the token is active and sees ${accounts.length} account${accounts.length === 1 ? "" : "s"}`);
|
|
178
|
-
let accountId = project.manifest.cloudflare?.accountId ?? "";
|
|
179
|
-
if (accounts.length === 1) {
|
|
180
|
-
accountId = accounts[0].id;
|
|
181
|
-
p.log.info(`account "${accounts[0].name}"`);
|
|
182
|
-
} else if (accounts.length > 1) {
|
|
183
|
-
const picked = await p.select({
|
|
184
|
-
message: "Which account is this shop in?",
|
|
185
|
-
options: accounts.map((account) => ({ value: account.id, label: account.name, hint: account.id }))
|
|
186
|
-
});
|
|
187
|
-
if (p.isCancel(picked)) {
|
|
188
|
-
p.cancel("nothing was stored.");
|
|
189
|
-
return 1;
|
|
190
|
-
}
|
|
191
|
-
accountId = String(picked);
|
|
192
|
-
}
|
|
193
|
-
const stored = await setSecret(root, TOKEN_SECRET, String(token));
|
|
194
|
-
if (!stored.ok) {
|
|
195
|
-
p.cancel(`GitHub refused the secret: ${stored.error ?? "unknown error"}`);
|
|
196
|
-
return 1;
|
|
197
|
-
}
|
|
198
|
-
p.log.success(`${color.green("✓")} ${TOKEN_SECRET} stored on ${slug}`);
|
|
199
|
-
if (accountId) {
|
|
200
|
-
const varred = await setVariable(root, ACCOUNT_VAR, accountId);
|
|
201
|
-
if (varred.ok)
|
|
202
|
-
p.log.success(`${color.green("✓")} ${ACCOUNT_VAR} set as a repository variable`);
|
|
203
|
-
project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
|
|
204
|
-
await writeManifest(root, project.manifest);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (declaredSecretNames(root).size === 0) {
|
|
208
|
-
p.log.step(`writing ${SECRETS_FILE}`);
|
|
209
|
-
await initSecrets(project);
|
|
210
|
-
}
|
|
211
|
-
const secrets = await secretNames(root);
|
|
212
|
-
const vars = await variableNames(root);
|
|
213
|
-
p.outro([
|
|
214
|
-
`${color.bold("Ready.")} From here, ${color.cyan("git push")} is the deploy.`,
|
|
215
|
-
"",
|
|
216
|
-
` ${secrets.has(PRIVATE_KEY_VAR) ? color.green("✓") : color.red("✗")} ${PRIVATE_KEY_VAR} ${color.dim("reads the shop's secrets")}`,
|
|
217
|
-
` ${secrets.has(TOKEN_SECRET) ? color.green("✓") : color.red("✗")} ${TOKEN_SECRET} ${color.dim("deploys to Cloudflare")}`,
|
|
218
|
-
` ${vars.has(ACCOUNT_VAR) ? color.green("✓") : color.dim("·")} ${ACCOUNT_VAR} ${color.dim("which account")}`,
|
|
219
|
-
"",
|
|
220
|
-
` Set the shop's own secrets with ${color.cyan("vc secrets set KEY")} — that needs no`,
|
|
221
|
-
` credential at all, because encryption uses the public key in the repository.`
|
|
222
|
-
].join(`
|
|
223
|
-
`));
|
|
224
|
-
return 0;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// src/deploy/index.ts
|
|
228
55
|
async function deployCommand(args) {
|
|
229
56
|
const own = new Set(["--cloudflare", "--provision", "--force"]);
|
|
230
57
|
const cloudflare = args.includes("--cloudflare");
|
|
@@ -247,12 +74,12 @@ async function deployCommand(args) {
|
|
|
247
74
|
const check = await preflight(project, "void");
|
|
248
75
|
printPreflight(project, check, "void");
|
|
249
76
|
if (!check.ready && check.remote !== null && !args.includes("--force")) {
|
|
250
|
-
console.error(`${
|
|
77
|
+
console.error(`${color.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
|
|
251
78
|
`);
|
|
252
79
|
return 1;
|
|
253
80
|
}
|
|
254
81
|
if (!check.ready && check.remote === null) {
|
|
255
|
-
console.log(
|
|
82
|
+
console.log(color.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
|
|
256
83
|
`));
|
|
257
84
|
}
|
|
258
85
|
return runVoid(["deploy", ...voids], project.appDir);
|
|
@@ -270,7 +97,7 @@ async function preflightCommand(args) {
|
|
|
270
97
|
return result.ready ? 0 : 1;
|
|
271
98
|
const pending = await uncommittedMigrations(project.root);
|
|
272
99
|
if (pending.length > 0) {
|
|
273
|
-
console.error(`${
|
|
100
|
+
console.error(`${color.red("✗")} ${pending.length} migration file${pending.length === 1 ? " is" : "s are"} not committed:
|
|
274
101
|
` + ` ${pending.join(`
|
|
275
102
|
`)}
|
|
276
103
|
|
|
@@ -295,13 +122,13 @@ async function deployHelp() {
|
|
|
295
122
|
line("vc's preflight — every required key set, the hostnames right — then", width),
|
|
296
123
|
line("void deploy, untouched. Or, with --cloudflare, your own account via wrangler.", width),
|
|
297
124
|
line("", width),
|
|
298
|
-
line(
|
|
125
|
+
line(color.bold("Usage"), width),
|
|
299
126
|
...row("vc deploy [void's flags]", "preflight, then void deploy — the Void platform, void's login", width, 2),
|
|
300
127
|
...row("vc deploy --cloudflare", "wrangler, your Cloudflare account, no Void login: preflight, build, scrub baked secrets, migrate D1, deploy", width, 2),
|
|
301
128
|
...row("vc deploy --cloudflare --provision", "first time: create the D1 database and the queue, record them", width, 2),
|
|
302
129
|
...row("vc deploy --cloudflare --force", "deploy a shop preflight says is not ready — deliberately", width, 2),
|
|
303
130
|
line("", width),
|
|
304
|
-
line(
|
|
131
|
+
line(color.bold("Needs, for --cloudflare"), width),
|
|
305
132
|
...row("CLOUDFLARE_API_TOKEN", "in the environment; the account is pinned for you when there is one", width, 2),
|
|
306
133
|
...row(PRIVATE_KEY_VAR, "to decrypt the shop's own secrets", width, 2),
|
|
307
134
|
line("", width),
|
|
@@ -397,9 +224,9 @@ async function guardCommand() {
|
|
|
397
224
|
if (!project)
|
|
398
225
|
return 0;
|
|
399
226
|
const root = project.root;
|
|
400
|
-
if (existsSync(
|
|
227
|
+
if (existsSync(join(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
|
|
401
228
|
console.error(`
|
|
402
|
-
${
|
|
229
|
+
${color.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gitignored.
|
|
403
230
|
|
|
404
231
|
` + ` It holds the private key that opens every secret in this repository.
|
|
405
232
|
` + ` Add ${LOCAL_KEY_FILE} to .gitignore before committing anything.
|
|
@@ -412,14 +239,14 @@ ${color2.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gi
|
|
|
412
239
|
const publicKey = committedPublicKey(root);
|
|
413
240
|
if (!publicKey) {
|
|
414
241
|
console.error(`
|
|
415
|
-
${
|
|
242
|
+
${color.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.
|
|
416
243
|
|
|
417
244
|
` + ` ${bare.map((entry) => entry.name).join(`
|
|
418
245
|
`)}
|
|
419
246
|
|
|
420
|
-
` + ` ${
|
|
247
|
+
` + ` ${color.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
|
|
421
248
|
|
|
422
|
-
` +
|
|
249
|
+
` + color.dim(` Committing a secret in the clear cannot be undone by a later commit;
|
|
423
250
|
the value stays in the history and must be treated as burned.
|
|
424
251
|
`));
|
|
425
252
|
return 1;
|
|
@@ -429,7 +256,7 @@ ${color2.red("✗ refusing the commit")}: ${bare.length} value${bare.length ===
|
|
|
429
256
|
const sealed = await encryptInto(root, publicKey, entry.name, entry.value);
|
|
430
257
|
if (!sealed.ok) {
|
|
431
258
|
console.error(`
|
|
432
|
-
${
|
|
259
|
+
${color.red("✗ refusing the commit")}: ${sealed.error}
|
|
433
260
|
`);
|
|
434
261
|
return 1;
|
|
435
262
|
}
|
|
@@ -437,7 +264,7 @@ ${color2.red("✗ refusing the commit")}: ${sealed.error}
|
|
|
437
264
|
const left = plaintextSecretNames(root);
|
|
438
265
|
if (left.length > 0) {
|
|
439
266
|
console.error(`
|
|
440
|
-
${
|
|
267
|
+
${color.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
|
|
441
268
|
`);
|
|
442
269
|
return 1;
|
|
443
270
|
}
|
|
@@ -445,58 +272,28 @@ ${color2.red("✗ refusing the commit")}: ${left.join(", ")} could not be encryp
|
|
|
445
272
|
const added = await run("git", ["add", "--", SECRETS_FILE], root);
|
|
446
273
|
if (added.code !== 0) {
|
|
447
274
|
console.error(`
|
|
448
|
-
${
|
|
275
|
+
${color.red("✗ refusing the commit")}: ${SECRETS_FILE} was encrypted but could not be re-staged,
|
|
449
276
|
` + ` so the commit would still carry the plaintext you staged. \`git add ${SECRETS_FILE}\`.
|
|
450
277
|
`);
|
|
451
278
|
return 1;
|
|
452
279
|
}
|
|
453
280
|
}
|
|
454
281
|
console.log(`
|
|
455
|
-
${
|
|
456
|
-
` +
|
|
282
|
+
${color.green("✓")} encrypted ${bare.length} value${bare.length === 1 ? "" : "s"} in ${SECRETS_FILE}${staged ? " and re-staged it" : ""}: ${bare.map((entry) => entry.name).join(", ")}
|
|
283
|
+
` + color.dim(` Encryption needs only the public key, so this needs no credential.
|
|
457
284
|
`));
|
|
458
285
|
return 0;
|
|
459
286
|
}
|
|
460
|
-
async function linkCliCommand(args) {
|
|
461
|
-
const project = await findProject();
|
|
462
|
-
if (!project) {
|
|
463
|
-
console.error("vc: no voidcommerce.json here.");
|
|
464
|
-
return 1;
|
|
465
|
-
}
|
|
466
|
-
return linkCommand(project, args);
|
|
467
|
-
}
|
|
468
|
-
async function linkHelp() {
|
|
469
|
-
const width = 80;
|
|
470
|
-
console.log(box("vc link", [
|
|
471
|
-
line("Put this shop's credentials on its GitHub repository, once, so that", width),
|
|
472
|
-
line("every deploy after this is a `git push`.", width),
|
|
473
|
-
line("", width),
|
|
474
|
-
...row("vc link", "store the encryption key and the Cloudflare token on the repository", width, 2),
|
|
475
|
-
...row("vc link --force", "replace what is already there", width, 2),
|
|
476
|
-
line("", width),
|
|
477
|
-
line(color2.bold("What it stores, and where"), width),
|
|
478
|
-
...row(PRIVATE_KEY_VAR, "a repository SECRET. A key already waiting locally is MOVED here and the local copy deleted", width, 2),
|
|
479
|
-
...row("CLOUDFLARE_API_TOKEN", "a repository SECRET, checked against the Cloudflare API before it is stored", width, 2),
|
|
480
|
-
...row("CLOUDFLARE_ACCOUNT_ID", "a repository VARIABLE — an identifier, not a credential", width, 2),
|
|
481
|
-
line("", width),
|
|
482
|
-
line(color2.bold("Why one token still has to be typed"), width),
|
|
483
|
-
line("GitHub cannot mint a Cloudflare credential. There is no OIDC federation", width),
|
|
484
|
-
line("between them, and the Cloudflare GitHub App runs the other way: it grants", width),
|
|
485
|
-
line("Cloudflare access to your repository, not your repository access to", width),
|
|
486
|
-
line("Cloudflare. So it is typed once, here, and never stored on this machine.", width)
|
|
487
|
-
], width));
|
|
488
|
-
return 0;
|
|
489
|
-
}
|
|
490
287
|
|
|
491
288
|
// src/init.ts
|
|
492
289
|
import * as p2 from "@clack/prompts";
|
|
493
290
|
import { mkdir } from "node:fs/promises";
|
|
494
|
-
import { basename, join as
|
|
495
|
-
import
|
|
291
|
+
import { basename, join as join2 } from "node:path";
|
|
292
|
+
import color3 from "picocolors";
|
|
496
293
|
|
|
497
294
|
// src/wizard.ts
|
|
498
295
|
import * as p from "@clack/prompts";
|
|
499
|
-
import
|
|
296
|
+
import color2 from "picocolors";
|
|
500
297
|
var bail = () => {
|
|
501
298
|
p.cancel("Nothing was written.");
|
|
502
299
|
process.exit(0);
|
|
@@ -518,8 +315,8 @@ async function askGroup(group2, previous, alsoRequired = []) {
|
|
|
518
315
|
const required = group2.choices.filter((choice) => requiredIds.has(choice.id));
|
|
519
316
|
const optional = group2.choices.filter((choice) => !requiredIds.has(choice.id));
|
|
520
317
|
const message = group2.title + (group2.intro ? `
|
|
521
|
-
${
|
|
522
|
-
${
|
|
318
|
+
${color2.dim(group2.intro)}` : "") + (required.length ? `
|
|
319
|
+
${color2.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
|
|
523
320
|
if (optional.length === 0) {
|
|
524
321
|
p.log.info(message);
|
|
525
322
|
return required.map((choice) => choice.id);
|
|
@@ -550,7 +347,7 @@ async function runWizard(existing, layout) {
|
|
|
550
347
|
validate: (value) => value.trim() ? undefined : "A shop has a name."
|
|
551
348
|
}),
|
|
552
349
|
domain: () => p.text({
|
|
553
|
-
message: `Domain ${
|
|
350
|
+
message: `Domain ${color2.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")}`,
|
|
554
351
|
placeholder: "northwind.com",
|
|
555
352
|
initialValue: existing?.shop.domain ?? "",
|
|
556
353
|
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."
|
|
@@ -561,7 +358,7 @@ async function runWizard(existing, layout) {
|
|
|
561
358
|
if (!domain || domain === guess)
|
|
562
359
|
return Promise.resolve(undefined);
|
|
563
360
|
return p.text({
|
|
564
|
-
message: `DNS zone ${
|
|
361
|
+
message: `DNS zone ${color2.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
|
|
565
362
|
placeholder: guess,
|
|
566
363
|
initialValue: existing?.shop.zone ?? guess,
|
|
567
364
|
validate: (value) => {
|
|
@@ -573,13 +370,13 @@ async function runWizard(existing, layout) {
|
|
|
573
370
|
});
|
|
574
371
|
},
|
|
575
372
|
country: () => p.text({
|
|
576
|
-
message: `Country ${
|
|
373
|
+
message: `Country ${color2.dim("— sets the declared VAT rate and the default region")}`,
|
|
577
374
|
placeholder: "US",
|
|
578
375
|
initialValue: existing?.shop.country ?? "",
|
|
579
376
|
validate: (value) => /^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."
|
|
580
377
|
}),
|
|
581
378
|
taxRegistered: () => p.select({
|
|
582
|
-
message: `Registered to charge tax? ${
|
|
379
|
+
message: `Registered to charge tax? ${color2.dim("— a business below its registration threshold must NOT add tax to a price, and one above it must")}`,
|
|
583
380
|
options: [
|
|
584
381
|
{ value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
|
|
585
382
|
{ value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" }
|
|
@@ -593,14 +390,14 @@ async function runWizard(existing, layout) {
|
|
|
593
390
|
validate: (value) => /^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."
|
|
594
391
|
}),
|
|
595
392
|
locale: () => p.text({
|
|
596
|
-
message: `Locale ${
|
|
393
|
+
message: `Locale ${color2.dim("— the language of emails and launch content")}`,
|
|
597
394
|
placeholder: "en",
|
|
598
395
|
initialValue: existing?.shop.locale ?? "",
|
|
599
396
|
validate: (value) => /^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."
|
|
600
397
|
}),
|
|
601
398
|
...onPages ? {
|
|
602
399
|
pagesHost: () => p.text({
|
|
603
|
-
message: `GitHub Pages host ${
|
|
400
|
+
message: `GitHub Pages host ${color2.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
|
|
604
401
|
placeholder: "northwind.github.io",
|
|
605
402
|
initialValue: existing?.shop.pagesHost ?? "",
|
|
606
403
|
validate: (value) => !value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank"
|
|
@@ -611,7 +408,7 @@ async function runWizard(existing, layout) {
|
|
|
611
408
|
for (const group2 of GROUPS) {
|
|
612
409
|
if (group2.id === "ui" && layout === "strict") {
|
|
613
410
|
p.log.info(`${group2.title}
|
|
614
|
-
${
|
|
411
|
+
${color2.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
|
|
615
412
|
chosen[group2.id] = ["admin"];
|
|
616
413
|
continue;
|
|
617
414
|
}
|
|
@@ -636,14 +433,14 @@ ${color3.dim("The generated storefront and panel. Strict has no custom code to h
|
|
|
636
433
|
}
|
|
637
434
|
function summarise(manifest) {
|
|
638
435
|
const lines = [
|
|
639
|
-
`${
|
|
436
|
+
`${color2.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
|
|
640
437
|
];
|
|
641
438
|
for (const group2 of GROUPS) {
|
|
642
439
|
const ids = manifest.chosen[group2.id] ?? [];
|
|
643
440
|
if (ids.length === 0)
|
|
644
441
|
continue;
|
|
645
442
|
const labels = ids.map((id) => group2.choices.find((choice) => choice.id === id)?.label ?? id).join(", ");
|
|
646
|
-
lines.push(`${
|
|
443
|
+
lines.push(`${color2.dim(group2.title.padEnd(34))} ${labels}`);
|
|
647
444
|
}
|
|
648
445
|
return lines.join(`
|
|
649
446
|
`);
|
|
@@ -684,10 +481,10 @@ async function init(args) {
|
|
|
684
481
|
if (flags.voids.length > 0)
|
|
685
482
|
return runVoid(["init", ...flags.voids]);
|
|
686
483
|
let root = process.cwd();
|
|
687
|
-
p2.intro(
|
|
484
|
+
p2.intro(color3.bgCyan(color3.black(" voidcommerce ")));
|
|
688
485
|
const existing = await readManifest(root);
|
|
689
486
|
if (existing) {
|
|
690
|
-
p2.log.info(`Found ${
|
|
487
|
+
p2.log.info(`Found ${color3.cyan("voidcommerce.json")} for ${color3.bold(existing.shop.name)} — answers are pre-filled; the layout (${existing.layout}) is fixed.`);
|
|
691
488
|
if (flags.layout && flags.layout !== existing.layout) {
|
|
692
489
|
p2.log.error(`This shop is laid out as "${existing.layout}". The layout cannot change after init — moving files is not a regeneration.`);
|
|
693
490
|
return 1;
|
|
@@ -695,10 +492,10 @@ async function init(args) {
|
|
|
695
492
|
}
|
|
696
493
|
const layout = existing?.layout ?? flags.layout ?? await askLayout();
|
|
697
494
|
if (layout === "strict") {
|
|
698
|
-
p2.log.step(`Strict: the app will be generated under ${
|
|
495
|
+
p2.log.step(`Strict: the app will be generated under ${color3.cyan(".vc/app")} — nothing there is yours to edit.`);
|
|
699
496
|
} else if (layout === "app") {
|
|
700
497
|
if (!isVoidApp(root)) {
|
|
701
|
-
p2.log.step(`No Void app here yet — ${
|
|
498
|
+
p2.log.step(`No Void app here yet — ${color3.cyan("void init")} first, then the shop.`);
|
|
702
499
|
const before = new Set(voidAppsIn(root));
|
|
703
500
|
const code = await runVoid(["init"]);
|
|
704
501
|
if (code !== 0)
|
|
@@ -711,7 +508,7 @@ async function init(args) {
|
|
|
711
508
|
}
|
|
712
509
|
root = created[0];
|
|
713
510
|
process.chdir(root);
|
|
714
|
-
p2.log.step(`Continuing in ${
|
|
511
|
+
p2.log.step(`Continuing in ${color3.cyan(`${basename(root)}/`)}`);
|
|
715
512
|
}
|
|
716
513
|
}
|
|
717
514
|
} else {
|
|
@@ -720,11 +517,11 @@ async function init(args) {
|
|
|
720
517
|
["frontend", "the storefront — prerendered and served from the worker's own assets"]
|
|
721
518
|
];
|
|
722
519
|
for (const [dir, starter] of parts) {
|
|
723
|
-
const target =
|
|
520
|
+
const target = join2(root, dir);
|
|
724
521
|
if (isVoidApp(target))
|
|
725
522
|
continue;
|
|
726
523
|
await mkdir(target, { recursive: true });
|
|
727
|
-
p2.log.step(`${
|
|
524
|
+
p2.log.step(`${color3.cyan("void init")} in ${color3.cyan(`${dir}/`)} — choose ${starter}.`);
|
|
728
525
|
const code = await runVoid(["init"], target);
|
|
729
526
|
if (code !== 0)
|
|
730
527
|
return code;
|
|
@@ -742,7 +539,7 @@ async function init(args) {
|
|
|
742
539
|
if (problems.length > 0) {
|
|
743
540
|
p2.log.error("These answers contradict each other:");
|
|
744
541
|
for (const problem of problems)
|
|
745
|
-
p2.log.message(` ${
|
|
542
|
+
p2.log.message(` ${color3.red("✗")} ${problem}`);
|
|
746
543
|
p2.cancel("Nothing was written.");
|
|
747
544
|
return 1;
|
|
748
545
|
}
|
|
@@ -758,16 +555,16 @@ async function init(args) {
|
|
|
758
555
|
spinner2.stop("Generated");
|
|
759
556
|
const { secrets, plaintext } = envSummary(manifest);
|
|
760
557
|
p2.note([
|
|
761
|
-
...result.written.map((file) => `${
|
|
762
|
-
...result.kept.map((file) => `${
|
|
763
|
-
...result.retired.map((file) => `${
|
|
558
|
+
...result.written.map((file) => `${color3.green("+")} ${file}`),
|
|
559
|
+
...result.kept.map((file) => `${color3.dim("=")} ${file} ${color3.dim("(kept — yours)")}`),
|
|
560
|
+
...result.retired.map((file) => `${color3.red("-")} ${file} ${color3.dim("(retired — no longer generated)")}`)
|
|
764
561
|
].join(`
|
|
765
562
|
`), "Files");
|
|
766
563
|
p2.note([
|
|
767
|
-
`${
|
|
564
|
+
`${color3.bold(String(secrets.length))} secrets ${color3.dim("→ wrangler secret put <NAME>")}`,
|
|
768
565
|
...secrets.map((key) => ` ${key}`),
|
|
769
566
|
"",
|
|
770
|
-
`${
|
|
567
|
+
`${color3.bold(String(plaintext.length))} plaintext ${color3.dim("→ .env.production, already written")}`,
|
|
771
568
|
...plaintext.map((key) => ` ${key}`)
|
|
772
569
|
].join(`
|
|
773
570
|
`), "Before going live");
|
|
@@ -777,21 +574,21 @@ async function init(args) {
|
|
|
777
574
|
return code;
|
|
778
575
|
}
|
|
779
576
|
const next = layout === "app" ? [
|
|
780
|
-
` ${
|
|
781
|
-
` ${
|
|
782
|
-
` ${
|
|
783
|
-
` ${
|
|
577
|
+
` ${color3.cyan("bun install")}`,
|
|
578
|
+
` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
|
|
579
|
+
` ${color3.cyan("vc dev")}`,
|
|
580
|
+
` ${color3.cyan("vc preflight")} ${color3.dim("what is still missing, and why it matters")}`
|
|
784
581
|
] : layout === "strict" ? [
|
|
785
|
-
` ${
|
|
786
|
-
` ${
|
|
787
|
-
` ${
|
|
788
|
-
` ${
|
|
582
|
+
` ${color3.cyan("bun install")}`,
|
|
583
|
+
` ${color3.cyan("vc generate")} ${color3.dim("the app under .vc/app, void's artifacts, the migrations")}`,
|
|
584
|
+
` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
|
|
585
|
+
` ${color3.cyan("vc dev")} ${color3.dim("runs in .vc/app")}`
|
|
789
586
|
] : [
|
|
790
|
-
` ${
|
|
791
|
-
` ${
|
|
792
|
-
` ${
|
|
793
|
-
` ${
|
|
794
|
-
` ${
|
|
587
|
+
` ${color3.cyan("bun install")} ${color3.dim("one install for both workspaces")}`,
|
|
588
|
+
` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
|
|
589
|
+
` ${color3.cyan("bun run dev:api")} ${color3.dim("the worker, with the panel")}`,
|
|
590
|
+
` ${color3.cyan("bun run dev:frontend")} ${color3.dim("the storefront, against the local API")}`,
|
|
591
|
+
` ${color3.cyan("bun run preflight")} ${color3.dim("what is still missing, and why it matters")}`
|
|
795
592
|
];
|
|
796
593
|
p2.outro(["Next:", ...next].join(`
|
|
797
594
|
`));
|
|
@@ -800,7 +597,7 @@ async function init(args) {
|
|
|
800
597
|
|
|
801
598
|
// src/regenerate.ts
|
|
802
599
|
import * as p3 from "@clack/prompts";
|
|
803
|
-
import
|
|
600
|
+
import color4 from "picocolors";
|
|
804
601
|
async function generateCommand() {
|
|
805
602
|
const project = await findProject();
|
|
806
603
|
if (!project) {
|
|
@@ -816,9 +613,9 @@ async function generateCommand() {
|
|
|
816
613
|
}
|
|
817
614
|
const result = await generate(project.root, project.manifest);
|
|
818
615
|
p3.log.step([
|
|
819
|
-
...result.written.map((file) => `${
|
|
820
|
-
...result.kept.map((file) => `${
|
|
821
|
-
...result.retired.map((file) => `${
|
|
616
|
+
...result.written.map((file) => `${color4.green("+")} ${file}`),
|
|
617
|
+
...result.kept.map((file) => `${color4.dim("=")} ${file} ${color4.dim("(kept)")}`),
|
|
618
|
+
...result.retired.map((file) => `${color4.red("-")} ${file} ${color4.dim("(retired — no longer generated)")}`)
|
|
822
619
|
].join(`
|
|
823
620
|
`));
|
|
824
621
|
return project.manifest.layout === "strict" ? finishStrict(project.root) : 0;
|
|
@@ -838,7 +635,7 @@ async function generateHelp() {
|
|
|
838
635
|
|
|
839
636
|
// src/scripts.ts
|
|
840
637
|
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
841
|
-
import { join as
|
|
638
|
+
import { join as join3, relative } from "node:path";
|
|
842
639
|
function appScript(name) {
|
|
843
640
|
return async (args) => {
|
|
844
641
|
const project = await findProject();
|
|
@@ -847,7 +644,7 @@ function appScript(name) {
|
|
|
847
644
|
const code = await ensureGenerated(project);
|
|
848
645
|
if (code !== 0)
|
|
849
646
|
return code;
|
|
850
|
-
const pkgPath =
|
|
647
|
+
const pkgPath = join3(project.appDir, "package.json");
|
|
851
648
|
const scripts = existsSync2(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {} : {};
|
|
852
649
|
if (!scripts[name]) {
|
|
853
650
|
console.error(`vc: ${relative(process.cwd(), pkgPath) || "package.json"} has no "${name}" script — void init writes one.`);
|
|
@@ -883,7 +680,6 @@ var EXTENDED = {
|
|
|
883
680
|
preflight: { run: preflightCommand, help: preflightHelp },
|
|
884
681
|
secrets: { run: secretsCliCommand, help: secretsHelp },
|
|
885
682
|
keys: { run: keysCliCommand, help: keysHelp },
|
|
886
|
-
link: { run: linkCliCommand, help: linkHelp },
|
|
887
683
|
guard: { run: guardCommand, help: async () => guardCommand() },
|
|
888
684
|
dev: { run: appScript("dev"), help: appScriptHelp("dev") },
|
|
889
685
|
build: { run: appScript("build"), help: appScriptHelp("build") },
|
package/dist/deploy/index.d.ts
CHANGED
|
@@ -43,6 +43,3 @@ 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
|
-
/** `vc link` — GitHub holds the credentials; this is what puts them there. */
|
|
47
|
-
export declare function linkCliCommand(args: string[]): Promise<number>;
|
|
48
|
-
export declare function linkHelp(): Promise<number>;
|
package/dist/deploy/secrets.d.ts
CHANGED
|
@@ -121,6 +121,16 @@ export declare function initSecrets(project: Project): Promise<number>;
|
|
|
121
121
|
* command runs, and shells keep it in history besides.
|
|
122
122
|
*/
|
|
123
123
|
export declare function setSecretValue(project: Project, args: string[]): Promise<number>;
|
|
124
|
+
/**
|
|
125
|
+
* One value out of `.env.secrets`, by name.
|
|
126
|
+
*
|
|
127
|
+
* How the deploy gets its Cloudflare credentials without anyone having put
|
|
128
|
+
* them in the environment: they are ordinary secrets in the committed file,
|
|
129
|
+
* so the deploy reads them out of what was pushed. Returns null rather than
|
|
130
|
+
* throwing — a caller that cannot decrypt has an env var to fall back on,
|
|
131
|
+
* and a better error to give than this one.
|
|
132
|
+
*/
|
|
133
|
+
export declare function secretValue(project: Project, name: string): Promise<string | null>;
|
|
124
134
|
/**
|
|
125
135
|
* Encrypt one value into `.env.secrets`, in process.
|
|
126
136
|
*
|