@saastemly/voidcommerce 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog.js +2 -1
- package/dist/cli.js +54 -3
- package/dist/deploy/index.d.ts +9 -0
- package/dist/deploy/keys.d.ts +34 -0
- package/dist/deploy/secrets.d.ts +15 -9
- package/dist/index-0v6na3yp.js +35 -0
- package/dist/{index-z4qazajn.js → index-844b3qn9.js} +1 -4
- package/dist/index-mpr7gm6k.js +4597 -0
- package/dist/{index-zh3tmj08.js → index-pz6m2hkm.js} +1 -1
- package/dist/{index-84wq4qp8.js → index-yzezvy5h.js} +273 -110
- package/dist/index.js +4 -3
- package/dist/manifest.js +3 -2
- package/package.json +2 -1
- package/src/cli.ts +4 -1
- package/src/deploy/index.ts +54 -1
- package/src/deploy/keys.ts +212 -0
- package/src/deploy/secrets.ts +43 -21
- package/src/dotenvx-primitives.d.ts +10 -0
- package/src/generate/index.ts +21 -0
- package/src/generate/strict.ts +2 -0
|
@@ -11,11 +11,14 @@ import {
|
|
|
11
11
|
workerHosts,
|
|
12
12
|
writeManifest,
|
|
13
13
|
zone
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-pz6m2hkm.js";
|
|
15
15
|
import {
|
|
16
|
-
CHOICES
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
CHOICES
|
|
17
|
+
} from "./index-844b3qn9.js";
|
|
18
|
+
import {
|
|
19
|
+
__require,
|
|
20
|
+
__toESM
|
|
21
|
+
} from "./index-0v6na3yp.js";
|
|
19
22
|
|
|
20
23
|
// src/generate/auth.ts
|
|
21
24
|
var VAT = {
|
|
@@ -841,6 +844,7 @@ function strictDependencies(manifest) {
|
|
|
841
844
|
for (const name of [
|
|
842
845
|
"@hono/node-server",
|
|
843
846
|
"@dotenvx/dotenvx",
|
|
847
|
+
"husky",
|
|
844
848
|
"@rolldown/plugin-babel",
|
|
845
849
|
"@tailwindcss/vite",
|
|
846
850
|
"@types/node",
|
|
@@ -1437,7 +1441,7 @@ import color from "picocolors";
|
|
|
1437
1441
|
// package.json
|
|
1438
1442
|
var package_default = {
|
|
1439
1443
|
name: "@saastemly/voidcommerce",
|
|
1440
|
-
version: "0.
|
|
1444
|
+
version: "0.4.0",
|
|
1441
1445
|
description: "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
|
|
1442
1446
|
type: "module",
|
|
1443
1447
|
license: "MIT",
|
|
@@ -1471,6 +1475,7 @@ var package_default = {
|
|
|
1471
1475
|
},
|
|
1472
1476
|
dependencies: {
|
|
1473
1477
|
"@clack/prompts": "^0.11.0",
|
|
1478
|
+
"@dotenvx/primitives": "^2.2.0",
|
|
1474
1479
|
picocolors: "^1.1.1"
|
|
1475
1480
|
},
|
|
1476
1481
|
devDependencies: {
|
|
@@ -2478,6 +2483,8 @@ async function generateStrictRoot(root, manifest, result) {
|
|
|
2478
2483
|
scripts["deploy"] ??= "vc deploy";
|
|
2479
2484
|
scripts["import:catalog"] ??= "vc import";
|
|
2480
2485
|
scripts["maildev"] ??= "maildev --smtp 1025 --web 1080";
|
|
2486
|
+
scripts["secrets"] ??= "vc secrets";
|
|
2487
|
+
scripts["prepare"] ??= "husky";
|
|
2481
2488
|
pkg["scripts"] = scripts;
|
|
2482
2489
|
}, result);
|
|
2483
2490
|
await put(root, ".gitignore", `node_modules
|
|
@@ -2492,6 +2499,12 @@ dist
|
|
|
2492
2499
|
.DS_Store
|
|
2493
2500
|
`, result, "own");
|
|
2494
2501
|
await put(root, "patches/void@0.10.13.patch", renderVoidPatch(), result, "regenerate");
|
|
2502
|
+
await put(root, ".husky/pre-commit", `#!/usr/bin/env sh
|
|
2503
|
+
# Generated by \`vc init\`. Refuses a commit that would put a secret in the
|
|
2504
|
+
# clear in .env.secrets — which cannot be undone by a later commit,
|
|
2505
|
+
# because the value stays in the history.
|
|
2506
|
+
bunx vc guard
|
|
2507
|
+
`, result, "regenerate");
|
|
2495
2508
|
await put(root, ".github/workflows/void-dist.yml", renderDistWorkflow(manifest), result, "regenerate");
|
|
2496
2509
|
await put(root, "DEPLOY.md", renderDeployReadme(manifest, zone(manifest), workerHosts(manifest)), result, "regenerate");
|
|
2497
2510
|
await put(root, ".env", renderEnvLocal(manifest), result, "own");
|
|
@@ -2856,30 +2869,167 @@ ${ran.out.trim()}`);
|
|
|
2856
2869
|
}
|
|
2857
2870
|
|
|
2858
2871
|
// src/deploy/preflight.ts
|
|
2859
|
-
import { existsSync as
|
|
2860
|
-
import { join as
|
|
2861
|
-
import
|
|
2872
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
|
|
2873
|
+
import { join as join9 } from "node:path";
|
|
2874
|
+
import color5 from "picocolors";
|
|
2862
2875
|
|
|
2863
2876
|
// src/deploy/secrets.ts
|
|
2864
|
-
import { existsSync as
|
|
2877
|
+
import { existsSync as existsSync8, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
2865
2878
|
import { spawn as spawn3 } from "node:child_process";
|
|
2866
|
-
import { delimiter as delimiter3, dirname as dirname5, join as
|
|
2879
|
+
import { delimiter as delimiter3, dirname as dirname5, join as join8 } from "node:path";
|
|
2867
2880
|
import { tmpdir } from "node:os";
|
|
2881
|
+
import color4 from "picocolors";
|
|
2882
|
+
|
|
2883
|
+
// src/deploy/keys.ts
|
|
2884
|
+
import { hkdfSync } from "node:crypto";
|
|
2885
|
+
import { existsSync as existsSync7, readFileSync as readFileSync2 } from "node:fs";
|
|
2886
|
+
import { join as join7 } from "node:path";
|
|
2868
2887
|
import color3 from "picocolors";
|
|
2888
|
+
var INFO = "voidcommerce/dotenvx/secrets/v1";
|
|
2889
|
+
function apiToken() {
|
|
2890
|
+
return process.env["CLOUDFLARE_API_TOKEN"] || process.env["CF_API_TOKEN"] || null;
|
|
2891
|
+
}
|
|
2892
|
+
function derivePrivateKey(token, accountId) {
|
|
2893
|
+
const bytes = hkdfSync("sha256", Buffer.from(token, "utf8"), Buffer.from(accountId, "utf8"), Buffer.from(INFO), 32);
|
|
2894
|
+
return Buffer.from(bytes).toString("hex");
|
|
2895
|
+
}
|
|
2896
|
+
async function publicKeyFor(privateKey) {
|
|
2897
|
+
try {
|
|
2898
|
+
const { derive } = await import("./index-mpr7gm6k.js").then((m)=>__toESM(m.default,1));
|
|
2899
|
+
return derive(privateKey);
|
|
2900
|
+
} catch {
|
|
2901
|
+
return null;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
function committedPublicKey(root) {
|
|
2905
|
+
const path = join7(root, SECRETS_FILE);
|
|
2906
|
+
if (!existsSync7(path))
|
|
2907
|
+
return null;
|
|
2908
|
+
const match = /^\s*DOTENV_PUBLIC_KEY_SECRETS\s*=\s*["']?([0-9a-fA-F]+)["']?/m.exec(readFileSync2(path, "utf8"));
|
|
2909
|
+
return match?.[1] ?? null;
|
|
2910
|
+
}
|
|
2911
|
+
async function keyFor(manifest, root) {
|
|
2912
|
+
const token = apiToken();
|
|
2913
|
+
if (!token) {
|
|
2914
|
+
return {
|
|
2915
|
+
ok: false,
|
|
2916
|
+
reason: `CLOUDFLARE_API_TOKEN is not set.
|
|
2917
|
+
The key is derived from it, so there is nothing to derive from. Note that
|
|
2918
|
+
` + " `wrangler login` does NOT set one — this scheme needs an API token, from\n" + " My Profile → API Tokens."
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
const accountId = manifest.cloudflare?.accountId || process.env["CLOUDFLARE_ACCOUNT_ID"] || "";
|
|
2922
|
+
if (!accountId) {
|
|
2923
|
+
return {
|
|
2924
|
+
ok: false,
|
|
2925
|
+
reason: "the account is not pinned, and it salts the derivation.\n Run `vc deploy --cloudflare --provision` once, or set CLOUDFLARE_ACCOUNT_ID."
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
const privateKey = derivePrivateKey(token, accountId);
|
|
2929
|
+
const publicKey = await publicKeyFor(privateKey);
|
|
2930
|
+
if (!publicKey) {
|
|
2931
|
+
return { ok: false, reason: "@dotenvx/dotenvx is not installed here, so a key cannot be derived. `bun add -d @dotenvx/dotenvx`" };
|
|
2932
|
+
}
|
|
2933
|
+
const committed = committedPublicKey(root);
|
|
2934
|
+
if (!committed)
|
|
2935
|
+
return { ok: true, privateKey, publicKey, fresh: true };
|
|
2936
|
+
if (committed.toLowerCase() !== publicKey.toLowerCase()) {
|
|
2937
|
+
return {
|
|
2938
|
+
ok: false,
|
|
2939
|
+
reason: `this token does not derive the key ${SECRETS_FILE} was encrypted under.
|
|
2940
|
+
|
|
2941
|
+
encrypted under: ${committed.slice(0, 16)}…
|
|
2942
|
+
` + ` this token gives: ${publicKey.slice(0, 16)}…
|
|
2943
|
+
|
|
2944
|
+
` + ` That is one of three things, and all of them are the same fix:
|
|
2945
|
+
` + ` · the token was rotated since the secrets were encrypted
|
|
2946
|
+
` + ` · this is a different admin's token
|
|
2947
|
+
` + ` · CLOUDFLARE_ACCOUNT_ID is not the account they were encrypted for
|
|
2948
|
+
|
|
2949
|
+
` + ` If you still have the ORIGINAL token, \`vc keys rotate\` re-encrypts
|
|
2950
|
+
everything under the new one. If you do not, the values are unrecoverable
|
|
2951
|
+
and must be entered again.`
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
return { ok: true, privateKey, publicKey, fresh: false };
|
|
2955
|
+
}
|
|
2956
|
+
async function keysCommand(manifest, root, args) {
|
|
2957
|
+
if (args.includes("--rotate"))
|
|
2958
|
+
return rotate(manifest, root);
|
|
2959
|
+
const state = await keyFor(manifest, root);
|
|
2960
|
+
console.log(`
|
|
2961
|
+
The key is DERIVED from CLOUDFLARE_API_TOKEN, salted with the account id.`);
|
|
2962
|
+
console.log(color3.dim(`Nothing is stored, so nothing can leak — and nothing can be recovered.
|
|
2963
|
+
`));
|
|
2964
|
+
if (!state.ok) {
|
|
2965
|
+
console.error(`${color3.red("✗")} ${state.reason}
|
|
2966
|
+
`);
|
|
2967
|
+
return 1;
|
|
2968
|
+
}
|
|
2969
|
+
console.log(` derives public key ${state.publicKey.slice(0, 20)}…`);
|
|
2970
|
+
console.log(state.fresh ? ` ${color3.dim(`${SECRETS_FILE} does not exist yet, so there is nothing to check against`)}` : ` ${color3.green("✓")} matches what ${SECRETS_FILE} was encrypted under`);
|
|
2971
|
+
console.log(color3.yellow(`
|
|
2972
|
+
! Rotating this API token makes every secret in ${SECRETS_FILE} unreadable.
|
|
2973
|
+
Run \`vc keys --rotate\` with the NEW token exported and the old one in
|
|
2974
|
+
CLOUDFLARE_API_TOKEN_OLD, BEFORE the old one stops working.
|
|
2975
|
+
`));
|
|
2976
|
+
return 0;
|
|
2977
|
+
}
|
|
2978
|
+
async function rotate(manifest, root) {
|
|
2979
|
+
const oldToken = process.env["CLOUDFLARE_API_TOKEN_OLD"];
|
|
2980
|
+
const newToken = apiToken();
|
|
2981
|
+
if (!oldToken || !newToken) {
|
|
2982
|
+
console.error(`
|
|
2983
|
+
vc: rotation needs BOTH tokens:
|
|
2984
|
+
CLOUDFLARE_API_TOKEN_OLD=<the one the secrets were encrypted under>
|
|
2985
|
+
CLOUDFLARE_API_TOKEN=<the new one>
|
|
2986
|
+
|
|
2987
|
+
The old one is the only thing that can read the current values.
|
|
2988
|
+
`);
|
|
2989
|
+
return 1;
|
|
2990
|
+
}
|
|
2991
|
+
const accountId = manifest.cloudflare?.accountId || process.env["CLOUDFLARE_ACCOUNT_ID"] || "";
|
|
2992
|
+
if (!accountId) {
|
|
2993
|
+
console.error("vc: the account is not pinned, and it salts the derivation.");
|
|
2994
|
+
return 1;
|
|
2995
|
+
}
|
|
2996
|
+
const oldKey = derivePrivateKey(oldToken, accountId);
|
|
2997
|
+
const oldPublic = await publicKeyFor(oldKey);
|
|
2998
|
+
const committed = committedPublicKey(root);
|
|
2999
|
+
if (committed && oldPublic && committed.toLowerCase() !== oldPublic.toLowerCase()) {
|
|
3000
|
+
console.error(`
|
|
3001
|
+
vc: CLOUDFLARE_API_TOKEN_OLD does not derive the key ${SECRETS_FILE} was encrypted under either.
|
|
3002
|
+
`);
|
|
3003
|
+
return 1;
|
|
3004
|
+
}
|
|
3005
|
+
console.log(`
|
|
3006
|
+
Rotation is a decrypt with the old key and an encrypt with the new one.
|
|
3007
|
+
Run these two, in this order, from ${root}:
|
|
3008
|
+
|
|
3009
|
+
${color3.cyan(`DOTENV_PRIVATE_KEY_SECRETS=<old> bunx dotenvx decrypt -f ${SECRETS_FILE}`)}
|
|
3010
|
+
${color3.cyan(`bunx dotenvx encrypt -f ${SECRETS_FILE}`)} ${color3.dim("# under the new derived key")}
|
|
3011
|
+
|
|
3012
|
+
` + color3.dim(`vc does not run them for you: the middle state is your secrets in
|
|
3013
|
+
plaintext on disk, and that is a moment to be deliberate about.
|
|
3014
|
+
`));
|
|
3015
|
+
return 0;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// src/deploy/secrets.ts
|
|
2869
3019
|
var SECRETS_FILE = ".env.secrets";
|
|
2870
3020
|
var PRIVATE_KEY_VAR = "DOTENV_PRIVATE_KEY_SECRETS";
|
|
2871
3021
|
function findDotenvx(from) {
|
|
2872
3022
|
let dir = from;
|
|
2873
3023
|
for (;; ) {
|
|
2874
|
-
const local =
|
|
2875
|
-
if (
|
|
3024
|
+
const local = join8(dir, "node_modules", ".bin", "dotenvx");
|
|
3025
|
+
if (existsSync8(local))
|
|
2876
3026
|
return local;
|
|
2877
3027
|
const parent = dirname5(dir);
|
|
2878
3028
|
if (parent === dir)
|
|
2879
3029
|
break;
|
|
2880
3030
|
dir = parent;
|
|
2881
3031
|
}
|
|
2882
|
-
return (process.env["PATH"] ?? "").split(delimiter3).filter(Boolean).some((entry) =>
|
|
3032
|
+
return (process.env["PATH"] ?? "").split(delimiter3).filter(Boolean).some((entry) => existsSync8(join8(entry, "dotenvx"))) ? "dotenvx" : null;
|
|
2883
3033
|
}
|
|
2884
3034
|
function run(cmd, args, cwd, env = {}) {
|
|
2885
3035
|
return new Promise((resolve) => {
|
|
@@ -2896,11 +3046,11 @@ function run(cmd, args, cwd, env = {}) {
|
|
|
2896
3046
|
});
|
|
2897
3047
|
}
|
|
2898
3048
|
function declaredSecretNames(root) {
|
|
2899
|
-
const path =
|
|
2900
|
-
if (!
|
|
3049
|
+
const path = join8(root, SECRETS_FILE);
|
|
3050
|
+
if (!existsSync8(path))
|
|
2901
3051
|
return new Set;
|
|
2902
3052
|
const names = new Set;
|
|
2903
|
-
for (const line2 of
|
|
3053
|
+
for (const line2 of readFileSync3(path, "utf8").split(`
|
|
2904
3054
|
`)) {
|
|
2905
3055
|
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=/.exec(line2);
|
|
2906
3056
|
if (match && !match[1].startsWith("DOTENV_"))
|
|
@@ -2909,11 +3059,11 @@ function declaredSecretNames(root) {
|
|
|
2909
3059
|
return names;
|
|
2910
3060
|
}
|
|
2911
3061
|
function plaintextSecretNames(root) {
|
|
2912
|
-
const path =
|
|
2913
|
-
if (!
|
|
3062
|
+
const path = join8(root, SECRETS_FILE);
|
|
3063
|
+
if (!existsSync8(path))
|
|
2914
3064
|
return [];
|
|
2915
3065
|
const bare = [];
|
|
2916
|
-
for (const line2 of
|
|
3066
|
+
for (const line2 of readFileSync3(path, "utf8").split(`
|
|
2917
3067
|
`)) {
|
|
2918
3068
|
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line2);
|
|
2919
3069
|
if (!match || match[1].startsWith("DOTENV_"))
|
|
@@ -2926,25 +3076,24 @@ function plaintextSecretNames(root) {
|
|
|
2926
3076
|
}
|
|
2927
3077
|
async function decryptSecrets(project) {
|
|
2928
3078
|
const root = project.root;
|
|
2929
|
-
if (!
|
|
3079
|
+
if (!existsSync8(join8(root, SECRETS_FILE)))
|
|
2930
3080
|
return { error: `${SECRETS_FILE} does not exist — \`vc secrets init\` writes one` };
|
|
2931
3081
|
const dotenvx = findDotenvx(root);
|
|
2932
3082
|
if (!dotenvx)
|
|
2933
3083
|
return { error: "dotenvx is not installed. `bun add -d @dotenvx/dotenvx`" };
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
}
|
|
2940
|
-
const result = await run(dotenvx, ["decrypt", "-f", SECRETS_FILE, "--stdout"], root);
|
|
3084
|
+
const key = await keyFor(project.manifest, root);
|
|
3085
|
+
if (!key.ok)
|
|
3086
|
+
return { error: key.reason };
|
|
3087
|
+
const result = await run(dotenvx, ["decrypt", "-f", SECRETS_FILE, "--stdout"], root, {
|
|
3088
|
+
[PRIVATE_KEY_VAR]: key.privateKey
|
|
3089
|
+
});
|
|
2941
3090
|
if (result.code !== 0)
|
|
2942
3091
|
return { error: `dotenvx could not decrypt ${SECRETS_FILE}: ${result.out.trim().split(`
|
|
2943
3092
|
`).slice(-2).join(" ")}` };
|
|
2944
3093
|
const lines = result.out.split(`
|
|
2945
3094
|
`).filter((line2) => /^\s*[A-Z][A-Z0-9_]*\s*=/.test(line2) && !line2.trimStart().startsWith("DOTENV_"));
|
|
2946
3095
|
const names = lines.map((line2) => line2.slice(0, line2.indexOf("=")).trim());
|
|
2947
|
-
const path =
|
|
3096
|
+
const path = join8(tmpdir(), `vc-secrets-${process.pid}-${Date.now()}.env`);
|
|
2948
3097
|
writeFileSync(path, `${lines.join(`
|
|
2949
3098
|
`)}
|
|
2950
3099
|
`, { mode: 384 });
|
|
@@ -2966,9 +3115,9 @@ async function secretsCommand(project, args) {
|
|
|
2966
3115
|
const bare = plaintextSecretNames(root);
|
|
2967
3116
|
if (args.includes("--init"))
|
|
2968
3117
|
return initSecrets(project);
|
|
2969
|
-
if (!
|
|
3118
|
+
if (!existsSync8(join8(root, SECRETS_FILE))) {
|
|
2970
3119
|
console.log(`
|
|
2971
|
-
${
|
|
3120
|
+
${color4.yellow("No " + SECRETS_FILE + " yet.")} Secrets are set by hand with \`wrangler secret put\`,
|
|
2972
3121
|
` + `which means the shop cannot be rebuilt from a checkout.
|
|
2973
3122
|
|
|
2974
3123
|
` + ` vc secrets --init write one, with every required key as \`unset\`
|
|
@@ -2979,15 +3128,15 @@ ${color3.yellow("No " + SECRETS_FILE + " yet.")} Secrets are set by hand with \`
|
|
|
2979
3128
|
${SECRETS_FILE} — committed, encrypted, and read by \`vc deploy --cloudflare\`
|
|
2980
3129
|
`);
|
|
2981
3130
|
for (const key of required) {
|
|
2982
|
-
const state = declared.has(key.key) ?
|
|
3131
|
+
const state = declared.has(key.key) ? color4.green("declared") : color4.red("MISSING ");
|
|
2983
3132
|
console.log(` ${state} ${key.key}`);
|
|
2984
3133
|
}
|
|
2985
3134
|
const extra = [...declared].filter((name) => !required.some((key) => key.key === name));
|
|
2986
3135
|
for (const name of extra)
|
|
2987
|
-
console.log(` ${
|
|
3136
|
+
console.log(` ${color4.dim("extra ")} ${name} ${color4.dim("— not required by this shop")}`);
|
|
2988
3137
|
if (bare.length > 0) {
|
|
2989
3138
|
console.log(`
|
|
2990
|
-
${
|
|
3139
|
+
${color4.red("✗")} ${bare.length} value${bare.length === 1 ? " is" : "s are"} committed IN THE CLEAR: ${bare.join(", ")}
|
|
2991
3140
|
` + ` Run \`dotenvx encrypt -f ${SECRETS_FILE}\` before committing again.
|
|
2992
3141
|
`);
|
|
2993
3142
|
return 1;
|
|
@@ -2995,23 +3144,30 @@ ${color3.red("✗")} ${bare.length} value${bare.length === 1 ? " is" : "s are"}
|
|
|
2995
3144
|
const missing = required.filter((key) => !declared.has(key.key));
|
|
2996
3145
|
if (missing.length > 0) {
|
|
2997
3146
|
console.log(`
|
|
2998
|
-
${
|
|
3147
|
+
${color4.red("✗")} ${missing.length} required secret${missing.length === 1 ? "" : "s"} not declared.
|
|
2999
3148
|
` + ` dotenvx set ${missing[0].key} '…' -f ${SECRETS_FILE}
|
|
3000
3149
|
`);
|
|
3001
3150
|
return 1;
|
|
3002
3151
|
}
|
|
3003
3152
|
console.log(`
|
|
3004
|
-
${
|
|
3153
|
+
${color4.green("✓")} every secret this shop needs is declared and encrypted.
|
|
3005
3154
|
`);
|
|
3006
3155
|
return 0;
|
|
3007
3156
|
}
|
|
3008
3157
|
async function initSecrets(project) {
|
|
3009
|
-
const path =
|
|
3010
|
-
if (
|
|
3158
|
+
const path = join8(project.root, SECRETS_FILE);
|
|
3159
|
+
if (existsSync8(path)) {
|
|
3011
3160
|
console.error(`vc: ${SECRETS_FILE} already exists; not overwriting it.`);
|
|
3012
3161
|
return 1;
|
|
3013
3162
|
}
|
|
3014
|
-
const
|
|
3163
|
+
const key = await keyFor(project.manifest, project.root);
|
|
3164
|
+
if (!key.ok) {
|
|
3165
|
+
console.error(`
|
|
3166
|
+
vc: ${key.reason}
|
|
3167
|
+
`);
|
|
3168
|
+
return 1;
|
|
3169
|
+
}
|
|
3170
|
+
const required = allEnvKeys(project.manifest).filter((key2) => !key2.plaintext);
|
|
3015
3171
|
const body = [
|
|
3016
3172
|
"# Secrets, encrypted, and COMMITTED.",
|
|
3017
3173
|
"#",
|
|
@@ -3019,22 +3175,29 @@ async function initSecrets(project) {
|
|
|
3019
3175
|
"# secret changed without showing what it changed to. `unset` is the",
|
|
3020
3176
|
"# documented placeholder and preflight refuses it.",
|
|
3021
3177
|
"#",
|
|
3022
|
-
"# The private key
|
|
3023
|
-
"#
|
|
3178
|
+
"# The private key is DERIVED from CLOUDFLARE_API_TOKEN, salted with the",
|
|
3179
|
+
"# account id — it is stored nowhere, so there is no .env.keys to leak.",
|
|
3180
|
+
"#",
|
|
3181
|
+
"# The cost of that: rotating the token makes every value below",
|
|
3182
|
+
"# permanently unreadable. `vc keys --rotate` re-encrypts while you still",
|
|
3183
|
+
"# have the old token; after that there is no recovery.",
|
|
3024
3184
|
"",
|
|
3025
|
-
|
|
3185
|
+
`DOTENV_PUBLIC_KEY_SECRETS="${key.publicKey}"`,
|
|
3186
|
+
"",
|
|
3187
|
+
...required.flatMap((key2) => [`# ${key2.breaks}${key2.where ? ` — from: ${key2.where}` : ""}`, `${key2.key}=unset`, ""])
|
|
3026
3188
|
].join(`
|
|
3027
3189
|
`);
|
|
3028
3190
|
writeFileSync(path, body, { mode: 384 });
|
|
3029
3191
|
console.log(`
|
|
3030
|
-
${
|
|
3192
|
+
${color4.green("+")} ${SECRETS_FILE} — ${required.length} keys, all \`unset\`, under the key this token derives
|
|
3031
3193
|
|
|
3032
3194
|
` + `Next:
|
|
3033
3195
|
` + ` 1. put the real values in, then
|
|
3034
|
-
` + ` 2. ${
|
|
3035
|
-
` + ` 3. commit
|
|
3036
|
-
|
|
3037
|
-
`
|
|
3196
|
+
` + ` 2. ${color4.cyan(`bunx dotenvx encrypt -f ${SECRETS_FILE}`)}
|
|
3197
|
+
` + ` 3. commit it — there is no key file to keep out
|
|
3198
|
+
|
|
3199
|
+
` + color4.yellow(`! Rotating CLOUDFLARE_API_TOKEN makes these unreadable. \`vc keys\` explains.
|
|
3200
|
+
`));
|
|
3038
3201
|
return 0;
|
|
3039
3202
|
}
|
|
3040
3203
|
|
|
@@ -3042,10 +3205,10 @@ ${color3.green("+")} ${SECRETS_FILE} — ${required.length} keys, all \`unset\`
|
|
|
3042
3205
|
var UNSET = "unset";
|
|
3043
3206
|
function productionEnv(appDir) {
|
|
3044
3207
|
const out = new Map;
|
|
3045
|
-
const path =
|
|
3046
|
-
if (!
|
|
3208
|
+
const path = join9(appDir, ".env.production");
|
|
3209
|
+
if (!existsSync9(path))
|
|
3047
3210
|
return out;
|
|
3048
|
-
for (const line2 of
|
|
3211
|
+
for (const line2 of readFileSync4(path, "utf8").split(`
|
|
3049
3212
|
`)) {
|
|
3050
3213
|
const match = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line2);
|
|
3051
3214
|
if (!match)
|
|
@@ -3070,12 +3233,12 @@ async function voidSecretNames(appDir) {
|
|
|
3070
3233
|
return names;
|
|
3071
3234
|
}
|
|
3072
3235
|
function routeProblem(project) {
|
|
3073
|
-
const path =
|
|
3074
|
-
if (!
|
|
3236
|
+
const path = join9(project.appDir, "wrangler.jsonc");
|
|
3237
|
+
if (!existsSync9(path))
|
|
3075
3238
|
return "wrangler.jsonc is missing";
|
|
3076
3239
|
let routes = [];
|
|
3077
3240
|
try {
|
|
3078
|
-
routes = parseJsonc(
|
|
3241
|
+
routes = parseJsonc(readFileSync4(path, "utf8")).routes ?? [];
|
|
3079
3242
|
} catch {
|
|
3080
3243
|
return "wrangler.jsonc cannot be parsed";
|
|
3081
3244
|
}
|
|
@@ -3115,7 +3278,7 @@ async function preflight(project, source) {
|
|
|
3115
3278
|
function printPreflight(project, result, source) {
|
|
3116
3279
|
const keys = allEnvKeys(project.manifest);
|
|
3117
3280
|
if (result.remote === null) {
|
|
3118
|
-
console.log(
|
|
3281
|
+
console.log(color5.dim(source === "wrangler" ? `Could not read the worker's secrets — not deployed yet, or wrangler is not logged in.
|
|
3119
3282
|
Anything not in .env.production is reported as missing.
|
|
3120
3283
|
` : `Could not read the project's secrets from Void — not logged in, or no linked project.
|
|
3121
3284
|
Anything not in .env.production is reported as missing.
|
|
@@ -3125,37 +3288,37 @@ Anything not in .env.production is reported as missing.
|
|
|
3125
3288
|
for (const key of keys) {
|
|
3126
3289
|
const value = result.present.get(key.key);
|
|
3127
3290
|
const set = value !== undefined && value !== "" && value !== UNSET;
|
|
3128
|
-
console.log(` ${set ?
|
|
3291
|
+
console.log(` ${set ? color5.green("set ") : color5.dim("unset")} ${key.key}${key.plaintext ? color5.dim(" (plaintext)") : ""}`);
|
|
3129
3292
|
}
|
|
3130
3293
|
if (result.ready) {
|
|
3131
3294
|
console.log(`
|
|
3132
|
-
${
|
|
3295
|
+
${color5.green("Ready to go live.")}
|
|
3133
3296
|
`);
|
|
3134
3297
|
return;
|
|
3135
3298
|
}
|
|
3136
3299
|
console.log(`
|
|
3137
|
-
${
|
|
3300
|
+
${color5.red("NOT ready to go live.")}
|
|
3138
3301
|
`);
|
|
3139
3302
|
if (result.routeProblem) {
|
|
3140
|
-
console.log(`${
|
|
3303
|
+
console.log(`${color5.red("✗")} the worker's hostname
|
|
3141
3304
|
${result.routeProblem}
|
|
3142
3305
|
`);
|
|
3143
3306
|
}
|
|
3144
3307
|
if (result.bareSecrets.length > 0) {
|
|
3145
|
-
console.log(`${
|
|
3308
|
+
console.log(`${color5.red("✗")} committed in the clear in ${SECRETS_FILE}: ${result.bareSecrets.join(", ")}
|
|
3146
3309
|
` + ` Run \`dotenvx encrypt -f ${SECRETS_FILE}\` — and treat those values as burned.
|
|
3147
3310
|
`);
|
|
3148
3311
|
}
|
|
3149
3312
|
for (const key of result.missing) {
|
|
3150
|
-
console.log(`${
|
|
3313
|
+
console.log(`${color5.red("✗")} ${key.key}`);
|
|
3151
3314
|
console.log(` ${key.breaks}`);
|
|
3152
3315
|
if (key.where)
|
|
3153
|
-
console.log(` ${
|
|
3316
|
+
console.log(` ${color5.dim(`from: ${key.where}`)}`);
|
|
3154
3317
|
console.log("");
|
|
3155
3318
|
}
|
|
3156
3319
|
const secrets = result.missing.filter((key) => !key.plaintext);
|
|
3157
3320
|
if (secrets.length > 0) {
|
|
3158
|
-
const inRepo =
|
|
3321
|
+
const inRepo = existsSync9(join9(project.root, SECRETS_FILE));
|
|
3159
3322
|
console.log(inRepo ? `Put each in ${SECRETS_FILE}, which the deploy uploads:` : source === "wrangler" ? "Set each secret on the worker (this also creates the draft worker on a first deploy):" : "Set each secret on the project:");
|
|
3160
3323
|
for (const key of secrets) {
|
|
3161
3324
|
console.log(inRepo ? ` dotenvx set ${key.key} '…' -f ${SECRETS_FILE}` : ` ${source === "wrangler" ? "wrangler secret put" : "void secret put"} ${key.key}`);
|
|
@@ -3168,17 +3331,17 @@ ${color4.red("NOT ready to go live.")}
|
|
|
3168
3331
|
}
|
|
3169
3332
|
|
|
3170
3333
|
// src/deploy/cloudflare.ts
|
|
3171
|
-
import { existsSync as
|
|
3172
|
-
import { dirname as dirname6, join as
|
|
3173
|
-
import
|
|
3334
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3335
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
3336
|
+
import color6 from "picocolors";
|
|
3174
3337
|
var fail = (message) => {
|
|
3175
3338
|
console.error(`
|
|
3176
|
-
${
|
|
3339
|
+
${color6.red("✗")} ${message}
|
|
3177
3340
|
`);
|
|
3178
3341
|
return 1;
|
|
3179
3342
|
};
|
|
3180
3343
|
function readConfig(path) {
|
|
3181
|
-
return parseJsonc(
|
|
3344
|
+
return parseJsonc(readFileSync5(path, "utf8"));
|
|
3182
3345
|
}
|
|
3183
3346
|
function findBuilder(from) {
|
|
3184
3347
|
let dir = from;
|
|
@@ -3187,8 +3350,8 @@ function findBuilder(from) {
|
|
|
3187
3350
|
["vp", "vp build"],
|
|
3188
3351
|
["vite", "vite build"]
|
|
3189
3352
|
]) {
|
|
3190
|
-
const path =
|
|
3191
|
-
if (
|
|
3353
|
+
const path = join10(dir, "node_modules", ".bin", bin);
|
|
3354
|
+
if (existsSync10(path))
|
|
3192
3355
|
return { cmd: path, label };
|
|
3193
3356
|
}
|
|
3194
3357
|
const parent = dirname6(dir);
|
|
@@ -3199,8 +3362,8 @@ function findBuilder(from) {
|
|
|
3199
3362
|
}
|
|
3200
3363
|
async function deployCloudflare(project, opts) {
|
|
3201
3364
|
const app = project.appDir;
|
|
3202
|
-
const configPath =
|
|
3203
|
-
if (!
|
|
3365
|
+
const configPath = join10(app, "wrangler.jsonc");
|
|
3366
|
+
if (!existsSync10(configPath))
|
|
3204
3367
|
return fail("wrangler.jsonc is missing — `vc generate` writes it.");
|
|
3205
3368
|
const bin = findWrangler(app);
|
|
3206
3369
|
if (!bin)
|
|
@@ -3213,16 +3376,16 @@ async function deployCloudflare(project, opts) {
|
|
|
3213
3376
|
${who.raw.trim().split(`
|
|
3214
3377
|
`).slice(-4).join(`
|
|
3215
3378
|
`)}`);
|
|
3216
|
-
console.log(`${
|
|
3379
|
+
console.log(`${color6.green("✓")} wrangler is logged in`);
|
|
3217
3380
|
let config = readConfig(configPath);
|
|
3218
3381
|
let accountId = config.account_id || process.env["CLOUDFLARE_ACCOUNT_ID"] || "";
|
|
3219
3382
|
if (!accountId) {
|
|
3220
3383
|
if (who.accounts.length === 1) {
|
|
3221
3384
|
accountId = who.accounts[0].id;
|
|
3222
|
-
writeFileSync2(configPath, upsertJsonc(
|
|
3385
|
+
writeFileSync2(configPath, upsertJsonc(readFileSync5(configPath, "utf8"), "account_id", accountId));
|
|
3223
3386
|
project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
|
|
3224
3387
|
await writeManifest(project.root, project.manifest);
|
|
3225
|
-
console.log(`${
|
|
3388
|
+
console.log(`${color6.green("✓")} pinned the account "${who.accounts[0].name}" in wrangler.jsonc`);
|
|
3226
3389
|
} else {
|
|
3227
3390
|
return fail(`the account is not pinned and wrangler sees ${who.accounts.length}. Set account_id in wrangler.jsonc to one of:
|
|
3228
3391
|
${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
@@ -3234,7 +3397,7 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3234
3397
|
if (!check.ready) {
|
|
3235
3398
|
if (!opts.force)
|
|
3236
3399
|
return fail("refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force for a deliberate partial deploy.");
|
|
3237
|
-
console.error(
|
|
3400
|
+
console.error(color6.yellow(`--force: deploying a shop that is NOT ready for customers.
|
|
3238
3401
|
`));
|
|
3239
3402
|
}
|
|
3240
3403
|
const worker = config.name || project.manifest.shop.domain.split(".")[0];
|
|
@@ -3245,15 +3408,15 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3245
3408
|
return fail("the D1 database is not provisioned. Run once with --provision to create it and record its id.");
|
|
3246
3409
|
const db = await ensureD1(bin, app, `${worker}-db`);
|
|
3247
3410
|
const entry = { binding: "DB", database_name: db.name, database_id: db.uuid, migrations_dir: "./db/migrations" };
|
|
3248
|
-
writeFileSync2(configPath, upsertJsonc(
|
|
3411
|
+
writeFileSync2(configPath, upsertJsonc(readFileSync5(configPath, "utf8"), "d1_databases", [entry]));
|
|
3249
3412
|
project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId, d1: { name: db.name, id: db.uuid } };
|
|
3250
3413
|
await writeManifest(project.root, project.manifest);
|
|
3251
|
-
console.log(`${
|
|
3414
|
+
console.log(`${color6.green("✓")} D1 "${db.name}" recorded in wrangler.jsonc and voidcommerce.json`);
|
|
3252
3415
|
config = readConfig(configPath);
|
|
3253
3416
|
}
|
|
3254
3417
|
if (opts.provision) {
|
|
3255
3418
|
await ensureQueue(bin, app, "commerce");
|
|
3256
|
-
console.log(`${
|
|
3419
|
+
console.log(`${color6.green("✓")} queue "commerce"`);
|
|
3257
3420
|
}
|
|
3258
3421
|
const builder = findBuilder(app);
|
|
3259
3422
|
if (!builder)
|
|
@@ -3263,10 +3426,10 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3263
3426
|
const built = await wrangler(builder.cmd, ["build"], app, true);
|
|
3264
3427
|
if (built.code !== 0)
|
|
3265
3428
|
return fail(`the build failed (exit ${built.code}).`);
|
|
3266
|
-
const emittedPath =
|
|
3267
|
-
if (!
|
|
3429
|
+
const emittedPath = join10(app, "dist", "ssr", "wrangler.json");
|
|
3430
|
+
if (!existsSync10(emittedPath))
|
|
3268
3431
|
return fail(`the build emitted no ${emittedPath} — is this a Void app on the Cloudflare target?`);
|
|
3269
|
-
const emitted = JSON.parse(
|
|
3432
|
+
const emitted = JSON.parse(readFileSync5(emittedPath, "utf8"));
|
|
3270
3433
|
const secretKeys = new Set(allEnvKeys(project.manifest).filter((key) => !key.plaintext).map((key) => key.key));
|
|
3271
3434
|
const scrubbed = [];
|
|
3272
3435
|
for (const [key, value] of Object.entries(emitted.vars ?? {})) {
|
|
@@ -3279,7 +3442,7 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3279
3442
|
if (!emittedD1 || emittedD1.database_id === "local")
|
|
3280
3443
|
return fail("the emitted config still carries a placeholder D1 id; wrangler.jsonc's DB binding was not picked up by the build.");
|
|
3281
3444
|
writeFileSync2(emittedPath, JSON.stringify(emitted, null, 2));
|
|
3282
|
-
console.log(`${
|
|
3445
|
+
console.log(`${color6.green("✓")} scrubbed ${scrubbed.length} baked value${scrubbed.length === 1 ? "" : "s"} from the worker's vars${scrubbed.length ? `: ${scrubbed.join(", ")}` : ""}`);
|
|
3283
3446
|
console.log(`
|
|
3284
3447
|
▸ wrangler d1 migrations apply ${emittedD1.database_name} --remote`);
|
|
3285
3448
|
const migrated = await wrangler(bin, ["d1", "migrations", "apply", emittedD1.database_name, "--remote"], app, true);
|
|
@@ -3287,34 +3450,34 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3287
3450
|
return fail(`applying migrations failed (exit ${migrated.code}); nothing was deployed.`);
|
|
3288
3451
|
const secretArgs = [];
|
|
3289
3452
|
let cleanupSecrets;
|
|
3290
|
-
if (
|
|
3453
|
+
if (existsSync10(join10(project.root, SECRETS_FILE))) {
|
|
3291
3454
|
const decrypted = await decryptSecrets(project);
|
|
3292
3455
|
if ("error" in decrypted)
|
|
3293
3456
|
return fail(`the repository's secrets could not be read: ${decrypted.error}`);
|
|
3294
3457
|
secretArgs.push("--secrets-file", decrypted.path);
|
|
3295
3458
|
cleanupSecrets = decrypted.cleanup;
|
|
3296
|
-
console.log(`${
|
|
3459
|
+
console.log(`${color6.green("✓")} ${decrypted.names.length} secrets from ${SECRETS_FILE}, uploaded with this version`);
|
|
3297
3460
|
}
|
|
3298
3461
|
console.log(`
|
|
3299
3462
|
▸ wrangler deploy -c dist/ssr/wrangler.json`);
|
|
3300
|
-
const deployed = await wrangler(bin, ["deploy", "-c",
|
|
3463
|
+
const deployed = await wrangler(bin, ["deploy", "-c", join10("dist", "ssr", "wrangler.json"), ...secretArgs], app, true);
|
|
3301
3464
|
cleanupSecrets?.();
|
|
3302
3465
|
if (deployed.code !== 0)
|
|
3303
3466
|
return fail(`wrangler deploy failed (exit ${deployed.code}).`);
|
|
3304
3467
|
const domain = project.manifest.shop.domain;
|
|
3305
3468
|
const url = `https://${workerHosts(project.manifest)[0]}`;
|
|
3306
3469
|
console.log(`
|
|
3307
|
-
${
|
|
3470
|
+
${color6.green("Live:")} ${url}`);
|
|
3308
3471
|
if (hasFrontend(project.manifest.layout))
|
|
3309
|
-
console.log(
|
|
3472
|
+
console.log(color6.dim("The storefront deploys itself from GitHub Actions on push."));
|
|
3310
3473
|
return 0;
|
|
3311
3474
|
}
|
|
3312
3475
|
|
|
3313
3476
|
// src/import.ts
|
|
3314
3477
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
3315
|
-
import { existsSync as
|
|
3316
|
-
import { join as
|
|
3317
|
-
import
|
|
3478
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
3479
|
+
import { join as join11 } from "node:path";
|
|
3480
|
+
import color7 from "picocolors";
|
|
3318
3481
|
function describe(counts) {
|
|
3319
3482
|
if (!counts)
|
|
3320
3483
|
return "";
|
|
@@ -3353,7 +3516,7 @@ async function post(url, key, body) {
|
|
|
3353
3516
|
return { ok: response.ok, status: response.status, body: { ...parsed, error: parsed.error ?? parsed.message } };
|
|
3354
3517
|
}
|
|
3355
3518
|
async function readJson(path) {
|
|
3356
|
-
if (!
|
|
3519
|
+
if (!existsSync11(path))
|
|
3357
3520
|
return null;
|
|
3358
3521
|
try {
|
|
3359
3522
|
return JSON.parse(await readFile3(path, "utf8"));
|
|
@@ -3381,41 +3544,41 @@ async function importCommand(args) {
|
|
|
3381
3544
|
}
|
|
3382
3545
|
const { url, how } = resolveTarget(project, args);
|
|
3383
3546
|
if (!dry)
|
|
3384
|
-
console.log(`${
|
|
3547
|
+
console.log(`${color7.dim("→")} ${url} ${color7.dim(`(${how})`)}
|
|
3385
3548
|
`);
|
|
3386
3549
|
if (!dry) {
|
|
3387
3550
|
let whoami2;
|
|
3388
3551
|
try {
|
|
3389
3552
|
whoami2 = await fetch(`${url}/api/auth/system/whoami`, { headers: { "x-system-key": key } });
|
|
3390
3553
|
} catch (error) {
|
|
3391
|
-
console.error(`${
|
|
3554
|
+
console.error(`${color7.red("✗")} could not reach ${url}: ${error instanceof Error ? error.message : String(error)}
|
|
3392
3555
|
` + ` Is the shop running? \`vc dev\` serves it locally; --local points here.
|
|
3393
3556
|
`);
|
|
3394
3557
|
return 1;
|
|
3395
3558
|
}
|
|
3396
3559
|
if (!whoami2.ok) {
|
|
3397
|
-
console.error(`${
|
|
3560
|
+
console.error(`${color7.red("✗")} the shop refused the system key (HTTP ${whoami2.status}).
|
|
3398
3561
|
` + ` Is SYSTEM_API_KEY the one this deployment was given?
|
|
3399
3562
|
`);
|
|
3400
3563
|
return 1;
|
|
3401
3564
|
}
|
|
3402
|
-
console.log(`${
|
|
3565
|
+
console.log(`${color7.green("✓")} authenticated as the system identity`);
|
|
3403
3566
|
}
|
|
3404
3567
|
const root = project.root;
|
|
3405
|
-
const products = await readJson(
|
|
3406
|
-
const categories = await readJson(
|
|
3407
|
-
const faqs = has(project.manifest, "faqs") ? await readJson(
|
|
3408
|
-
const posts = has(project.manifest, "blogs") ? await readJson(
|
|
3568
|
+
const products = await readJson(join11(root, "data", "catalog.json"));
|
|
3569
|
+
const categories = await readJson(join11(root, "data", "categories.json"));
|
|
3570
|
+
const faqs = has(project.manifest, "faqs") ? await readJson(join11(root, "content", "faqs.json")) : null;
|
|
3571
|
+
const posts = has(project.manifest, "blogs") ? await readJson(join11(root, "content", "posts.json")) : null;
|
|
3409
3572
|
if (!products && !faqs?.length && !posts?.length) {
|
|
3410
3573
|
console.log(`
|
|
3411
|
-
${
|
|
3574
|
+
${color7.yellow("Nothing to import.")} data/catalog.json is absent and the content files are empty.
|
|
3412
3575
|
` + `Products go in data/catalog.json as a JSON array; \`vc import --dry-run\` checks it without pushing.
|
|
3413
3576
|
`);
|
|
3414
3577
|
return 0;
|
|
3415
3578
|
}
|
|
3416
3579
|
if (dry) {
|
|
3417
3580
|
console.log(`
|
|
3418
|
-
${
|
|
3581
|
+
${color7.dim("--dry-run: nothing was pushed.")}`);
|
|
3419
3582
|
console.log(` ${products?.length ?? 0} products, ${categories?.length ?? 0} categories`);
|
|
3420
3583
|
console.log(` ${faqs?.length ?? 0} FAQ entries, ${posts?.length ?? 0} posts
|
|
3421
3584
|
`);
|
|
@@ -3431,15 +3594,15 @@ ${color6.dim("--dry-run: nothing was pushed.")}`);
|
|
|
3431
3594
|
});
|
|
3432
3595
|
if (result.ok) {
|
|
3433
3596
|
const report = result.body.report ?? {};
|
|
3434
|
-
console.log(`${
|
|
3597
|
+
console.log(`${color7.green("✓")} products: ${describe(report.products)}`);
|
|
3435
3598
|
if (report.categories)
|
|
3436
|
-
console.log(`${
|
|
3599
|
+
console.log(`${color7.green("✓")} categories: ${describe(report.categories)}`);
|
|
3437
3600
|
if (report.prices)
|
|
3438
|
-
console.log(`${
|
|
3601
|
+
console.log(`${color7.green("✓")} prices: ${describe(report.prices)}`);
|
|
3439
3602
|
if (report.addons)
|
|
3440
|
-
console.log(`${
|
|
3603
|
+
console.log(`${color7.green("✓")} addons: ${describe(report.addons)}`);
|
|
3441
3604
|
} else {
|
|
3442
|
-
console.error(`${
|
|
3605
|
+
console.error(`${color7.red("✗")} catalogue: ${result.body.error ?? `HTTP ${result.status}`}`);
|
|
3443
3606
|
failed = true;
|
|
3444
3607
|
}
|
|
3445
3608
|
}
|
|
@@ -3451,20 +3614,20 @@ ${color6.dim("--dry-run: nothing was pushed.")}`);
|
|
|
3451
3614
|
continue;
|
|
3452
3615
|
const result = await post(`${url}${path}`, key, { entries: rows, posts: rows });
|
|
3453
3616
|
if (result.ok) {
|
|
3454
|
-
console.log(`${
|
|
3617
|
+
console.log(`${color7.green("✓")} ${label}: ${describe(result.body)}`);
|
|
3455
3618
|
} else {
|
|
3456
|
-
console.error(`${
|
|
3619
|
+
console.error(`${color7.red("✗")} ${label}: ${result.body.error ?? `HTTP ${result.status}`}`);
|
|
3457
3620
|
failed = true;
|
|
3458
3621
|
}
|
|
3459
3622
|
}
|
|
3460
3623
|
if (failed) {
|
|
3461
3624
|
console.error(`
|
|
3462
|
-
${
|
|
3625
|
+
${color7.red("Some of it did not land.")} The import is an upsert, so fixing the cause and running it again is safe.
|
|
3463
3626
|
`);
|
|
3464
3627
|
return 1;
|
|
3465
3628
|
}
|
|
3466
3629
|
console.log(`
|
|
3467
|
-
${
|
|
3630
|
+
${color7.green("Imported.")} It is an upsert, so running it again costs one pass and changes nothing.
|
|
3468
3631
|
`);
|
|
3469
3632
|
return 0;
|
|
3470
3633
|
}
|
|
@@ -3485,4 +3648,4 @@ async function importHelp() {
|
|
|
3485
3648
|
return 0;
|
|
3486
3649
|
}
|
|
3487
3650
|
|
|
3488
|
-
export { renderAuthTs, allEnvKeys, renderEnvTs, renderEnvExample, renderEnvLocal, renderEnvProduction, envSummary, FRONTEND_BRANCH, renderFrontendApiTs, renderFrontendEnvProduction, renderStorefrontPage, renderFrontendWorkflow, runVoid, runInherit, captureVoid, isVoidApp, voidAppsIn, STRICT_APP, VOID_VERSION, strictDependencies, renderVoidPatch, renderViteConfig, renderStrictTsconfig, renderVoidJson, renderDbSchema, renderDbSeed, renderLayoutTsx, renderAppCss, renderIndexServer, INDEX_PAGE, renderDashboardPage, CLIENT_ONLY, renderSignInPage, renderAuthClient, renderContentTs, renderCron, renderQueue, renderLiveStream, renderLiveRoute, renderStrictAppPackageJson, DATA_README, BRANDING_README, MIGRATIONS_README, finishStrict, line, row, box, fullHelp, initHelp, version, findProject, ensureGenerated, DIST_DIR, DIST_BRANCH, distCommand, distHelp, renderDistWorkflow, renderDeployReadme, renderRequirementsTs, renderDomainTs, generate, parseJsonc, upsertJsonc, findWrangler, parseWhoAmI, SECRETS_FILE, PRIVATE_KEY_VAR, secretsCommand, productionEnv, routeProblem, preflight, printPreflight, deployCloudflare, importCommand, importHelp };
|
|
3651
|
+
export { renderAuthTs, allEnvKeys, renderEnvTs, renderEnvExample, renderEnvLocal, renderEnvProduction, envSummary, FRONTEND_BRANCH, renderFrontendApiTs, renderFrontendEnvProduction, renderStorefrontPage, renderFrontendWorkflow, runVoid, runInherit, captureVoid, isVoidApp, voidAppsIn, STRICT_APP, VOID_VERSION, strictDependencies, renderVoidPatch, renderViteConfig, renderStrictTsconfig, renderVoidJson, renderDbSchema, renderDbSeed, renderLayoutTsx, renderAppCss, renderIndexServer, INDEX_PAGE, renderDashboardPage, CLIENT_ONLY, renderSignInPage, renderAuthClient, renderContentTs, renderCron, renderQueue, renderLiveStream, renderLiveRoute, renderStrictAppPackageJson, DATA_README, BRANDING_README, MIGRATIONS_README, finishStrict, line, row, box, fullHelp, initHelp, version, findProject, ensureGenerated, DIST_DIR, DIST_BRANCH, distCommand, distHelp, renderDistWorkflow, renderDeployReadme, renderRequirementsTs, renderDomainTs, generate, parseJsonc, upsertJsonc, findWrangler, parseWhoAmI, keysCommand, SECRETS_FILE, PRIVATE_KEY_VAR, plaintextSecretNames, secretsCommand, productionEnv, routeProblem, preflight, printPreflight, deployCloudflare, importCommand, importHelp };
|