@saastemly/voidcommerce 0.8.0 → 0.9.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 CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  runVoid,
22
22
  version,
23
23
  voidAppsIn
24
- } from "./index-y6qzjd9h.js";
24
+ } from "./index-0w66yc8y.js";
25
25
  import {
26
26
  LOCAL_KEY_FILE,
27
27
  PRIVATE_KEY_VAR,
@@ -50,7 +50,7 @@ import {
50
50
  setVariable,
51
51
  variableNames,
52
52
  verifyCloudflareToken
53
- } from "./index-j5xjh6qp.js";
53
+ } from "./index-wykd4zmk.js";
54
54
  import {
55
55
  LAYOUTS,
56
56
  oneOrigin,
@@ -9,7 +9,7 @@ import {
9
9
  renderEnvLocal,
10
10
  renderEnvProduction,
11
11
  renderEnvTs
12
- } from "./index-j5xjh6qp.js";
12
+ } from "./index-wykd4zmk.js";
13
13
  import {
14
14
  MANIFEST_FILE,
15
15
  has,
@@ -1344,7 +1344,7 @@ import color from "picocolors";
1344
1344
  // package.json
1345
1345
  var package_default = {
1346
1346
  name: "@saastemly/voidcommerce",
1347
- version: "0.8.0",
1347
+ version: "0.9.0",
1348
1348
  description: "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
1349
1349
  type: "module",
1350
1350
  license: "MIT",
@@ -2293,6 +2293,39 @@ async function exists(path) {
2293
2293
  return false;
2294
2294
  }
2295
2295
  }
2296
+ async function putEnv(root, file, rendered, result) {
2297
+ const path = join4(root, file);
2298
+ if (!await exists(path)) {
2299
+ await mkdir2(dirname2(path), { recursive: true });
2300
+ await writeFile2(path, rendered, "utf8");
2301
+ result.written.push(file);
2302
+ return;
2303
+ }
2304
+ const read = (text) => {
2305
+ const out = new Map;
2306
+ for (const line2 of text.split(`
2307
+ `)) {
2308
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line2);
2309
+ if (match)
2310
+ out.set(match[1], match[2]);
2311
+ }
2312
+ return out;
2313
+ };
2314
+ const existing = read(await readFile2(path, "utf8"));
2315
+ const wanted = read(rendered);
2316
+ const merged = rendered.split(`
2317
+ `).map((line2) => {
2318
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line2);
2319
+ if (!match)
2320
+ return line2;
2321
+ const had = existing.get(match[1]);
2322
+ return had !== undefined && had !== "" && had !== "unset" ? `${match[1]}=${had}` : line2;
2323
+ }).join(`
2324
+ `);
2325
+ const dropped = [...existing.keys()].filter((key) => !wanted.has(key) && !key.startsWith("VITE_"));
2326
+ await writeFile2(path, merged, "utf8");
2327
+ result.written.push(dropped.length > 0 ? `${file} ${`(dropped ${dropped.join(", ")})`}` : file);
2328
+ }
2296
2329
  async function put(root, file, content, result, mode) {
2297
2330
  const path = join4(root, file);
2298
2331
  if (mode === "own" && await exists(path)) {
@@ -2364,7 +2397,7 @@ async function generateApi(root, dir, manifest, result, opts) {
2364
2397
  await put(root, at(".env.production"), renderEnvProduction(manifest), result, "regenerate");
2365
2398
  await put(root, at("lib/deploy/requirements.ts"), renderRequirementsTs(manifest), result, "regenerate");
2366
2399
  if (opts.env)
2367
- await put(root, at(".env"), renderEnvLocal(manifest), result, "own");
2400
+ await putEnv(root, at(".env"), renderEnvLocal(manifest), result);
2368
2401
  await put(root, at("lib/domain.ts"), renderDomainTs(manifest.layout), result, own);
2369
2402
  await put(root, at("lib/payment.ts"), renderPaymentTs(manifest), result, own);
2370
2403
  await put(root, at("lib/notifications.ts"), renderNotificationsTs(), result, own);
@@ -2517,7 +2550,7 @@ bunx vc guard
2517
2550
  await put(root, ".github/workflows/deploy.yml", renderDistWorkflow(manifest), result, "regenerate");
2518
2551
  await retire(root, ".github/workflows/void-dist.yml", result);
2519
2552
  await put(root, "DEPLOY.md", renderDeployReadme(manifest, zone(manifest), workerHosts(manifest)), result, "regenerate");
2520
- await put(root, ".env", renderEnvLocal(manifest), result, "own");
2553
+ await putEnv(root, ".env", renderEnvLocal(manifest), result);
2521
2554
  await put(root, "data/README.md", DATA_README, result, "own");
2522
2555
  await put(root, "branding/README.md", BRANDING_README, result, "own");
2523
2556
  await put(root, "migrations/README.md", MIGRATIONS_README, result, "own");
@@ -384,7 +384,7 @@ async function decryptSecrets(project) {
384
384
  }
385
385
  const expected = committedPublicKey(root);
386
386
  if (expected) {
387
- const { publicKeyFor } = await import("./keys-3zjezaxk.js");
387
+ const { publicKeyFor } = await import("./keys-68c6vmds.js");
388
388
  const derived = await publicKeyFor(privateKey);
389
389
  if (derived && derived.toLowerCase() !== expected.toLowerCase()) {
390
390
  return {
@@ -432,7 +432,7 @@ async function secretsCommand(project, args) {
432
432
  if (args[0] === "set")
433
433
  return setSecretValue(project, args.slice(1));
434
434
  if (args.includes("--sync"))
435
- return syncSecrets(project);
435
+ return syncSecrets(project, args.includes("--prune"));
436
436
  if (!existsSync2(join2(root, SECRETS_FILE))) {
437
437
  console.log(`
438
438
  ${color.yellow("No " + SECRETS_FILE + " yet.")} Secrets are set by hand with \`wrangler secret put\`,
@@ -625,7 +625,7 @@ function upsertLine(path, name, value) {
625
625
  `)}${name}=${value}
626
626
  `);
627
627
  }
628
- async function syncSecrets(project) {
628
+ async function syncSecrets(project, prune) {
629
629
  const root = project.root;
630
630
  const path = join2(root, SECRETS_FILE);
631
631
  if (!existsSync2(path)) {
@@ -656,13 +656,39 @@ ${color.green("+")} ${missing.length} key${missing.length === 1 ? "" : "s"} adde
656
656
  ${color.green("✓")} every secret this shop needs is already declared.
657
657
  `);
658
658
  }
659
- if (extra.length > 0) {
659
+ if (extra.length > 0 && prune) {
660
+ const body = readFileSync(path, "utf8");
661
+ const kept = [];
662
+ const lines = body.split(`
663
+ `);
664
+ for (let i = 0;i < lines.length; i++) {
665
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=/.exec(lines[i] ?? "");
666
+ if (match && extra.includes(match[1])) {
667
+ while (kept.length > 0 && (kept[kept.length - 1].startsWith("#") || kept[kept.length - 1].trim() === ""))
668
+ kept.pop();
669
+ if (lines[i + 1]?.trim() === "")
670
+ i++;
671
+ continue;
672
+ }
673
+ kept.push(lines[i]);
674
+ }
675
+ writeFileSync(path, `${kept.join(`
676
+ `).replace(/\n{3,}/g, `
677
+
678
+ `).replace(/\n*$/, "")}
679
+ `);
680
+ console.log(`${color.red("-")} ${extra.length} key${extra.length === 1 ? "" : "s"} removed from ${SECRETS_FILE}: ${extra.join(", ")}
681
+
682
+ ` + color.dim(` Their ciphertext is still in git history, so nothing is unrecoverable —
683
+ but nothing in this shop reads them any more.
684
+ `));
685
+ } else if (extra.length > 0) {
660
686
  console.log(`${color.yellow("!")} ${extra.length} declared key${extra.length === 1 ? " is" : "s are"} no longer required by this shop:
661
687
  ${extra.join(`
662
688
  `)}
663
689
 
664
- ` + color.dim(` Left alone — they cost nothing, and deleting a value you may still want
665
- is not this command's call. Remove them by hand when you are sure.
690
+ ` + color.dim(` Left alone: deleting a value you may still want is not this command's
691
+ call to make by default. \`vc secrets --sync --prune\` makes it.
666
692
  `));
667
693
  }
668
694
  return 0;
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ import {
51
51
  routeProblem,
52
52
  strictDependencies,
53
53
  upsertJsonc
54
- } from "./index-y6qzjd9h.js";
54
+ } from "./index-0w66yc8y.js";
55
55
  import {
56
56
  allEnvKeys,
57
57
  envSummary,
@@ -59,7 +59,7 @@ import {
59
59
  renderEnvLocal,
60
60
  renderEnvProduction,
61
61
  renderEnvTs
62
- } from "./index-j5xjh6qp.js";
62
+ } from "./index-wykd4zmk.js";
63
63
  import {
64
64
  LAYOUTS,
65
65
  MANIFEST_FILE,
@@ -8,7 +8,7 @@ import {
8
8
  localPrivateKey,
9
9
  provisionKey,
10
10
  publicKeyFor
11
- } from "./index-j5xjh6qp.js";
11
+ } from "./index-wykd4zmk.js";
12
12
  import"./index-tjy6yygc.js";
13
13
  import"./index-mnb7fz2t.js";
14
14
  import"./index-0v6na3yp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saastemly/voidcommerce",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -250,7 +250,7 @@ export async function secretsCommand(project: Project, args: string[]): Promise<
250
250
 
251
251
  if (args.includes("--init")) return initSecrets(project);
252
252
  if (args[0] === "set") return setSecretValue(project, args.slice(1));
253
- if (args.includes("--sync")) return syncSecrets(project);
253
+ if (args.includes("--sync")) return syncSecrets(project, args.includes("--prune"));
254
254
 
255
255
  if (!existsSync(join(root, SECRETS_FILE))) {
256
256
  console.log(
@@ -482,7 +482,7 @@ function upsertLine(path: string, name: string, value: string): void {
482
482
  * claims — the value may still be wanted, and it is not this command's to
483
483
  * destroy. Extras are reported instead.
484
484
  */
485
- async function syncSecrets(project: Project): Promise<number> {
485
+ async function syncSecrets(project: Project, prune: boolean): Promise<number> {
486
486
  const root = project.root;
487
487
  const path = join(root, SECRETS_FILE);
488
488
  if (!existsSync(path)) {
@@ -505,11 +505,33 @@ async function syncSecrets(project: Project): Promise<number> {
505
505
  console.log(`\n${color.green("✓")} every secret this shop needs is already declared.\n`);
506
506
  }
507
507
 
508
- if (extra.length > 0) {
508
+ if (extra.length > 0 && prune) {
509
+ // Deleting a value is the operator's call, and --prune is them making
510
+ // it. The whole entry goes — its comment line too — so the file reads
511
+ // as though the key was never required.
512
+ const body = readFileSync(path, "utf8");
513
+ const kept: string[] = [];
514
+ const lines = body.split("\n");
515
+ for (let i = 0; i < lines.length; i++) {
516
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=/.exec(lines[i] ?? "");
517
+ if (match && extra.includes(match[1]!)) {
518
+ // Drop the comment that introduces it, and the blank line after.
519
+ while (kept.length > 0 && (kept[kept.length - 1]!.startsWith("#") || kept[kept.length - 1]!.trim() === "")) kept.pop();
520
+ if (lines[i + 1]?.trim() === "") i++;
521
+ continue;
522
+ }
523
+ kept.push(lines[i]!);
524
+ }
525
+ writeFileSync(path, `${kept.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "")}\n`);
526
+ console.log(
527
+ `${color.red("-")} ${extra.length} key${extra.length === 1 ? "" : "s"} removed from ${SECRETS_FILE}: ${extra.join(", ")}\n\n` +
528
+ color.dim(` Their ciphertext is still in git history, so nothing is unrecoverable —\n but nothing in this shop reads them any more.\n`),
529
+ );
530
+ } else if (extra.length > 0) {
509
531
  console.log(
510
532
  `${color.yellow("!")} ${extra.length} declared key${extra.length === 1 ? " is" : "s are"} no longer required by this shop:\n` +
511
533
  ` ${extra.join("\n ")}\n\n` +
512
- color.dim(" Left alone — they cost nothing, and deleting a value you may still want\n is not this command's call. Remove them by hand when you are sure.\n"),
534
+ color.dim(` Left alone: deleting a value you may still want is not this command's\n call to make by default. \`vc secrets --sync --prune\` makes it.\n`),
513
535
  );
514
536
  }
515
537
  return 0;
@@ -89,6 +89,57 @@ async function exists(path: string): Promise<boolean> {
89
89
  }
90
90
  }
91
91
 
92
+ /**
93
+ * Write a dotenv file that RECONCILES rather than freezes.
94
+ *
95
+ * `.env` used to be written once and never touched, so a shop that swapped a
96
+ * carrier or a payment rail kept the old keys forever and never gained the
97
+ * new ones. devprints ended up carrying five Adyen keys and a Resend key for
98
+ * a shop that uses neither, while missing two it needed. That is the same
99
+ * owned-file drift that shipped a `.gitignore` without `.env.keys` and an
100
+ * api.ts pointing at another shop's domain.
101
+ *
102
+ * So: the template decides which keys EXIST, and the file on disk decides
103
+ * what they are SET TO. A value a person put there is always kept; a key the
104
+ * manifest no longer declares goes, and is reported rather than vanishing.
105
+ */
106
+ async function putEnv(root: string, file: string, rendered: string, result: GenerateResult) {
107
+ const path = join(root, file);
108
+ if (!(await exists(path))) {
109
+ await mkdir(dirname(path), { recursive: true });
110
+ await writeFile(path, rendered, "utf8");
111
+ result.written.push(file);
112
+ return;
113
+ }
114
+
115
+ const read = (text: string): Map<string, string> => {
116
+ const out = new Map<string, string>();
117
+ for (const line of text.split("\n")) {
118
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line);
119
+ if (match) out.set(match[1]!, match[2]!);
120
+ }
121
+ return out;
122
+ };
123
+ const existing = read(await readFile(path, "utf8"));
124
+ const wanted = read(rendered);
125
+
126
+ const merged = rendered
127
+ .split("\n")
128
+ .map((line) => {
129
+ const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line);
130
+ if (!match) return line;
131
+ const had = existing.get(match[1]!);
132
+ // A real value the operator set beats the template's placeholder.
133
+ // `unset` and empty are placeholders, not answers.
134
+ return had !== undefined && had !== "" && had !== "unset" ? `${match[1]}=${had}` : line;
135
+ })
136
+ .join("\n");
137
+
138
+ const dropped = [...existing.keys()].filter((key) => !wanted.has(key) && !key.startsWith("VITE_"));
139
+ await writeFile(path, merged, "utf8");
140
+ result.written.push(dropped.length > 0 ? `${file} ${`(dropped ${dropped.join(", ")})`}` : file);
141
+ }
142
+
92
143
  async function put(root: string, file: string, content: string, result: GenerateResult, mode: "regenerate" | "own") {
93
144
  const path = join(root, file);
94
145
  if (mode === "own" && (await exists(path))) {
@@ -182,7 +233,7 @@ async function generateApi(root: string, dir: string, manifest: Manifest, result
182
233
  await put(root, at(".env.production"), renderEnvProduction(manifest), result, "regenerate");
183
234
  await put(root, at("lib/deploy/requirements.ts"), renderRequirementsTs(manifest), result, "regenerate");
184
235
 
185
- if (opts.env) await put(root, at(".env"), renderEnvLocal(manifest), result, "own");
236
+ if (opts.env) await putEnv(root, at(".env"), renderEnvLocal(manifest), result);
186
237
  await put(root, at("lib/domain.ts"), renderDomainTs(manifest.layout), result, own);
187
238
  await put(root, at("lib/payment.ts"), renderPaymentTs(manifest), result, own);
188
239
  await put(root, at("lib/notifications.ts"), renderNotificationsTs(), result, own);
@@ -395,7 +446,7 @@ bunx vc guard
395
446
  // build. Two workflows on the same push would deploy the shop twice.
396
447
  await retire(root, ".github/workflows/void-dist.yml", result);
397
448
  await put(root, "DEPLOY.md", renderDeployReadme(manifest, zone(manifest), workerHosts(manifest)), result, "regenerate");
398
- await put(root, ".env", renderEnvLocal(manifest), result, "own");
449
+ await putEnv(root, ".env", renderEnvLocal(manifest), result);
399
450
  await put(root, "data/README.md", DATA_README, result, "own");
400
451
  await put(root, "branding/README.md", BRANDING_README, result, "own");
401
452
  await put(root, "migrations/README.md", MIGRATIONS_README, result, "own");