@betterstart/cli 0.1.30 → 0.1.31

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
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-6JCWMKSY.js";
5
5
 
6
6
  // src/cli.ts
7
- import { Command as Command8 } from "commander";
7
+ import { Command as Command9 } from "commander";
8
8
 
9
9
  // src/commands/generate.ts
10
10
  import path22 from "path";
@@ -901,8 +901,8 @@ function toPascalCase3(str) {
901
901
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
902
902
  }
903
903
  function toCamelCase(str) {
904
- const p6 = toPascalCase3(str);
905
- return p6.charAt(0).toLowerCase() + p6.slice(1);
904
+ const p7 = toPascalCase3(str);
905
+ return p7.charAt(0).toLowerCase() + p7.slice(1);
906
906
  }
907
907
  function toKebabCase(str) {
908
908
  return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
@@ -993,7 +993,7 @@ function generateFormAdminPages(schema, cwd, pagesDir, options) {
993
993
  const adminDir = path4.join(cwd, pagesDir, "forms", kebab);
994
994
  if (!fs4.existsSync(adminDir)) fs4.mkdirSync(adminDir, { recursive: true });
995
995
  const files = [];
996
- const rel = (p6) => path4.relative(cwd, p6);
996
+ const rel = (p7) => path4.relative(cwd, p7);
997
997
  const pagePath = path4.join(adminDir, "page.tsx");
998
998
  if (!fs4.existsSync(pagePath) || options.force) {
999
999
  fs4.writeFileSync(pagePath, generatePage(pascal, kebab), "utf-8");
@@ -1934,8 +1934,8 @@ function toPascalCase4(str) {
1934
1934
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
1935
1935
  }
1936
1936
  function toCamelCase2(str) {
1937
- const p6 = toPascalCase4(str);
1938
- return p6.charAt(0).toLowerCase() + p6.slice(1);
1937
+ const p7 = toPascalCase4(str);
1938
+ return p7.charAt(0).toLowerCase() + p7.slice(1);
1939
1939
  }
1940
1940
  function toKebabCase3(str) {
1941
1941
  return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
@@ -2650,8 +2650,8 @@ function toPascalCase5(str) {
2650
2650
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
2651
2651
  }
2652
2652
  function toCamelCase3(str) {
2653
- const p6 = toPascalCase5(str);
2654
- return p6.charAt(0).toLowerCase() + p6.slice(1);
2653
+ const p7 = toPascalCase5(str);
2654
+ return p7.charAt(0).toLowerCase() + p7.slice(1);
2655
2655
  }
2656
2656
  function singularize2(str) {
2657
2657
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -2711,11 +2711,11 @@ function generateActions(schema, cwd, actionsDir, options = {}) {
2711
2711
  const listFieldsWithRels = findListFieldsWithRelationships(dbFields);
2712
2712
  const hasListRels = listFieldsWithRels.length > 0;
2713
2713
  const allListRelQueries = [];
2714
- for (const { field: listField, path: path42 } of listFieldsWithRels) {
2714
+ for (const { field: listField, path: path43 } of listFieldsWithRels) {
2715
2715
  const rels = (listField.fields || []).filter((f) => f.type === "relationship" && f.relationship);
2716
2716
  for (const relField of rels) {
2717
2717
  allListRelQueries.push({
2718
- fieldPath: path42.join("_"),
2718
+ fieldPath: path43.join("_"),
2719
2719
  relField,
2720
2720
  relTable: toCamelCase3(relField.relationship),
2721
2721
  listFieldName: listField.name
@@ -3374,8 +3374,8 @@ function toPascalCase6(str) {
3374
3374
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
3375
3375
  }
3376
3376
  function toCamelCase4(str) {
3377
- const p6 = toPascalCase6(str);
3378
- return p6.charAt(0).toLowerCase() + p6.slice(1);
3377
+ const p7 = toPascalCase6(str);
3378
+ return p7.charAt(0).toLowerCase() + p7.slice(1);
3379
3379
  }
3380
3380
  function singularize3(str) {
3381
3381
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -4506,8 +4506,8 @@ function toPascalCase9(str) {
4506
4506
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
4507
4507
  }
4508
4508
  function toCamelCase5(str) {
4509
- const p6 = toPascalCase9(str);
4510
- return p6.charAt(0).toLowerCase() + p6.slice(1);
4509
+ const p7 = toPascalCase9(str);
4510
+ return p7.charAt(0).toLowerCase() + p7.slice(1);
4511
4511
  }
4512
4512
  function singularize6(str) {
4513
4513
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -4755,8 +4755,8 @@ function toPascalCase10(str) {
4755
4755
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
4756
4756
  }
4757
4757
  function toCamelCase6(str) {
4758
- const p6 = toPascalCase10(str);
4759
- return p6.charAt(0).toLowerCase() + p6.slice(1);
4758
+ const p7 = toPascalCase10(str);
4759
+ return p7.charAt(0).toLowerCase() + p7.slice(1);
4760
4760
  }
4761
4761
  function singularize7(str) {
4762
4762
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -5417,6 +5417,36 @@ ${indent} </FormControl>${nestedHint}
5417
5417
  ${indent} <FormMessage />
5418
5418
  ${indent} </FormItem>
5419
5419
  ${indent} )}
5420
+ ${indent} />`;
5421
+ }
5422
+ if (nf.type === "video") {
5423
+ return `${indent} <FormField
5424
+ ${indent} control={form.control}
5425
+ ${indent} name={\`${field.name}.\${index}.${nf.name}\`}
5426
+ ${indent} render={({ field: formField }) => (
5427
+ ${indent} <FormItem>
5428
+ ${indent} <FormLabel>${nestedLabel}</FormLabel>
5429
+ ${indent} <FormControl>
5430
+ ${indent} <VideoUploadField value={formField.value} onChange={formField.onChange} onBlur={formField.onBlur} disabled={isPending} maxSizeInMB={100} label="" />
5431
+ ${indent} </FormControl>${nestedHint}
5432
+ ${indent} <FormMessage />
5433
+ ${indent} </FormItem>
5434
+ ${indent} )}
5435
+ ${indent} />`;
5436
+ }
5437
+ if (nf.type === "media") {
5438
+ return `${indent} <FormField
5439
+ ${indent} control={form.control}
5440
+ ${indent} name={\`${field.name}.\${index}.${nf.name}\`}
5441
+ ${indent} render={({ field: formField }) => (
5442
+ ${indent} <FormItem>
5443
+ ${indent} <FormLabel>${nestedLabel}</FormLabel>
5444
+ ${indent} <FormControl>
5445
+ ${indent} <MediaUploadField value={formField.value} onChange={formField.onChange} onBlur={formField.onBlur} disabled={isPending} maxSizeInMB={100} label="" />
5446
+ ${indent} </FormControl>${nestedHint}
5447
+ ${indent} <FormMessage />
5448
+ ${indent} </FormItem>
5449
+ ${indent} )}
5420
5450
  ${indent} />`;
5421
5451
  }
5422
5452
  return `${indent} <FormField
@@ -6945,8 +6975,8 @@ function toPascalCase16(str) {
6945
6975
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
6946
6976
  }
6947
6977
  function toCamelCase7(str) {
6948
- const p6 = toPascalCase16(str);
6949
- return p6.charAt(0).toLowerCase() + p6.slice(1);
6978
+ const p7 = toPascalCase16(str);
6979
+ return p7.charAt(0).toLowerCase() + p7.slice(1);
6950
6980
  }
6951
6981
  function singularize12(str) {
6952
6982
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -7631,7 +7661,7 @@ function runPostGenerate(cwd, schemaName, options = {}) {
7631
7661
  console.log("\n Running drizzle-kit push...");
7632
7662
  const drizzleBin = path21.join(cwd, "node_modules", ".bin", "drizzle-kit");
7633
7663
  try {
7634
- execFileSync2(drizzleBin, ["push", "--force"], { cwd, stdio: "pipe" });
7664
+ execFileSync2(drizzleBin, ["push", "--force"], { cwd, stdio: "inherit" });
7635
7665
  result.dbPush = "success";
7636
7666
  console.log(" Database schema synced");
7637
7667
  } catch {
@@ -13419,13 +13449,13 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13419
13449
  clack.cancel("Cancelled.");
13420
13450
  process.exit(0);
13421
13451
  }
13422
- const password3 = await clack.password({
13452
+ const password4 = await clack.password({
13423
13453
  message: "Admin password",
13424
13454
  validate: (v) => {
13425
13455
  if (!v || v.length < 8) return "Password must be at least 8 characters";
13426
13456
  }
13427
13457
  });
13428
- if (clack.isCancel(password3)) {
13458
+ if (clack.isCancel(password4)) {
13429
13459
  clack.cancel("Cancelled.");
13430
13460
  process.exit(0);
13431
13461
  }
@@ -13455,7 +13485,7 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13455
13485
  env: {
13456
13486
  ...process.env,
13457
13487
  SEED_EMAIL: email,
13458
- SEED_PASSWORD: password3,
13488
+ SEED_PASSWORD: password4,
13459
13489
  SEED_NAME: name || "Admin",
13460
13490
  ...overwrite ? { SEED_OVERWRITE: "true" } : {}
13461
13491
  }
@@ -13471,13 +13501,13 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13471
13501
  }
13472
13502
  );
13473
13503
  });
13474
- const spinner5 = clack.spinner();
13475
- spinner5.start("Creating admin user...");
13504
+ const spinner6 = clack.spinner();
13505
+ spinner6.start("Creating admin user...");
13476
13506
  try {
13477
13507
  const result = await runSeed2(false);
13478
13508
  if (result.code === 2) {
13479
13509
  const existingName = result.stdout.split("\n").find((l) => l.startsWith("EXISTING_USER:"))?.replace("EXISTING_USER:", "")?.trim() || "unknown";
13480
- spinner5.stop(`Account already exists for ${email}`);
13510
+ spinner6.stop(`Account already exists for ${email}`);
13481
13511
  const overwrite = await clack.confirm({
13482
13512
  message: `An admin account (${existingName}) already exists with this email. Replace it?`
13483
13513
  });
@@ -13489,14 +13519,14 @@ var seedCommand = new Command2("seed").description("Create the initial admin use
13489
13519
  }
13490
13520
  process.exit(0);
13491
13521
  }
13492
- spinner5.start("Replacing admin user...");
13522
+ spinner6.start("Replacing admin user...");
13493
13523
  await runSeed2(true);
13494
- spinner5.stop("Admin user replaced");
13524
+ spinner6.stop("Admin user replaced");
13495
13525
  } else {
13496
- spinner5.stop("Admin user created");
13526
+ spinner6.stop("Admin user created");
13497
13527
  }
13498
13528
  } catch (err) {
13499
- spinner5.stop("Failed to create admin user");
13529
+ spinner6.stop("Failed to create admin user");
13500
13530
  const errMsg = err instanceof Error ? err.message : String(err);
13501
13531
  clack.log.error(errMsg);
13502
13532
  clack.log.info("You can run the seed script manually:");
@@ -14005,7 +14035,7 @@ function hasDbUrl(cwd) {
14005
14035
  }
14006
14036
  return false;
14007
14037
  }
14008
- function runSeed(cwd, cmsDir, email, password3, overwrite = false) {
14038
+ function runSeed(cwd, cmsDir, email, password4, overwrite = false) {
14009
14039
  const scriptsDir = path37.join(cwd, cmsDir, "scripts");
14010
14040
  const seedPath = path37.join(scriptsDir, "seed.ts");
14011
14041
  if (!fs32.existsSync(scriptsDir)) {
@@ -14029,7 +14059,7 @@ function runSeed(cwd, cmsDir, email, password3, overwrite = false) {
14029
14059
  env: {
14030
14060
  ...process.env,
14031
14061
  SEED_EMAIL: email,
14032
- SEED_PASSWORD: password3,
14062
+ SEED_PASSWORD: password4,
14033
14063
  SEED_NAME: "Admin",
14034
14064
  ...overwrite ? { SEED_OVERWRITE: "true" } : {}
14035
14065
  }
@@ -14116,8 +14146,8 @@ function toPascalCase17(str) {
14116
14146
  return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
14117
14147
  }
14118
14148
  function toCamelCase8(str) {
14119
- const p6 = toPascalCase17(str);
14120
- return p6.charAt(0).toLowerCase() + p6.slice(1);
14149
+ const p7 = toPascalCase17(str);
14150
+ return p7.charAt(0).toLowerCase() + p7.slice(1);
14121
14151
  }
14122
14152
  function singularize13(str) {
14123
14153
  if (str.endsWith("ies")) return `${str.slice(0, -3)}y`;
@@ -14329,15 +14359,298 @@ var removeCommand = new Command4("remove").alias("rm").description("Remove all g
14329
14359
  console.log("");
14330
14360
  });
14331
14361
 
14332
- // src/commands/uninstall.ts
14333
- import fs35 from "fs";
14362
+ // src/commands/setup-r2.ts
14363
+ import { execFileSync as execFileSync5, spawnSync } from "child_process";
14364
+ import fs34 from "fs";
14365
+ import os from "os";
14334
14366
  import path39 from "path";
14335
14367
  import * as p5 from "@clack/prompts";
14336
14368
  import { Command as Command5 } from "commander";
14337
14369
  import pc3 from "picocolors";
14370
+ var setupR2Command = new Command5("setup-r2").description("Create a Cloudflare R2 bucket and configure storage env vars").option("--cwd <path>", "Project root path").option("--bucket <name>", "Bucket name (skips prompt)").action(async (options) => {
14371
+ const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
14372
+ p5.intro(pc3.bgCyan(pc3.black(" BetterStart \u2014 R2 Storage Setup ")));
14373
+ const s = p5.spinner();
14374
+ s.start("Looking for wrangler CLI");
14375
+ const wrangler = findWrangler(cwd);
14376
+ if (!wrangler) {
14377
+ s.stop(`${pc3.red("\u2717")} Wrangler CLI not found`);
14378
+ p5.log.error(
14379
+ `Install it first:
14380
+ ${pc3.cyan("npm install -g wrangler")}
14381
+ ${pc3.dim("or")} ${pc3.cyan("npx wrangler --version")}`
14382
+ );
14383
+ process.exit(1);
14384
+ }
14385
+ s.stop(`Wrangler: ${pc3.cyan(wrangler.bin === "npx" ? "npx wrangler" : "wrangler")}`);
14386
+ s.start("Checking Cloudflare authentication");
14387
+ const whoami = runWrangler(wrangler, ["whoami"], { cwd });
14388
+ const whoamiOut = whoami.stdout?.toString() ?? "";
14389
+ if (whoami.status !== 0 || whoamiOut.includes("Not authenticated")) {
14390
+ s.stop(`${pc3.yellow("\u25B2")} Not logged in to Cloudflare`);
14391
+ const login = await p5.confirm({
14392
+ message: "Open browser to log in to Cloudflare?",
14393
+ initialValue: true
14394
+ });
14395
+ if (p5.isCancel(login) || !login) {
14396
+ p5.cancel("Setup cancelled. Run `wrangler login` manually first.");
14397
+ process.exit(0);
14398
+ }
14399
+ s.start("Waiting for Cloudflare login");
14400
+ const loginResult = runWrangler(wrangler, ["login"], { cwd, stdio: "inherit", timeout: 12e4 });
14401
+ if (loginResult.status !== 0) {
14402
+ s.stop(`${pc3.red("\u2717")} Login failed`);
14403
+ p5.cancel("Could not authenticate with Cloudflare. Run `wrangler login` manually.");
14404
+ process.exit(1);
14405
+ }
14406
+ s.stop(`${pc3.green("\u2713")} Logged in to Cloudflare`);
14407
+ } else {
14408
+ const emailMatch = whoamiOut.match(/associated with the email\s+(\S+)/i);
14409
+ const tableMatch = whoamiOut.match(/│\s*([^│]+?)\s*│\s*[a-f0-9]{32}\s*│/);
14410
+ const accountLabel = emailMatch?.[1] ?? tableMatch?.[1]?.trim() ?? "authenticated";
14411
+ s.stop(`Logged in as ${pc3.cyan(accountLabel)}`);
14412
+ }
14413
+ s.start("Fetching account ID");
14414
+ const accountId = extractAccountId(wrangler, cwd);
14415
+ if (!accountId) {
14416
+ s.stop(`${pc3.red("\u2717")} Could not determine account ID`);
14417
+ p5.log.info(
14418
+ `You can find it at: ${pc3.cyan("https://dash.cloudflare.com/?to=/:account/r2")}`
14419
+ );
14420
+ process.exit(1);
14421
+ }
14422
+ s.stop(`Account ID: ${pc3.dim(accountId)}`);
14423
+ let bucketName = options.bucket;
14424
+ if (!bucketName) {
14425
+ const result = await p5.text({
14426
+ message: "R2 bucket name",
14427
+ placeholder: "betterstart-uploads",
14428
+ defaultValue: "betterstart-uploads",
14429
+ validate: (v) => {
14430
+ if (!v || v.length < 3) return "Bucket name must be at least 3 characters";
14431
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(v))
14432
+ return "Bucket name must be lowercase alphanumeric with hyphens, no leading/trailing hyphens";
14433
+ }
14434
+ });
14435
+ if (p5.isCancel(result)) {
14436
+ p5.cancel("Setup cancelled.");
14437
+ process.exit(0);
14438
+ }
14439
+ bucketName = result;
14440
+ }
14441
+ s.start(`Creating R2 bucket: ${bucketName}`);
14442
+ const createResult = runWrangler(wrangler, ["r2", "bucket", "create", bucketName], {
14443
+ cwd,
14444
+ timeout: 3e4
14445
+ });
14446
+ const createOut = (createResult.stdout?.toString() ?? "") + (createResult.stderr?.toString() ?? "");
14447
+ if (createResult.status !== 0) {
14448
+ if (createOut.includes("already exists") || createOut.includes("AlreadyExists")) {
14449
+ s.stop(`Bucket ${pc3.cyan(bucketName)} already exists \u2014 using it`);
14450
+ } else {
14451
+ s.stop("Failed to create bucket");
14452
+ p5.log.error(createOut.trim() || "Unknown error from wrangler");
14453
+ process.exit(1);
14454
+ }
14455
+ } else {
14456
+ s.stop(`Created bucket: ${pc3.cyan(bucketName)}`);
14457
+ }
14458
+ let publicUrl = "";
14459
+ const oauthToken = readWranglerToken();
14460
+ if (oauthToken) {
14461
+ s.start("Enabling public r2.dev URL");
14462
+ const domainResult = await enablePublicDomain(accountId, bucketName, oauthToken);
14463
+ if (domainResult.success && domainResult.domain) {
14464
+ publicUrl = `https://${domainResult.domain}`;
14465
+ s.stop(`Public URL: ${pc3.cyan(publicUrl)}`);
14466
+ } else {
14467
+ s.stop("Could not enable public URL automatically");
14468
+ p5.log.warning(domainResult.error ?? "Unknown error");
14469
+ p5.log.info(
14470
+ `You can enable it manually in the dashboard:
14471
+ ${pc3.cyan(`https://dash.cloudflare.com/${accountId}/r2/default/buckets/${bucketName}/settings`)}`
14472
+ );
14473
+ }
14474
+ } else {
14475
+ p5.log.warning("Could not read wrangler OAuth token \u2014 skipping public URL setup");
14476
+ p5.log.info(
14477
+ `Enable it manually: ${pc3.cyan(`https://dash.cloudflare.com/${accountId}/r2/default/buckets/${bucketName}/settings`)}`
14478
+ );
14479
+ }
14480
+ p5.note(
14481
+ [
14482
+ `Create an R2 API token with ${pc3.bold("Object Read & Write")} permission.`,
14483
+ "",
14484
+ `Dashboard: ${pc3.cyan(`https://dash.cloudflare.com/${accountId}/r2/api-tokens`)}`,
14485
+ "",
14486
+ pc3.dim("The dashboard will give you an Access Key ID and Secret Access Key.")
14487
+ ].join("\n"),
14488
+ "Create R2 API Token"
14489
+ );
14490
+ const openDashboard = await p5.confirm({
14491
+ message: "Open the R2 API tokens page in your browser?",
14492
+ initialValue: true
14493
+ });
14494
+ if (!p5.isCancel(openDashboard) && openDashboard) {
14495
+ const url = `https://dash.cloudflare.com/${accountId}/r2/api-tokens`;
14496
+ try {
14497
+ execFileSync5("open", [url], { stdio: "pipe", timeout: 5e3 });
14498
+ } catch {
14499
+ p5.log.warning(`Could not open browser. Visit: ${pc3.cyan(url)}`);
14500
+ }
14501
+ }
14502
+ const credentials = await p5.group(
14503
+ {
14504
+ accessKeyId: () => p5.text({
14505
+ message: "R2 Access Key ID",
14506
+ placeholder: "Paste from Cloudflare dashboard",
14507
+ validate: (v) => {
14508
+ if (!v || v.trim().length < 10) return "Please paste a valid Access Key ID";
14509
+ }
14510
+ }),
14511
+ secretAccessKey: () => p5.password({
14512
+ message: "R2 Secret Access Key",
14513
+ validate: (v) => {
14514
+ if (!v || v.trim().length < 10) return "Please paste a valid Secret Access Key";
14515
+ }
14516
+ })
14517
+ },
14518
+ {
14519
+ onCancel: () => {
14520
+ p5.cancel("Setup cancelled.");
14521
+ process.exit(0);
14522
+ }
14523
+ }
14524
+ );
14525
+ s.start("Writing environment variables");
14526
+ const envResult = appendEnvVars(cwd, [
14527
+ {
14528
+ header: "Storage (Cloudflare R2)",
14529
+ vars: [
14530
+ { key: "BETTERSTART_R2_ACCOUNT_ID", value: accountId },
14531
+ { key: "BETTERSTART_R2_ACCESS_KEY_ID", value: credentials.accessKeyId.trim() },
14532
+ { key: "BETTERSTART_R2_SECRET_ACCESS_KEY", value: credentials.secretAccessKey.trim() },
14533
+ { key: "BETTERSTART_R2_BUCKET_NAME", value: bucketName },
14534
+ ...publicUrl ? [{ key: "BETTERSTART_R2_PUBLIC_URL", value: publicUrl }] : []
14535
+ ]
14536
+ }
14537
+ ], /* @__PURE__ */ new Set([
14538
+ "BETTERSTART_R2_ACCOUNT_ID",
14539
+ "BETTERSTART_R2_ACCESS_KEY_ID",
14540
+ "BETTERSTART_R2_SECRET_ACCESS_KEY",
14541
+ "BETTERSTART_R2_BUCKET_NAME",
14542
+ "BETTERSTART_R2_PUBLIC_URL"
14543
+ ]));
14544
+ const totalChanged = envResult.added.length + envResult.updated.length;
14545
+ if (totalChanged > 0) {
14546
+ s.stop(`Updated .env.local ${pc3.dim(`(${envResult.added.length} added, ${envResult.updated.length} updated)`)}`);
14547
+ } else {
14548
+ s.stop("All R2 env vars already set in .env.local");
14549
+ }
14550
+ const summaryLines = [
14551
+ `Bucket: ${pc3.cyan(bucketName)}`,
14552
+ `Account: ${pc3.dim(accountId)}`,
14553
+ `Access Key: ${pc3.dim(credentials.accessKeyId.trim().slice(0, 8) + "...")}`
14554
+ ];
14555
+ if (publicUrl) {
14556
+ summaryLines.push(`Public URL: ${pc3.cyan(publicUrl)}`);
14557
+ }
14558
+ summaryLines.push(`Env file: ${pc3.dim(".env.local")}`);
14559
+ p5.note(summaryLines.join("\n"), pc3.green("R2 storage configured"));
14560
+ p5.outro("Done! Your CMS can now upload files to R2.");
14561
+ });
14562
+ function findWrangler(cwd) {
14563
+ const localBin = path39.join(cwd, "node_modules", ".bin", "wrangler");
14564
+ if (fs34.existsSync(localBin)) return { bin: localBin, prefix: [] };
14565
+ const result = spawnSync("which", ["wrangler"], { stdio: "pipe", timeout: 5e3 });
14566
+ if (result.status === 0) {
14567
+ const found = result.stdout?.toString().trim();
14568
+ if (found) return { bin: found, prefix: [] };
14569
+ }
14570
+ const npxResult = spawnSync("npx", ["wrangler", "--version"], {
14571
+ stdio: "pipe",
14572
+ timeout: 15e3
14573
+ });
14574
+ if (npxResult.status === 0) return { bin: "npx", prefix: ["wrangler"] };
14575
+ return null;
14576
+ }
14577
+ function runWrangler(ref, args, opts) {
14578
+ const fullArgs = [...ref.prefix, ...args];
14579
+ return spawnSync(ref.bin, fullArgs, {
14580
+ cwd: opts.cwd,
14581
+ stdio: opts.stdio ?? "pipe",
14582
+ timeout: opts.timeout ?? 15e3
14583
+ });
14584
+ }
14585
+ function extractAccountId(ref, cwd) {
14586
+ const result = runWrangler(ref, ["whoami"], { cwd });
14587
+ const output = result.stdout?.toString() ?? "";
14588
+ const idMatch = output.match(/Account ID[:\s]+([a-f0-9]{32})/i);
14589
+ if (idMatch) return idMatch[1];
14590
+ const hexMatch = output.match(/\b([a-f0-9]{32})\b/);
14591
+ if (hexMatch) return hexMatch[1];
14592
+ return null;
14593
+ }
14594
+ function readWranglerToken() {
14595
+ const candidates = [
14596
+ path39.join(os.homedir(), "Library", "Preferences", ".wrangler", "config", "default.toml"),
14597
+ // macOS
14598
+ path39.join(os.homedir(), ".config", ".wrangler", "config", "default.toml"),
14599
+ // Linux
14600
+ path39.join(os.homedir(), ".wrangler", "config", "default.toml")
14601
+ // fallback
14602
+ ];
14603
+ if (process.env.WRANGLER_CONFIG_PATH) {
14604
+ candidates.unshift(process.env.WRANGLER_CONFIG_PATH);
14605
+ }
14606
+ if (process.env.XDG_CONFIG_HOME) {
14607
+ candidates.unshift(
14608
+ path39.join(process.env.XDG_CONFIG_HOME, ".wrangler", "config", "default.toml")
14609
+ );
14610
+ }
14611
+ for (const configPath of candidates) {
14612
+ if (!fs34.existsSync(configPath)) continue;
14613
+ try {
14614
+ const content = fs34.readFileSync(configPath, "utf-8");
14615
+ const match = content.match(/^oauth_token\s*=\s*"([^"]+)"/m);
14616
+ if (match) return match[1];
14617
+ } catch {
14618
+ continue;
14619
+ }
14620
+ }
14621
+ return null;
14622
+ }
14623
+ async function enablePublicDomain(accountId, bucketName, token) {
14624
+ try {
14625
+ const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets/${bucketName}/domains/managed`;
14626
+ const res = await fetch(url, {
14627
+ method: "PUT",
14628
+ headers: {
14629
+ Authorization: `Bearer ${token}`,
14630
+ "Content-Type": "application/json"
14631
+ },
14632
+ body: JSON.stringify({ enabled: true })
14633
+ });
14634
+ const data = await res.json();
14635
+ if (data.success && data.result?.domain) {
14636
+ return { success: true, domain: data.result.domain };
14637
+ }
14638
+ const errMsg = data.errors?.[0]?.message ?? "API returned success=false";
14639
+ return { success: false, error: errMsg };
14640
+ } catch (err) {
14641
+ return { success: false, error: err instanceof Error ? err.message : "fetch failed" };
14642
+ }
14643
+ }
14644
+
14645
+ // src/commands/uninstall.ts
14646
+ import fs36 from "fs";
14647
+ import path40 from "path";
14648
+ import * as p6 from "@clack/prompts";
14649
+ import { Command as Command6 } from "commander";
14650
+ import pc4 from "picocolors";
14338
14651
 
14339
14652
  // src/commands/uninstall-cleaners.ts
14340
- import fs34 from "fs";
14653
+ import fs35 from "fs";
14341
14654
  function stripJsonComments2(input) {
14342
14655
  let result = "";
14343
14656
  let i = 0;
@@ -14371,8 +14684,8 @@ function stripJsonComments2(input) {
14371
14684
  return result;
14372
14685
  }
14373
14686
  function cleanTsconfig(tsconfigPath) {
14374
- if (!fs34.existsSync(tsconfigPath)) return [];
14375
- const raw = fs34.readFileSync(tsconfigPath, "utf-8");
14687
+ if (!fs35.existsSync(tsconfigPath)) return [];
14688
+ const raw = fs35.readFileSync(tsconfigPath, "utf-8");
14376
14689
  const stripped = stripJsonComments2(raw).replace(/,\s*([\]}])/g, "$1");
14377
14690
  let tsconfig;
14378
14691
  try {
@@ -14396,13 +14709,13 @@ function cleanTsconfig(tsconfigPath) {
14396
14709
  compilerOptions.paths = paths;
14397
14710
  }
14398
14711
  tsconfig.compilerOptions = compilerOptions;
14399
- fs34.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
14712
+ fs35.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
14400
14713
  `, "utf-8");
14401
14714
  return removed;
14402
14715
  }
14403
14716
  function cleanCss(cssPath) {
14404
- if (!fs34.existsSync(cssPath)) return [];
14405
- const content = fs34.readFileSync(cssPath, "utf-8");
14717
+ if (!fs35.existsSync(cssPath)) return [];
14718
+ const content = fs35.readFileSync(cssPath, "utf-8");
14406
14719
  const lines = content.split("\n");
14407
14720
  const sourcePattern = /^@source\s+"[^"]*cms[^"]*";\s*$/;
14408
14721
  const removed = [];
@@ -14416,12 +14729,12 @@ function cleanCss(cssPath) {
14416
14729
  }
14417
14730
  if (removed.length === 0) return [];
14418
14731
  const cleaned = kept.join("\n").replace(/\n{3,}/g, "\n\n");
14419
- fs34.writeFileSync(cssPath, cleaned, "utf-8");
14732
+ fs35.writeFileSync(cssPath, cleaned, "utf-8");
14420
14733
  return removed;
14421
14734
  }
14422
14735
  function cleanEnvFile(envPath) {
14423
- if (!fs34.existsSync(envPath)) return [];
14424
- const content = fs34.readFileSync(envPath, "utf-8");
14736
+ if (!fs35.existsSync(envPath)) return [];
14737
+ const content = fs35.readFileSync(envPath, "utf-8");
14425
14738
  const lines = content.split("\n");
14426
14739
  const removed = [];
14427
14740
  const kept = [];
@@ -14454,9 +14767,9 @@ function cleanEnvFile(envPath) {
14454
14767
  if (removed.length === 0) return [];
14455
14768
  const result = kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
14456
14769
  if (result === "") {
14457
- fs34.unlinkSync(envPath);
14770
+ fs35.unlinkSync(envPath);
14458
14771
  } else {
14459
- fs34.writeFileSync(envPath, `${result}
14772
+ fs35.writeFileSync(envPath, `${result}
14460
14773
  `, "utf-8");
14461
14774
  }
14462
14775
  return removed;
@@ -14482,15 +14795,15 @@ function findMainCss2(cwd) {
14482
14795
  "globals.css"
14483
14796
  ];
14484
14797
  for (const candidate of candidates) {
14485
- const filePath = path39.join(cwd, candidate);
14486
- if (fs35.existsSync(filePath)) return filePath;
14798
+ const filePath = path40.join(cwd, candidate);
14799
+ if (fs36.existsSync(filePath)) return filePath;
14487
14800
  }
14488
14801
  return void 0;
14489
14802
  }
14490
14803
  function isCLICreatedBiome(biomePath) {
14491
- if (!fs35.existsSync(biomePath)) return false;
14804
+ if (!fs36.existsSync(biomePath)) return false;
14492
14805
  try {
14493
- const content = JSON.parse(fs35.readFileSync(biomePath, "utf-8"));
14806
+ const content = JSON.parse(fs36.readFileSync(biomePath, "utf-8"));
14494
14807
  return content.$schema?.includes("biomejs.dev") && content.formatter?.indentStyle === "space" && content.javascript?.formatter?.quoteStyle === "single" && Array.isArray(content.files?.ignore) && content.files.ignore.includes(".next");
14495
14808
  } catch {
14496
14809
  return false;
@@ -14498,13 +14811,13 @@ function isCLICreatedBiome(biomePath) {
14498
14811
  }
14499
14812
  function buildUninstallPlan(cwd) {
14500
14813
  const steps = [];
14501
- const hasSrc = fs35.existsSync(path39.join(cwd, "src"));
14814
+ const hasSrc = fs36.existsSync(path40.join(cwd, "src"));
14502
14815
  const appBase = hasSrc ? "src/app" : "app";
14503
14816
  const dirs = [];
14504
- const cmsDir = path39.join(cwd, "cms");
14505
- const cmsRouteGroup = path39.join(cwd, appBase, "(cms)");
14506
- if (fs35.existsSync(cmsDir)) dirs.push("cms/");
14507
- if (fs35.existsSync(cmsRouteGroup)) dirs.push(`${appBase}/(cms)/`);
14817
+ const cmsDir = path40.join(cwd, "cms");
14818
+ const cmsRouteGroup = path40.join(cwd, appBase, "(cms)");
14819
+ if (fs36.existsSync(cmsDir)) dirs.push("cms/");
14820
+ if (fs36.existsSync(cmsRouteGroup)) dirs.push(`${appBase}/(cms)/`);
14508
14821
  if (dirs.length > 0) {
14509
14822
  steps.push({
14510
14823
  label: "CMS directories",
@@ -14512,25 +14825,25 @@ function buildUninstallPlan(cwd) {
14512
14825
  count: dirs.length,
14513
14826
  unit: dirs.length === 1 ? "directory" : "directories",
14514
14827
  execute() {
14515
- if (fs35.existsSync(cmsDir)) fs35.rmSync(cmsDir, { recursive: true, force: true });
14516
- if (fs35.existsSync(cmsRouteGroup)) fs35.rmSync(cmsRouteGroup, { recursive: true, force: true });
14828
+ if (fs36.existsSync(cmsDir)) fs36.rmSync(cmsDir, { recursive: true, force: true });
14829
+ if (fs36.existsSync(cmsRouteGroup)) fs36.rmSync(cmsRouteGroup, { recursive: true, force: true });
14517
14830
  }
14518
14831
  });
14519
14832
  }
14520
14833
  const configFiles = [];
14521
14834
  const configPaths = [];
14522
14835
  const candidates = [
14523
- ["cms.config.ts", path39.join(cwd, "cms.config.ts")],
14524
- ["drizzle.config.ts", path39.join(cwd, "drizzle.config.ts")],
14525
- ["CMS.md", path39.join(cwd, "CMS.md")]
14836
+ ["cms.config.ts", path40.join(cwd, "cms.config.ts")],
14837
+ ["drizzle.config.ts", path40.join(cwd, "drizzle.config.ts")],
14838
+ ["CMS.md", path40.join(cwd, "CMS.md")]
14526
14839
  ];
14527
14840
  for (const [label, fullPath] of candidates) {
14528
- if (fs35.existsSync(fullPath)) {
14841
+ if (fs36.existsSync(fullPath)) {
14529
14842
  configFiles.push(label);
14530
14843
  configPaths.push(fullPath);
14531
14844
  }
14532
14845
  }
14533
- const biomePath = path39.join(cwd, "biome.json");
14846
+ const biomePath = path40.join(cwd, "biome.json");
14534
14847
  if (isCLICreatedBiome(biomePath)) {
14535
14848
  configFiles.push("biome.json (CLI-created)");
14536
14849
  configPaths.push(biomePath);
@@ -14542,15 +14855,15 @@ function buildUninstallPlan(cwd) {
14542
14855
  count: configFiles.length,
14543
14856
  unit: configFiles.length === 1 ? "file" : "files",
14544
14857
  execute() {
14545
- for (const p6 of configPaths) {
14546
- if (fs35.existsSync(p6)) fs35.unlinkSync(p6);
14858
+ for (const p7 of configPaths) {
14859
+ if (fs36.existsSync(p7)) fs36.unlinkSync(p7);
14547
14860
  }
14548
14861
  }
14549
14862
  });
14550
14863
  }
14551
- const tsconfigPath = path39.join(cwd, "tsconfig.json");
14552
- if (fs35.existsSync(tsconfigPath)) {
14553
- const content = fs35.readFileSync(tsconfigPath, "utf-8");
14864
+ const tsconfigPath = path40.join(cwd, "tsconfig.json");
14865
+ if (fs36.existsSync(tsconfigPath)) {
14866
+ const content = fs36.readFileSync(tsconfigPath, "utf-8");
14554
14867
  const aliasMatches = content.match(/"@cms\//g);
14555
14868
  if (aliasMatches && aliasMatches.length > 0) {
14556
14869
  const aliasCount = aliasMatches.length;
@@ -14567,10 +14880,10 @@ function buildUninstallPlan(cwd) {
14567
14880
  }
14568
14881
  const cssFile = findMainCss2(cwd);
14569
14882
  if (cssFile) {
14570
- const cssContent = fs35.readFileSync(cssFile, "utf-8");
14883
+ const cssContent = fs36.readFileSync(cssFile, "utf-8");
14571
14884
  const sourceLines = cssContent.split("\n").filter((l) => /^@source\s+"[^"]*cms[^"]*";\s*$/.test(l));
14572
14885
  if (sourceLines.length > 0) {
14573
- const relCss = path39.relative(cwd, cssFile);
14886
+ const relCss = path40.relative(cwd, cssFile);
14574
14887
  steps.push({
14575
14888
  label: `CSS @source lines (${relCss})`,
14576
14889
  items: [`@source lines in ${relCss}`],
@@ -14582,9 +14895,9 @@ function buildUninstallPlan(cwd) {
14582
14895
  });
14583
14896
  }
14584
14897
  }
14585
- const envPath = path39.join(cwd, ".env.local");
14586
- if (fs35.existsSync(envPath)) {
14587
- const envContent = fs35.readFileSync(envPath, "utf-8");
14898
+ const envPath = path40.join(cwd, ".env.local");
14899
+ if (fs36.existsSync(envPath)) {
14900
+ const envContent = fs36.readFileSync(envPath, "utf-8");
14588
14901
  const bsVars = envContent.split("\n").filter((l) => l.trim().match(/^BETTERSTART_\w+=/)).map((l) => l.split("=")[0]);
14589
14902
  if (bsVars.length > 0) {
14590
14903
  steps.push({
@@ -14600,32 +14913,32 @@ function buildUninstallPlan(cwd) {
14600
14913
  }
14601
14914
  return steps;
14602
14915
  }
14603
- var uninstallCommand = new Command5("uninstall").description("Remove all CMS files and undo modifications made by betterstart init").option("-f, --force", "Skip all confirmation prompts", false).option("--cwd <path>", "Project root path").action(async (options) => {
14604
- const cwd = options.cwd ? path39.resolve(options.cwd) : process.cwd();
14605
- p5.intro(pc3.bgRed(pc3.white(" BetterStart Uninstall ")));
14916
+ var uninstallCommand = new Command6("uninstall").description("Remove all CMS files and undo modifications made by betterstart init").option("-f, --force", "Skip all confirmation prompts", false).option("--cwd <path>", "Project root path").action(async (options) => {
14917
+ const cwd = options.cwd ? path40.resolve(options.cwd) : process.cwd();
14918
+ p6.intro(pc4.bgRed(pc4.white(" BetterStart Uninstall ")));
14606
14919
  const steps = buildUninstallPlan(cwd);
14607
14920
  if (steps.length === 0) {
14608
- p5.log.success(`${pc3.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
14609
- p5.outro("Done");
14921
+ p6.log.success(`${pc4.green("\u2713")} Nothing to remove \u2014 project is already clean.`);
14922
+ p6.outro("Done");
14610
14923
  return;
14611
14924
  }
14612
14925
  const planLines = steps.map((step) => {
14613
14926
  const names = step.items.join(" ");
14614
- const countLabel = pc3.dim(`${step.count} ${step.unit}`);
14615
- return `${pc3.red("\xD7")} ${names} ${countLabel}`;
14927
+ const countLabel = pc4.dim(`${step.count} ${step.unit}`);
14928
+ return `${pc4.red("\xD7")} ${names} ${countLabel}`;
14616
14929
  });
14617
- p5.note(planLines.join("\n"), "Uninstall plan");
14930
+ p6.note(planLines.join("\n"), "Uninstall plan");
14618
14931
  if (!options.force) {
14619
- const confirmed = await p5.confirm({
14932
+ const confirmed = await p6.confirm({
14620
14933
  message: "Proceed with uninstall?",
14621
14934
  initialValue: false
14622
14935
  });
14623
- if (p5.isCancel(confirmed) || !confirmed) {
14624
- p5.cancel("Uninstall cancelled.");
14936
+ if (p6.isCancel(confirmed) || !confirmed) {
14937
+ p6.cancel("Uninstall cancelled.");
14625
14938
  process.exit(0);
14626
14939
  }
14627
14940
  }
14628
- const s = p5.spinner();
14941
+ const s = p6.spinner();
14629
14942
  s.start(steps[0].label);
14630
14943
  for (const step of steps) {
14631
14944
  s.message(step.label);
@@ -14633,16 +14946,16 @@ var uninstallCommand = new Command5("uninstall").description("Remove all CMS fil
14633
14946
  }
14634
14947
  const parts = steps.map((step) => `${step.count} ${step.unit}`);
14635
14948
  s.stop(`Removed ${parts.join(", ")}`);
14636
- p5.note(pc3.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
14637
- p5.outro("Uninstall complete");
14949
+ p6.note(pc4.dim("Database tables were NOT dropped \u2014 drop them manually if needed."), "Next steps");
14950
+ p6.outro("Uninstall complete");
14638
14951
  });
14639
14952
 
14640
14953
  // src/commands/update-deps.ts
14641
- import path40 from "path";
14954
+ import path41 from "path";
14642
14955
  import * as clack2 from "@clack/prompts";
14643
- import { Command as Command6 } from "commander";
14644
- var updateDepsCommand = new Command6("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
14645
- const cwd = options.cwd ? path40.resolve(options.cwd) : process.cwd();
14956
+ import { Command as Command7 } from "commander";
14957
+ var updateDepsCommand = new Command7("update-deps").description("Install or update all CMS dependencies").option("--cwd <path>", "Project root path").action(async (options) => {
14958
+ const cwd = options.cwd ? path41.resolve(options.cwd) : process.cwd();
14646
14959
  clack2.intro("BetterStart Update Dependencies");
14647
14960
  const pm = detectPackageManager(cwd);
14648
14961
  clack2.log.info(`Package manager: ${pm}`);
@@ -14667,32 +14980,33 @@ var updateDepsCommand = new Command6("update-deps").description("Install or upda
14667
14980
  });
14668
14981
 
14669
14982
  // src/commands/update-styles.ts
14670
- import fs36 from "fs";
14671
- import path41 from "path";
14983
+ import fs37 from "fs";
14984
+ import path42 from "path";
14672
14985
  import * as clack3 from "@clack/prompts";
14673
- import { Command as Command7 } from "commander";
14674
- var updateStylesCommand = new Command7("update-styles").description("Replace cms-globals.css with the latest version from the CLI").option("--cwd <path>", "Project root path").action(async (options) => {
14675
- const cwd = options.cwd ? path41.resolve(options.cwd) : process.cwd();
14986
+ import { Command as Command8 } from "commander";
14987
+ var updateStylesCommand = new Command8("update-styles").description("Replace cms-globals.css with the latest version from the CLI").option("--cwd <path>", "Project root path").action(async (options) => {
14988
+ const cwd = options.cwd ? path42.resolve(options.cwd) : process.cwd();
14676
14989
  clack3.intro("BetterStart Update Styles");
14677
14990
  const config = await resolveConfig(cwd);
14678
14991
  const cmsDir = config.paths?.cms ?? "./cms";
14679
- const targetPath = path41.join(cwd, cmsDir, "cms-globals.css");
14680
- if (!fs36.existsSync(targetPath)) {
14681
- clack3.cancel(`cms-globals.css not found at ${path41.relative(cwd, targetPath)}`);
14992
+ const targetPath = path42.join(cwd, cmsDir, "cms-globals.css");
14993
+ if (!fs37.existsSync(targetPath)) {
14994
+ clack3.cancel(`cms-globals.css not found at ${path42.relative(cwd, targetPath)}`);
14682
14995
  process.exit(1);
14683
14996
  }
14684
- fs36.writeFileSync(targetPath, cmsGlobalsCssTemplate(), "utf-8");
14685
- clack3.log.success(`Updated ${path41.relative(cwd, targetPath)}`);
14997
+ fs37.writeFileSync(targetPath, cmsGlobalsCssTemplate(), "utf-8");
14998
+ clack3.log.success(`Updated ${path42.relative(cwd, targetPath)}`);
14686
14999
  clack3.outro("Styles updated");
14687
15000
  });
14688
15001
 
14689
15002
  // src/cli.ts
14690
- var program = new Command8();
15003
+ var program = new Command9();
14691
15004
  program.name("betterstart").description("Scaffold a full-featured CMS into any Next.js 16 application").version("0.1.0");
14692
15005
  program.addCommand(initCommand);
14693
15006
  program.addCommand(generateCommand);
14694
15007
  program.addCommand(removeCommand);
14695
15008
  program.addCommand(seedCommand);
15009
+ program.addCommand(setupR2Command);
14696
15010
  program.addCommand(uninstallCommand);
14697
15011
  program.addCommand(updateDepsCommand);
14698
15012
  program.addCommand(updateStylesCommand);