@beechcms/cli 0.7.0 → 0.8.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/index.js CHANGED
@@ -123,12 +123,12 @@ function findWranglerConfig() {
123
123
  let dir = process.cwd();
124
124
  while (true) {
125
125
  for (const name of ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]) {
126
- const p = resolve(dir, name);
127
- if (existsSync(p)) return p;
126
+ const p3 = resolve(dir, name);
127
+ if (existsSync(p3)) return p3;
128
128
  }
129
129
  for (const name of ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]) {
130
- const p = resolve(dir, "apps", "api", name);
131
- if (existsSync(p)) return p;
130
+ const p3 = resolve(dir, "apps", "api", name);
131
+ if (existsSync(p3)) return p3;
132
132
  }
133
133
  const parent = resolve(dir, "..");
134
134
  if (parent === dir) break;
@@ -136,9 +136,6 @@ function findWranglerConfig() {
136
136
  }
137
137
  return null;
138
138
  }
139
- function sqlQuote(value) {
140
- return `'${value.replace(/'/g, "''")}'`;
141
- }
142
139
  function resolveDbName(configPath) {
143
140
  if (!configPath) return "beech-db";
144
141
  try {
@@ -171,7 +168,7 @@ var init_exports = {};
171
168
  __export(init_exports, {
172
169
  init: () => init
173
170
  });
174
- import pc4 from "picocolors";
171
+ import pc2 from "picocolors";
175
172
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
176
173
  import { createInterface } from "node:readline/promises";
177
174
  import { resolve as resolve2, basename } from "node:path";
@@ -262,10 +259,10 @@ function patchWranglerConfig(configPath, dbId) {
262
259
  }
263
260
  }
264
261
  function printManualDbInstructions() {
265
- console.log(pc4.dim("\n Update your D1 database_id in wrangler.jsonc,"));
266
- console.log(pc4.dim(" or create a new database with:"));
267
- console.log(pc4.cyan("\n \u2192 Run: npx wrangler d1 create my-project-db"));
268
- console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
262
+ console.log(pc2.dim("\n Update your D1 database_id in wrangler.jsonc,"));
263
+ console.log(pc2.dim(" or create a new database with:"));
264
+ console.log(pc2.cyan("\n \u2192 Run: npx wrangler d1 create my-project-db"));
265
+ console.log(pc2.cyan(" \u2192 Then: npx beech init --db\n"));
269
266
  }
270
267
  function echoApiKeys(configPath) {
271
268
  if (!configPath) return;
@@ -277,16 +274,16 @@ function echoApiKeys(configPath) {
277
274
  const readKey = vars["PUBLIC_READ_API_KEY"];
278
275
  const writeKey = vars["PUBLIC_WRITE_API_KEY"];
279
276
  if (!readKey && !writeKey) return;
280
- console.log(pc4.dim(" API keys detected in wrangler.jsonc:\n"));
277
+ console.log(pc2.dim(" API keys detected in wrangler.jsonc:\n"));
281
278
  if (readKey) {
282
279
  const masked = readKey.length > 8 ? readKey.slice(0, 4) + "****" + readKey.slice(-4) : "****";
283
- console.log(pc4.dim(` PUBLIC_READ_API_KEY = ${masked}`));
280
+ console.log(pc2.dim(` PUBLIC_READ_API_KEY = ${masked}`));
284
281
  }
285
282
  if (writeKey) {
286
283
  const masked = writeKey.length > 8 ? writeKey.slice(0, 4) + "****" + writeKey.slice(-4) : "****";
287
- console.log(pc4.dim(` PUBLIC_WRITE_API_KEY = ${masked}`));
284
+ console.log(pc2.dim(` PUBLIC_WRITE_API_KEY = ${masked}`));
288
285
  }
289
- console.log(pc4.dim("\n Use these in your frontend as the X-API-Key header.\n"));
286
+ console.log(pc2.dim("\n Use these in your frontend as the X-API-Key header.\n"));
290
287
  } catch {
291
288
  }
292
289
  }
@@ -294,42 +291,34 @@ function checkFiles(cwd, checkDevVars) {
294
291
  let ok = true;
295
292
  const workerExists = existsSync2(resolve2(cwd, "worker.ts")) || existsSync2(resolve2(cwd, "worker.js"));
296
293
  if (!workerExists) {
297
- console.log(pc4.red(" \u2717 worker.ts \u2014 missing (required)"));
294
+ console.log(pc2.red(" \u2717 worker.ts \u2014 missing (required)"));
298
295
  ok = false;
299
296
  } else {
300
- console.log(pc4.green(" \u2713 worker.ts"));
297
+ console.log(pc2.green(" \u2713 worker.ts"));
301
298
  }
302
299
  const configPath = findWranglerConfig();
303
300
  const configInCwd = configPath && (configPath === resolve2(cwd, "wrangler.jsonc") || configPath === resolve2(cwd, "wrangler.json") || configPath === resolve2(cwd, "wrangler.toml"));
304
301
  if (!configInCwd) {
305
- console.log(pc4.red(" \u2717 wrangler.jsonc \u2014 missing (required)"));
302
+ console.log(pc2.red(" \u2717 wrangler.jsonc \u2014 missing (required)"));
306
303
  ok = false;
307
304
  } else {
308
- console.log(pc4.green(` \u2713 ${basename(configPath)}`));
309
- }
310
- const seedsExists = existsSync2(resolve2(cwd, "seeds.ts")) || existsSync2(resolve2(cwd, "seeds.js")) || existsSync2(resolve2(cwd, "seed.ts")) || existsSync2(resolve2(cwd, "seed.js"));
311
- if (!seedsExists) {
312
- console.log(pc4.yellow(" \u26A0 seeds.ts \u2014 not found (optional: needed only for the one-time code \u2192 DB load; after `beech seed:load`, the DB is canonical)"));
313
- } else {
314
- console.log(pc4.green(" \u2713 seeds.ts"));
305
+ console.log(pc2.green(` \u2713 ${basename(configPath)}`));
315
306
  }
316
307
  if (checkDevVars) {
317
308
  if (!existsSync2(resolve2(cwd, ".dev.vars"))) {
318
- console.log(pc4.dim(" \u25CB .dev.vars \u2014 not found (optional: only needed for production R2 credentials)"));
309
+ console.log(pc2.dim(" \u25CB .dev.vars \u2014 not found (optional: only needed for production R2 credentials)"));
319
310
  } else {
320
- console.log(pc4.green(" \u2713 .dev.vars"));
311
+ console.log(pc2.green(" \u2713 .dev.vars"));
321
312
  }
322
313
  }
323
314
  return ok;
324
315
  }
325
316
  function printNextSteps(local) {
326
- const localFlag = local ? " --local" : "";
327
- console.log(pc4.dim(" Next steps:"));
328
- console.log(pc4.cyan(` 1. npx beech seed:load${localFlag}`));
329
- console.log(pc4.dim(" \u2192 create content tables and register seed definitions in D1"));
330
- console.log(pc4.cyan(" 2. npx wrangler dev"));
331
- console.log(pc4.dim(" \u2192 start API + dashboard"));
332
- console.log(pc4.dim(" 3. Open http://localhost:8789/admin\n"));
317
+ console.log(pc2.dim(" Next steps:"));
318
+ console.log(pc2.cyan(" 1. npx wrangler dev"));
319
+ console.log(pc2.dim(" \u2192 start API + dashboard"));
320
+ console.log(pc2.cyan(" 2. Open http://localhost:8789/admin"));
321
+ console.log(pc2.dim(" \u2192 complete initial admin setup and manage content types\n"));
333
322
  }
334
323
  function getExistingTables(options) {
335
324
  try {
@@ -344,39 +333,38 @@ function getExistingTables(options) {
344
333
  }
345
334
  async function init(args) {
346
335
  const cwd = process.cwd();
347
- console.log(pc4.cyan("\n beech init \u2014 project check\n"));
336
+ console.log(pc2.cyan("\n beech init \u2014 project check\n"));
348
337
  const filesOk = checkFiles(cwd, args.local);
349
338
  if (!filesOk) {
350
- console.log(pc4.red("\n \u2717 Required files missing\n"));
351
- console.log(pc4.dim(" Fix the errors above before initialising the database."));
352
- console.log(pc4.cyan("\n \u2192 See: https://beechcms.dev/docs/getting-started\n"));
339
+ console.log(pc2.red("\n \u2717 Required files missing\n"));
340
+ console.log(pc2.dim(" Fix the errors above before initialising the database."));
341
+ console.log(pc2.cyan("\n \u2192 See: https://beechcms.dev/docs/getting-started\n"));
353
342
  process.exit(1);
354
343
  }
355
- console.log(pc4.green("\n All required files present.\n"));
344
+ console.log(pc2.green("\n All required files present.\n"));
356
345
  if (!args.initDb) {
357
346
  echoApiKeys(findWranglerConfig());
358
347
  const localFlag = args.local ? " --local" : "";
359
- console.log(pc4.dim(" Next steps:"));
360
- console.log(pc4.dim(` 1. npx beech init --db${localFlag} # initialise D1 database`));
361
- console.log(pc4.dim(` 2. npx beech seed:load${localFlag} # create content tables`));
362
- console.log(pc4.dim(" 3. npx wrangler dev # start API + dashboard"));
363
- console.log(pc4.dim(" 4. Open http://localhost:8789/admin\n"));
348
+ console.log(pc2.dim(" Next steps:"));
349
+ console.log(pc2.dim(` 1. npx beech init --db${localFlag} # initialise D1 database`));
350
+ console.log(pc2.dim(" 2. npx wrangler dev # start API + dashboard"));
351
+ console.log(pc2.dim(" 3. Open http://localhost:8789/admin\n"));
364
352
  return;
365
353
  }
366
354
  const configPath = findWranglerConfig();
367
355
  if (configPath) {
368
356
  const placeholders = checkWranglerPlaceholders(configPath);
369
357
  if (placeholders.length > 0) {
370
- console.log(pc4.yellow(" \u26A0 wrangler.jsonc contains placeholder values:\n"));
358
+ console.log(pc2.yellow(" \u26A0 wrangler.jsonc contains placeholder values:\n"));
371
359
  for (const issue of placeholders) {
372
- console.log(pc4.yellow(` - ${issue}`));
360
+ console.log(pc2.yellow(` - ${issue}`));
373
361
  }
374
362
  if (process.stdin.isTTY && !args.nonInteractive) {
375
363
  const rl = createInterface({ input: process.stdin, output: process.stdout });
376
364
  let autoCreate = false;
377
365
  try {
378
366
  const answer = (await rl.question(
379
- pc4.cyan("\n \u2192 Create a new D1 database (and R2 bucket) on Cloudflare automatically? (Y/n): ")
367
+ pc2.cyan("\n \u2192 Create a new D1 database (and R2 bucket) on Cloudflare automatically? (Y/n): ")
380
368
  )).trim().toLowerCase();
381
369
  autoCreate = !answer || answer === "y" || answer === "yes";
382
370
  } finally {
@@ -385,44 +373,44 @@ async function init(args) {
385
373
  if (autoCreate) {
386
374
  const authed = checkWranglerAuth();
387
375
  if (!authed) {
388
- console.log(pc4.red("\n \u2717 Not logged in to Cloudflare\n"));
389
- console.log(pc4.dim(" BeechCMS needs access to your Cloudflare account to create the database."));
390
- console.log(pc4.cyan("\n \u2192 Run: npx wrangler login"));
391
- console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
376
+ console.log(pc2.red("\n \u2717 Not logged in to Cloudflare\n"));
377
+ console.log(pc2.dim(" BeechCMS needs access to your Cloudflare account to create the database."));
378
+ console.log(pc2.cyan("\n \u2192 Run: npx wrangler login"));
379
+ console.log(pc2.cyan(" \u2192 Then: npx beech init --db\n"));
392
380
  process.exit(1);
393
381
  }
394
382
  const projectName = readProjectName(configPath);
395
383
  const dbName = `${projectName}-db`;
396
384
  const bucketName = readBucketName(configPath) || `${projectName}-media`;
397
- console.log(pc4.dim(`
385
+ console.log(pc2.dim(`
398
386
  Creating D1 database "${dbName}"\u2026`));
399
387
  const dbId = createD1Database(dbName);
400
388
  if (!dbId) {
401
- console.log(pc4.red("\n \u2717 Failed to create D1 database\n"));
402
- console.log(pc4.dim(" Create it manually and retry:"));
403
- console.log(pc4.cyan(`
389
+ console.log(pc2.red("\n \u2717 Failed to create D1 database\n"));
390
+ console.log(pc2.dim(" Create it manually and retry:"));
391
+ console.log(pc2.cyan(`
404
392
  \u2192 Run: npx wrangler d1 create ${dbName}`));
405
- console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
393
+ console.log(pc2.cyan(" \u2192 Then: npx beech init --db\n"));
406
394
  process.exit(1);
407
395
  }
408
- console.log(pc4.green(` \u2713 D1 database created (id: ${dbId})`));
409
- console.log(pc4.dim(`
396
+ console.log(pc2.green(` \u2713 D1 database created (id: ${dbId})`));
397
+ console.log(pc2.dim(`
410
398
  Creating R2 bucket "${bucketName}"\u2026`));
411
399
  const r2Ok = createR2Bucket(bucketName);
412
400
  if (r2Ok) {
413
- console.log(pc4.green(` \u2713 R2 bucket "${bucketName}" created`));
401
+ console.log(pc2.green(` \u2713 R2 bucket "${bucketName}" created`));
414
402
  } else {
415
- console.log(pc4.yellow(` \u26A0 R2 bucket creation failed (may already exist \u2014 continuing)`));
403
+ console.log(pc2.yellow(` \u26A0 R2 bucket creation failed (may already exist \u2014 continuing)`));
416
404
  }
417
- console.log(pc4.dim("\n Updating wrangler.jsonc\u2026"));
405
+ console.log(pc2.dim("\n Updating wrangler.jsonc\u2026"));
418
406
  const patched = patchWranglerConfig(configPath, dbId);
419
407
  if (patched) {
420
- console.log(pc4.green(" \u2713 wrangler.jsonc updated\n"));
408
+ console.log(pc2.green(" \u2713 wrangler.jsonc updated\n"));
421
409
  } else {
422
- console.log(pc4.yellow(` \u26A0 Could not update wrangler.jsonc automatically
410
+ console.log(pc2.yellow(` \u26A0 Could not update wrangler.jsonc automatically
423
411
  `));
424
- console.log(pc4.dim(` Set database_id = "${dbId}" in wrangler.jsonc manually, then retry:`));
425
- console.log(pc4.cyan(" \u2192 Run: npx beech init --db\n"));
412
+ console.log(pc2.dim(` Set database_id = "${dbId}" in wrangler.jsonc manually, then retry:`));
413
+ console.log(pc2.cyan(" \u2192 Run: npx beech init --db\n"));
426
414
  process.exit(1);
427
415
  }
428
416
  } else {
@@ -430,11 +418,11 @@ async function init(args) {
430
418
  process.exit(1);
431
419
  }
432
420
  } else if (args.nonInteractive && args.local) {
433
- console.log(pc4.dim(" --yes: proceeding with local mode (no remote resources needed)\n"));
421
+ console.log(pc2.dim(" --yes: proceeding with local mode (no remote resources needed)\n"));
434
422
  } else {
435
423
  if (args.nonInteractive) {
436
- console.log(pc4.red("\n \u2717 Cannot proceed non-interactively: remote database has a placeholder database_id\n"));
437
- console.log(pc4.dim(" Set a real database_id in wrangler.jsonc, then retry."));
424
+ console.log(pc2.red("\n \u2717 Cannot proceed non-interactively: remote database has a placeholder database_id\n"));
425
+ console.log(pc2.dim(" Set a real database_id in wrangler.jsonc, then retry."));
438
426
  process.exit(1);
439
427
  }
440
428
  printManualDbInstructions();
@@ -445,66 +433,65 @@ async function init(args) {
445
433
  if (!args.local) {
446
434
  const authed = checkWranglerAuth();
447
435
  if (!authed) {
448
- console.log(pc4.red(" \u2717 Not logged in to Cloudflare\n"));
449
- console.log(pc4.dim(" BeechCMS needs access to your Cloudflare account to manage the D1 database."));
450
- console.log(pc4.cyan("\n \u2192 Run: npx wrangler login"));
451
- console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
436
+ console.log(pc2.red(" \u2717 Not logged in to Cloudflare\n"));
437
+ console.log(pc2.dim(" BeechCMS needs access to your Cloudflare account to manage the D1 database."));
438
+ console.log(pc2.cyan("\n \u2192 Run: npx wrangler login"));
439
+ console.log(pc2.cyan(" \u2192 Then: npx beech init --db\n"));
452
440
  process.exit(1);
453
441
  }
454
442
  }
455
443
  const db = args.db ?? resolveDbName(configPath);
456
444
  const options = { db, local: args.local, configPath };
457
- console.log(pc4.cyan(` Checking database "${db}" (${args.local ? "local" : "remote"})\u2026
445
+ console.log(pc2.cyan(` Checking database "${db}" (${args.local ? "local" : "remote"})\u2026
458
446
  `));
459
447
  const existingTables = getExistingTables(options);
460
448
  const missingTables = SYSTEM_TABLES.filter((t) => !existingTables?.includes(t));
461
449
  if (!args.local) {
462
450
  if (existingTables === null) {
463
- console.log(pc4.red(" \u2717 Remote database unreachable\n"));
464
- console.log(pc4.dim(" Most likely causes:"));
465
- console.log(pc4.dim(" - Wrong database_id in wrangler.jsonc"));
466
- console.log(pc4.dim(" - Worker not yet deployed"));
467
- console.log(pc4.cyan("\n \u2192 Fix: Update d1_databases.database_id in wrangler.jsonc"));
468
- console.log(pc4.cyan(" \u2192 Then: npm run deploy\n"));
451
+ console.log(pc2.red(" \u2717 Remote database unreachable\n"));
452
+ console.log(pc2.dim(" Most likely causes:"));
453
+ console.log(pc2.dim(" - Wrong database_id in wrangler.jsonc"));
454
+ console.log(pc2.dim(" - Worker not yet deployed"));
455
+ console.log(pc2.cyan("\n \u2192 Fix: Update d1_databases.database_id in wrangler.jsonc"));
456
+ console.log(pc2.cyan(" \u2192 Then: npm run deploy\n"));
469
457
  process.exit(1);
470
458
  }
471
459
  if (missingTables.length > 0) {
472
- console.log(pc4.yellow(` \u26A0 Missing system tables: ${missingTables.join(", ")}
460
+ console.log(pc2.yellow(` \u26A0 Missing system tables: ${missingTables.join(", ")}
473
461
  `));
474
- console.log(pc4.dim(" Most likely causes:"));
475
- console.log(pc4.dim(" - Wrong database_id in wrangler.jsonc"));
476
- console.log(pc4.dim(" - Migrations did not run during deploy"));
477
- console.log(pc4.cyan("\n \u2192 Fix: Update d1_databases.database_id in wrangler.jsonc"));
478
- console.log(pc4.cyan(" \u2192 Then: npm run deploy\n"));
462
+ console.log(pc2.dim(" Most likely causes:"));
463
+ console.log(pc2.dim(" - Wrong database_id in wrangler.jsonc"));
464
+ console.log(pc2.dim(" - Migrations did not run during deploy"));
465
+ console.log(pc2.cyan("\n \u2192 Fix: Update d1_databases.database_id in wrangler.jsonc"));
466
+ console.log(pc2.cyan(" \u2192 Then: npm run deploy\n"));
479
467
  process.exit(1);
480
468
  }
481
469
  for (const table of SYSTEM_TABLES) {
482
- console.log(pc4.green(` \u2713 ${table}`));
470
+ console.log(pc2.green(` \u2713 ${table}`));
483
471
  }
484
- console.log(pc4.green("\n All system tables present. Remote database is initialized.\n"));
472
+ console.log(pc2.green("\n All system tables present. Remote database is initialized.\n"));
485
473
  return;
486
474
  }
487
475
  if (existingTables === null) {
488
- console.log(pc4.yellow(" Database unreachable or not yet created \u2014 applying base schema\u2026\n"));
476
+ console.log(pc2.yellow(" Database unreachable or not yet created \u2014 applying base schema\u2026\n"));
489
477
  } else if (missingTables.length === 0) {
490
- console.log(pc4.green(" \u2713 All system tables present. Database already initialised.\n"));
478
+ console.log(pc2.green(" \u2713 All system tables present. Database already initialised.\n"));
491
479
  printNextSteps(args.local);
492
480
  return;
493
481
  } else {
494
- console.log(pc4.yellow(` Missing system tables: ${missingTables.join(", ")}`));
495
- console.log(pc4.cyan("\n Applying base schema\u2026\n"));
482
+ console.log(pc2.yellow(` Missing system tables: ${missingTables.join(", ")}`));
483
+ console.log(pc2.cyan("\n Applying base schema\u2026\n"));
496
484
  }
497
485
  const ok = executeD1File(BASE_SCHEMA_SQL, options);
498
486
  if (!ok) {
499
- console.log(pc4.red("\n \u2717 Database initialisation failed\n"));
500
- console.log(pc4.dim(" wrangler reported an error above."));
501
- console.log(pc4.cyan("\n \u2192 Run: npx beech init --db --local\n"));
487
+ console.log(pc2.red("\n \u2717 Database initialisation failed\n"));
488
+ console.log(pc2.dim(" wrangler reported an error above."));
489
+ console.log(pc2.cyan("\n \u2192 Run: npx beech init --db --local\n"));
502
490
  process.exit(1);
503
491
  }
504
- console.log(pc4.green("\n \u2713 worker.ts"));
505
- console.log(pc4.green(` \u2713 ${configPath ? basename(configPath) : "wrangler.jsonc"}`));
506
- console.log(pc4.green(" \u2713 seeds.ts"));
507
- console.log(pc4.green(" \u2713 Local D1 system tables ready\n"));
492
+ console.log(pc2.green("\n \u2713 worker.ts"));
493
+ console.log(pc2.green(` \u2713 ${configPath ? basename(configPath) : "wrangler.jsonc"}`));
494
+ console.log(pc2.green(" \u2713 Local D1 system tables ready\n"));
508
495
  echoApiKeys(configPath);
509
496
  printNextSteps(args.local);
510
497
  }
@@ -714,515 +701,46 @@ CREATE TABLE IF NOT EXISTS setup_completed (
714
701
  });
715
702
 
716
703
  // src/commands/seed-load.ts
717
- init_wrangler();
718
- import pc3 from "picocolors";
719
- import {
720
- SEED_REGISTRY as SEED_REGISTRY2,
721
- generateCreateTable,
722
- generateDraftTable,
723
- generateIndexes,
724
- generateFtsTable,
725
- generateFtsTriggers,
726
- generateJunctionTable,
727
- generateJunctionIndexes,
728
- generateJunctionDraftTable,
729
- sortSeedsByDependencies
730
- } from "@beechcms/core";
731
-
732
- // src/lib/schema-diff.ts
733
- init_wrangler();
734
704
  import pc from "picocolors";
735
- import { getExpectedColumns } from "@beechcms/core";
736
- function isSeedClean(diff) {
737
- return diff.tableExists && diff.columns.every((c) => c.status === "ok");
738
- }
739
- function renderSeedDiff(diff) {
740
- const table = `content_${diff.slug}`;
741
- if (!diff.tableExists) {
742
- console.log(pc.red(` \u2717 ${table} \u2014 table missing`));
743
- return;
744
- }
745
- const problems = diff.columns.filter((c) => c.status !== "ok");
746
- if (problems.length === 0) {
747
- console.log(pc.green(` \u2713 ${table}`));
748
- return;
749
- }
750
- console.log(pc.yellow(` \u26A0 ${table}`));
751
- for (const col of problems) {
752
- switch (col.status) {
753
- case "missing":
754
- console.log(pc.red(` + missing column: ${col.name} ${col.expectedType}`));
755
- break;
756
- case "extra":
757
- console.log(pc.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) \u2014 in DB, not in seeds.ts`));
758
- break;
759
- case "type_mismatch":
760
- console.log(pc.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
761
- break;
762
- case "fk_missing":
763
- console.log(pc.red(` \u292C missing FK: ${col.name} \u2192 content_${col.expectedTarget}(id)`));
764
- break;
765
- case "fk_mismatch":
766
- console.log(pc.yellow(` \u292C FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`));
767
- break;
768
- case "index_missing":
769
- console.log(pc.yellow(` \u2298 missing index on ${col.name}`));
770
- break;
771
- }
772
- }
773
- }
774
- async function diffSeed(seed, options) {
775
- const tableName = `content_${seed.slug}`;
776
- const expected = getExpectedColumns(seed);
777
- let actual;
778
- try {
779
- actual = queryD1(`PRAGMA table_info(${tableName})`, options);
780
- } catch {
781
- return { slug: seed.slug, tableExists: false, columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType })) };
782
- }
783
- if (actual.length === 0) {
784
- return { slug: seed.slug, tableExists: false, columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType })) };
785
- }
786
- const actualMap = new Map(actual.map((r) => [r.name, r]));
787
- const expectedSet = new Set(expected.map((c) => c.name));
788
- const columns = [];
789
- for (const col of expected) {
790
- const actualRow = actualMap.get(col.name);
791
- if (!actualRow) {
792
- columns.push({ name: col.name, status: "missing", expectedType: col.sqlType });
793
- } else if (actualRow.type.toUpperCase() !== col.sqlType) {
794
- columns.push({ name: col.name, status: "type_mismatch", expectedType: col.sqlType, actualType: actualRow.type });
795
- } else {
796
- columns.push({ name: col.name, status: "ok" });
797
- }
798
- }
799
- for (const row of actual) {
800
- if (!expectedSet.has(row.name)) {
801
- columns.push({ name: row.name, status: "extra", actualType: row.type });
802
- }
803
- }
804
- const relationBranches = seed.branches.filter((b) => b.type === "relation" && b.targetSeed);
805
- if (relationBranches.length > 0) {
806
- let fkList = [];
807
- let indexList = [];
808
- try {
809
- fkList = queryD1(`PRAGMA foreign_key_list(${tableName})`, options);
810
- indexList = queryD1(`PRAGMA index_list(${tableName})`, options);
811
- } catch {
812
- }
813
- const fkByCol = /* @__PURE__ */ new Map();
814
- for (const fk of fkList) {
815
- fkByCol.set(fk.from, fk);
816
- }
817
- const indexNames = new Set(indexList.map((i) => i.name));
818
- for (const branch of relationBranches) {
819
- const expectedFkTable = `content_${branch.targetSeed}`;
820
- const expectedOnDelete = (branch.onDelete ?? "SET NULL").toUpperCase();
821
- const expectedIndexName = `idx_${seed.slug}_${branch.alias}`;
822
- const colDiff = columns.find((c) => c.name === branch.alias);
823
- if (!colDiff || colDiff.status === "missing") continue;
824
- const fk = fkByCol.get(branch.alias);
825
- if (!fk) {
826
- colDiff.status = "fk_missing";
827
- colDiff.expectedTarget = branch.targetSeed;
828
- } else {
829
- const actualTable = fk.table;
830
- const actualOnDelete = fk.on_delete.toUpperCase();
831
- if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
832
- colDiff.status = "fk_mismatch";
833
- colDiff.expected = `\u2192 ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`;
834
- colDiff.actual = `\u2192 ${actualTable}(id) ON DELETE ${actualOnDelete}`;
835
- colDiff.expectedTarget = branch.targetSeed;
836
- }
837
- }
838
- if (!indexNames.has(expectedIndexName)) {
839
- if (colDiff.status === "ok") {
840
- colDiff.status = "index_missing";
841
- } else {
842
- columns.push({ name: branch.alias, status: "index_missing" });
843
- }
844
- }
845
- }
846
- }
847
- return { slug: seed.slug, tableExists: true, columns };
705
+ async function seedLoad(_args = {}) {
706
+ console.log(pc.yellow('\n \u26A0 "beech seed:load" is deprecated'));
707
+ console.log(pc.dim(" Content schemas in BeechCMS are managed dynamically at runtime in Cloudflare D1."));
708
+ console.log(pc.dim(" Static seeds.ts files are no longer synchronized to the database."));
709
+ console.log(pc.cyan("\n \u2192 To manage content types, open the dashboard at /admin or use the /api/seeds API.\n"));
848
710
  }
849
711
 
712
+ // src/index.ts
713
+ init_init();
714
+
850
715
  // src/commands/validate.ts
851
- import pc2 from "picocolors";
852
- import { SEED_REGISTRY, validateSeedDefinitions } from "@beechcms/core";
716
+ import pc3 from "picocolors";
717
+ import { validateSeedDefinitions } from "@beechcms/core";
853
718
  function validateSeeds(registry) {
854
719
  return validateSeedDefinitions(Object.values(registry));
855
720
  }
856
- async function validate(args) {
857
- const registry = args.registry ?? SEED_REGISTRY;
858
- if (Object.keys(registry).length === 0) {
859
- console.warn(pc2.yellow("\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n"));
860
- return;
861
- }
862
- console.log(pc2.cyan("\n beech validate \u2014 checking seeds\n"));
863
- const errors = validateSeeds(registry);
864
- const fatalErrors = errors.filter((e) => e.fatal);
865
- const warnings = errors.filter((e) => !e.fatal);
866
- for (const e of fatalErrors) {
867
- console.log(pc2.red(` \u2717 ${e.slug} (fatal)`));
868
- for (const msg of e.messages) {
869
- console.log(pc2.red(` \u2192 ${msg}`));
870
- }
871
- }
872
- const warningMap = new Map(warnings.map((e) => [e.slug, e.messages]));
873
- const allWarningSlugsSeen = new Set(warnings.map((e) => e.slug));
874
- for (const seed of Object.values(registry)) {
875
- const msgs = warningMap.get(seed.slug);
876
- if (!msgs) {
877
- if (!allWarningSlugsSeen.has(seed.slug)) {
878
- const hasFatal = fatalErrors.some((e) => e.slug === seed.slug);
879
- if (!hasFatal) console.log(pc2.green(` \u2713 ${seed.slug}`));
880
- }
881
- } else {
882
- console.log(pc2.yellow(` \u26A0 ${seed.slug}`));
883
- for (const msg of msgs) {
884
- console.log(pc2.yellow(` \u2192 ${msg}`));
885
- }
886
- }
887
- }
888
- console.log("");
889
- const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
890
- const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0);
891
- if (totalFatal > 0) {
892
- const s = totalFatal !== 1 ? "s" : "";
893
- console.log(pc2.red(` Found ${totalFatal} fatal error${s}. Fix before loading.
894
- `));
895
- process.exit(1);
896
- } else if (totalWarnings > 0) {
897
- const s = totalWarnings !== 1 ? "s" : "";
898
- console.log(pc2.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.
899
- `));
900
- } else {
901
- console.log(pc2.green(" All seeds valid.\n"));
902
- }
721
+ async function validate(_args = {}) {
722
+ console.log(pc3.cyan("\n beech validate\n"));
723
+ console.log(pc3.dim(" Schema validation is enforced dynamically at runtime by @beechcms/core on all /api/seeds mutations."));
724
+ console.log(pc3.green(" \u2713 Runtime schema validation active.\n"));
903
725
  }
904
726
 
905
- // src/commands/seed-load.ts
906
- function buildSeedRegistrationSql(seed) {
907
- const json = sqlQuote(JSON.stringify(seed));
908
- return [
909
- `INSERT INTO seeds (slug, definition, status, source, created_at, updated_at)`,
910
- `VALUES (${sqlQuote(seed.slug)}, ${json}, 'active', 'code', unixepoch(), unixepoch())`,
911
- `ON CONFLICT(slug) DO UPDATE SET definition = excluded.definition, status = 'active', updated_at = excluded.updated_at;`
912
- ].join("\n");
913
- }
914
- var SEED_META_BUMP_SQL = `UPDATE seed_meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE id = 'registry_version';`;
915
- function buildStatements(seed) {
916
- const stmts = [generateCreateTable(seed), ...generateIndexes(seed)];
917
- const draft = generateDraftTable(seed);
918
- if (draft) stmts.push(draft);
919
- const fts = generateFtsTable(seed);
920
- if (fts) {
921
- stmts.push(fts, ...generateFtsTriggers(seed));
922
- }
923
- for (const branch of seed.branches) {
924
- if (branch.type !== "relation" || branch.multiple !== true) continue;
925
- stmts.push(generateJunctionTable(seed, branch), ...generateJunctionIndexes(seed, branch));
926
- const draftJunction = generateJunctionDraftTable(seed, branch);
927
- if (draftJunction) stmts.push(draftJunction);
928
- }
929
- return stmts;
930
- }
931
- async function runDiff(options, registry) {
932
- const seeds = sortSeedsByDependencies(Object.values(registry));
933
- console.log(pc3.cyan("\n Diffing schema\u2026\n"));
934
- let allOk = true;
935
- for (const seed of seeds) {
936
- const result = await diffSeed(seed, options);
937
- renderSeedDiff(result);
938
- if (!isSeedClean(result)) allOk = false;
939
- }
940
- console.log("");
941
- if (allOk) {
942
- console.log(pc3.green(" Schema matches seeds. No action needed.\n"));
943
- } else {
944
- console.log(pc3.yellow(" Run `beech seed:load` to apply missing tables/columns.\n"));
945
- }
946
- }
947
- async function runLoad(options, dryRun, registry) {
948
- const seeds = sortSeedsByDependencies(Object.values(registry));
949
- if (dryRun) {
950
- console.log(pc3.cyan("\n -- dry-run: SQL that would be executed\n"));
951
- for (const seed of seeds) {
952
- const stmts = buildStatements(seed);
953
- console.log(pc3.dim(` -- content_${seed.slug}`));
954
- for (const stmt of stmts) {
955
- console.log(stmt + "\n");
956
- }
957
- console.log(pc3.dim(` -- register ${seed.slug} in seeds table`));
958
- console.log(buildSeedRegistrationSql(seed) + "\n");
959
- }
960
- console.log(pc3.dim(" -- bump registry_version"));
961
- console.log(SEED_META_BUMP_SQL + "\n");
962
- return;
963
- }
964
- console.log(pc3.cyan(`
965
- Loading seeds into ${options.local ? "local" : "remote"} D1 (${options.db})\u2026
966
- `));
967
- for (const seed of seeds) {
968
- const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)];
969
- const sql = stmts.join("\n\n") + "\n";
970
- process.stdout.write(` ${pc3.dim("\u2192")} content_${seed.slug}\u2026 `);
971
- const ok = executeD1File(sql, options);
972
- if (!ok) {
973
- console.log(pc3.red("failed"));
974
- console.log(pc3.red(`
975
- \u2717 Failed to apply schema for content_${seed.slug}
976
- `));
977
- console.log(pc3.dim(" wrangler reported an error above."));
978
- console.log(pc3.dim(` Most likely causes:`));
979
- console.log(pc3.dim(` - Database "${options.db}" not found or wrong database_id`));
980
- if (!options.local) {
981
- console.log(pc3.dim(" - Not logged in to Cloudflare"));
982
- console.log(pc3.cyan("\n \u2192 Run: npx wrangler login"));
983
- console.log(pc3.cyan(" \u2192 Then: npx beech seed:load\n"));
984
- } else {
985
- console.log(pc3.cyan("\n \u2192 Run: npx beech init --db --local # re-initialise local DB"));
986
- console.log(pc3.cyan(" \u2192 Then: npx beech seed:load --local\n"));
987
- }
988
- process.exit(1);
989
- }
990
- console.log(pc3.green("done"));
991
- }
992
- executeD1File(SEED_META_BUMP_SQL, options);
993
- console.log(pc3.green("\n All seeds loaded.\n"));
994
- console.log(pc3.dim(" Definitions registered in the database."));
995
- console.log(pc3.dim(" seed.ts is no longer required at runtime \u2014 you may keep it for code-first edits or delete it.\n"));
996
- }
997
- async function seedLoad(args) {
998
- const registry = args.registry ?? SEED_REGISTRY2;
999
- if (Object.keys(registry).length === 0) {
1000
- console.log(pc3.yellow("\n \u2717 No seeds found\n"));
1001
- console.log(pc3.dim(" Create a seeds.ts file in your project root with at least one content type."));
1002
- console.log(pc3.cyan("\n \u2192 Run: npx beech seed:create\n"));
1003
- return;
1004
- }
1005
- const validationErrors = validateSeeds(registry);
1006
- const fatalErrors = validationErrors.filter((e) => e.fatal);
1007
- const warnings = validationErrors.filter((e) => !e.fatal);
1008
- if (fatalErrors.length > 0) {
1009
- const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
1010
- const s = total !== 1 ? "s" : "";
1011
- console.log(pc3.red(`
1012
- \u2717 Seed validation found ${total} fatal error${s}. Cannot load schema.
1013
- `));
1014
- for (const e of fatalErrors) {
1015
- console.log(pc3.red(` \u2717 ${e.slug}`));
1016
- for (const msg of e.messages) {
1017
- console.log(pc3.red(` \u2192 ${msg}`));
1018
- }
1019
- }
1020
- console.log("");
1021
- process.exit(1);
1022
- }
1023
- if (warnings.length > 0) {
1024
- const total = warnings.reduce((n, e) => n + e.messages.length, 0);
1025
- const s = total !== 1 ? "s" : "";
1026
- console.log(pc3.yellow(`
1027
- \u26A0 Seed validation found ${total} issue${s}. Schema changes will still be applied.
1028
- `));
1029
- console.log(pc3.dim(' Run "npx beech validate" for details.\n'));
1030
- }
1031
- const configPath = findWranglerConfig();
1032
- const db = args.db ?? resolveDbName(configPath);
1033
- const options = {
1034
- db,
1035
- local: args.local,
1036
- configPath
1037
- };
1038
- if (!args.dryRun && !args.diff) {
1039
- try {
1040
- const rows = queryD1(
1041
- `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
1042
- options
1043
- );
1044
- if (rows.length < 2) {
1045
- console.log(pc3.red("\n \u2717 System tables not found (seeds, seed_meta)\n"));
1046
- console.log(pc3.dim(" Run `beech init --db` first to initialise the database."));
1047
- const flag = args.local ? " --local" : "";
1048
- console.log(pc3.cyan(`
1049
- \u2192 Run: npx beech init --db${flag}
1050
- `));
1051
- process.exit(1);
1052
- }
1053
- } catch {
1054
- console.log(pc3.red("\n \u2717 Could not query the database\n"));
1055
- console.log(pc3.dim(" Run `beech init --db` first to initialise the database."));
1056
- process.exit(1);
1057
- }
1058
- }
1059
- if (args.diff) {
1060
- await runDiff(options, registry);
1061
- } else {
1062
- await runLoad(options, args.dryRun, registry);
1063
- }
1064
- }
1065
-
1066
- // src/index.ts
1067
- init_init();
1068
-
1069
727
  // src/commands/seed-create.ts
1070
- import pc5 from "picocolors";
1071
- import { createInterface as createInterface2 } from "node:readline/promises";
1072
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
1073
- import { resolve as resolve3 } from "node:path";
1074
- var BRANCH_TYPES = ["text", "number", "boolean", "date", "richtext", "file", "tags"];
1075
- function slugify(str) {
1076
- return str.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
1077
- }
1078
- function toConstName(slug) {
1079
- return slug.replace(/-/g, "_").toUpperCase() + "_SEED";
1080
- }
1081
- function toLabel(alias) {
1082
- return alias.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
1083
- }
1084
- function generateSeedBlock(slug, label, labelPlural, branches) {
1085
- const displayAlias = branches.find((b) => b.type === "text")?.alias ?? branches[0]?.alias ?? "name";
1086
- const cName = toConstName(slug);
1087
- const branchLines = branches.map((b) => {
1088
- const parts = [
1089
- `alias: '${b.alias}'`,
1090
- `label: '${b.label}'`,
1091
- `type: '${b.type}'`
1092
- ];
1093
- if (b.required) parts.push("requiredOnCreate: true");
1094
- return ` { ${parts.join(", ")} },`;
1095
- });
1096
- return [
1097
- "",
1098
- `export const ${cName} = defineSeed({`,
1099
- ` slug: '${slug}',`,
1100
- ` label: '${label}',`,
1101
- ` labelPlural: '${labelPlural}',`,
1102
- ` displayNameAlias: '${displayAlias}',`,
1103
- " branches: [",
1104
- ...branchLines,
1105
- " ],",
1106
- " dashboard: {",
1107
- " icon: 'Folder',",
1108
- " group: 'Content',",
1109
- " },",
1110
- "})",
1111
- ""
1112
- ].join("\n");
1113
- }
1114
- function ensureDefineSeedImport(content) {
1115
- if (content.includes("defineSeed")) return content;
1116
- return `import { defineSeed } from '@beechcms/core'
1117
- ` + content;
1118
- }
1119
- function tryInsertRegistryEntry(content, slug, cName) {
1120
- const match = content.match(/(SEED_REGISTRY[^{]*\{)([\s\S]*?)(\n\})/m);
1121
- if (!match) return null;
1122
- const [full, open, inner, close] = match;
1123
- const newEntry = `
1124
- ${slug}: ${cName},`;
1125
- return content.replace(full, open + inner + newEntry + close);
1126
- }
1127
- function findSeedsFile() {
1128
- const cwd = process.cwd();
1129
- const searchDirs = [cwd, resolve3(cwd, "apps", "api")];
1130
- for (const dir of searchDirs) {
1131
- for (const name of ["seeds.ts", "seed.ts"]) {
1132
- const p = resolve3(dir, name);
1133
- if (existsSync3(p)) return p;
1134
- }
1135
- }
1136
- return null;
1137
- }
1138
- async function seedCreate(_args) {
1139
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
1140
- const ask = async (q, fallback = "") => {
1141
- const hint = fallback ? pc5.dim(` [${fallback}]`) : "";
1142
- const answer = await rl.question(` ${q}${hint}: `);
1143
- return answer.trim() || fallback;
1144
- };
1145
- const askYN = async (q, defaultYes = true) => {
1146
- const hint = defaultYes ? pc5.dim(" (Y/n)") : pc5.dim(" (y/N)");
1147
- const answer = (await rl.question(` ${q}${hint}: `)).trim().toLowerCase();
1148
- if (!answer) return defaultYes;
1149
- return answer === "y" || answer === "yes";
1150
- };
1151
- console.log(pc5.cyan("\n beech seed:create \u2014 new content type wizard\n"));
1152
- try {
1153
- const label = await ask('Content type name (singular, e.g. "Article")');
1154
- if (!label) {
1155
- rl.close();
1156
- console.log(pc5.red("\n \u2717 Name required.\n"));
1157
- process.exit(1);
1158
- }
1159
- const defaultSlug = slugify(label) + "s";
1160
- const slug = slugify(await ask("Slug (plural, used in URL + table name)", defaultSlug)) || defaultSlug;
1161
- const labelPlural = await ask("Plural label", label + "s") || label + "s";
1162
- const branches = [];
1163
- console.log(pc5.dim("\n Now define the fields. Press Enter to accept defaults.\n"));
1164
- let addMore = true;
1165
- while (addMore) {
1166
- console.log(pc5.dim(` \u2500\u2500\u2500 Field ${branches.length + 1} \u2500\u2500\u2500`));
1167
- const alias = await ask(' Alias (camelCase, e.g. "title", "publishedAt")');
1168
- if (!alias) {
1169
- console.log(pc5.yellow(" Alias required \u2014 skipping."));
1170
- addMore = await askYN("\n Add a field?");
1171
- continue;
1172
- }
1173
- const fieldLabel = await ask(" Label", toLabel(alias)) || toLabel(alias);
1174
- const typeList = BRANCH_TYPES.join(" | ");
1175
- const rawType = (await ask(` Type (${typeList})`, "text")).toLowerCase();
1176
- const type = BRANCH_TYPES.includes(rawType) ? rawType : "text";
1177
- const required = await askYN(" Required on create?", false);
1178
- branches.push({ alias, label: fieldLabel, type, required });
1179
- addMore = await askYN("\n Add another field?");
1180
- }
1181
- rl.close();
1182
- if (branches.length === 0) {
1183
- console.log(pc5.yellow("\n No fields defined \u2014 seed not created.\n"));
1184
- process.exit(0);
1185
- }
1186
- const seedBlock = generateSeedBlock(slug, label, labelPlural, branches);
1187
- const cName = toConstName(slug);
1188
- const seedsPath = findSeedsFile();
1189
- if (!seedsPath) {
1190
- console.log(pc5.yellow("\n Could not find seeds.ts. Add this to your seeds file manually:\n"));
1191
- console.log(seedBlock);
1192
- process.exit(0);
1193
- }
1194
- let content = readFileSync3(seedsPath, "utf-8");
1195
- content = ensureDefineSeedImport(content);
1196
- const withSeed = content + seedBlock;
1197
- const withRegistry = tryInsertRegistryEntry(withSeed, slug, cName);
1198
- writeFileSync3(seedsPath, withRegistry ?? withSeed, "utf-8");
1199
- console.log(pc5.green(`
1200
- \u2713 Seed "${slug}" appended to ${seedsPath}
1201
- `));
1202
- if (!withRegistry) {
1203
- console.log(pc5.yellow(` \u26A0 Could not auto-update SEED_REGISTRY \u2014 add this entry manually:
1204
- `));
1205
- console.log(pc5.cyan(` ${slug}: ${cName},
1206
- `));
1207
- }
1208
- console.log(pc5.dim(" Next steps:"));
1209
- console.log(pc5.cyan(" npx beech seed:load --local"));
1210
- console.log(pc5.dim(" \u2192 create the new content table in your local D1 database\n"));
1211
- } catch (err) {
1212
- rl.close();
1213
- throw err;
1214
- }
728
+ import pc4 from "picocolors";
729
+ async function seedCreate(_args = {}) {
730
+ console.log(pc4.yellow('\n \u26A0 "beech seed:create" is deprecated'));
731
+ console.log(pc4.dim(" Content schemas in BeechCMS are managed dynamically at runtime in Cloudflare D1."));
732
+ console.log(pc4.cyan("\n \u2192 Create new content types directly in the BeechCMS Dashboard (/admin) or via POST /api/seeds.\n"));
1215
733
  }
1216
734
 
1217
735
  // src/commands/deploy.ts
1218
736
  init_wrangler();
1219
- import pc6 from "picocolors";
737
+ import pc5 from "picocolors";
1220
738
  import { spawnSync as spawnSync3 } from "node:child_process";
1221
- import { readFileSync as readFileSync4 } from "node:fs";
739
+ import { readFileSync as readFileSync3 } from "node:fs";
1222
740
  function readWorkerName(configPath) {
1223
741
  if (!configPath) return null;
1224
742
  try {
1225
- const raw = readFileSync4(configPath, "utf-8");
743
+ const raw = readFileSync3(configPath, "utf-8");
1226
744
  const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1227
745
  const parsed = JSON.parse(stripped);
1228
746
  return parsed?.name ?? null;
@@ -1247,8 +765,8 @@ async function checkAdmin(url) {
1247
765
  }
1248
766
  }
1249
767
  async function deploy(args) {
1250
- console.log(pc6.cyan("\n beech deploy\n"));
1251
- console.log(pc6.dim(" [1/3] Deploying Worker\u2026\n"));
768
+ console.log(pc5.cyan("\n beech deploy\n"));
769
+ console.log(pc5.dim(" [1/2] Deploying Worker\u2026\n"));
1252
770
  const deployResult = spawnSync3("npm", ["run", "deploy"], {
1253
771
  stdio: ["inherit", "pipe", "inherit"],
1254
772
  encoding: "utf-8",
@@ -1258,33 +776,16 @@ async function deploy(args) {
1258
776
  const deployStdout = deployResult.stdout ?? "";
1259
777
  if (deployStdout) process.stdout.write(deployStdout);
1260
778
  if (deployResult.status !== 0) {
1261
- console.log(pc6.red("\n \u2717 Worker deploy failed\n"));
1262
- console.log(pc6.dim(" Check the wrangler output above for details."));
1263
- console.log(pc6.cyan("\n \u2192 Run: npx wrangler login # if not authenticated"));
1264
- console.log(pc6.cyan(" \u2192 Or: Update wrangler.jsonc # if database_id is wrong\n"));
779
+ console.log(pc5.red("\n \u2717 Worker deploy failed\n"));
780
+ console.log(pc5.dim(" Check the wrangler output above for details."));
781
+ console.log(pc5.cyan("\n \u2192 Run: npx wrangler login # if not authenticated"));
782
+ console.log(pc5.cyan(" \u2192 Or: Update wrangler.jsonc # if database_id is wrong\n"));
1265
783
  process.exit(1);
1266
784
  }
1267
785
  const deployedUrl = extractWorkerUrl(deployStdout);
1268
- console.log(pc6.green("\n \u2713 Worker deployed"));
1269
- if (args.skipSeed) {
1270
- console.log(pc6.dim("\n [2/3] Skipping seed:load (--skip-seed)"));
1271
- } else {
1272
- console.log(pc6.dim("\n [2/3] Syncing content schema to remote D1\u2026\n"));
1273
- const seedResult = spawnSync3("npx", ["beech", "seed:load"], {
1274
- stdio: "inherit",
1275
- cwd: process.cwd(),
1276
- shell: true
1277
- });
1278
- if (seedResult.status !== 0) {
1279
- console.log(pc6.yellow("\n \u26A0 seed:load failed\n"));
1280
- console.log(pc6.dim(" Sync the remote content schema manually:"));
1281
- console.log(pc6.cyan(" \u2192 Run: npx beech seed:load\n"));
1282
- } else {
1283
- console.log(pc6.green("\n \u2713 Content schema synced"));
1284
- }
1285
- }
786
+ console.log(pc5.green("\n \u2713 Worker deployed"));
1286
787
  if (args.skipCheck) {
1287
- console.log(pc6.dim("\n [3/3] Skipping admin check (--skip-check)\n"));
788
+ console.log(pc5.dim("\n [2/2] Skipping admin check (--skip-check)\n"));
1288
789
  return;
1289
790
  }
1290
791
  const adminBase = deployedUrl ?? (() => {
@@ -1292,106 +793,101 @@ async function deploy(args) {
1292
793
  return workerName ? `https://${workerName}.workers.dev` : null;
1293
794
  })();
1294
795
  if (!adminBase) {
1295
- console.log(pc6.dim("\n [3/3] Could not determine worker URL \u2014 skipping admin check\n"));
1296
- console.log(pc6.dim(" The deployed URL is printed by wrangler above. Open <url>/admin to verify.\n"));
796
+ console.log(pc5.dim("\n [2/2] Could not determine worker URL \u2014 skipping admin check\n"));
797
+ console.log(pc5.dim(" The deployed URL is printed by wrangler above. Open <url>/admin to verify.\n"));
1297
798
  return;
1298
799
  }
1299
- console.log(pc6.dim(`
1300
- [3/3] Checking ${adminBase}/admin\u2026
800
+ console.log(pc5.dim(`
801
+ [2/2] Checking ${adminBase}/admin\u2026
1301
802
  `));
1302
803
  const { ok, status } = await checkAdmin(adminBase);
1303
804
  if (ok) {
1304
- console.log(pc6.green(` \u2713 Admin reachable at: ${adminBase}/admin
805
+ console.log(pc5.green(` \u2713 Admin reachable at: ${adminBase}/admin
1305
806
  `));
1306
807
  } else {
1307
808
  const statusStr = status != null ? ` (HTTP ${status})` : "";
1308
- console.log(pc6.yellow(` \u26A0 Admin returned an error${statusStr} at: ${adminBase}/admin
809
+ console.log(pc5.yellow(` \u26A0 Admin returned an error${statusStr} at: ${adminBase}/admin
1309
810
  `));
1310
- console.log(pc6.dim(" The database may not be fully initialized."));
1311
- console.log(pc6.cyan(" \u2192 Run: npx beech init --db --remote\n"));
811
+ console.log(pc5.dim(" The database may not be fully initialized."));
812
+ console.log(pc5.cyan(" \u2192 Run: npx beech init --db --remote\n"));
1312
813
  }
1313
814
  }
1314
815
 
1315
816
  // src/commands/onboard.ts
1316
817
  init_init();
1317
- import pc7 from "picocolors";
818
+ import pc6 from "picocolors";
1318
819
  async function onboard(args) {
1319
- console.log(pc7.cyan("\n beech onboard \u2014 full provisioning\n"));
820
+ console.log(pc6.cyan("\n beech onboard \u2014 full provisioning\n"));
1320
821
  await init({ initDb: true, local: args.local, db: args.db, nonInteractive: args.yes });
1321
- await seedLoad({ dryRun: false, diff: false, local: args.local, db: args.db, registry: args.registry ?? null });
1322
- console.log(pc7.cyan("\n Provisioning complete.\n"));
1323
- console.log(pc7.dim(" Next steps:"));
1324
- console.log(pc7.cyan(" 1. npx wrangler dev"));
1325
- console.log(pc7.dim(" \u2192 start API + dashboard"));
1326
- console.log(pc7.dim(" 2. Open http://localhost:8789/admin"));
1327
- console.log(pc7.dim(" \u2192 complete setup wizard to create admin user\n"));
822
+ console.log(pc6.cyan("\n Provisioning complete.\n"));
823
+ console.log(pc6.dim(" Next steps:"));
824
+ console.log(pc6.cyan(" 1. npx wrangler dev"));
825
+ console.log(pc6.dim(" \u2192 start API + dashboard"));
826
+ console.log(pc6.cyan(" 2. Open http://localhost:8789/admin"));
827
+ console.log(pc6.dim(" \u2192 complete setup wizard to create admin user\n"));
1328
828
  }
1329
829
 
1330
830
  // src/commands/update.ts
1331
- import pc8 from "picocolors";
831
+ import pc7 from "picocolors";
1332
832
  import { spawnSync as spawnSync4 } from "node:child_process";
1333
833
  async function update(_args) {
1334
- console.log(pc8.cyan("\n beech update\n"));
1335
- console.log(pc8.dim(" [1/2] Installing latest BeechCMS packages\u2026\n"));
834
+ console.log(pc7.cyan("\n beech update\n"));
835
+ console.log(pc7.dim(" [1/2] Installing latest BeechCMS packages\u2026\n"));
1336
836
  const installResult = spawnSync4(
1337
837
  "npm",
1338
838
  ["install", "@beechcms/api@latest", "@beechcms/core@latest"],
1339
839
  { stdio: "inherit", cwd: process.cwd(), shell: true }
1340
840
  );
1341
841
  if (installResult.status !== 0) {
1342
- console.log(pc8.red("\n \u2717 npm install failed\n"));
1343
- console.log(pc8.dim(" Check the output above for details."));
1344
- console.log(pc8.dim(" You may need to resolve version conflicts manually."));
1345
- console.log(pc8.cyan("\n \u2192 Try: npm install --legacy-peer-deps\n"));
842
+ console.log(pc7.red("\n \u2717 npm install failed\n"));
843
+ console.log(pc7.dim(" Check the output above for details."));
844
+ console.log(pc7.dim(" You may need to resolve version conflicts manually."));
845
+ console.log(pc7.cyan("\n \u2192 Try: npm install --legacy-peer-deps\n"));
1346
846
  process.exit(1);
1347
847
  }
1348
- console.log(pc8.green("\n \u2713 Packages updated"));
1349
- console.log(pc8.dim("\n [2/2] Applying system migrations to local database\u2026\n"));
848
+ console.log(pc7.green("\n \u2713 Packages updated"));
849
+ console.log(pc7.dim("\n [2/2] Applying system migrations to local database\u2026\n"));
1350
850
  const initResult = spawnSync4(
1351
851
  "npx",
1352
852
  ["beech", "init", "--db", "--local"],
1353
853
  { stdio: "inherit", cwd: process.cwd(), shell: true }
1354
854
  );
1355
855
  if (initResult.status !== 0) {
1356
- console.log(pc8.yellow("\n \u26A0 Local DB update failed\n"));
1357
- console.log(pc8.dim(" Apply system migrations manually:"));
1358
- console.log(pc8.cyan(" \u2192 Run: npx beech init --db --local\n"));
856
+ console.log(pc7.yellow("\n \u26A0 Local DB update failed\n"));
857
+ console.log(pc7.dim(" Apply system migrations manually:"));
858
+ console.log(pc7.cyan(" \u2192 Run: npx beech init --db --local\n"));
1359
859
  } else {
1360
- console.log(pc8.green("\n \u2713 Local database updated"));
1361
- }
1362
- console.log(pc8.dim("\n Local update complete.\n"));
1363
- console.log(pc8.dim(" Next steps:"));
1364
- console.log(pc8.cyan(" 1. npx beech seed:load --local"));
1365
- console.log(pc8.dim(" \u2192 sync content schema to local DB"));
1366
- console.log(pc8.cyan(" 2. npm run deploy"));
1367
- console.log(pc8.dim(" \u2192 deploy updated API + dashboard"));
1368
- console.log(pc8.cyan(" 3. npx beech seed:load"));
1369
- console.log(pc8.dim(" \u2192 sync remote schema\n"));
860
+ console.log(pc7.green("\n \u2713 Local database updated"));
861
+ }
862
+ console.log(pc7.dim("\n Local update complete.\n"));
863
+ console.log(pc7.dim(" Next steps:"));
864
+ console.log(pc7.cyan(" 1. npm run deploy"));
865
+ console.log(pc7.dim(" \u2192 deploy updated API + dashboard\n"));
1370
866
  }
1371
867
 
1372
868
  // src/commands/reset.ts
1373
- import pc11 from "picocolors";
1374
- import { createInterface as createInterface3 } from "node:readline/promises";
869
+ import pc10 from "picocolors";
870
+ import { createInterface as createInterface2 } from "node:readline/promises";
1375
871
 
1376
872
  // src/commands/db-reset.ts
1377
- import pc9 from "picocolors";
873
+ import pc8 from "picocolors";
1378
874
  import { spawnSync as spawnSync5 } from "node:child_process";
1379
- import { existsSync as existsSync4, readFileSync as readFileSync5, rmSync as rmSync2 } from "node:fs";
1380
- import { resolve as resolve4 } from "node:path";
875
+ import { existsSync as existsSync3, readFileSync as readFileSync4, rmSync as rmSync2 } from "node:fs";
876
+ import { resolve as resolve3 } from "node:path";
1381
877
  async function dbReset(_args) {
1382
- console.log(pc9.cyan("\n beech db:reset \u2014 reset local database\n"));
878
+ console.log(pc8.cyan("\n beech db:reset \u2014 reset local database\n"));
1383
879
  const cwd = process.cwd();
1384
- const apiDir = resolve4(cwd, "apps", "api");
880
+ const apiDir = resolve3(cwd, "apps", "api");
1385
881
  let dbResetSuccess = false;
1386
- if (existsSync4(resolve4(apiDir, "package.json"))) {
882
+ if (existsSync3(resolve3(apiDir, "package.json"))) {
1387
883
  const result = spawnSync5("npm", ["run", "db:reset:local"], {
1388
884
  stdio: "inherit",
1389
885
  cwd: apiDir,
1390
886
  shell: true
1391
887
  });
1392
888
  dbResetSuccess = result.status === 0;
1393
- } else if (existsSync4(resolve4(cwd, "package.json"))) {
1394
- const pkg = JSON.parse(readFileSync5(resolve4(cwd, "package.json"), "utf-8"));
889
+ } else if (existsSync3(resolve3(cwd, "package.json"))) {
890
+ const pkg = JSON.parse(readFileSync4(resolve3(cwd, "package.json"), "utf-8"));
1395
891
  if (pkg.scripts?.["db:reset:local"]) {
1396
892
  const result = spawnSync5("npm", ["run", "db:reset:local"], {
1397
893
  stdio: "inherit",
@@ -1400,12 +896,12 @@ async function dbReset(_args) {
1400
896
  });
1401
897
  dbResetSuccess = result.status === 0;
1402
898
  } else {
1403
- const wranglerStateDir = resolve4(cwd, ".wrangler/state");
1404
- if (existsSync4(wranglerStateDir)) {
1405
- console.log(pc9.dim(" Removing .wrangler/state\u2026"));
899
+ const wranglerStateDir = resolve3(cwd, ".wrangler/state");
900
+ if (existsSync3(wranglerStateDir)) {
901
+ console.log(pc8.dim(" Removing .wrangler/state\u2026"));
1406
902
  rmSync2(wranglerStateDir, { recursive: true, force: true });
1407
903
  }
1408
- if (existsSync4(resolve4(cwd, "scripts", "bootstrap-d1.mjs"))) {
904
+ if (existsSync3(resolve3(cwd, "scripts", "bootstrap-d1.mjs"))) {
1409
905
  const result = spawnSync5("node", ["scripts/bootstrap-d1.mjs"], {
1410
906
  stdio: "inherit",
1411
907
  cwd,
@@ -1413,7 +909,7 @@ async function dbReset(_args) {
1413
909
  });
1414
910
  dbResetSuccess = result.status === 0;
1415
911
  } else {
1416
- console.log(pc9.yellow(" \u26A0 Could not find database reset script."));
912
+ console.log(pc8.yellow(" \u26A0 Could not find database reset script."));
1417
913
  const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1418
914
  try {
1419
915
  await init2({ initDb: true, local: true });
@@ -1424,24 +920,24 @@ async function dbReset(_args) {
1424
920
  }
1425
921
  }
1426
922
  } else {
1427
- const wranglerStateDir = resolve4(cwd, ".wrangler/state");
1428
- if (existsSync4(wranglerStateDir)) {
1429
- console.log(pc9.dim(" Removing .wrangler/state\u2026"));
923
+ const wranglerStateDir = resolve3(cwd, ".wrangler/state");
924
+ if (existsSync3(wranglerStateDir)) {
925
+ console.log(pc8.dim(" Removing .wrangler/state\u2026"));
1430
926
  rmSync2(wranglerStateDir, { recursive: true, force: true });
1431
927
  }
1432
928
  dbResetSuccess = true;
1433
929
  }
1434
930
  if (dbResetSuccess) {
1435
- console.log(pc9.green("\n \u2713 Local database reset completed."));
931
+ console.log(pc8.green("\n \u2713 Local database reset completed."));
1436
932
  } else {
1437
- console.log(pc9.red("\n \u2717 Database reset failed."));
933
+ console.log(pc8.red("\n \u2717 Database reset failed."));
1438
934
  process.exit(1);
1439
935
  return;
1440
936
  }
1441
937
  }
1442
938
 
1443
939
  // src/commands/dev-reset.ts
1444
- import pc10 from "picocolors";
940
+ import pc9 from "picocolors";
1445
941
  import { spawnSync as spawnSync6 } from "node:child_process";
1446
942
  function isDockerInstalled() {
1447
943
  try {
@@ -1460,27 +956,27 @@ function isDockerRunning() {
1460
956
  }
1461
957
  }
1462
958
  async function devReset() {
1463
- console.log(pc10.cyan("\n beech dev:reset \u2014 reset Docker environment\n"));
959
+ console.log(pc9.cyan("\n beech dev:reset \u2014 reset Docker environment\n"));
1464
960
  if (!isDockerInstalled()) {
1465
- console.log(pc10.red(" \u2717 Docker is not installed or not found in your PATH."));
961
+ console.log(pc9.red(" \u2717 Docker is not installed or not found in your PATH."));
1466
962
  process.exit(1);
1467
963
  return;
1468
964
  }
1469
965
  if (!isDockerRunning()) {
1470
- console.log(pc10.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
966
+ console.log(pc9.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
1471
967
  process.exit(1);
1472
968
  return;
1473
969
  }
1474
- console.log(pc10.dim(" Resetting Docker containers and volumes\u2026\n"));
970
+ console.log(pc9.dim(" Resetting Docker containers and volumes\u2026\n"));
1475
971
  const result = spawnSync6("docker", ["compose", "-f", "docker/docker-compose.yml", "down", "-v"], {
1476
972
  stdio: "inherit",
1477
973
  cwd: process.cwd(),
1478
974
  shell: true
1479
975
  });
1480
976
  if (result.status === 0) {
1481
- console.log(pc10.green("\n \u2713 Docker containers stopped and volumes removed."));
977
+ console.log(pc9.green("\n \u2713 Docker containers stopped and volumes removed."));
1482
978
  } else {
1483
- console.log(pc10.red("\n \u2717 Docker reset failed."));
979
+ console.log(pc9.red("\n \u2717 Docker reset failed."));
1484
980
  process.exit(1);
1485
981
  return;
1486
982
  }
@@ -1488,7 +984,7 @@ async function devReset() {
1488
984
 
1489
985
  // src/commands/reset.ts
1490
986
  async function reset(args) {
1491
- console.log(pc11.cyan("\n beech reset \u2014 cleanup environments\n"));
987
+ console.log(pc10.cyan("\n beech reset \u2014 cleanup environments\n"));
1492
988
  let resetDb = args.db || args.all;
1493
989
  let resetDocker = args.docker || args.all;
1494
990
  if (!args.db && !args.docker && !args.all) {
@@ -1496,23 +992,23 @@ async function reset(args) {
1496
992
  resetDb = true;
1497
993
  resetDocker = true;
1498
994
  } else if (process.stdin.isTTY) {
1499
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
995
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1500
996
  try {
1501
997
  const answer = (await rl.question(
1502
- pc11.cyan(" \u2192 No options provided. Would you like to reset everything (DB & Docker)? (y/N): ")
998
+ pc10.cyan(" \u2192 No options provided. Would you like to reset everything (DB & Docker)? (y/N): ")
1503
999
  )).trim().toLowerCase();
1504
1000
  if (answer === "y" || answer === "yes") {
1505
1001
  resetDb = true;
1506
1002
  resetDocker = true;
1507
1003
  } else {
1508
- console.log(pc11.dim("\n Reset cancelled. Use --db, --docker, or --all.\n"));
1004
+ console.log(pc10.dim("\n Reset cancelled. Use --db, --docker, or --all.\n"));
1509
1005
  return;
1510
1006
  }
1511
1007
  } finally {
1512
1008
  rl.close();
1513
1009
  }
1514
1010
  } else {
1515
- console.log(pc11.red("\n \u2717 Error: Please specify what to reset using --db, --docker, or --all.\n"));
1011
+ console.log(pc10.red("\n \u2717 Error: Please specify what to reset using --db, --docker, or --all.\n"));
1516
1012
  process.exit(1);
1517
1013
  }
1518
1014
  }
@@ -1526,237 +1022,170 @@ async function reset(args) {
1526
1022
 
1527
1023
  // src/commands/generate-types.ts
1528
1024
  init_wrangler();
1529
- import { writeFileSync as writeFileSync4, mkdirSync } from "node:fs";
1530
- import { dirname, resolve as resolve5 } from "node:path";
1531
- import pc12 from "picocolors";
1025
+ import { writeFileSync as writeFileSync3, mkdirSync } from "node:fs";
1026
+ import { dirname, resolve as resolve4 } from "node:path";
1027
+ import pc11 from "picocolors";
1532
1028
  import { generateSeedTypes } from "@beechcms/core";
1533
- function loadSeedsFromD1(db) {
1029
+ async function generateTypes(args = {}) {
1030
+ const isLocal = args.local !== false;
1534
1031
  const configPath = findWranglerConfig();
1535
- const options = { db, local: false, configPath };
1536
- const rows = queryD1(
1537
- `SELECT slug, definition FROM seeds WHERE status = 'active';`,
1538
- options
1539
- );
1540
- return rows.map((r) => JSON.parse(r.definition));
1541
- }
1542
- async function generateTypes(args) {
1543
- let seeds;
1544
- if (args.local) {
1545
- const registry = args.registry ?? {};
1546
- if (Object.keys(registry).length === 0) {
1547
- console.log(pc12.red("\n \u2717 No seeds found (seeds.ts empty or missing).\n"));
1032
+ const db = args.db ?? resolveDbName(configPath);
1033
+ if (isLocal) {
1034
+ const sqlitePath = getLocalD1SqlitePath();
1035
+ if (!sqlitePath) {
1036
+ console.error(
1037
+ pc11.red("\n \u2717 Local D1 database state not found.") + pc11.gray("\n Start your local development environment with `beech dev` or initialize with `beech init --db`.\n")
1038
+ );
1548
1039
  process.exit(1);
1549
1040
  }
1550
- seeds = Object.values(registry);
1551
- } else {
1552
- const db = args.db ?? resolveDbName(findWranglerConfig());
1553
- seeds = loadSeedsFromD1(db);
1554
- if (seeds.length === 0) {
1555
- console.log(pc12.red(`
1556
- \u2717 No active seeds in D1 (${db}). Run \`beech seed:load\` first.
1557
- `));
1558
- process.exit(1);
1041
+ }
1042
+ const options = {
1043
+ db,
1044
+ local: isLocal,
1045
+ configPath
1046
+ };
1047
+ let rows;
1048
+ try {
1049
+ rows = queryD1(
1050
+ `SELECT slug, definition FROM seeds WHERE status = 'active' ORDER BY slug ASC;`,
1051
+ options
1052
+ );
1053
+ } catch (error) {
1054
+ const errMsg = error?.message || String(error);
1055
+ if (errMsg.includes("no such table: seeds")) {
1056
+ console.error(
1057
+ pc11.red("\n \u2717 System table `seeds` not found in database.") + pc11.gray("\n Run `beech init --db` or `beech onboard` to initialize system tables.\n")
1058
+ );
1059
+ } else {
1060
+ console.error(
1061
+ pc11.red(`
1062
+ \u2717 Failed to introspect D1 database (${db}):`) + pc11.gray(`
1063
+ ${errMsg}
1064
+ `)
1065
+ );
1559
1066
  }
1067
+ process.exit(1);
1068
+ }
1069
+ if (!rows || rows.length === 0) {
1070
+ console.error(
1071
+ pc11.red(`
1072
+ \u2717 No active seeds found in D1 database (${db}).`) + pc11.gray("\n Create or activate seeds via the dashboard (/admin) or REST API (/api/seeds).\n")
1073
+ );
1074
+ process.exit(1);
1075
+ }
1076
+ let seeds;
1077
+ try {
1078
+ seeds = rows.map((r) => JSON.parse(r.definition));
1079
+ } catch (err) {
1080
+ console.error(
1081
+ pc11.red("\n \u2717 Failed to parse seed definitions from database:") + pc11.gray(`
1082
+ ${err?.message || err}
1083
+ `)
1084
+ );
1085
+ process.exit(1);
1560
1086
  }
1561
1087
  const code = generateSeedTypes(seeds);
1562
- const outPath = resolve5(process.cwd(), args.out);
1563
- mkdirSync(dirname(outPath), { recursive: true });
1564
- writeFileSync4(outPath, code, "utf-8");
1565
- console.log(pc12.green(`
1088
+ if (args.out) {
1089
+ const outPath = resolve4(process.cwd(), args.out);
1090
+ mkdirSync(dirname(outPath), { recursive: true });
1091
+ writeFileSync3(outPath, code, "utf-8");
1092
+ console.log(pc11.green(`
1566
1093
  \u2713 Generated ${seeds.length} interface(s) \u2192 ${args.out}
1567
1094
  `));
1568
- }
1569
-
1570
- // src/commands/schema-diff.ts
1571
- init_wrangler();
1572
- import pc13 from "picocolors";
1573
- import { resolve as resolve6 } from "node:path";
1574
- import { SEED_REGISTRY as SEED_REGISTRY3, sortSeedsByDependencies as sortSeedsByDependencies2 } from "@beechcms/core";
1575
-
1576
- // src/lib/migration-writer.ts
1577
- import { readdirSync as readdirSync2, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "node:fs";
1578
- import { join as join2 } from "node:path";
1579
- import {
1580
- generateAddColumn,
1581
- generateIndexes as generateIndexes2,
1582
- planCreateSeed
1583
- } from "@beechcms/core";
1584
- var DESTRUCTIVE = /* @__PURE__ */ new Set([
1585
- "extra",
1586
- "type_mismatch",
1587
- "fk_mismatch"
1588
- ]);
1589
- function nextMigrationIndex(migrationsDir) {
1590
- if (!existsSync5(migrationsDir)) return "0000";
1591
- let max = -1;
1592
- for (const f of readdirSync2(migrationsDir)) {
1593
- const m = /^(\d{4})_/.exec(f);
1594
- if (m) max = Math.max(max, Number(m[1]));
1595
- }
1596
- return String(max + 1).padStart(4, "0");
1597
- }
1598
- function buildMigrationSql(diffs, registry) {
1599
- const lines = [];
1600
- let additiveCount = 0;
1601
- const destructiveSlugs = [];
1602
- for (const diff of diffs) {
1603
- const seed = registry[diff.slug];
1604
- if (!seed) continue;
1605
- if (!diff.tableExists) {
1606
- lines.push(`-- ${diff.slug}: create table from scratch`);
1607
- for (const stmt of planCreateSeed(seed)) {
1608
- lines.push(stmt);
1609
- additiveCount++;
1610
- }
1611
- lines.push("");
1612
- continue;
1613
- }
1614
- const missing = diff.columns.filter((c) => c.status === "missing");
1615
- const idxMissing = diff.columns.filter((c) => c.status === "index_missing");
1616
- const destructive = diff.columns.filter((c) => DESTRUCTIVE.has(c.status));
1617
- if (missing.length || idxMissing.length) {
1618
- lines.push(`-- ${diff.slug}: additive changes`);
1619
- for (const col of missing) {
1620
- const branch = seed.branches.find((b) => b.alias === col.name);
1621
- if (branch) {
1622
- lines.push(generateAddColumn(seed, branch));
1623
- additiveCount++;
1624
- }
1625
- }
1626
- if (idxMissing.length) {
1627
- for (const stmt of generateIndexes2(seed)) {
1628
- lines.push(stmt);
1629
- additiveCount++;
1630
- }
1631
- }
1632
- lines.push("");
1633
- }
1634
- if (destructive.length) {
1635
- destructiveSlugs.push(diff.slug);
1636
- lines.push(`-- \u26A0 ${diff.slug}: DESTRUCTIVE drift NOT auto-migrated \u2014 review manually:`);
1637
- for (const col of destructive) {
1638
- lines.push(`-- ${col.status}: ${col.name}` + (col.actualType ? ` (db: ${col.actualType})` : ""));
1639
- }
1640
- lines.push("");
1641
- }
1095
+ } else {
1096
+ process.stdout.write(code);
1642
1097
  }
1643
- return { sql: lines.join("\n").trimEnd() + "\n", additiveCount, destructiveSlugs };
1644
- }
1645
- function writeMigrationFile(migrationsDir, index, name, sql) {
1646
- if (!existsSync5(migrationsDir)) mkdirSync2(migrationsDir, { recursive: true });
1647
- const safe = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "schema_sync";
1648
- const file = join2(migrationsDir, `${index}_${safe}.sql`);
1649
- writeFileSync5(file, sql, "utf-8");
1650
- return file;
1651
1098
  }
1652
1099
 
1653
1100
  // src/commands/schema-diff.ts
1654
- function resolveMigrationsDir(override) {
1655
- if (override) return resolve6(process.cwd(), override);
1656
- return resolve6(process.cwd(), "apps", "api", "migrations");
1657
- }
1658
- async function schemaDiff(args) {
1659
- const registry = args.registry ?? SEED_REGISTRY3;
1660
- if (Object.keys(registry).length === 0) {
1661
- console.log(pc13.yellow("\n \u2717 No seeds found \u2014 nothing to diff.\n"));
1662
- return;
1663
- }
1664
- const configPath = findWranglerConfig();
1665
- const options = { db: args.db ?? resolveDbName(configPath), local: args.local, configPath };
1666
- const seeds = sortSeedsByDependencies2(Object.values(registry));
1667
- console.log(pc13.cyan(`
1668
- Diffing schema vs ${args.local ? "local" : "remote"} D1 (${options.db})\u2026
1669
- `));
1670
- const diffs = [];
1671
- let clean = true;
1672
- for (const seed of seeds) {
1673
- const d = await diffSeed(seed, options);
1674
- diffs.push(d);
1675
- renderSeedDiff(d);
1676
- if (!isSeedClean(d)) clean = false;
1677
- }
1678
- if (clean) {
1679
- console.log(pc13.green("\n Schema matches seeds. No migration needed.\n"));
1680
- return;
1681
- }
1682
- const plan = buildMigrationSql(diffs, registry);
1683
- if (!args.write) {
1684
- console.log(pc13.dim("\n -- proposed additive migration (preview):\n"));
1685
- console.log(plan.sql);
1686
- if (plan.destructiveSlugs.length) {
1687
- console.log(pc13.yellow(`
1688
- \u26A0 Destructive drift in: ${plan.destructiveSlugs.join(", ")} \u2014 not auto-migrated.`));
1689
- }
1690
- console.log(pc13.cyan("\n \u2192 Re-run with --write to save the migration file.\n"));
1691
- return;
1692
- }
1693
- if (plan.additiveCount === 0) {
1694
- console.log(pc13.yellow("\n \u26A0 Only destructive drift detected \u2014 no additive migration written."));
1695
- console.log(pc13.dim(" Author a reviewed migration by hand for renames/drops/type changes.\n"));
1696
- return;
1697
- }
1698
- const dir = resolveMigrationsDir(args.migrationsDir);
1699
- const index = nextMigrationIndex(dir);
1700
- const file = writeMigrationFile(dir, index, args.name ?? "schema_sync", plan.sql);
1701
- console.log(pc13.green(`
1702
- \u2713 Wrote ${file} (${plan.additiveCount} statement(s)).`));
1703
- console.log(pc13.dim(" Review, commit, then `wrangler d1 migrations apply --remote` in CI.\n"));
1704
- if (plan.destructiveSlugs.length) {
1705
- console.log(pc13.yellow(` \u26A0 Destructive drift in ${plan.destructiveSlugs.join(", ")} was NOT included.
1706
- `));
1707
- }
1101
+ import pc12 from "picocolors";
1102
+ async function schemaDiff(_args = {}) {
1103
+ console.log(pc12.yellow('\n \u26A0 "beech schema:diff" is deprecated'));
1104
+ console.log(pc12.dim(" Cloudflare D1 is the canonical authority for schema definitions."));
1105
+ console.log(pc12.dim(" Runtime schema mutations are handled automatically by the Botanical Engine."));
1106
+ console.log(pc12.cyan("\n \u2192 Schema diffing from static files is no longer supported.\n"));
1708
1107
  }
1709
1108
 
1710
1109
  // src/commands/db-migrate.ts
1711
- import pc14 from "picocolors";
1110
+ import pc13 from "picocolors";
1712
1111
  import { spawnSync as spawnSync7 } from "node:child_process";
1713
- import { existsSync as existsSync6 } from "node:fs";
1714
- import { resolve as resolve7 } from "node:path";
1112
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
1113
+ import { resolve as resolve5 } from "node:path";
1715
1114
  async function dbMigrate(_args) {
1716
- console.log(pc14.cyan("\n beech db:migrate \u2014 apply migrations\n"));
1115
+ console.log(pc13.cyan("\n beech db:migrate \u2014 apply migrations\n"));
1717
1116
  const cwd = process.cwd();
1718
- const apiDir = resolve7(cwd, "apps", "api");
1719
- if (existsSync6(resolve7(apiDir, "package.json"))) {
1117
+ const apiDir = resolve5(cwd, "apps", "api");
1118
+ if (existsSync4(resolve5(apiDir, "package.json"))) {
1720
1119
  const result = spawnSync7("npm", ["run", "db:migrate:local"], {
1721
1120
  stdio: "inherit",
1722
1121
  cwd: apiDir,
1723
1122
  shell: true
1724
1123
  });
1725
1124
  if (result.status !== 0) {
1726
- console.log(pc14.red("\n \u2717 Failed to apply migrations."));
1125
+ console.log(pc13.red("\n \u2717 Failed to apply migrations."));
1727
1126
  process.exit(1);
1728
1127
  return;
1729
1128
  }
1730
- } else if (existsSync6(resolve7(cwd, "scripts", "bootstrap-d1.mjs"))) {
1129
+ console.log(pc13.green("\n \u2713 Migrations applied successfully."));
1130
+ return;
1131
+ }
1132
+ if (existsSync4(resolve5(cwd, "scripts", "bootstrap-d1.mjs"))) {
1731
1133
  const result = spawnSync7("node", ["scripts/bootstrap-d1.mjs"], {
1732
1134
  stdio: "inherit",
1733
1135
  cwd,
1734
1136
  shell: true
1735
1137
  });
1736
1138
  if (result.status !== 0) {
1737
- console.log(pc14.red("\n \u2717 Failed to apply migrations."));
1139
+ console.log(pc13.red("\n \u2717 Failed to apply migrations."));
1738
1140
  process.exit(1);
1739
1141
  return;
1740
1142
  }
1741
- } else {
1742
- console.log(pc14.yellow(" \u26A0 Could not find database migration script."));
1743
- process.exit(1);
1143
+ console.log(pc13.green("\n \u2713 Migrations applied successfully."));
1744
1144
  return;
1745
1145
  }
1746
- console.log(pc14.green("\n \u2713 Migrations applied successfully."));
1146
+ const rootPkgPath = resolve5(cwd, "package.json");
1147
+ if (existsSync4(rootPkgPath)) {
1148
+ try {
1149
+ const pkg = JSON.parse(readFileSync5(rootPkgPath, "utf-8"));
1150
+ if (pkg.scripts?.["db:migrate:local"]) {
1151
+ const result = spawnSync7("npm", ["run", "db:migrate:local"], {
1152
+ stdio: "inherit",
1153
+ cwd,
1154
+ shell: true
1155
+ });
1156
+ if (result.status !== 0) {
1157
+ console.log(pc13.red("\n \u2717 Failed to apply migrations."));
1158
+ process.exit(1);
1159
+ return;
1160
+ }
1161
+ console.log(pc13.green("\n \u2713 Migrations applied successfully."));
1162
+ return;
1163
+ }
1164
+ } catch {
1165
+ }
1166
+ }
1167
+ console.log(pc13.dim(" No migration script found \u2014 initialising database via beech init --db...\n"));
1168
+ const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1169
+ try {
1170
+ await init2({ initDb: true, local: true, nonInteractive: true });
1171
+ } catch {
1172
+ console.log(pc13.red("\n \u2717 Database initialisation failed."));
1173
+ console.log(pc13.dim(" Run: npm run db:migrate:local or npx beech init --db\n"));
1174
+ process.exit(1);
1175
+ }
1747
1176
  }
1748
1177
 
1749
1178
  // src/commands/dev.ts
1750
- import pc15 from "picocolors";
1179
+ import pc14 from "picocolors";
1751
1180
  import { spawnSync as spawnSync8 } from "node:child_process";
1752
- import { existsSync as existsSync7 } from "node:fs";
1753
- import { resolve as resolve8 } from "node:path";
1181
+ import { existsSync as existsSync5 } from "node:fs";
1182
+ import { resolve as resolve6 } from "node:path";
1754
1183
  async function dev(args) {
1755
- console.log(pc15.cyan("\n beech dev \u2014 start development environment\n"));
1184
+ console.log(pc14.cyan("\n beech dev \u2014 start development environment\n"));
1756
1185
  const cwd = process.cwd();
1757
- const devScript = resolve8(cwd, "scripts", "dev.mjs");
1758
- if (!existsSync7(devScript)) {
1759
- console.log(pc15.red(" \u2717 Could not find development script (scripts/dev.mjs)."));
1186
+ const devScript = resolve6(cwd, "scripts", "dev.mjs");
1187
+ if (!existsSync5(devScript)) {
1188
+ console.log(pc14.red(" \u2717 Could not find development script (scripts/dev.mjs)."));
1760
1189
  process.exit(1);
1761
1190
  return;
1762
1191
  }
@@ -1777,7 +1206,7 @@ async function dev(args) {
1777
1206
  }
1778
1207
 
1779
1208
  // src/commands/dev-stop.ts
1780
- import pc16 from "picocolors";
1209
+ import pc15 from "picocolors";
1781
1210
  import { spawnSync as spawnSync9 } from "node:child_process";
1782
1211
  function isDockerInstalled2() {
1783
1212
  try {
@@ -1796,44 +1225,44 @@ function isDockerRunning2() {
1796
1225
  }
1797
1226
  }
1798
1227
  async function devStop() {
1799
- console.log(pc16.cyan("\n beech dev:stop \u2014 stop Docker environment\n"));
1228
+ console.log(pc15.cyan("\n beech dev:stop \u2014 stop Docker environment\n"));
1800
1229
  if (!isDockerInstalled2()) {
1801
- console.log(pc16.red(" \u2717 Docker is not installed or not found in your PATH."));
1230
+ console.log(pc15.red(" \u2717 Docker is not installed or not found in your PATH."));
1802
1231
  process.exit(1);
1803
1232
  return;
1804
1233
  }
1805
1234
  if (!isDockerRunning2()) {
1806
- console.log(pc16.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
1235
+ console.log(pc15.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
1807
1236
  process.exit(1);
1808
1237
  return;
1809
1238
  }
1810
- console.log(pc16.dim(" Stopping Docker containers\u2026\n"));
1239
+ console.log(pc15.dim(" Stopping Docker containers\u2026\n"));
1811
1240
  const result = spawnSync9("docker", ["compose", "-f", "docker/docker-compose.yml", "stop"], {
1812
1241
  stdio: "inherit",
1813
1242
  cwd: process.cwd(),
1814
1243
  shell: true
1815
1244
  });
1816
1245
  if (result.status === 0) {
1817
- console.log(pc16.green("\n \u2713 Docker containers stopped."));
1246
+ console.log(pc15.green("\n \u2713 Docker containers stopped."));
1818
1247
  } else {
1819
- console.log(pc16.red("\n \u2717 Failed to stop Docker containers."));
1248
+ console.log(pc15.red("\n \u2717 Failed to stop Docker containers."));
1820
1249
  process.exit(1);
1821
1250
  return;
1822
1251
  }
1823
1252
  }
1824
1253
 
1825
1254
  // src/commands/dev-tunnel.ts
1826
- import pc17 from "picocolors";
1255
+ import pc16 from "picocolors";
1827
1256
  import { spawnSync as spawnSync10 } from "node:child_process";
1828
1257
  async function devTunnel() {
1829
- console.log(pc17.cyan("\n beech dev:tunnel \u2014 get Cloudflare Tunnel URL\n"));
1258
+ console.log(pc16.cyan("\n beech dev:tunnel \u2014 get Cloudflare Tunnel URL\n"));
1830
1259
  const result = spawnSync10("docker", ["compose", "-f", "docker/docker-compose.yml", "logs", "tunnel"], {
1831
1260
  encoding: "utf-8",
1832
1261
  cwd: process.cwd(),
1833
1262
  shell: true
1834
1263
  });
1835
1264
  if (result.status !== 0) {
1836
- console.log(pc17.red(" \u2717 Failed to retrieve tunnel logs."));
1265
+ console.log(pc16.red(" \u2717 Failed to retrieve tunnel logs."));
1837
1266
  process.exit(1);
1838
1267
  return;
1839
1268
  }
@@ -1841,38 +1270,38 @@ async function devTunnel() {
1841
1270
  const match = logs2.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/g);
1842
1271
  if (match && match.length > 0) {
1843
1272
  const url = match[match.length - 1];
1844
- console.log(pc17.green(` \u2713 Active Cloudflare Tunnel URL: ${pc17.bold(url)}`));
1273
+ console.log(pc16.green(` \u2713 Active Cloudflare Tunnel URL: ${pc16.bold(url)}`));
1845
1274
  } else {
1846
- console.log(pc17.yellow(" \u26A0 No active Cloudflare Tunnel URL found in logs."));
1847
- console.log(pc17.dim(" Make sure the dev server is running with Docker (`pnpm beech dev`)."));
1275
+ console.log(pc16.yellow(" \u26A0 No active Cloudflare Tunnel URL found in logs."));
1276
+ console.log(pc16.dim(" Make sure the dev server is running with Docker (`pnpm beech dev`)."));
1848
1277
  }
1849
1278
  }
1850
1279
 
1851
1280
  // src/commands/mailpit-clear.ts
1852
- import pc18 from "picocolors";
1281
+ import pc17 from "picocolors";
1853
1282
  async function mailpitClear() {
1854
- console.log(pc18.cyan("\n beech mailpit:clear \u2014 clear test emails\n"));
1283
+ console.log(pc17.cyan("\n beech mailpit:clear \u2014 clear test emails\n"));
1855
1284
  try {
1856
1285
  const res = await fetch("http://localhost:8025/api/v1/messages", {
1857
1286
  method: "DELETE"
1858
1287
  });
1859
1288
  if (res.ok) {
1860
- console.log(pc18.green(" \u2713 Mailpit inbox cleared successfully."));
1289
+ console.log(pc17.green(" \u2713 Mailpit inbox cleared successfully."));
1861
1290
  } else {
1862
- console.log(pc18.red(` \u2717 Failed to clear Mailpit inbox: ${res.statusText}`));
1291
+ console.log(pc17.red(` \u2717 Failed to clear Mailpit inbox: ${res.statusText}`));
1863
1292
  process.exit(1);
1864
1293
  return;
1865
1294
  }
1866
1295
  } catch (err) {
1867
- console.log(pc18.red(` \u2717 Error connecting to Mailpit: ${err.message}`));
1868
- console.log(pc18.dim(" Make sure Mailpit is running (default port 8025)."));
1296
+ console.log(pc17.red(` \u2717 Error connecting to Mailpit: ${err.message}`));
1297
+ console.log(pc17.dim(" Make sure Mailpit is running (default port 8025)."));
1869
1298
  process.exit(1);
1870
1299
  return;
1871
1300
  }
1872
1301
  }
1873
1302
 
1874
1303
  // src/commands/logs.ts
1875
- import pc19 from "picocolors";
1304
+ import pc18 from "picocolors";
1876
1305
  import { spawnSync as spawnSync11 } from "node:child_process";
1877
1306
  var SERVICE_MAP = {
1878
1307
  mailpit: "mailpit",
@@ -1885,18 +1314,18 @@ var SERVICE_MAP = {
1885
1314
  async function logs(args) {
1886
1315
  const inputService = args.service?.toLowerCase();
1887
1316
  if (!inputService || !SERVICE_MAP[inputService]) {
1888
- console.log(pc19.red("\n \u2717 Error: Please specify a valid service name."));
1889
- console.log(pc19.dim("\n Accepted services:"));
1890
- console.log(` - ${pc19.cyan("mailpit")}`);
1891
- console.log(` - ${pc19.cyan("db")} / ${pc19.cyan("sqlite")}`);
1892
- console.log(` - ${pc19.cyan("tunnel")}`);
1893
- console.log(` - ${pc19.cyan("storage")} / ${pc19.cyan("minio")}
1317
+ console.log(pc18.red("\n \u2717 Error: Please specify a valid service name."));
1318
+ console.log(pc18.dim("\n Accepted services:"));
1319
+ console.log(` - ${pc18.cyan("mailpit")}`);
1320
+ console.log(` - ${pc18.cyan("db")} / ${pc18.cyan("sqlite")}`);
1321
+ console.log(` - ${pc18.cyan("tunnel")}`);
1322
+ console.log(` - ${pc18.cyan("storage")} / ${pc18.cyan("minio")}
1894
1323
  `);
1895
1324
  process.exit(1);
1896
1325
  return;
1897
1326
  }
1898
1327
  const service = SERVICE_MAP[inputService];
1899
- console.log(pc19.cyan(`
1328
+ console.log(pc18.cyan(`
1900
1329
  beech logs ${inputService} \u2014 streaming logs for ${service}\u2026
1901
1330
  `));
1902
1331
  const result = spawnSync11("docker", ["compose", "-f", "docker/docker-compose.yml", "logs", "-f", service], {
@@ -1911,22 +1340,22 @@ async function logs(args) {
1911
1340
  }
1912
1341
 
1913
1342
  // src/commands/test.ts
1914
- import pc20 from "picocolors";
1343
+ import pc19 from "picocolors";
1915
1344
  import { spawnSync as spawnSync12 } from "node:child_process";
1916
- import { existsSync as existsSync8 } from "node:fs";
1917
- import { resolve as resolve9 } from "node:path";
1345
+ import { existsSync as existsSync6 } from "node:fs";
1346
+ import { resolve as resolve7 } from "node:path";
1918
1347
  async function test(args) {
1919
- console.log(pc20.cyan("\n beech test \u2014 run test suite\n"));
1348
+ console.log(pc19.cyan("\n beech test \u2014 run test suite\n"));
1920
1349
  const cwd = process.cwd();
1921
1350
  let command = "turbo";
1922
1351
  let commandArgs = ["run", "test"];
1923
1352
  if (args.diff) {
1924
- const diffScript = resolve9(cwd, "scripts", "test-coverage-diff.mjs");
1925
- if (existsSync8(diffScript)) {
1353
+ const diffScript = resolve7(cwd, "scripts", "test-coverage-diff.mjs");
1354
+ if (existsSync6(diffScript)) {
1926
1355
  command = "node";
1927
1356
  commandArgs = ["scripts/test-coverage-diff.mjs"];
1928
1357
  } else {
1929
- console.log(pc20.red(" \u2717 Coverage diff script not found (scripts/test-coverage-diff.mjs)."));
1358
+ console.log(pc19.red(" \u2717 Coverage diff script not found (scripts/test-coverage-diff.mjs)."));
1930
1359
  process.exit(1);
1931
1360
  return;
1932
1361
  }
@@ -1945,10 +1374,10 @@ async function test(args) {
1945
1374
  }
1946
1375
 
1947
1376
  // src/commands/lint.ts
1948
- import pc21 from "picocolors";
1377
+ import pc20 from "picocolors";
1949
1378
  import { spawnSync as spawnSync13 } from "node:child_process";
1950
1379
  async function lint() {
1951
- console.log(pc21.cyan("\n beech lint \u2014 check code style\n"));
1380
+ console.log(pc20.cyan("\n beech lint \u2014 check code style\n"));
1952
1381
  const result = spawnSync13("turbo", ["run", "lint"], {
1953
1382
  stdio: "inherit",
1954
1383
  cwd: process.cwd(),
@@ -1960,11 +1389,13 @@ async function lint() {
1960
1389
  }
1961
1390
 
1962
1391
  // src/commands/doctor.ts
1963
- import pc22 from "picocolors";
1392
+ import pc21 from "picocolors";
1964
1393
  import { spawnSync as spawnSync14 } from "node:child_process";
1965
1394
  async function doctor() {
1966
- console.log(pc22.cyan("\n beech doctor \u2014 React diagnostics\n"));
1967
- const result = spawnSync14("pnpm", ["dlx", "react-doctor@latest"], {
1395
+ console.log(pc21.cyan("\n beech doctor \u2014 React diagnostics\n"));
1396
+ const cmd = process.env.npm_config_user_agent?.includes("pnpm") ? "pnpm" : "npx";
1397
+ const args = cmd === "pnpm" ? ["dlx", "react-doctor@latest"] : ["--yes", "react-doctor@latest"];
1398
+ const result = spawnSync14(cmd, args, {
1968
1399
  stdio: "inherit",
1969
1400
  cwd: process.cwd(),
1970
1401
  shell: true
@@ -1973,6 +1404,917 @@ async function doctor() {
1973
1404
  process.exit(result.status ?? 1);
1974
1405
  }
1975
1406
  }
1407
+
1408
+ // src/commands/forms.ts
1409
+ import * as p from "@clack/prompts";
1410
+ import pc22 from "picocolors";
1411
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "node:fs";
1412
+ import { dirname as dirname2, resolve as resolve8 } from "node:path";
1413
+ function getReactTemplate(seedSlug, mode) {
1414
+ if (mode === "headless") {
1415
+ return `import React, { useState, useEffect } from 'react'
1416
+
1417
+ export interface BeechFormProps {
1418
+ baseUrl?: string
1419
+ apiKey?: string
1420
+ seed?: string
1421
+ onSuccess?: (res: { id?: string; data: Record<string, unknown> }) => void
1422
+ onError?: (err: { status: number; message: string }) => void
1423
+ }
1424
+
1425
+ export function BeechForm({
1426
+ baseUrl = process.env.NEXT_PUBLIC_BEECH_API_URL || 'https://api.yourdomain.com',
1427
+ apiKey = process.env.NEXT_PUBLIC_BEECH_WRITE_KEY || '',
1428
+ seed = '${seedSlug}',
1429
+ onSuccess,
1430
+ onError,
1431
+ }: BeechFormProps) {
1432
+ const [values, setValues] = useState<Record<string, string>>({})
1433
+ const [honeypotValue, setHoneypotValue] = useState('')
1434
+ const [timeTrapToken, setTimeTrapToken] = useState<string | null>(null)
1435
+ const [isSubmitting, setIsSubmitting] = useState(false)
1436
+ const [isSuccess, setIsSuccess] = useState(false)
1437
+ const [errorMessage, setErrorMessage] = useState<string | null>(null)
1438
+
1439
+ useEffect(() => {
1440
+ fetch(\`\${baseUrl.replace(/\\/+$/, '')}/api/v1/public/timetrap/token\`)
1441
+ .then((res) => res.json())
1442
+ .then((data) => {
1443
+ if (data?.token) setTimeTrapToken(data.token)
1444
+ })
1445
+ .catch(() => {})
1446
+ }, [baseUrl])
1447
+
1448
+ const handleSubmit = async (e: React.FormEvent) => {
1449
+ e.preventDefault()
1450
+ setErrorMessage(null)
1451
+
1452
+ // Honeypot bot protection check
1453
+ if (honeypotValue.trim() !== '') {
1454
+ setErrorMessage('Submission rejected.')
1455
+ return
1456
+ }
1457
+
1458
+ setIsSubmitting(true)
1459
+ try {
1460
+ const endpoint = \`\${baseUrl.replace(/\\/+$/, '')}/api/v1/public/\${encodeURIComponent(seed)}/add\`
1461
+ const res = await fetch(endpoint, {
1462
+ method: 'POST',
1463
+ headers: {
1464
+ 'Content-Type': 'application/json',
1465
+ ...(apiKey ? { 'X-API-Key': apiKey } : {}),
1466
+ ...(timeTrapToken ? { 'x-time-trap': timeTrapToken } : {}),
1467
+ },
1468
+ body: JSON.stringify({
1469
+ data: values,
1470
+ ...(timeTrapToken ? { _timeTrapToken: timeTrapToken } : {}),
1471
+ }),
1472
+ })
1473
+
1474
+ const json = await res.json()
1475
+ if (!res.ok) {
1476
+ throw new Error(json.detail || json.title || 'Form submission failed')
1477
+ }
1478
+
1479
+ setIsSuccess(true)
1480
+ onSuccess?.({ id: json?.data?.id, data: values })
1481
+ } catch (err) {
1482
+ const msg = err instanceof Error ? err.message : 'Error submitting form'
1483
+ setErrorMessage(msg)
1484
+ onError?.({ status: 500, message: msg })
1485
+ } finally {
1486
+ setIsSubmitting(false)
1487
+ }
1488
+ }
1489
+
1490
+ if (isSuccess) {
1491
+ return <div>Thank you! Your message has been sent.</div>
1492
+ }
1493
+
1494
+ return (
1495
+ <form onSubmit={handleSubmit}>
1496
+ {/* \u{1F6E1}\uFE0F Invisible Honeypot Anti-Bot Decoy */}
1497
+ <div style={{ position: 'absolute', left: '-9999px', width: '1px', height: '1px', opacity: 0 }} aria-hidden="true">
1498
+ <input
1499
+ name="fax_number"
1500
+ type="text"
1501
+ value={honeypotValue}
1502
+ onChange={(e) => setHoneypotValue(e.target.value)}
1503
+ tabIndex={-1}
1504
+ autoComplete="off"
1505
+ />
1506
+ </div>
1507
+
1508
+ <div>
1509
+ <label>Name</label>
1510
+ <input
1511
+ name="name"
1512
+ type="text"
1513
+ value={values.name || ''}
1514
+ onChange={(e) => setValues({ ...values, name: e.target.value })}
1515
+ required
1516
+ />
1517
+ </div>
1518
+
1519
+ <div>
1520
+ <label>Email</label>
1521
+ <input
1522
+ name="email"
1523
+ type="email"
1524
+ value={values.email || ''}
1525
+ onChange={(e) => setValues({ ...values, email: e.target.value })}
1526
+ required
1527
+ />
1528
+ </div>
1529
+
1530
+ <div>
1531
+ <label>Message</label>
1532
+ <textarea
1533
+ name="message"
1534
+ value={values.message || ''}
1535
+ onChange={(e) => setValues({ ...values, message: e.target.value })}
1536
+ rows={4}
1537
+ />
1538
+ </div>
1539
+
1540
+ {errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
1541
+
1542
+ <button type="submit" disabled={isSubmitting}>
1543
+ {isSubmitting ? 'Sending...' : 'Send'}
1544
+ </button>
1545
+ </form>
1546
+ )
1547
+ }
1548
+ `;
1549
+ }
1550
+ return `import React, { useState, useEffect } from 'react'
1551
+
1552
+ export interface BeechFormProps {
1553
+ baseUrl?: string
1554
+ apiKey?: string
1555
+ seed?: string
1556
+ onSuccess?: (res: { id?: string; data: Record<string, unknown> }) => void
1557
+ onError?: (err: { status: number; message: string }) => void
1558
+ className?: string
1559
+ }
1560
+
1561
+ export function BeechForm({
1562
+ baseUrl = process.env.NEXT_PUBLIC_BEECH_API_URL || 'https://api.yourdomain.com',
1563
+ apiKey = process.env.NEXT_PUBLIC_BEECH_WRITE_KEY || '',
1564
+ seed = '${seedSlug}',
1565
+ onSuccess,
1566
+ onError,
1567
+ className = '',
1568
+ }: BeechFormProps) {
1569
+ const [values, setValues] = useState<Record<string, string>>({})
1570
+ const [honeypotValue, setHoneypotValue] = useState('')
1571
+ const [timeTrapToken, setTimeTrapToken] = useState<string | null>(null)
1572
+ const [isSubmitting, setIsSubmitting] = useState(false)
1573
+ const [isSuccess, setIsSuccess] = useState(false)
1574
+ const [errorMessage, setErrorMessage] = useState<string | null>(null)
1575
+
1576
+ useEffect(() => {
1577
+ fetch(\`\${baseUrl.replace(/\\/+$/, '')}/api/v1/public/timetrap/token\`)
1578
+ .then((res) => res.json())
1579
+ .then((data) => {
1580
+ if (data?.token) setTimeTrapToken(data.token)
1581
+ })
1582
+ .catch(() => {})
1583
+ }, [baseUrl])
1584
+
1585
+ const handleSubmit = async (e: React.FormEvent) => {
1586
+ e.preventDefault()
1587
+ setErrorMessage(null)
1588
+
1589
+ if (honeypotValue.trim() !== '') {
1590
+ setErrorMessage('Submission rejected.')
1591
+ return
1592
+ }
1593
+
1594
+ setIsSubmitting(true)
1595
+ try {
1596
+ const endpoint = \`\${baseUrl.replace(/\\/+$/, '')}/api/v1/public/\${encodeURIComponent(seed)}/add\`
1597
+ const res = await fetch(endpoint, {
1598
+ method: 'POST',
1599
+ headers: {
1600
+ 'Content-Type': 'application/json',
1601
+ ...(apiKey ? { 'X-API-Key': apiKey } : {}),
1602
+ ...(timeTrapToken ? { 'x-time-trap': timeTrapToken } : {}),
1603
+ },
1604
+ body: JSON.stringify({
1605
+ data: values,
1606
+ ...(timeTrapToken ? { _timeTrapToken: timeTrapToken } : {}),
1607
+ }),
1608
+ })
1609
+
1610
+ const json = await res.json()
1611
+ if (!res.ok) {
1612
+ throw new Error(json.detail || json.title || 'Form submission failed')
1613
+ }
1614
+
1615
+ setIsSuccess(true)
1616
+ onSuccess?.({ id: json?.data?.id, data: values })
1617
+ } catch (err) {
1618
+ const msg = err instanceof Error ? err.message : 'Error submitting form'
1619
+ setErrorMessage(msg)
1620
+ onError?.({ status: 500, message: msg })
1621
+ } finally {
1622
+ setIsSubmitting(false)
1623
+ }
1624
+ }
1625
+
1626
+ if (isSuccess) {
1627
+ return (
1628
+ <div className="rounded-lg border border-emerald-200 bg-emerald-50 p-4 text-emerald-800 text-center font-medium">
1629
+ Thank you! Your message has been sent successfully.
1630
+ </div>
1631
+ )
1632
+ }
1633
+
1634
+ return (
1635
+ <form onSubmit={handleSubmit} className={\`space-y-4 max-w-lg mx-auto \${className}\`} noValidate>
1636
+ {/* \u{1F6E1}\uFE0F Invisible Honeypot Anti-Bot Decoy */}
1637
+ <div style={{ position: 'absolute', left: '-9999px', width: '1px', height: '1px', opacity: 0 }} aria-hidden="true">
1638
+ <input
1639
+ name="fax_number"
1640
+ type="text"
1641
+ value={honeypotValue}
1642
+ onChange={(e) => setHoneypotValue(e.target.value)}
1643
+ tabIndex={-1}
1644
+ autoComplete="off"
1645
+ />
1646
+ </div>
1647
+
1648
+ <div>
1649
+ <label className="block text-sm font-medium text-gray-700">Full Name *</label>
1650
+ <input
1651
+ type="text"
1652
+ value={values.name || ''}
1653
+ onChange={(e) => setValues({ ...values, name: e.target.value })}
1654
+ placeholder="Jane Doe"
1655
+ className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none"
1656
+ required
1657
+ />
1658
+ </div>
1659
+
1660
+ <div>
1661
+ <label className="block text-sm font-medium text-gray-700">Work Email *</label>
1662
+ <input
1663
+ type="email"
1664
+ value={values.email || ''}
1665
+ onChange={(e) => setValues({ ...values, email: e.target.value })}
1666
+ placeholder="jane@company.com"
1667
+ className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none"
1668
+ required
1669
+ />
1670
+ </div>
1671
+
1672
+ <div>
1673
+ <label className="block text-sm font-medium text-gray-700">Message</label>
1674
+ <textarea
1675
+ value={values.message || ''}
1676
+ onChange={(e) => setValues({ ...values, message: e.target.value })}
1677
+ placeholder="How can we help you?"
1678
+ rows={4}
1679
+ className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none"
1680
+ />
1681
+ </div>
1682
+
1683
+ {errorMessage && (
1684
+ <div className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700">
1685
+ {errorMessage}
1686
+ </div>
1687
+ )}
1688
+
1689
+ <button
1690
+ type="submit"
1691
+ disabled={isSubmitting}
1692
+ className="w-full rounded-md bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow hover:bg-blue-700 disabled:opacity-50 transition-colors"
1693
+ >
1694
+ {isSubmitting ? 'Sending...' : 'Send Message'}
1695
+ </button>
1696
+ </form>
1697
+ )
1698
+ }
1699
+ `;
1700
+ }
1701
+ function getVueTemplate(seedSlug, mode) {
1702
+ const isStyled = mode === "styled";
1703
+ return `<script setup lang="ts">
1704
+ import { ref, onMounted } from 'vue'
1705
+
1706
+ const props = withDefaults(
1707
+ defineProps<{
1708
+ baseUrl?: string
1709
+ apiKey?: string
1710
+ seed?: string
1711
+ }>(),
1712
+ {
1713
+ baseUrl: 'https://api.yourdomain.com',
1714
+ apiKey: '',
1715
+ seed: '${seedSlug}',
1716
+ }
1717
+ )
1718
+
1719
+ const emit = defineEmits<{
1720
+ (e: 'success', data: { id?: string; data: Record<string, unknown> }): void
1721
+ (e: 'error', err: { status: number; message: string }): void
1722
+ }>>()
1723
+
1724
+ const name = ref('')
1725
+ const email = ref('')
1726
+ const message = ref('')
1727
+ const honeypot = ref('')
1728
+ const timeTrapToken = ref<string | null>(null)
1729
+ const isSubmitting = ref(false)
1730
+ const isSuccess = ref(false)
1731
+ const errorMessage = ref<string | null>(null)
1732
+
1733
+ onMounted(async () => {
1734
+ try {
1735
+ const res = await fetch(\`\${props.baseUrl.replace(/\\/+$/, '')}/api/v1/public/timetrap/token\`)
1736
+ const data = await res.json()
1737
+ if (data?.token) timeTrapToken.value = data.token
1738
+ } catch {}
1739
+ })
1740
+
1741
+ async function handleSubmit() {
1742
+ errorMessage.value = null
1743
+ if (honeypot.value.trim() !== '') {
1744
+ errorMessage.value = 'Submission rejected.'
1745
+ return
1746
+ }
1747
+
1748
+ isSubmitting.value = true
1749
+ try {
1750
+ const payload = { name: name.value, email: email.value, message: message.value }
1751
+ const endpoint = \`\${props.baseUrl.replace(/\\/+$/, '')}/api/v1/public/\${encodeURIComponent(props.seed)}/add\`
1752
+ const res = await fetch(endpoint, {
1753
+ method: 'POST',
1754
+ headers: {
1755
+ 'Content-Type': 'application/json',
1756
+ ...(props.apiKey ? { 'X-API-Key': props.apiKey } : {}),
1757
+ ...(timeTrapToken.value ? { 'x-time-trap': timeTrapToken.value } : {}),
1758
+ },
1759
+ body: JSON.stringify({
1760
+ data: payload,
1761
+ ...(timeTrapToken.value ? { _timeTrapToken: timeTrapToken.value } : {}),
1762
+ }),
1763
+ })
1764
+
1765
+ const json = await res.json()
1766
+ if (!res.ok) throw new Error(json.detail || json.title || 'Submission failed')
1767
+
1768
+ isSuccess.value = true
1769
+ emit('success', { id: json?.data?.id, data: payload })
1770
+ } catch (err: any) {
1771
+ const msg = err.message || 'Submission error'
1772
+ errorMessage.value = msg
1773
+ emit('error', { status: 500, message: msg })
1774
+ } finally {
1775
+ isSubmitting.value = false
1776
+ }
1777
+ }
1778
+ </script>
1779
+
1780
+ <template>
1781
+ <div v-if="isSuccess" class="${isStyled ? "rounded-lg border border-emerald-200 bg-emerald-50 p-4 text-emerald-800 text-center font-medium" : "success-msg"}">
1782
+ Thank you! Your message has been sent successfully.
1783
+ </div>
1784
+
1785
+ <form v-else @submit.prevent="handleSubmit" class="${isStyled ? "space-y-4 max-w-lg mx-auto" : "beech-form"}">
1786
+ <!-- \u{1F6E1}\uFE0F Invisible Honeypot Decoy -->
1787
+ <div style="position: absolute; left: -9999px; opacity: 0;" aria-hidden="true">
1788
+ <input v-model="honeypot" name="fax_number" type="text" tabindex="-1" autocomplete="off" />
1789
+ </div>
1790
+
1791
+ <div>
1792
+ <label class="${isStyled ? "block text-sm font-medium text-gray-700" : ""}">Full Name *</label>
1793
+ <input v-model="name" type="text" required class="${isStyled ? "mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:outline-none" : ""}" />
1794
+ </div>
1795
+
1796
+ <div>
1797
+ <label class="${isStyled ? "block text-sm font-medium text-gray-700" : ""}">Work Email *</label>
1798
+ <input v-model="email" type="email" required class="${isStyled ? "mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:outline-none" : ""}" />
1799
+ </div>
1800
+
1801
+ <div>
1802
+ <label class="${isStyled ? "block text-sm font-medium text-gray-700" : ""}">Message</label>
1803
+ <textarea v-model="message" rows="4" class="${isStyled ? "mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:outline-none" : ""}"></textarea>
1804
+ </div>
1805
+
1806
+ <div v-if="errorMessage" class="${isStyled ? "rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700" : "error-msg"}">
1807
+ {{ errorMessage }}
1808
+ </div>
1809
+
1810
+ <button type="submit" :disabled="isSubmitting" class="${isStyled ? "w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:opacity-50" : ""}">
1811
+ {{ isSubmitting ? 'Sending...' : 'Send Message' }}
1812
+ </button>
1813
+ </form>
1814
+ </template>
1815
+ `;
1816
+ }
1817
+ function getSvelteTemplate(seedSlug, mode) {
1818
+ const isStyled = mode === "styled";
1819
+ return `<script lang="ts">
1820
+ import { onMount } from 'svelte'
1821
+
1822
+ interface Props {
1823
+ baseUrl?: string
1824
+ apiKey?: string
1825
+ seed?: string
1826
+ onSuccess?: (res: { id?: string; data: Record<string, unknown> }) => void
1827
+ onError?: (err: { status: number; message: string }) => void
1828
+ }
1829
+
1830
+ let {
1831
+ baseUrl = 'https://api.yourdomain.com',
1832
+ apiKey = '',
1833
+ seed = '${seedSlug}',
1834
+ onSuccess,
1835
+ onError,
1836
+ }: Props = $props()
1837
+
1838
+ let name = $state('')
1839
+ let email = $state('')
1840
+ let message = $state('')
1841
+ let honeypot = $state('')
1842
+ let timeTrapToken = $state<string | null>(null)
1843
+ let isSubmitting = $state(false)
1844
+ let isSuccess = $state(false)
1845
+ let errorMessage = $state<string | null>(null)
1846
+
1847
+ onMount(async () => {
1848
+ try {
1849
+ const res = await fetch(\`\${baseUrl.replace(/\\/+$/, '')}/api/v1/public/timetrap/token\`)
1850
+ const data = await res.json()
1851
+ if (data?.token) timeTrapToken = data.token
1852
+ } catch {}
1853
+ })
1854
+
1855
+ async function handleSubmit(e: Event) {
1856
+ e.preventDefault()
1857
+ errorMessage = null
1858
+ if (honeypot.trim() !== '') {
1859
+ errorMessage = 'Submission rejected.'
1860
+ return
1861
+ }
1862
+
1863
+ isSubmitting = true
1864
+ try {
1865
+ const payload = { name, email, message }
1866
+ const endpoint = \`\${baseUrl.replace(/\\/+$/, '')}/api/v1/public/\${encodeURIComponent(seed)}/add\`
1867
+ const res = await fetch(endpoint, {
1868
+ method: 'POST',
1869
+ headers: {
1870
+ 'Content-Type': 'application/json',
1871
+ ...(apiKey ? { 'X-API-Key': apiKey } : {}),
1872
+ ...(timeTrapToken ? { 'x-time-trap': timeTrapToken } : {}),
1873
+ },
1874
+ body: JSON.stringify({
1875
+ data: payload,
1876
+ ...(timeTrapToken ? { _timeTrapToken: timeTrapToken } : {}),
1877
+ }),
1878
+ })
1879
+
1880
+ const json = await res.json()
1881
+ if (!res.ok) throw new Error(json.detail || json.title || 'Submission failed')
1882
+
1883
+ isSuccess = true
1884
+ onSuccess?.({ id: json?.data?.id, data: payload })
1885
+ } catch (err: any) {
1886
+ const msg = err.message || 'Submission error'
1887
+ errorMessage = msg
1888
+ onError?.({ status: 500, message: msg })
1889
+ } finally {
1890
+ isSubmitting = false
1891
+ }
1892
+ }
1893
+ </script>
1894
+
1895
+ {#if isSuccess}
1896
+ <div class="${isStyled ? "rounded-lg border border-emerald-200 bg-emerald-50 p-4 text-emerald-800 text-center font-medium" : "success"}">
1897
+ Thank you! Your message has been sent successfully.
1898
+ </div>
1899
+ {:else}
1900
+ <form onsubmit={handleSubmit} class="${isStyled ? "space-y-4 max-w-lg mx-auto" : "beech-form"}">
1901
+ <!-- \u{1F6E1}\uFE0F Invisible Honeypot Anti-Bot Decoy -->
1902
+ <div style="position: absolute; left: -9999px; opacity: 0;" aria-hidden="true">
1903
+ <input bind:value={honeypot} name="fax_number" type="text" tabindex="-1" autocomplete="off" />
1904
+ </div>
1905
+
1906
+ <div>
1907
+ <label class="${isStyled ? "block text-sm font-medium text-gray-700" : ""}">Full Name *</label>
1908
+ <input bind:value={name} type="text" required class="${isStyled ? "mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:outline-none" : ""}" />
1909
+ </div>
1910
+
1911
+ <div>
1912
+ <label class="${isStyled ? "block text-sm font-medium text-gray-700" : ""}">Work Email *</label>
1913
+ <input bind:value={email} type="email" required class="${isStyled ? "mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:outline-none" : ""}" />
1914
+ </div>
1915
+
1916
+ <div>
1917
+ <label class="${isStyled ? "block text-sm font-medium text-gray-700" : ""}">Message</label>
1918
+ <textarea bind:value={message} rows="4" class="${isStyled ? "mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:outline-none" : ""}"></textarea>
1919
+ </div>
1920
+
1921
+ {#if errorMessage}
1922
+ <div class="${isStyled ? "rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700" : "error"}">
1923
+ {errorMessage}
1924
+ </div>
1925
+ {/if}
1926
+
1927
+ <button type="submit" disabled={isSubmitting} class="${isStyled ? "w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700 disabled:opacity-50" : ""}">
1928
+ {isSubmitting ? 'Sending...' : 'Send Message'}
1929
+ </button>
1930
+ </form>
1931
+ {/if}
1932
+ `;
1933
+ }
1934
+ function getVanillaTemplate(seedSlug) {
1935
+ return `/**
1936
+ * BeechCMS Universal Web Component
1937
+ * Usage in HTML:
1938
+ * <script type="module" src="./src/components/BeechForm.js"></script>
1939
+ * <beech-form seed="${seedSlug}" base-url="https://api.yourdomain.com"></beech-form>
1940
+ */
1941
+
1942
+ class BeechFormElement extends HTMLElement {
1943
+ connectedCallback() {
1944
+ const seed = this.getAttribute('seed') || '${seedSlug}'
1945
+ const baseUrl = (this.getAttribute('base-url') || 'https://api.yourdomain.com').replace(/\\/+$/, '')
1946
+ const apiKey = this.getAttribute('api-key') || ''
1947
+
1948
+ this.innerHTML = \`
1949
+ <form class="beech-form" style="max-width: 480px; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; font-family: system-ui, sans-serif;">
1950
+ <div style="position: absolute; left: -9999px; opacity: 0;" aria-hidden="true">
1951
+ <input name="fax_number" type="text" tabindex="-1" autocomplete="off" />
1952
+ </div>
1953
+ <label style="display: flex; flex-direction: column; font-size: 14px; font-weight: 500;">
1954
+ Full Name *
1955
+ <input name="name" type="text" required style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; margin-top: 4px;" />
1956
+ </label>
1957
+ <label style="display: flex; flex-direction: column; font-size: 14px; font-weight: 500;">
1958
+ Work Email *
1959
+ <input name="email" type="email" required style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; margin-top: 4px;" />
1960
+ </label>
1961
+ <label style="display: flex; flex-direction: column; font-size: 14px; font-weight: 500;">
1962
+ Message
1963
+ <textarea name="message" rows="4" style="padding: 8px 12px; border: 1px solid #ccc; border-radius: 6px; margin-top: 4px;"></textarea>
1964
+ </label>
1965
+ <div class="feedback" style="display: none; padding: 10px; border-radius: 6px; font-size: 14px;"></div>
1966
+ <button type="submit" style="padding: 10px 16px; background: #2563eb; color: #fff; border: none; border-radius: 6px; font-weight: 600; cursor: pointer;">
1967
+ Send Message
1968
+ </button>
1969
+ </form>
1970
+ \`
1971
+
1972
+ const form = this.querySelector('form')
1973
+ const feedback = this.querySelector('.feedback')
1974
+ let timeTrapToken = null
1975
+
1976
+ fetch(\`\${baseUrl}/api/v1/public/timetrap/token\`)
1977
+ .then((r) => r.json())
1978
+ .then((d) => { if (d?.token) timeTrapToken = d.token })
1979
+ .catch(() => {})
1980
+
1981
+ form?.addEventListener('submit', async (e) => {
1982
+ e.preventDefault()
1983
+ const fd = new FormData(form)
1984
+ const data = Object.fromEntries(fd.entries())
1985
+
1986
+ if (data.fax_number) {
1987
+ alert('Bot rejected')
1988
+ return
1989
+ }
1990
+
1991
+ delete data.fax_number
1992
+ const btn = form.querySelector('button')
1993
+ if (btn) btn.disabled = true
1994
+
1995
+ try {
1996
+ const res = await fetch(\`\${baseUrl}/api/v1/public/\${encodeURIComponent(seed)}/add\`, {
1997
+ method: 'POST',
1998
+ headers: {
1999
+ 'Content-Type': 'application/json',
2000
+ ...(apiKey ? { 'X-API-Key': apiKey } : {}),
2001
+ ...(timeTrapToken ? { 'x-time-trap': timeTrapToken } : {}),
2002
+ },
2003
+ body: JSON.stringify({
2004
+ data,
2005
+ ...(timeTrapToken ? { _timeTrapToken: timeTrapToken } : {}),
2006
+ }),
2007
+ })
2008
+
2009
+ const json = await res.json()
2010
+ if (!res.ok) throw new Error(json.detail || json.title || 'Submission failed')
2011
+
2012
+ if (feedback) {
2013
+ feedback.style.display = 'block'
2014
+ feedback.style.background = '#f0fdf4'
2015
+ feedback.style.color = '#166534'
2016
+ feedback.style.border = '1px solid #bbf7d0'
2017
+ feedback.textContent = 'Thank you! Your message has been sent successfully.'
2018
+ }
2019
+ form.reset()
2020
+ } catch (err) {
2021
+ if (feedback) {
2022
+ feedback.style.display = 'block'
2023
+ feedback.style.background = '#fef2f2'
2024
+ feedback.style.color = '#991b1b'
2025
+ feedback.style.border = '1px solid #fecaca'
2026
+ feedback.textContent = err.message || 'Error sending message'
2027
+ }
2028
+ } finally {
2029
+ if (btn) btn.disabled = false
2030
+ }
2031
+ })
2032
+ }
2033
+ }
2034
+
2035
+ if (typeof window !== 'undefined' && !customElements.get('beech-form')) {
2036
+ customElements.define('beech-form', BeechFormElement)
2037
+ }
2038
+
2039
+ export { BeechFormElement }
2040
+ `;
2041
+ }
2042
+ async function forms(options = {}) {
2043
+ let { framework, seed, mode, out, yes, json } = options;
2044
+ if (!yes && !json) {
2045
+ p.intro(pc22.bgCyan(pc22.black(" \u{1F332} BeechCMS Form Generator ")));
2046
+ if (!framework) {
2047
+ const selected = await p.select({
2048
+ message: "Which framework are you using?",
2049
+ options: [
2050
+ { value: "react", label: "React", hint: "Next.js / Vite / Remix (.tsx)" },
2051
+ { value: "vue", label: "Vue 3", hint: "Nuxt / Vite (.vue)" },
2052
+ { value: "svelte", label: "Svelte 5", hint: "SvelteKit (.svelte)" },
2053
+ { value: "vanilla", label: "Vanilla JS / Web Component", hint: "HTML / Astro / Universal (.js)" }
2054
+ ],
2055
+ initialValue: "react"
2056
+ });
2057
+ if (p.isCancel(selected)) {
2058
+ p.cancel("Operation cancelled.");
2059
+ process.exit(0);
2060
+ }
2061
+ framework = selected;
2062
+ }
2063
+ if (!seed) {
2064
+ const seedInput = await p.text({
2065
+ message: "Which Seed do you want to bind this form to?",
2066
+ placeholder: "clienti",
2067
+ defaultValue: "clienti",
2068
+ validate: (value) => {
2069
+ if (!value.trim()) return "Seed slug cannot be empty";
2070
+ }
2071
+ });
2072
+ if (p.isCancel(seedInput)) {
2073
+ p.cancel("Operation cancelled.");
2074
+ process.exit(0);
2075
+ }
2076
+ seed = seedInput.trim();
2077
+ }
2078
+ if (!mode && framework !== "vanilla") {
2079
+ const modeSelected = await p.select({
2080
+ message: "Choose styling preset:",
2081
+ options: [
2082
+ { value: "styled", label: "Tailwind CSS", hint: "Full responsive ready-to-use component" },
2083
+ { value: "headless", label: "Headless / Unstyled", hint: "Minimal markup with full anti-bot protection" }
2084
+ ],
2085
+ initialValue: "styled"
2086
+ });
2087
+ if (p.isCancel(modeSelected)) {
2088
+ p.cancel("Operation cancelled.");
2089
+ process.exit(0);
2090
+ }
2091
+ mode = modeSelected;
2092
+ }
2093
+ }
2094
+ framework = framework || "react";
2095
+ seed = seed || "clienti";
2096
+ mode = mode || "styled";
2097
+ const ext = framework === "react" ? "tsx" : framework === "vue" ? "vue" : framework === "svelte" ? "svelte" : "js";
2098
+ const filename = `BeechForm.${ext}`;
2099
+ const targetPath = out ? resolve8(process.cwd(), out) : resolve8(process.cwd(), "src", "components", filename);
2100
+ let content = "";
2101
+ if (framework === "react") {
2102
+ content = getReactTemplate(seed, mode);
2103
+ } else if (framework === "vue") {
2104
+ content = getVueTemplate(seed, mode);
2105
+ } else if (framework === "svelte") {
2106
+ content = getSvelteTemplate(seed, mode);
2107
+ } else {
2108
+ content = getVanillaTemplate(seed);
2109
+ }
2110
+ mkdirSync2(dirname2(targetPath), { recursive: true });
2111
+ writeFileSync4(targetPath, content, "utf-8");
2112
+ if (json) {
2113
+ console.log(
2114
+ JSON.stringify(
2115
+ {
2116
+ success: true,
2117
+ file: filename,
2118
+ path: targetPath,
2119
+ framework,
2120
+ seed,
2121
+ mode
2122
+ },
2123
+ null,
2124
+ 2
2125
+ )
2126
+ );
2127
+ return;
2128
+ }
2129
+ if (!yes) {
2130
+ p.outro(pc22.green(`\u2714 Form component created at ${out ? out : `src/components/${filename}`}`));
2131
+ console.log(`
2132
+ ${pc22.bold("Next steps:")}`);
2133
+ if (framework === "react") {
2134
+ console.log(pc22.cyan(` import { BeechForm } from './components/BeechForm'`));
2135
+ console.log(pc22.cyan(` <BeechForm seed="${seed}" />
2136
+ `));
2137
+ } else if (framework === "vue") {
2138
+ console.log(pc22.cyan(` import BeechForm from './components/BeechForm.vue'`));
2139
+ console.log(pc22.cyan(` <BeechForm seed="${seed}" />
2140
+ `));
2141
+ } else if (framework === "svelte") {
2142
+ console.log(pc22.cyan(` import BeechForm from './components/BeechForm.svelte'`));
2143
+ console.log(pc22.cyan(` <BeechForm seed="${seed}" />
2144
+ `));
2145
+ } else {
2146
+ console.log(pc22.cyan(` <script type="module" src="./src/components/BeechForm.js"></script>`));
2147
+ console.log(pc22.cyan(` <beech-form seed="${seed}"></beech-form>
2148
+ `));
2149
+ }
2150
+ } else {
2151
+ console.log(`Created ${out ? out : `src/components/${filename}`}`);
2152
+ }
2153
+ }
2154
+
2155
+ // src/commands/setup-cloudflare.ts
2156
+ init_wrangler();
2157
+ import pc23 from "picocolors";
2158
+ import * as p2 from "@clack/prompts";
2159
+ import { spawnSync as spawnSync15 } from "node:child_process";
2160
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "node:fs";
2161
+ import { resolve as resolve9 } from "node:path";
2162
+ function parseWranglerConfig(configPath) {
2163
+ const raw = readFileSync6(configPath, "utf-8");
2164
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
2165
+ const parsed = JSON.parse(stripped);
2166
+ return { name: parsed.name || "my-beech-project", raw, parsed };
2167
+ }
2168
+ async function setupCloudflare(options = {}) {
2169
+ p2.intro(pc23.cyan("\u26A1 BeechCMS \u2014 Automated Cloudflare Provisioning"));
2170
+ const configPath = findWranglerConfig();
2171
+ let projectName = options.projectName;
2172
+ if (!projectName && configPath) {
2173
+ try {
2174
+ const cfg = parseWranglerConfig(configPath);
2175
+ projectName = cfg.name;
2176
+ } catch {
2177
+ }
2178
+ }
2179
+ if (!projectName) {
2180
+ if (options.nonInteractive) {
2181
+ projectName = "my-beech-project";
2182
+ } else {
2183
+ const input = await p2.text({
2184
+ message: "Project name",
2185
+ placeholder: "my-website",
2186
+ validate: (v) => {
2187
+ if (!v.trim()) return "Required";
2188
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(v.trim())) return "Lowercase letters, numbers and hyphens only";
2189
+ }
2190
+ });
2191
+ if (p2.isCancel(input)) {
2192
+ p2.cancel("Setup cancelled");
2193
+ return;
2194
+ }
2195
+ projectName = input.trim();
2196
+ }
2197
+ }
2198
+ const d1Name = `${projectName}-db`;
2199
+ const r2Bucket = `${projectName}-media`;
2200
+ const s = p2.spinner();
2201
+ s.start(`Creating Cloudflare D1 database: ${pc23.bold(d1Name)}\u2026`);
2202
+ const d1Result = spawnSync15("npx", ["wrangler", "d1", "create", d1Name], {
2203
+ encoding: "utf-8",
2204
+ shell: true,
2205
+ cwd: process.cwd()
2206
+ });
2207
+ let databaseId = null;
2208
+ const d1Output = (d1Result.stdout || "") + (d1Result.stderr || "");
2209
+ const idMatch = d1Output.match(/database_id\s*=\s*["']?([a-f0-9-]{36})["']?/i) || d1Output.match(/"database_id":\s*"([a-f0-9-]{36})"/i) || d1Output.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
2210
+ if (idMatch) {
2211
+ databaseId = idMatch[1];
2212
+ s.stop(pc23.green(`\u2713 D1 database ready: ${pc23.bold(d1Name)} (${databaseId})`));
2213
+ } else if (d1Output.includes("already exists") || d1Result.status === 0) {
2214
+ s.stop(pc23.yellow(`\u2139 D1 database ${pc23.bold(d1Name)} already exists or created.`));
2215
+ } else {
2216
+ s.stop(pc23.yellow(`\u26A0 Note on D1 create: ${d1Output.trim().slice(0, 150)}`));
2217
+ }
2218
+ s.start(`Creating Cloudflare R2 bucket: ${pc23.bold(r2Bucket)}\u2026`);
2219
+ const r2Result = spawnSync15("npx", ["wrangler", "r2", "bucket", "create", r2Bucket], {
2220
+ encoding: "utf-8",
2221
+ shell: true,
2222
+ cwd: process.cwd()
2223
+ });
2224
+ const r2Output = (r2Result.stdout || "") + (r2Result.stderr || "");
2225
+ if (r2Result.status === 0 || r2Output.includes("already exists")) {
2226
+ s.stop(pc23.green(`\u2713 R2 bucket ready: ${pc23.bold(r2Bucket)}`));
2227
+ } else {
2228
+ s.stop(pc23.yellow(`\u26A0 Note on R2 create: ${r2Output.trim().slice(0, 150)}`));
2229
+ }
2230
+ if (configPath && existsSync8(configPath)) {
2231
+ try {
2232
+ let content = readFileSync6(configPath, "utf-8");
2233
+ content = content.replace(/"database_name":\s*"[^"]*"/, `"database_name": "${d1Name}"`);
2234
+ if (databaseId) {
2235
+ content = content.replace(/"database_id":\s*"[^"]*"/, `"database_id": "${databaseId}"`);
2236
+ }
2237
+ content = content.replace(/"bucket_name":\s*"[^"]*"/, `"bucket_name": "${r2Bucket}"`);
2238
+ writeFileSync5(configPath, content, "utf-8");
2239
+ p2.log.success(pc23.green(`Updated ${pc23.bold(configPath)} with database_id and bucket_name.`));
2240
+ } catch (err) {
2241
+ p2.log.warn(`Could not update wrangler config automatically: ${err.message}`);
2242
+ }
2243
+ }
2244
+ if (!options.nonInteractive) {
2245
+ p2.note(
2246
+ [
2247
+ "Direct uploads via Presigned URLs (SigV4) stream directly from browser to R2 with zero Worker CPU/RAM.",
2248
+ "To configure Presigned uploads:",
2249
+ ' 1. Open Cloudflare Dashboard \u2192 R2 \u2192 "Manage R2 API Tokens"',
2250
+ ` 2. Click "Create API Token" \u2192 Object Read & Write \u2192 bucket: ${r2Bucket}`,
2251
+ " Guide: https://developers.cloudflare.com/r2/api/s3/tokens/"
2252
+ ].join("\n"),
2253
+ "R2 S3 Credentials Setup"
2254
+ );
2255
+ const configureSecrets = await p2.confirm({
2256
+ message: "Do you have your R2 API Token ready to configure now?",
2257
+ initialValue: true
2258
+ });
2259
+ if (!p2.isCancel(configureSecrets) && configureSecrets) {
2260
+ const accountId = await p2.text({
2261
+ message: "Cloudflare Account ID (from dash.cloudflare.com)",
2262
+ placeholder: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
2263
+ validate: (v) => {
2264
+ if (!v.trim()) return "Required";
2265
+ }
2266
+ });
2267
+ const accessKeyId = !p2.isCancel(accountId) ? await p2.text({
2268
+ message: "R2 Access Key ID",
2269
+ validate: (v) => {
2270
+ if (!v.trim()) return "Required";
2271
+ }
2272
+ }) : null;
2273
+ const secretAccessKey = !p2.isCancel(accessKeyId) && accessKeyId ? await p2.password({
2274
+ message: "R2 Secret Access Key",
2275
+ validate: (v) => {
2276
+ if (!v.trim()) return "Required";
2277
+ }
2278
+ }) : null;
2279
+ if (!p2.isCancel(accountId) && !p2.isCancel(accessKeyId) && !p2.isCancel(secretAccessKey) && accountId && accessKeyId && secretAccessKey) {
2280
+ const endpoint = `https://${accountId.trim()}.r2.cloudflarestorage.com`;
2281
+ const devVarsPath = resolve9(process.cwd(), ".dev.vars");
2282
+ const devVarsContent = [
2283
+ "# Cloudflare R2 S3 credentials (for direct Presigned URL uploads)",
2284
+ `R2_ACCESS_KEY_ID=${accessKeyId.trim()}`,
2285
+ `R2_SECRET_ACCESS_KEY=${secretAccessKey.trim()}`,
2286
+ `R2_ENDPOINT=${endpoint}`,
2287
+ `R2_BUCKET_NAME=${r2Bucket}`
2288
+ ].join("\n") + "\n";
2289
+ writeFileSync5(devVarsPath, devVarsContent, "utf-8");
2290
+ p2.log.success(pc23.green(`Created ${pc23.bold(".dev.vars")} with local development credentials.`));
2291
+ s.start("Setting production secrets on Cloudflare Worker\u2026");
2292
+ spawnSync15("npx", ["wrangler", "secret", "put", "R2_ACCESS_KEY_ID"], {
2293
+ input: accessKeyId.trim(),
2294
+ encoding: "utf-8",
2295
+ shell: true
2296
+ });
2297
+ spawnSync15("npx", ["wrangler", "secret", "put", "R2_SECRET_ACCESS_KEY"], {
2298
+ input: secretAccessKey.trim(),
2299
+ encoding: "utf-8",
2300
+ shell: true
2301
+ });
2302
+ spawnSync15("npx", ["wrangler", "secret", "put", "R2_ENDPOINT"], {
2303
+ input: endpoint,
2304
+ encoding: "utf-8",
2305
+ shell: true
2306
+ });
2307
+ spawnSync15("npx", ["wrangler", "secret", "put", "R2_BUCKET_NAME"], {
2308
+ input: r2Bucket,
2309
+ encoding: "utf-8",
2310
+ shell: true
2311
+ });
2312
+ s.stop(pc23.green("\u2713 Production secrets configured on Cloudflare Worker."));
2313
+ }
2314
+ }
2315
+ }
2316
+ p2.outro(pc23.green("\u2728 Cloudflare infrastructure setup complete! You are ready to run `npx beech deploy`."));
2317
+ }
1976
2318
  export {
1977
2319
  dbMigrate,
1978
2320
  dbReset,
@@ -1982,6 +2324,7 @@ export {
1982
2324
  devStop,
1983
2325
  devTunnel,
1984
2326
  doctor,
2327
+ forms,
1985
2328
  generateTypes,
1986
2329
  init,
1987
2330
  lint,
@@ -1992,6 +2335,7 @@ export {
1992
2335
  schemaDiff,
1993
2336
  seedCreate,
1994
2337
  seedLoad,
2338
+ setupCloudflare,
1995
2339
  test,
1996
2340
  update,
1997
2341
  validate,