@saastemly/voidcommerce 0.18.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 +71 -411
- package/dist/deploy/index.d.ts +0 -6
- package/dist/deploy/secrets.d.ts +10 -0
- package/dist/{index-khzk6a9z.js → index-68m1eg3f.js} +29 -49
- package/dist/{index-3j6jtjmk.js → index-y4dk27kf.js} +45 -64
- package/dist/index.js +2 -2
- package/dist/{keys-fn6wbv40.js → keys-e6w5dcxx.js} +1 -1
- package/package.json +1 -1
- package/src/cli.ts +0 -6
- package/src/deploy/cloudflare.ts +23 -1
- package/src/deploy/index.ts +0 -71
- package/src/deploy/secrets.ts +27 -1
- package/src/generate/ci.ts +30 -62
- package/src/generate/env.ts +22 -0
- package/dist/deploy/link.d.ts +0 -2
- package/dist/deploy/publish.d.ts +0 -32
- package/src/deploy/link.ts +0 -220
- package/src/deploy/publish.ts +0 -145
package/dist/cli.js
CHANGED
|
@@ -21,312 +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
|
-
findGh,
|
|
36
|
-
getVariable,
|
|
37
|
-
ghAuth,
|
|
38
32
|
ignoresKeyFile,
|
|
39
|
-
initSecrets,
|
|
40
|
-
keyState,
|
|
41
33
|
keysCommand,
|
|
42
|
-
localPrivateKey,
|
|
43
34
|
plaintextSecretEntries,
|
|
44
35
|
plaintextSecretNames,
|
|
45
|
-
provisionKey,
|
|
46
|
-
publicKeyFor,
|
|
47
|
-
repoSlug,
|
|
48
36
|
run,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
secretsCommand,
|
|
52
|
-
setSecret,
|
|
53
|
-
setVariable,
|
|
54
|
-
variableNames,
|
|
55
|
-
verifyCloudflareToken
|
|
56
|
-
} from "./index-khzk6a9z.js";
|
|
37
|
+
secretsCommand
|
|
38
|
+
} from "./index-68m1eg3f.js";
|
|
57
39
|
import {
|
|
58
40
|
LAYOUTS,
|
|
59
41
|
oneOrigin,
|
|
60
42
|
readManifest,
|
|
61
43
|
validate,
|
|
62
|
-
writeManifest,
|
|
63
44
|
zoneOf
|
|
64
45
|
} from "./index-30y19qz5.js";
|
|
65
46
|
import {
|
|
66
47
|
GROUPS
|
|
67
48
|
} from "./index-xyjhy6kp.js";
|
|
68
|
-
import
|
|
69
|
-
__require
|
|
70
|
-
} from "./index-0v6na3yp.js";
|
|
49
|
+
import"./index-0v6na3yp.js";
|
|
71
50
|
|
|
72
51
|
// src/deploy/index.ts
|
|
73
|
-
import color3 from "picocolors";
|
|
74
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
75
|
-
import { join as join3 } from "node:path";
|
|
76
|
-
|
|
77
|
-
// src/deploy/link.ts
|
|
78
|
-
import { rmSync } from "node:fs";
|
|
79
|
-
import { join } from "node:path";
|
|
80
52
|
import color from "picocolors";
|
|
81
|
-
var TOKEN_SECRET = "CLOUDFLARE_API_TOKEN";
|
|
82
|
-
var ACCOUNT_VAR = "CLOUDFLARE_ACCOUNT_ID";
|
|
83
|
-
var SCOPES = [
|
|
84
|
-
["Account · Workers Scripts: Edit", "deploy the worker"],
|
|
85
|
-
["Account · D1: Edit", "create the database and apply migrations"],
|
|
86
|
-
["Account · Queues: Edit", "create the order queue"],
|
|
87
|
-
["Account · Workers KV Storage: Edit", "sessions and caches"],
|
|
88
|
-
["Account · Workers R2 Storage: Edit", "product images"],
|
|
89
|
-
["Account · Account Settings: Read", "confirm which account this is"],
|
|
90
|
-
["Zone · Workers Routes: Edit", "answer on your domain"],
|
|
91
|
-
["Zone · Zone: Read", "confirm the domain's zone is on this account"],
|
|
92
|
-
["Zone · DNS: Read", "notice a hostname that already answers, before taking it over"],
|
|
93
|
-
["User · User Details: Read", "wrangler asks at startup"]
|
|
94
|
-
];
|
|
95
|
-
async function linkCommand(project, args) {
|
|
96
|
-
const p = await import("@clack/prompts");
|
|
97
|
-
const root = project.root;
|
|
98
|
-
const relink = args.includes("--force");
|
|
99
|
-
p.intro(color.bgCyan(color.black(" vc link ")));
|
|
100
|
-
const auth = await ghAuth(root);
|
|
101
|
-
if (!auth.ok) {
|
|
102
|
-
p.cancel(auth.reason ?? "gh is unavailable");
|
|
103
|
-
return 1;
|
|
104
|
-
}
|
|
105
|
-
const slug = await repoSlug(root);
|
|
106
|
-
if (!slug) {
|
|
107
|
-
p.cancel(`this checkout has no GitHub repository, and everything below is stored on one.
|
|
108
|
-
|
|
109
|
-
${color.cyan("gh repo create --source=. --private --push")}
|
|
110
|
-
|
|
111
|
-
then run \`vc link\` again.`);
|
|
112
|
-
return 1;
|
|
113
|
-
}
|
|
114
|
-
p.log.success(`${color.green("✓")} ${slug}, as ${auth.user ?? "you"}`);
|
|
115
|
-
const keys = await keyState(project);
|
|
116
|
-
if (keys.inGitHub && !relink) {
|
|
117
|
-
p.log.info(`${PRIVATE_KEY_VAR} is already set — leaving it alone (--force replaces it, which orphans ${SECRETS_FILE})`);
|
|
118
|
-
} else if (keys.inGitHub && relink) {
|
|
119
|
-
p.log.warn(`--force: replacing ${PRIVATE_KEY_VAR} makes every value in ${SECRETS_FILE} unreadable. Use \`vc keys --rotate\` to re-encrypt instead.`);
|
|
120
|
-
return 1;
|
|
121
|
-
} else {
|
|
122
|
-
const parked = localPrivateKey(root);
|
|
123
|
-
if (parked) {
|
|
124
|
-
const sent = await setVariable(root, PRIVATE_KEY_VAR, parked);
|
|
125
|
-
if (!sent.ok) {
|
|
126
|
-
p.cancel(`GitHub refused the variable: ${sent.error ?? "unknown error"}`);
|
|
127
|
-
return 1;
|
|
128
|
-
}
|
|
129
|
-
const publicKey = await publicKeyFor(parked);
|
|
130
|
-
if (publicKey)
|
|
131
|
-
committedPublicKeyInto(root, publicKey);
|
|
132
|
-
const readBack = await getVariable(root, PRIVATE_KEY_VAR);
|
|
133
|
-
if (readBack !== parked) {
|
|
134
|
-
p.log.warn(`${color.yellow("!")} ${PRIVATE_KEY_VAR} was written to ${slug} but could not be read back,
|
|
135
|
-
so ${LOCAL_KEY_FILE} has been LEFT IN PLACE. It is currently the only copy
|
|
136
|
-
` + " of the key that opens this shop — do not delete it until `vc keys --restore` works.");
|
|
137
|
-
} else {
|
|
138
|
-
rmSync(join(root, LOCAL_KEY_FILE), { force: true });
|
|
139
|
-
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`);
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
const made = await provisionKey(project);
|
|
143
|
-
if (!made.ok) {
|
|
144
|
-
p.cancel(made.reason);
|
|
145
|
-
return 1;
|
|
146
|
-
}
|
|
147
|
-
committedPublicKeyInto(root, made.publicKey);
|
|
148
|
-
p.log.success(`${color.green("✓")} ${PRIVATE_KEY_VAR} generated and stored on GitHub; public half in ${SECRETS_FILE}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const existing = await secretNames(root);
|
|
152
|
-
if (existing.has(TOKEN_SECRET) && !relink) {
|
|
153
|
-
p.log.info(`${TOKEN_SECRET} is already set — pass --force to replace it`);
|
|
154
|
-
} else {
|
|
155
|
-
p.log.step(`A Cloudflare API token, from ${color.cyan("https://dash.cloudflare.com/profile/api-tokens")} → Create Token → Custom token`);
|
|
156
|
-
console.log(SCOPES.map(([scope, why]) => ` ${scope.padEnd(38)} ${color.dim(why)}`).join(`
|
|
157
|
-
`));
|
|
158
|
-
console.log(color.dim(`
|
|
159
|
-
The token Cloudflare generates for its own Workers Builds will NOT do:
|
|
160
|
-
it has no D1 and no Queues permission, so it cannot create the database.
|
|
161
|
-
`));
|
|
162
|
-
const token = await p.password({
|
|
163
|
-
message: "Paste it (nothing is written to disk)",
|
|
164
|
-
validate: (input) => input && input.length >= 20 ? undefined : "that is too short to be a Cloudflare token"
|
|
165
|
-
});
|
|
166
|
-
if (p.isCancel(token)) {
|
|
167
|
-
p.cancel("nothing was stored.");
|
|
168
|
-
return 1;
|
|
169
|
-
}
|
|
170
|
-
const spinner = p.spinner();
|
|
171
|
-
spinner.start("asking Cloudflare whether that token is real");
|
|
172
|
-
const verified = await verifyCloudflareToken(String(token));
|
|
173
|
-
if (!verified.ok) {
|
|
174
|
-
spinner.stop(`${color.red("✗")} Cloudflare rejected it: ${verified.detail}`);
|
|
175
|
-
p.cancel("nothing was stored.");
|
|
176
|
-
return 1;
|
|
177
|
-
}
|
|
178
|
-
const accounts = await cloudflareAccounts(String(token));
|
|
179
|
-
spinner.stop(`${color.green("✓")} the token is active and sees ${accounts.length} account${accounts.length === 1 ? "" : "s"}`);
|
|
180
|
-
let accountId = project.manifest.cloudflare?.accountId ?? "";
|
|
181
|
-
if (accounts.length === 1) {
|
|
182
|
-
accountId = accounts[0].id;
|
|
183
|
-
p.log.info(`account "${accounts[0].name}"`);
|
|
184
|
-
} else if (accounts.length > 1) {
|
|
185
|
-
const picked = await p.select({
|
|
186
|
-
message: "Which account is this shop in?",
|
|
187
|
-
options: accounts.map((account) => ({ value: account.id, label: account.name, hint: account.id }))
|
|
188
|
-
});
|
|
189
|
-
if (p.isCancel(picked)) {
|
|
190
|
-
p.cancel("nothing was stored.");
|
|
191
|
-
return 1;
|
|
192
|
-
}
|
|
193
|
-
accountId = String(picked);
|
|
194
|
-
}
|
|
195
|
-
const stored = await setSecret(root, TOKEN_SECRET, String(token));
|
|
196
|
-
if (!stored.ok) {
|
|
197
|
-
p.cancel(`GitHub refused the secret: ${stored.error ?? "unknown error"}`);
|
|
198
|
-
return 1;
|
|
199
|
-
}
|
|
200
|
-
p.log.success(`${color.green("✓")} ${TOKEN_SECRET} stored on ${slug}`);
|
|
201
|
-
if (accountId) {
|
|
202
|
-
const varred = await setVariable(root, ACCOUNT_VAR, accountId);
|
|
203
|
-
if (varred.ok)
|
|
204
|
-
p.log.success(`${color.green("✓")} ${ACCOUNT_VAR} set as a repository variable`);
|
|
205
|
-
project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
|
|
206
|
-
await writeManifest(root, project.manifest);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (declaredSecretNames(root).size === 0) {
|
|
210
|
-
p.log.step(`writing ${SECRETS_FILE}`);
|
|
211
|
-
await initSecrets(project);
|
|
212
|
-
}
|
|
213
|
-
const secrets = await secretNames(root);
|
|
214
|
-
const vars = await variableNames(root);
|
|
215
|
-
p.outro([
|
|
216
|
-
`${color.bold("Ready.")} From here, ${color.cyan("git push")} is the deploy.`,
|
|
217
|
-
"",
|
|
218
|
-
` ${secrets.has(PRIVATE_KEY_VAR) ? color.green("✓") : color.red("✗")} ${PRIVATE_KEY_VAR} ${color.dim("reads the shop's secrets")}`,
|
|
219
|
-
` ${secrets.has(TOKEN_SECRET) ? color.green("✓") : color.red("✗")} ${TOKEN_SECRET} ${color.dim("deploys to Cloudflare")}`,
|
|
220
|
-
` ${vars.has(ACCOUNT_VAR) ? color.green("✓") : color.dim("·")} ${ACCOUNT_VAR} ${color.dim("which account")}`,
|
|
221
|
-
"",
|
|
222
|
-
` Set the shop's own secrets with ${color.cyan("vc secrets set KEY")} — that needs no`,
|
|
223
|
-
` credential at all, because encryption uses the public key in the repository.`
|
|
224
|
-
].join(`
|
|
225
|
-
`));
|
|
226
|
-
return 0;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// src/deploy/publish.ts
|
|
230
53
|
import { existsSync } from "node:fs";
|
|
231
|
-
import { join
|
|
232
|
-
import color2 from "picocolors";
|
|
233
|
-
async function publishCommand(project, args) {
|
|
234
|
-
const p = await import("@clack/prompts");
|
|
235
|
-
const root = project.root;
|
|
236
|
-
const gh = findGh();
|
|
237
|
-
p.intro(color2.bgCyan(color2.black(" vc publish ")));
|
|
238
|
-
const auth = await ghAuth(root);
|
|
239
|
-
if (!auth.ok || !gh) {
|
|
240
|
-
p.cancel(auth.reason ?? "the GitHub CLI is not installed. https://cli.github.com");
|
|
241
|
-
return 1;
|
|
242
|
-
}
|
|
243
|
-
if (!existsSync(join2(root, ".git"))) {
|
|
244
|
-
p.cancel(`${root} is not a git repository. \`git init\` first, and commit what you have.`);
|
|
245
|
-
return 1;
|
|
246
|
-
}
|
|
247
|
-
let slug = await repoSlug(root);
|
|
248
|
-
if (slug) {
|
|
249
|
-
p.log.info(`${slug} already exists — using it`);
|
|
250
|
-
} else {
|
|
251
|
-
const suggested = project.manifest.shop.domain.split(".")[0] ?? "shop";
|
|
252
|
-
const name = await p.text({
|
|
253
|
-
message: "Repository name",
|
|
254
|
-
initialValue: suggested,
|
|
255
|
-
validate: (value) => /^[A-Za-z0-9._-]+$/.test(value.trim()) ? undefined : "letters, digits, dot, dash or underscore"
|
|
256
|
-
});
|
|
257
|
-
if (p.isCancel(name)) {
|
|
258
|
-
p.cancel("nothing was created.");
|
|
259
|
-
return 1;
|
|
260
|
-
}
|
|
261
|
-
const visibility = await p.select({
|
|
262
|
-
message: "Visibility",
|
|
263
|
-
options: [
|
|
264
|
-
{ value: "--private", label: "Private", hint: "the key that opens your secrets is readable to anyone with access" },
|
|
265
|
-
{ value: "--public", label: "Public", hint: "only if this shop keeps no secrets at all" }
|
|
266
|
-
],
|
|
267
|
-
initialValue: "--private"
|
|
268
|
-
});
|
|
269
|
-
if (p.isCancel(visibility)) {
|
|
270
|
-
p.cancel("nothing was created.");
|
|
271
|
-
return 1;
|
|
272
|
-
}
|
|
273
|
-
const spinner2 = p.spinner();
|
|
274
|
-
spinner2.start(`creating ${String(name)}`);
|
|
275
|
-
const created = await run(gh, ["repo", "create", String(name).trim(), "--source=.", String(visibility)], root);
|
|
276
|
-
if (created.code !== 0) {
|
|
277
|
-
spinner2.stop(`${color2.red("✗")} could not create it`);
|
|
278
|
-
p.cancel(created.out.trim().split(`
|
|
279
|
-
`).slice(-2).join(" "));
|
|
280
|
-
return 1;
|
|
281
|
-
}
|
|
282
|
-
slug = await repoSlug(root);
|
|
283
|
-
spinner2.stop(`${color2.green("✓")} ${slug ?? String(name)} created, and set as origin`);
|
|
284
|
-
}
|
|
285
|
-
p.log.step("giving the repository what the deploy needs");
|
|
286
|
-
const linked = await linkCommand(project, args);
|
|
287
|
-
if (linked !== 0) {
|
|
288
|
-
p.cancel("the repository exists but is not configured, so a push would not deploy. Fix the above and run `vc publish` again.");
|
|
289
|
-
return 1;
|
|
290
|
-
}
|
|
291
|
-
const dirty = (await run("git", ["status", "--porcelain"], root)).out.trim();
|
|
292
|
-
if (dirty) {
|
|
293
|
-
const staged = await run("git", ["add", "-A"], root);
|
|
294
|
-
const committed = await run("git", ["commit", "-m", "vc publish: link this shop to its repository"], root);
|
|
295
|
-
if (staged.code !== 0 || committed.code !== 0) {
|
|
296
|
-
p.cancel(`could not commit the changes vc link made:
|
|
297
|
-
${committed.out.trim().split(`
|
|
298
|
-
`).slice(-3).join(`
|
|
299
|
-
`)}`);
|
|
300
|
-
return 1;
|
|
301
|
-
}
|
|
302
|
-
p.log.success(`${color2.green("✓")} committed what linking changed`);
|
|
303
|
-
}
|
|
304
|
-
const branch = (await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], root)).out.trim() || "main";
|
|
305
|
-
const spinner = p.spinner();
|
|
306
|
-
spinner.start(`pushing ${branch}`);
|
|
307
|
-
const pushed = await run("git", ["push", "-u", "origin", `${branch}:main`], root);
|
|
308
|
-
if (pushed.code !== 0) {
|
|
309
|
-
spinner.stop(`${color2.red("✗")} the push failed`);
|
|
310
|
-
p.cancel(pushed.out.trim().split(`
|
|
311
|
-
`).slice(-4).join(`
|
|
312
|
-
`));
|
|
313
|
-
return 1;
|
|
314
|
-
}
|
|
315
|
-
spinner.stop(`${color2.green("✓")} pushed to main`);
|
|
316
|
-
p.outro([
|
|
317
|
-
`${color2.bold("Published.")} The deploy is running now.`,
|
|
318
|
-
"",
|
|
319
|
-
` ${color2.cyan(`https://github.com/${slug}/actions`)}`,
|
|
320
|
-
"",
|
|
321
|
-
` From here ${color2.cyan("git push")} is the whole loop. \`vc\` is only needed to`,
|
|
322
|
-
` change a secret (${color2.cyan("vc secrets set KEY")}) or read one back`,
|
|
323
|
-
` (${color2.cyan("vc keys --restore")}).`
|
|
324
|
-
].join(`
|
|
325
|
-
`));
|
|
326
|
-
return 0;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// src/deploy/index.ts
|
|
54
|
+
import { join } from "node:path";
|
|
330
55
|
async function deployCommand(args) {
|
|
331
56
|
const own = new Set(["--cloudflare", "--provision", "--force"]);
|
|
332
57
|
const cloudflare = args.includes("--cloudflare");
|
|
@@ -349,12 +74,12 @@ async function deployCommand(args) {
|
|
|
349
74
|
const check = await preflight(project, "void");
|
|
350
75
|
printPreflight(project, check, "void");
|
|
351
76
|
if (!check.ready && check.remote !== null && !args.includes("--force")) {
|
|
352
|
-
console.error(`${
|
|
77
|
+
console.error(`${color.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
|
|
353
78
|
`);
|
|
354
79
|
return 1;
|
|
355
80
|
}
|
|
356
81
|
if (!check.ready && check.remote === null) {
|
|
357
|
-
console.log(
|
|
82
|
+
console.log(color.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
|
|
358
83
|
`));
|
|
359
84
|
}
|
|
360
85
|
return runVoid(["deploy", ...voids], project.appDir);
|
|
@@ -372,7 +97,7 @@ async function preflightCommand(args) {
|
|
|
372
97
|
return result.ready ? 0 : 1;
|
|
373
98
|
const pending = await uncommittedMigrations(project.root);
|
|
374
99
|
if (pending.length > 0) {
|
|
375
|
-
console.error(`${
|
|
100
|
+
console.error(`${color.red("✗")} ${pending.length} migration file${pending.length === 1 ? " is" : "s are"} not committed:
|
|
376
101
|
` + ` ${pending.join(`
|
|
377
102
|
`)}
|
|
378
103
|
|
|
@@ -384,7 +109,7 @@ async function preflightCommand(args) {
|
|
|
384
109
|
return result.ready ? 0 : 1;
|
|
385
110
|
}
|
|
386
111
|
async function uncommittedMigrations(root) {
|
|
387
|
-
const status = await
|
|
112
|
+
const status = await run("git", ["status", "--porcelain", "--", "migrations"], root);
|
|
388
113
|
if (status.code !== 0)
|
|
389
114
|
return [];
|
|
390
115
|
return status.out.split(`
|
|
@@ -397,13 +122,13 @@ async function deployHelp() {
|
|
|
397
122
|
line("vc's preflight — every required key set, the hostnames right — then", width),
|
|
398
123
|
line("void deploy, untouched. Or, with --cloudflare, your own account via wrangler.", width),
|
|
399
124
|
line("", width),
|
|
400
|
-
line(
|
|
125
|
+
line(color.bold("Usage"), width),
|
|
401
126
|
...row("vc deploy [void's flags]", "preflight, then void deploy — the Void platform, void's login", width, 2),
|
|
402
127
|
...row("vc deploy --cloudflare", "wrangler, your Cloudflare account, no Void login: preflight, build, scrub baked secrets, migrate D1, deploy", width, 2),
|
|
403
128
|
...row("vc deploy --cloudflare --provision", "first time: create the D1 database and the queue, record them", width, 2),
|
|
404
129
|
...row("vc deploy --cloudflare --force", "deploy a shop preflight says is not ready — deliberately", width, 2),
|
|
405
130
|
line("", width),
|
|
406
|
-
line(
|
|
131
|
+
line(color.bold("Needs, for --cloudflare"), width),
|
|
407
132
|
...row("CLOUDFLARE_API_TOKEN", "in the environment; the account is pinned for you when there is one", width, 2),
|
|
408
133
|
...row(PRIVATE_KEY_VAR, "to decrypt the shop's own secrets", width, 2),
|
|
409
134
|
line("", width),
|
|
@@ -499,9 +224,9 @@ async function guardCommand() {
|
|
|
499
224
|
if (!project)
|
|
500
225
|
return 0;
|
|
501
226
|
const root = project.root;
|
|
502
|
-
if (
|
|
227
|
+
if (existsSync(join(root, LOCAL_KEY_FILE)) && !ignoresKeyFile(root)) {
|
|
503
228
|
console.error(`
|
|
504
|
-
${
|
|
229
|
+
${color.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gitignored.
|
|
505
230
|
|
|
506
231
|
` + ` It holds the private key that opens every secret in this repository.
|
|
507
232
|
` + ` Add ${LOCAL_KEY_FILE} to .gitignore before committing anything.
|
|
@@ -514,24 +239,24 @@ ${color3.red("✗ refusing the commit")}: ${LOCAL_KEY_FILE} exists and is NOT gi
|
|
|
514
239
|
const publicKey = committedPublicKey(root);
|
|
515
240
|
if (!publicKey) {
|
|
516
241
|
console.error(`
|
|
517
|
-
${
|
|
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.
|
|
518
243
|
|
|
519
244
|
` + ` ${bare.map((entry) => entry.name).join(`
|
|
520
245
|
`)}
|
|
521
246
|
|
|
522
|
-
` + ` ${
|
|
247
|
+
` + ` ${color.cyan("vc keys --init")} makes one — no GitHub repository needed yet.
|
|
523
248
|
|
|
524
|
-
` +
|
|
249
|
+
` + color.dim(` Committing a secret in the clear cannot be undone by a later commit;
|
|
525
250
|
the value stays in the history and must be treated as burned.
|
|
526
251
|
`));
|
|
527
252
|
return 1;
|
|
528
253
|
}
|
|
529
|
-
const staged = (await
|
|
254
|
+
const staged = (await run("git", ["diff", "--cached", "--name-only", "--", SECRETS_FILE], root)).out.trim().length > 0;
|
|
530
255
|
for (const entry of bare) {
|
|
531
256
|
const sealed = await encryptInto(root, publicKey, entry.name, entry.value);
|
|
532
257
|
if (!sealed.ok) {
|
|
533
258
|
console.error(`
|
|
534
|
-
${
|
|
259
|
+
${color.red("✗ refusing the commit")}: ${sealed.error}
|
|
535
260
|
`);
|
|
536
261
|
return 1;
|
|
537
262
|
}
|
|
@@ -539,99 +264,36 @@ ${color3.red("✗ refusing the commit")}: ${sealed.error}
|
|
|
539
264
|
const left = plaintextSecretNames(root);
|
|
540
265
|
if (left.length > 0) {
|
|
541
266
|
console.error(`
|
|
542
|
-
${
|
|
267
|
+
${color.red("✗ refusing the commit")}: ${left.join(", ")} could not be encrypted.
|
|
543
268
|
`);
|
|
544
269
|
return 1;
|
|
545
270
|
}
|
|
546
271
|
if (staged) {
|
|
547
|
-
const added = await
|
|
272
|
+
const added = await run("git", ["add", "--", SECRETS_FILE], root);
|
|
548
273
|
if (added.code !== 0) {
|
|
549
274
|
console.error(`
|
|
550
|
-
${
|
|
275
|
+
${color.red("✗ refusing the commit")}: ${SECRETS_FILE} was encrypted but could not be re-staged,
|
|
551
276
|
` + ` so the commit would still carry the plaintext you staged. \`git add ${SECRETS_FILE}\`.
|
|
552
277
|
`);
|
|
553
278
|
return 1;
|
|
554
279
|
}
|
|
555
280
|
}
|
|
556
281
|
console.log(`
|
|
557
|
-
${
|
|
558
|
-
` +
|
|
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.
|
|
559
284
|
`));
|
|
560
285
|
return 0;
|
|
561
286
|
}
|
|
562
|
-
async function linkCliCommand(args) {
|
|
563
|
-
const project = await findProject();
|
|
564
|
-
if (!project) {
|
|
565
|
-
console.error("vc: no voidcommerce.json here.");
|
|
566
|
-
return 1;
|
|
567
|
-
}
|
|
568
|
-
return linkCommand(project, args);
|
|
569
|
-
}
|
|
570
|
-
async function linkHelp() {
|
|
571
|
-
const width = 80;
|
|
572
|
-
console.log(box("vc link", [
|
|
573
|
-
line("Put this shop's credentials on its GitHub repository, once, so that", width),
|
|
574
|
-
line("every deploy after this is a `git push`.", width),
|
|
575
|
-
line("", width),
|
|
576
|
-
...row("vc link", "store the encryption key and the Cloudflare token on the repository", width, 2),
|
|
577
|
-
...row("vc link --force", "replace what is already there", width, 2),
|
|
578
|
-
line("", width),
|
|
579
|
-
line(color3.bold("What it stores, and where"), width),
|
|
580
|
-
...row(PRIVATE_KEY_VAR, "a repository SECRET. A key already waiting locally is MOVED here and the local copy deleted", width, 2),
|
|
581
|
-
...row("CLOUDFLARE_API_TOKEN", "a repository SECRET, checked against the Cloudflare API before it is stored", width, 2),
|
|
582
|
-
...row("CLOUDFLARE_ACCOUNT_ID", "a repository VARIABLE — an identifier, not a credential", width, 2),
|
|
583
|
-
line("", width),
|
|
584
|
-
line(color3.bold("Why one token still has to be typed"), width),
|
|
585
|
-
line("GitHub cannot mint a Cloudflare credential. There is no OIDC federation", width),
|
|
586
|
-
line("between them, and the Cloudflare GitHub App runs the other way: it grants", width),
|
|
587
|
-
line("Cloudflare access to your repository, not your repository access to", width),
|
|
588
|
-
line("Cloudflare. So it is typed once, here, and never stored on this machine.", width)
|
|
589
|
-
], width));
|
|
590
|
-
return 0;
|
|
591
|
-
}
|
|
592
|
-
async function publishCliCommand(args) {
|
|
593
|
-
const project = await findProject();
|
|
594
|
-
if (!project) {
|
|
595
|
-
console.error("vc: no voidcommerce.json here.");
|
|
596
|
-
return 1;
|
|
597
|
-
}
|
|
598
|
-
return publishCommand(project, args);
|
|
599
|
-
}
|
|
600
|
-
async function publishHelp() {
|
|
601
|
-
const width = 80;
|
|
602
|
-
console.log(box("vc publish", [
|
|
603
|
-
line("Create this shop's GitHub repository, give it what the deploy needs,", width),
|
|
604
|
-
line("and push. The push IS the deploy. Run once; after that, `git push`.", width),
|
|
605
|
-
line("", width),
|
|
606
|
-
...row("vc publish", "create the repository, set the credentials, push main", width, 2),
|
|
607
|
-
line("", width),
|
|
608
|
-
line(color3.bold("What it does for you"), width),
|
|
609
|
-
...row("the encryption key", "vc already holds it — set with no input from you", width, 2),
|
|
610
|
-
...row("the account id", "read from voidcommerce.json", width, 2),
|
|
611
|
-
...row("the Cloudflare token", "the one thing it has to ask for", width, 2),
|
|
612
|
-
line("", width),
|
|
613
|
-
line(color3.bold("Why the token cannot be automated away"), width),
|
|
614
|
-
line("There is no OIDC between GitHub and Cloudflare, so something must carry", width),
|
|
615
|
-
line("a token across, and only a person can fetch one from the dashboard. It", width),
|
|
616
|
-
line("is asked for once, checked against the Cloudflare API, and stored on the", width),
|
|
617
|
-
line("repository — never on this machine.", width),
|
|
618
|
-
line("", width),
|
|
619
|
-
line("The repository is created WITHOUT pushing, then configured, then pushed.", width),
|
|
620
|
-
line("Pushing first would start a deploy against a repository that has no key", width),
|
|
621
|
-
line("and no token: a red run, for no reason but the wrong order.", width)
|
|
622
|
-
], width));
|
|
623
|
-
return 0;
|
|
624
|
-
}
|
|
625
287
|
|
|
626
288
|
// src/init.ts
|
|
627
289
|
import * as p2 from "@clack/prompts";
|
|
628
290
|
import { mkdir } from "node:fs/promises";
|
|
629
|
-
import { basename, join as
|
|
630
|
-
import
|
|
291
|
+
import { basename, join as join2 } from "node:path";
|
|
292
|
+
import color3 from "picocolors";
|
|
631
293
|
|
|
632
294
|
// src/wizard.ts
|
|
633
295
|
import * as p from "@clack/prompts";
|
|
634
|
-
import
|
|
296
|
+
import color2 from "picocolors";
|
|
635
297
|
var bail = () => {
|
|
636
298
|
p.cancel("Nothing was written.");
|
|
637
299
|
process.exit(0);
|
|
@@ -653,8 +315,8 @@ async function askGroup(group2, previous, alsoRequired = []) {
|
|
|
653
315
|
const required = group2.choices.filter((choice) => requiredIds.has(choice.id));
|
|
654
316
|
const optional = group2.choices.filter((choice) => !requiredIds.has(choice.id));
|
|
655
317
|
const message = group2.title + (group2.intro ? `
|
|
656
|
-
${
|
|
657
|
-
${
|
|
318
|
+
${color2.dim(group2.intro)}` : "") + (required.length ? `
|
|
319
|
+
${color2.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
|
|
658
320
|
if (optional.length === 0) {
|
|
659
321
|
p.log.info(message);
|
|
660
322
|
return required.map((choice) => choice.id);
|
|
@@ -685,7 +347,7 @@ async function runWizard(existing, layout) {
|
|
|
685
347
|
validate: (value) => value.trim() ? undefined : "A shop has a name."
|
|
686
348
|
}),
|
|
687
349
|
domain: () => p.text({
|
|
688
|
-
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")}`,
|
|
689
351
|
placeholder: "northwind.com",
|
|
690
352
|
initialValue: existing?.shop.domain ?? "",
|
|
691
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."
|
|
@@ -696,7 +358,7 @@ async function runWizard(existing, layout) {
|
|
|
696
358
|
if (!domain || domain === guess)
|
|
697
359
|
return Promise.resolve(undefined);
|
|
698
360
|
return p.text({
|
|
699
|
-
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`)}`,
|
|
700
362
|
placeholder: guess,
|
|
701
363
|
initialValue: existing?.shop.zone ?? guess,
|
|
702
364
|
validate: (value) => {
|
|
@@ -708,13 +370,13 @@ async function runWizard(existing, layout) {
|
|
|
708
370
|
});
|
|
709
371
|
},
|
|
710
372
|
country: () => p.text({
|
|
711
|
-
message: `Country ${
|
|
373
|
+
message: `Country ${color2.dim("— sets the declared VAT rate and the default region")}`,
|
|
712
374
|
placeholder: "US",
|
|
713
375
|
initialValue: existing?.shop.country ?? "",
|
|
714
376
|
validate: (value) => /^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."
|
|
715
377
|
}),
|
|
716
378
|
taxRegistered: () => p.select({
|
|
717
|
-
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")}`,
|
|
718
380
|
options: [
|
|
719
381
|
{ value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
|
|
720
382
|
{ value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" }
|
|
@@ -728,14 +390,14 @@ async function runWizard(existing, layout) {
|
|
|
728
390
|
validate: (value) => /^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."
|
|
729
391
|
}),
|
|
730
392
|
locale: () => p.text({
|
|
731
|
-
message: `Locale ${
|
|
393
|
+
message: `Locale ${color2.dim("— the language of emails and launch content")}`,
|
|
732
394
|
placeholder: "en",
|
|
733
395
|
initialValue: existing?.shop.locale ?? "",
|
|
734
396
|
validate: (value) => /^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."
|
|
735
397
|
}),
|
|
736
398
|
...onPages ? {
|
|
737
399
|
pagesHost: () => p.text({
|
|
738
|
-
message: `GitHub Pages host ${
|
|
400
|
+
message: `GitHub Pages host ${color2.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
|
|
739
401
|
placeholder: "northwind.github.io",
|
|
740
402
|
initialValue: existing?.shop.pagesHost ?? "",
|
|
741
403
|
validate: (value) => !value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank"
|
|
@@ -746,7 +408,7 @@ async function runWizard(existing, layout) {
|
|
|
746
408
|
for (const group2 of GROUPS) {
|
|
747
409
|
if (group2.id === "ui" && layout === "strict") {
|
|
748
410
|
p.log.info(`${group2.title}
|
|
749
|
-
${
|
|
411
|
+
${color2.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
|
|
750
412
|
chosen[group2.id] = ["admin"];
|
|
751
413
|
continue;
|
|
752
414
|
}
|
|
@@ -771,14 +433,14 @@ ${color4.dim("The generated storefront and panel. Strict has no custom code to h
|
|
|
771
433
|
}
|
|
772
434
|
function summarise(manifest) {
|
|
773
435
|
const lines = [
|
|
774
|
-
`${
|
|
436
|
+
`${color2.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
|
|
775
437
|
];
|
|
776
438
|
for (const group2 of GROUPS) {
|
|
777
439
|
const ids = manifest.chosen[group2.id] ?? [];
|
|
778
440
|
if (ids.length === 0)
|
|
779
441
|
continue;
|
|
780
442
|
const labels = ids.map((id) => group2.choices.find((choice) => choice.id === id)?.label ?? id).join(", ");
|
|
781
|
-
lines.push(`${
|
|
443
|
+
lines.push(`${color2.dim(group2.title.padEnd(34))} ${labels}`);
|
|
782
444
|
}
|
|
783
445
|
return lines.join(`
|
|
784
446
|
`);
|
|
@@ -819,10 +481,10 @@ async function init(args) {
|
|
|
819
481
|
if (flags.voids.length > 0)
|
|
820
482
|
return runVoid(["init", ...flags.voids]);
|
|
821
483
|
let root = process.cwd();
|
|
822
|
-
p2.intro(
|
|
484
|
+
p2.intro(color3.bgCyan(color3.black(" voidcommerce ")));
|
|
823
485
|
const existing = await readManifest(root);
|
|
824
486
|
if (existing) {
|
|
825
|
-
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.`);
|
|
826
488
|
if (flags.layout && flags.layout !== existing.layout) {
|
|
827
489
|
p2.log.error(`This shop is laid out as "${existing.layout}". The layout cannot change after init — moving files is not a regeneration.`);
|
|
828
490
|
return 1;
|
|
@@ -830,10 +492,10 @@ async function init(args) {
|
|
|
830
492
|
}
|
|
831
493
|
const layout = existing?.layout ?? flags.layout ?? await askLayout();
|
|
832
494
|
if (layout === "strict") {
|
|
833
|
-
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.`);
|
|
834
496
|
} else if (layout === "app") {
|
|
835
497
|
if (!isVoidApp(root)) {
|
|
836
|
-
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.`);
|
|
837
499
|
const before = new Set(voidAppsIn(root));
|
|
838
500
|
const code = await runVoid(["init"]);
|
|
839
501
|
if (code !== 0)
|
|
@@ -846,7 +508,7 @@ async function init(args) {
|
|
|
846
508
|
}
|
|
847
509
|
root = created[0];
|
|
848
510
|
process.chdir(root);
|
|
849
|
-
p2.log.step(`Continuing in ${
|
|
511
|
+
p2.log.step(`Continuing in ${color3.cyan(`${basename(root)}/`)}`);
|
|
850
512
|
}
|
|
851
513
|
}
|
|
852
514
|
} else {
|
|
@@ -855,11 +517,11 @@ async function init(args) {
|
|
|
855
517
|
["frontend", "the storefront — prerendered and served from the worker's own assets"]
|
|
856
518
|
];
|
|
857
519
|
for (const [dir, starter] of parts) {
|
|
858
|
-
const target =
|
|
520
|
+
const target = join2(root, dir);
|
|
859
521
|
if (isVoidApp(target))
|
|
860
522
|
continue;
|
|
861
523
|
await mkdir(target, { recursive: true });
|
|
862
|
-
p2.log.step(`${
|
|
524
|
+
p2.log.step(`${color3.cyan("void init")} in ${color3.cyan(`${dir}/`)} — choose ${starter}.`);
|
|
863
525
|
const code = await runVoid(["init"], target);
|
|
864
526
|
if (code !== 0)
|
|
865
527
|
return code;
|
|
@@ -877,7 +539,7 @@ async function init(args) {
|
|
|
877
539
|
if (problems.length > 0) {
|
|
878
540
|
p2.log.error("These answers contradict each other:");
|
|
879
541
|
for (const problem of problems)
|
|
880
|
-
p2.log.message(` ${
|
|
542
|
+
p2.log.message(` ${color3.red("✗")} ${problem}`);
|
|
881
543
|
p2.cancel("Nothing was written.");
|
|
882
544
|
return 1;
|
|
883
545
|
}
|
|
@@ -893,16 +555,16 @@ async function init(args) {
|
|
|
893
555
|
spinner2.stop("Generated");
|
|
894
556
|
const { secrets, plaintext } = envSummary(manifest);
|
|
895
557
|
p2.note([
|
|
896
|
-
...result.written.map((file) => `${
|
|
897
|
-
...result.kept.map((file) => `${
|
|
898
|
-
...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)")}`)
|
|
899
561
|
].join(`
|
|
900
562
|
`), "Files");
|
|
901
563
|
p2.note([
|
|
902
|
-
`${
|
|
564
|
+
`${color3.bold(String(secrets.length))} secrets ${color3.dim("→ wrangler secret put <NAME>")}`,
|
|
903
565
|
...secrets.map((key) => ` ${key}`),
|
|
904
566
|
"",
|
|
905
|
-
`${
|
|
567
|
+
`${color3.bold(String(plaintext.length))} plaintext ${color3.dim("→ .env.production, already written")}`,
|
|
906
568
|
...plaintext.map((key) => ` ${key}`)
|
|
907
569
|
].join(`
|
|
908
570
|
`), "Before going live");
|
|
@@ -912,21 +574,21 @@ async function init(args) {
|
|
|
912
574
|
return code;
|
|
913
575
|
}
|
|
914
576
|
const next = layout === "app" ? [
|
|
915
|
-
` ${
|
|
916
|
-
` ${
|
|
917
|
-
` ${
|
|
918
|
-
` ${
|
|
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")}`
|
|
919
581
|
] : layout === "strict" ? [
|
|
920
|
-
` ${
|
|
921
|
-
` ${
|
|
922
|
-
` ${
|
|
923
|
-
` ${
|
|
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")}`
|
|
924
586
|
] : [
|
|
925
|
-
` ${
|
|
926
|
-
` ${
|
|
927
|
-
` ${
|
|
928
|
-
` ${
|
|
929
|
-
` ${
|
|
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")}`
|
|
930
592
|
];
|
|
931
593
|
p2.outro(["Next:", ...next].join(`
|
|
932
594
|
`));
|
|
@@ -935,7 +597,7 @@ async function init(args) {
|
|
|
935
597
|
|
|
936
598
|
// src/regenerate.ts
|
|
937
599
|
import * as p3 from "@clack/prompts";
|
|
938
|
-
import
|
|
600
|
+
import color4 from "picocolors";
|
|
939
601
|
async function generateCommand() {
|
|
940
602
|
const project = await findProject();
|
|
941
603
|
if (!project) {
|
|
@@ -951,9 +613,9 @@ async function generateCommand() {
|
|
|
951
613
|
}
|
|
952
614
|
const result = await generate(project.root, project.manifest);
|
|
953
615
|
p3.log.step([
|
|
954
|
-
...result.written.map((file) => `${
|
|
955
|
-
...result.kept.map((file) => `${
|
|
956
|
-
...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)")}`)
|
|
957
619
|
].join(`
|
|
958
620
|
`));
|
|
959
621
|
return project.manifest.layout === "strict" ? finishStrict(project.root) : 0;
|
|
@@ -972,8 +634,8 @@ async function generateHelp() {
|
|
|
972
634
|
}
|
|
973
635
|
|
|
974
636
|
// src/scripts.ts
|
|
975
|
-
import { existsSync as
|
|
976
|
-
import { join as
|
|
637
|
+
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
638
|
+
import { join as join3, relative } from "node:path";
|
|
977
639
|
function appScript(name) {
|
|
978
640
|
return async (args) => {
|
|
979
641
|
const project = await findProject();
|
|
@@ -982,8 +644,8 @@ function appScript(name) {
|
|
|
982
644
|
const code = await ensureGenerated(project);
|
|
983
645
|
if (code !== 0)
|
|
984
646
|
return code;
|
|
985
|
-
const pkgPath =
|
|
986
|
-
const scripts =
|
|
647
|
+
const pkgPath = join3(project.appDir, "package.json");
|
|
648
|
+
const scripts = existsSync2(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {} : {};
|
|
987
649
|
if (!scripts[name]) {
|
|
988
650
|
console.error(`vc: ${relative(process.cwd(), pkgPath) || "package.json"} has no "${name}" script — void init writes one.`);
|
|
989
651
|
return 1;
|
|
@@ -1018,8 +680,6 @@ var EXTENDED = {
|
|
|
1018
680
|
preflight: { run: preflightCommand, help: preflightHelp },
|
|
1019
681
|
secrets: { run: secretsCliCommand, help: secretsHelp },
|
|
1020
682
|
keys: { run: keysCliCommand, help: keysHelp },
|
|
1021
|
-
link: { run: linkCliCommand, help: linkHelp },
|
|
1022
|
-
publish: { run: publishCliCommand, help: publishHelp },
|
|
1023
683
|
guard: { run: guardCommand, help: async () => guardCommand() },
|
|
1024
684
|
dev: { run: appScript("dev"), help: appScriptHelp("dev") },
|
|
1025
685
|
build: { run: appScript("build"), help: appScriptHelp("build") },
|