@beechcms/cli 0.6.7 → 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
  }
@@ -527,7 +514,9 @@ var init_init = __esm({
527
514
  "automations",
528
515
  "seeds",
529
516
  "seed_meta",
530
- "site_settings"
517
+ "seed_layouts",
518
+ "site_settings",
519
+ "setup_completed"
531
520
  ];
532
521
  BASE_SCHEMA_SQL = `
533
522
  CREATE TABLE IF NOT EXISTS users (
@@ -616,7 +605,7 @@ CREATE TABLE IF NOT EXISTS notifications (
616
605
  id TEXT NOT NULL PRIMARY KEY,
617
606
  title TEXT NOT NULL,
618
607
  message TEXT NOT NULL,
619
- type TEXT NOT NULL DEFAULT 'info' CHECK (type IN ('info', 'warning', 'error')),
608
+ type TEXT NOT NULL DEFAULT 'info' CHECK (type IN ('info', 'success', 'warning', 'error')),
620
609
  is_read INTEGER NOT NULL DEFAULT 0 CHECK (is_read IN (0, 1)),
621
610
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
622
611
  );
@@ -690,6 +679,18 @@ CREATE TABLE IF NOT EXISTS site_settings (
690
679
  key TEXT NOT NULL PRIMARY KEY,
691
680
  value TEXT NOT NULL
692
681
  );
682
+
683
+ CREATE TABLE IF NOT EXISTS seed_layouts (
684
+ slug TEXT NOT NULL PRIMARY KEY,
685
+ layout TEXT NOT NULL,
686
+ view_config TEXT,
687
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
688
+ updated_by TEXT NOT NULL
689
+ );
690
+
691
+ CREATE TABLE IF NOT EXISTS setup_completed (
692
+ id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1)
693
+ );
693
694
  `.trim();
694
695
  PLACEHOLDER_DB_IDS = [
695
696
  "INCOLLA_QUI_IL_TUO_ID_D1",
@@ -700,515 +701,46 @@ CREATE TABLE IF NOT EXISTS site_settings (
700
701
  });
701
702
 
702
703
  // src/commands/seed-load.ts
703
- init_wrangler();
704
- import pc3 from "picocolors";
705
- import {
706
- SEED_REGISTRY as SEED_REGISTRY2,
707
- generateCreateTable,
708
- generateDraftTable,
709
- generateIndexes,
710
- generateFtsTable,
711
- generateFtsTriggers,
712
- generateJunctionTable,
713
- generateJunctionIndexes,
714
- generateJunctionDraftTable,
715
- sortSeedsByDependencies
716
- } from "@beechcms/core";
717
-
718
- // src/lib/schema-diff.ts
719
- init_wrangler();
720
704
  import pc from "picocolors";
721
- import { getExpectedColumns } from "@beechcms/core";
722
- function isSeedClean(diff) {
723
- return diff.tableExists && diff.columns.every((c) => c.status === "ok");
724
- }
725
- function renderSeedDiff(diff) {
726
- const table = `content_${diff.slug}`;
727
- if (!diff.tableExists) {
728
- console.log(pc.red(` \u2717 ${table} \u2014 table missing`));
729
- return;
730
- }
731
- const problems = diff.columns.filter((c) => c.status !== "ok");
732
- if (problems.length === 0) {
733
- console.log(pc.green(` \u2713 ${table}`));
734
- return;
735
- }
736
- console.log(pc.yellow(` \u26A0 ${table}`));
737
- for (const col of problems) {
738
- switch (col.status) {
739
- case "missing":
740
- console.log(pc.red(` + missing column: ${col.name} ${col.expectedType}`));
741
- break;
742
- case "extra":
743
- console.log(pc.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) \u2014 in DB, not in seeds.ts`));
744
- break;
745
- case "type_mismatch":
746
- console.log(pc.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
747
- break;
748
- case "fk_missing":
749
- console.log(pc.red(` \u292C missing FK: ${col.name} \u2192 content_${col.expectedTarget}(id)`));
750
- break;
751
- case "fk_mismatch":
752
- console.log(pc.yellow(` \u292C FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`));
753
- break;
754
- case "index_missing":
755
- console.log(pc.yellow(` \u2298 missing index on ${col.name}`));
756
- break;
757
- }
758
- }
759
- }
760
- async function diffSeed(seed, options) {
761
- const tableName = `content_${seed.slug}`;
762
- const expected = getExpectedColumns(seed);
763
- let actual;
764
- try {
765
- actual = queryD1(`PRAGMA table_info(${tableName})`, options);
766
- } catch {
767
- return { slug: seed.slug, tableExists: false, columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType })) };
768
- }
769
- if (actual.length === 0) {
770
- return { slug: seed.slug, tableExists: false, columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType })) };
771
- }
772
- const actualMap = new Map(actual.map((r) => [r.name, r]));
773
- const expectedSet = new Set(expected.map((c) => c.name));
774
- const columns = [];
775
- for (const col of expected) {
776
- const actualRow = actualMap.get(col.name);
777
- if (!actualRow) {
778
- columns.push({ name: col.name, status: "missing", expectedType: col.sqlType });
779
- } else if (actualRow.type.toUpperCase() !== col.sqlType) {
780
- columns.push({ name: col.name, status: "type_mismatch", expectedType: col.sqlType, actualType: actualRow.type });
781
- } else {
782
- columns.push({ name: col.name, status: "ok" });
783
- }
784
- }
785
- for (const row of actual) {
786
- if (!expectedSet.has(row.name)) {
787
- columns.push({ name: row.name, status: "extra", actualType: row.type });
788
- }
789
- }
790
- const relationBranches = seed.branches.filter((b) => b.type === "relation" && b.targetSeed);
791
- if (relationBranches.length > 0) {
792
- let fkList = [];
793
- let indexList = [];
794
- try {
795
- fkList = queryD1(`PRAGMA foreign_key_list(${tableName})`, options);
796
- indexList = queryD1(`PRAGMA index_list(${tableName})`, options);
797
- } catch {
798
- }
799
- const fkByCol = /* @__PURE__ */ new Map();
800
- for (const fk of fkList) {
801
- fkByCol.set(fk.from, fk);
802
- }
803
- const indexNames = new Set(indexList.map((i) => i.name));
804
- for (const branch of relationBranches) {
805
- const expectedFkTable = `content_${branch.targetSeed}`;
806
- const expectedOnDelete = (branch.onDelete ?? "SET NULL").toUpperCase();
807
- const expectedIndexName = `idx_${seed.slug}_${branch.alias}`;
808
- const colDiff = columns.find((c) => c.name === branch.alias);
809
- if (!colDiff || colDiff.status === "missing") continue;
810
- const fk = fkByCol.get(branch.alias);
811
- if (!fk) {
812
- colDiff.status = "fk_missing";
813
- colDiff.expectedTarget = branch.targetSeed;
814
- } else {
815
- const actualTable = fk.table;
816
- const actualOnDelete = fk.on_delete.toUpperCase();
817
- if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
818
- colDiff.status = "fk_mismatch";
819
- colDiff.expected = `\u2192 ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`;
820
- colDiff.actual = `\u2192 ${actualTable}(id) ON DELETE ${actualOnDelete}`;
821
- colDiff.expectedTarget = branch.targetSeed;
822
- }
823
- }
824
- if (!indexNames.has(expectedIndexName)) {
825
- if (colDiff.status === "ok") {
826
- colDiff.status = "index_missing";
827
- } else {
828
- columns.push({ name: branch.alias, status: "index_missing" });
829
- }
830
- }
831
- }
832
- }
833
- 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"));
834
710
  }
835
711
 
712
+ // src/index.ts
713
+ init_init();
714
+
836
715
  // src/commands/validate.ts
837
- import pc2 from "picocolors";
838
- import { SEED_REGISTRY, validateSeedDefinitions } from "@beechcms/core";
716
+ import pc3 from "picocolors";
717
+ import { validateSeedDefinitions } from "@beechcms/core";
839
718
  function validateSeeds(registry) {
840
719
  return validateSeedDefinitions(Object.values(registry));
841
720
  }
842
- async function validate(args) {
843
- const registry = args.registry ?? SEED_REGISTRY;
844
- if (Object.keys(registry).length === 0) {
845
- console.warn(pc2.yellow("\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n"));
846
- return;
847
- }
848
- console.log(pc2.cyan("\n beech validate \u2014 checking seeds\n"));
849
- const errors = validateSeeds(registry);
850
- const fatalErrors = errors.filter((e) => e.fatal);
851
- const warnings = errors.filter((e) => !e.fatal);
852
- for (const e of fatalErrors) {
853
- console.log(pc2.red(` \u2717 ${e.slug} (fatal)`));
854
- for (const msg of e.messages) {
855
- console.log(pc2.red(` \u2192 ${msg}`));
856
- }
857
- }
858
- const warningMap = new Map(warnings.map((e) => [e.slug, e.messages]));
859
- const allWarningSlugsSeen = new Set(warnings.map((e) => e.slug));
860
- for (const seed of Object.values(registry)) {
861
- const msgs = warningMap.get(seed.slug);
862
- if (!msgs) {
863
- if (!allWarningSlugsSeen.has(seed.slug)) {
864
- const hasFatal = fatalErrors.some((e) => e.slug === seed.slug);
865
- if (!hasFatal) console.log(pc2.green(` \u2713 ${seed.slug}`));
866
- }
867
- } else {
868
- console.log(pc2.yellow(` \u26A0 ${seed.slug}`));
869
- for (const msg of msgs) {
870
- console.log(pc2.yellow(` \u2192 ${msg}`));
871
- }
872
- }
873
- }
874
- console.log("");
875
- const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
876
- const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0);
877
- if (totalFatal > 0) {
878
- const s = totalFatal !== 1 ? "s" : "";
879
- console.log(pc2.red(` Found ${totalFatal} fatal error${s}. Fix before loading.
880
- `));
881
- process.exit(1);
882
- } else if (totalWarnings > 0) {
883
- const s = totalWarnings !== 1 ? "s" : "";
884
- console.log(pc2.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.
885
- `));
886
- } else {
887
- console.log(pc2.green(" All seeds valid.\n"));
888
- }
889
- }
890
-
891
- // src/commands/seed-load.ts
892
- function buildSeedRegistrationSql(seed) {
893
- const json = sqlQuote(JSON.stringify(seed));
894
- return [
895
- `INSERT INTO seeds (slug, definition, status, source, created_at, updated_at)`,
896
- `VALUES (${sqlQuote(seed.slug)}, ${json}, 'active', 'code', unixepoch(), unixepoch())`,
897
- `ON CONFLICT(slug) DO UPDATE SET definition = excluded.definition, status = 'active', updated_at = excluded.updated_at;`
898
- ].join("\n");
899
- }
900
- var SEED_META_BUMP_SQL = `UPDATE seed_meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE id = 'registry_version';`;
901
- function buildStatements(seed) {
902
- const stmts = [generateCreateTable(seed), ...generateIndexes(seed)];
903
- const draft = generateDraftTable(seed);
904
- if (draft) stmts.push(draft);
905
- const fts = generateFtsTable(seed);
906
- if (fts) {
907
- stmts.push(fts, ...generateFtsTriggers(seed));
908
- }
909
- for (const branch of seed.branches) {
910
- if (branch.type !== "relation" || branch.multiple !== true) continue;
911
- stmts.push(generateJunctionTable(seed, branch), ...generateJunctionIndexes(seed, branch));
912
- const draftJunction = generateJunctionDraftTable(seed, branch);
913
- if (draftJunction) stmts.push(draftJunction);
914
- }
915
- return stmts;
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"));
916
725
  }
917
- async function runDiff(options, registry) {
918
- const seeds = sortSeedsByDependencies(Object.values(registry));
919
- console.log(pc3.cyan("\n Diffing schema\u2026\n"));
920
- let allOk = true;
921
- for (const seed of seeds) {
922
- const result = await diffSeed(seed, options);
923
- renderSeedDiff(result);
924
- if (!isSeedClean(result)) allOk = false;
925
- }
926
- console.log("");
927
- if (allOk) {
928
- console.log(pc3.green(" Schema matches seeds. No action needed.\n"));
929
- } else {
930
- console.log(pc3.yellow(" Run `beech seed:load` to apply missing tables/columns.\n"));
931
- }
932
- }
933
- async function runLoad(options, dryRun, registry) {
934
- const seeds = sortSeedsByDependencies(Object.values(registry));
935
- if (dryRun) {
936
- console.log(pc3.cyan("\n -- dry-run: SQL that would be executed\n"));
937
- for (const seed of seeds) {
938
- const stmts = buildStatements(seed);
939
- console.log(pc3.dim(` -- content_${seed.slug}`));
940
- for (const stmt of stmts) {
941
- console.log(stmt + "\n");
942
- }
943
- console.log(pc3.dim(` -- register ${seed.slug} in seeds table`));
944
- console.log(buildSeedRegistrationSql(seed) + "\n");
945
- }
946
- console.log(pc3.dim(" -- bump registry_version"));
947
- console.log(SEED_META_BUMP_SQL + "\n");
948
- return;
949
- }
950
- console.log(pc3.cyan(`
951
- Loading seeds into ${options.local ? "local" : "remote"} D1 (${options.db})\u2026
952
- `));
953
- for (const seed of seeds) {
954
- const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)];
955
- const sql = stmts.join("\n\n") + "\n";
956
- process.stdout.write(` ${pc3.dim("\u2192")} content_${seed.slug}\u2026 `);
957
- const ok = executeD1File(sql, options);
958
- if (!ok) {
959
- console.log(pc3.red("failed"));
960
- console.log(pc3.red(`
961
- \u2717 Failed to apply schema for content_${seed.slug}
962
- `));
963
- console.log(pc3.dim(" wrangler reported an error above."));
964
- console.log(pc3.dim(` Most likely causes:`));
965
- console.log(pc3.dim(` - Database "${options.db}" not found or wrong database_id`));
966
- if (!options.local) {
967
- console.log(pc3.dim(" - Not logged in to Cloudflare"));
968
- console.log(pc3.cyan("\n \u2192 Run: npx wrangler login"));
969
- console.log(pc3.cyan(" \u2192 Then: npx beech seed:load\n"));
970
- } else {
971
- console.log(pc3.cyan("\n \u2192 Run: npx beech init --db --local # re-initialise local DB"));
972
- console.log(pc3.cyan(" \u2192 Then: npx beech seed:load --local\n"));
973
- }
974
- process.exit(1);
975
- }
976
- console.log(pc3.green("done"));
977
- }
978
- executeD1File(SEED_META_BUMP_SQL, options);
979
- console.log(pc3.green("\n All seeds loaded.\n"));
980
- console.log(pc3.dim(" Definitions registered in the database."));
981
- 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"));
982
- }
983
- async function seedLoad(args) {
984
- const registry = args.registry ?? SEED_REGISTRY2;
985
- if (Object.keys(registry).length === 0) {
986
- console.log(pc3.yellow("\n \u2717 No seeds found\n"));
987
- console.log(pc3.dim(" Create a seeds.ts file in your project root with at least one content type."));
988
- console.log(pc3.cyan("\n \u2192 Run: npx beech seed:create\n"));
989
- return;
990
- }
991
- const validationErrors = validateSeeds(registry);
992
- const fatalErrors = validationErrors.filter((e) => e.fatal);
993
- const warnings = validationErrors.filter((e) => !e.fatal);
994
- if (fatalErrors.length > 0) {
995
- const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
996
- const s = total !== 1 ? "s" : "";
997
- console.log(pc3.red(`
998
- \u2717 Seed validation found ${total} fatal error${s}. Cannot load schema.
999
- `));
1000
- for (const e of fatalErrors) {
1001
- console.log(pc3.red(` \u2717 ${e.slug}`));
1002
- for (const msg of e.messages) {
1003
- console.log(pc3.red(` \u2192 ${msg}`));
1004
- }
1005
- }
1006
- console.log("");
1007
- process.exit(1);
1008
- }
1009
- if (warnings.length > 0) {
1010
- const total = warnings.reduce((n, e) => n + e.messages.length, 0);
1011
- const s = total !== 1 ? "s" : "";
1012
- console.log(pc3.yellow(`
1013
- \u26A0 Seed validation found ${total} issue${s}. Schema changes will still be applied.
1014
- `));
1015
- console.log(pc3.dim(' Run "npx beech validate" for details.\n'));
1016
- }
1017
- const configPath = findWranglerConfig();
1018
- const db = args.db ?? resolveDbName(configPath);
1019
- const options = {
1020
- db,
1021
- local: args.local,
1022
- configPath
1023
- };
1024
- if (!args.dryRun && !args.diff) {
1025
- try {
1026
- const rows = queryD1(
1027
- `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
1028
- options
1029
- );
1030
- if (rows.length < 2) {
1031
- console.log(pc3.red("\n \u2717 System tables not found (seeds, seed_meta)\n"));
1032
- console.log(pc3.dim(" Run `beech init --db` first to initialise the database."));
1033
- const flag = args.local ? " --local" : "";
1034
- console.log(pc3.cyan(`
1035
- \u2192 Run: npx beech init --db${flag}
1036
- `));
1037
- process.exit(1);
1038
- }
1039
- } catch {
1040
- console.log(pc3.red("\n \u2717 Could not query the database\n"));
1041
- console.log(pc3.dim(" Run `beech init --db` first to initialise the database."));
1042
- process.exit(1);
1043
- }
1044
- }
1045
- if (args.diff) {
1046
- await runDiff(options, registry);
1047
- } else {
1048
- await runLoad(options, args.dryRun, registry);
1049
- }
1050
- }
1051
-
1052
- // src/index.ts
1053
- init_init();
1054
726
 
1055
727
  // src/commands/seed-create.ts
1056
- import pc5 from "picocolors";
1057
- import { createInterface as createInterface2 } from "node:readline/promises";
1058
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
1059
- import { resolve as resolve3 } from "node:path";
1060
- var BRANCH_TYPES = ["text", "number", "boolean", "date", "richtext", "file", "tags"];
1061
- function slugify(str) {
1062
- return str.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
1063
- }
1064
- function toConstName(slug) {
1065
- return slug.replace(/-/g, "_").toUpperCase() + "_SEED";
1066
- }
1067
- function toLabel(alias) {
1068
- return alias.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
1069
- }
1070
- function generateSeedBlock(slug, label, labelPlural, branches) {
1071
- const displayAlias = branches.find((b) => b.type === "text")?.alias ?? branches[0]?.alias ?? "name";
1072
- const cName = toConstName(slug);
1073
- const branchLines = branches.map((b) => {
1074
- const parts = [
1075
- `alias: '${b.alias}'`,
1076
- `label: '${b.label}'`,
1077
- `type: '${b.type}'`
1078
- ];
1079
- if (b.required) parts.push("requiredOnCreate: true");
1080
- return ` { ${parts.join(", ")} },`;
1081
- });
1082
- return [
1083
- "",
1084
- `export const ${cName} = defineSeed({`,
1085
- ` slug: '${slug}',`,
1086
- ` label: '${label}',`,
1087
- ` labelPlural: '${labelPlural}',`,
1088
- ` displayNameAlias: '${displayAlias}',`,
1089
- " branches: [",
1090
- ...branchLines,
1091
- " ],",
1092
- " dashboard: {",
1093
- " icon: 'Folder',",
1094
- " group: 'Content',",
1095
- " },",
1096
- "})",
1097
- ""
1098
- ].join("\n");
1099
- }
1100
- function ensureDefineSeedImport(content) {
1101
- if (content.includes("defineSeed")) return content;
1102
- return `import { defineSeed } from '@beechcms/core'
1103
- ` + content;
1104
- }
1105
- function tryInsertRegistryEntry(content, slug, cName) {
1106
- const match = content.match(/(SEED_REGISTRY[^{]*\{)([\s\S]*?)(\n\})/m);
1107
- if (!match) return null;
1108
- const [full, open, inner, close] = match;
1109
- const newEntry = `
1110
- ${slug}: ${cName},`;
1111
- return content.replace(full, open + inner + newEntry + close);
1112
- }
1113
- function findSeedsFile() {
1114
- const cwd = process.cwd();
1115
- const searchDirs = [cwd, resolve3(cwd, "apps", "api")];
1116
- for (const dir of searchDirs) {
1117
- for (const name of ["seeds.ts", "seed.ts"]) {
1118
- const p = resolve3(dir, name);
1119
- if (existsSync3(p)) return p;
1120
- }
1121
- }
1122
- return null;
1123
- }
1124
- async function seedCreate(_args) {
1125
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
1126
- const ask = async (q, fallback = "") => {
1127
- const hint = fallback ? pc5.dim(` [${fallback}]`) : "";
1128
- const answer = await rl.question(` ${q}${hint}: `);
1129
- return answer.trim() || fallback;
1130
- };
1131
- const askYN = async (q, defaultYes = true) => {
1132
- const hint = defaultYes ? pc5.dim(" (Y/n)") : pc5.dim(" (y/N)");
1133
- const answer = (await rl.question(` ${q}${hint}: `)).trim().toLowerCase();
1134
- if (!answer) return defaultYes;
1135
- return answer === "y" || answer === "yes";
1136
- };
1137
- console.log(pc5.cyan("\n beech seed:create \u2014 new content type wizard\n"));
1138
- try {
1139
- const label = await ask('Content type name (singular, e.g. "Article")');
1140
- if (!label) {
1141
- rl.close();
1142
- console.log(pc5.red("\n \u2717 Name required.\n"));
1143
- process.exit(1);
1144
- }
1145
- const defaultSlug = slugify(label) + "s";
1146
- const slug = slugify(await ask("Slug (plural, used in URL + table name)", defaultSlug)) || defaultSlug;
1147
- const labelPlural = await ask("Plural label", label + "s") || label + "s";
1148
- const branches = [];
1149
- console.log(pc5.dim("\n Now define the fields. Press Enter to accept defaults.\n"));
1150
- let addMore = true;
1151
- while (addMore) {
1152
- console.log(pc5.dim(` \u2500\u2500\u2500 Field ${branches.length + 1} \u2500\u2500\u2500`));
1153
- const alias = await ask(' Alias (camelCase, e.g. "title", "publishedAt")');
1154
- if (!alias) {
1155
- console.log(pc5.yellow(" Alias required \u2014 skipping."));
1156
- addMore = await askYN("\n Add a field?");
1157
- continue;
1158
- }
1159
- const fieldLabel = await ask(" Label", toLabel(alias)) || toLabel(alias);
1160
- const typeList = BRANCH_TYPES.join(" | ");
1161
- const rawType = (await ask(` Type (${typeList})`, "text")).toLowerCase();
1162
- const type = BRANCH_TYPES.includes(rawType) ? rawType : "text";
1163
- const required = await askYN(" Required on create?", false);
1164
- branches.push({ alias, label: fieldLabel, type, required });
1165
- addMore = await askYN("\n Add another field?");
1166
- }
1167
- rl.close();
1168
- if (branches.length === 0) {
1169
- console.log(pc5.yellow("\n No fields defined \u2014 seed not created.\n"));
1170
- process.exit(0);
1171
- }
1172
- const seedBlock = generateSeedBlock(slug, label, labelPlural, branches);
1173
- const cName = toConstName(slug);
1174
- const seedsPath = findSeedsFile();
1175
- if (!seedsPath) {
1176
- console.log(pc5.yellow("\n Could not find seeds.ts. Add this to your seeds file manually:\n"));
1177
- console.log(seedBlock);
1178
- process.exit(0);
1179
- }
1180
- let content = readFileSync3(seedsPath, "utf-8");
1181
- content = ensureDefineSeedImport(content);
1182
- const withSeed = content + seedBlock;
1183
- const withRegistry = tryInsertRegistryEntry(withSeed, slug, cName);
1184
- writeFileSync3(seedsPath, withRegistry ?? withSeed, "utf-8");
1185
- console.log(pc5.green(`
1186
- \u2713 Seed "${slug}" appended to ${seedsPath}
1187
- `));
1188
- if (!withRegistry) {
1189
- console.log(pc5.yellow(` \u26A0 Could not auto-update SEED_REGISTRY \u2014 add this entry manually:
1190
- `));
1191
- console.log(pc5.cyan(` ${slug}: ${cName},
1192
- `));
1193
- }
1194
- console.log(pc5.dim(" Next steps:"));
1195
- console.log(pc5.cyan(" npx beech seed:load --local"));
1196
- console.log(pc5.dim(" \u2192 create the new content table in your local D1 database\n"));
1197
- } catch (err) {
1198
- rl.close();
1199
- throw err;
1200
- }
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"));
1201
733
  }
1202
734
 
1203
735
  // src/commands/deploy.ts
1204
736
  init_wrangler();
1205
- import pc6 from "picocolors";
737
+ import pc5 from "picocolors";
1206
738
  import { spawnSync as spawnSync3 } from "node:child_process";
1207
- import { readFileSync as readFileSync4 } from "node:fs";
739
+ import { readFileSync as readFileSync3 } from "node:fs";
1208
740
  function readWorkerName(configPath) {
1209
741
  if (!configPath) return null;
1210
742
  try {
1211
- const raw = readFileSync4(configPath, "utf-8");
743
+ const raw = readFileSync3(configPath, "utf-8");
1212
744
  const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1213
745
  const parsed = JSON.parse(stripped);
1214
746
  return parsed?.name ?? null;
@@ -1233,8 +765,8 @@ async function checkAdmin(url) {
1233
765
  }
1234
766
  }
1235
767
  async function deploy(args) {
1236
- console.log(pc6.cyan("\n beech deploy\n"));
1237
- 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"));
1238
770
  const deployResult = spawnSync3("npm", ["run", "deploy"], {
1239
771
  stdio: ["inherit", "pipe", "inherit"],
1240
772
  encoding: "utf-8",
@@ -1244,33 +776,16 @@ async function deploy(args) {
1244
776
  const deployStdout = deployResult.stdout ?? "";
1245
777
  if (deployStdout) process.stdout.write(deployStdout);
1246
778
  if (deployResult.status !== 0) {
1247
- console.log(pc6.red("\n \u2717 Worker deploy failed\n"));
1248
- console.log(pc6.dim(" Check the wrangler output above for details."));
1249
- console.log(pc6.cyan("\n \u2192 Run: npx wrangler login # if not authenticated"));
1250
- 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"));
1251
783
  process.exit(1);
1252
784
  }
1253
785
  const deployedUrl = extractWorkerUrl(deployStdout);
1254
- console.log(pc6.green("\n \u2713 Worker deployed"));
1255
- if (args.skipSeed) {
1256
- console.log(pc6.dim("\n [2/3] Skipping seed:load (--skip-seed)"));
1257
- } else {
1258
- console.log(pc6.dim("\n [2/3] Syncing content schema to remote D1\u2026\n"));
1259
- const seedResult = spawnSync3("npx", ["beech", "seed:load"], {
1260
- stdio: "inherit",
1261
- cwd: process.cwd(),
1262
- shell: true
1263
- });
1264
- if (seedResult.status !== 0) {
1265
- console.log(pc6.yellow("\n \u26A0 seed:load failed\n"));
1266
- console.log(pc6.dim(" Sync the remote content schema manually:"));
1267
- console.log(pc6.cyan(" \u2192 Run: npx beech seed:load\n"));
1268
- } else {
1269
- console.log(pc6.green("\n \u2713 Content schema synced"));
1270
- }
1271
- }
786
+ console.log(pc5.green("\n \u2713 Worker deployed"));
1272
787
  if (args.skipCheck) {
1273
- 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"));
1274
789
  return;
1275
790
  }
1276
791
  const adminBase = deployedUrl ?? (() => {
@@ -1278,106 +793,101 @@ async function deploy(args) {
1278
793
  return workerName ? `https://${workerName}.workers.dev` : null;
1279
794
  })();
1280
795
  if (!adminBase) {
1281
- console.log(pc6.dim("\n [3/3] Could not determine worker URL \u2014 skipping admin check\n"));
1282
- 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"));
1283
798
  return;
1284
799
  }
1285
- console.log(pc6.dim(`
1286
- [3/3] Checking ${adminBase}/admin\u2026
800
+ console.log(pc5.dim(`
801
+ [2/2] Checking ${adminBase}/admin\u2026
1287
802
  `));
1288
803
  const { ok, status } = await checkAdmin(adminBase);
1289
804
  if (ok) {
1290
- console.log(pc6.green(` \u2713 Admin reachable at: ${adminBase}/admin
805
+ console.log(pc5.green(` \u2713 Admin reachable at: ${adminBase}/admin
1291
806
  `));
1292
807
  } else {
1293
808
  const statusStr = status != null ? ` (HTTP ${status})` : "";
1294
- 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
1295
810
  `));
1296
- console.log(pc6.dim(" The database may not be fully initialized."));
1297
- 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"));
1298
813
  }
1299
814
  }
1300
815
 
1301
816
  // src/commands/onboard.ts
1302
817
  init_init();
1303
- import pc7 from "picocolors";
818
+ import pc6 from "picocolors";
1304
819
  async function onboard(args) {
1305
- console.log(pc7.cyan("\n beech onboard \u2014 full provisioning\n"));
820
+ console.log(pc6.cyan("\n beech onboard \u2014 full provisioning\n"));
1306
821
  await init({ initDb: true, local: args.local, db: args.db, nonInteractive: args.yes });
1307
- await seedLoad({ dryRun: false, diff: false, local: args.local, db: args.db, registry: args.registry ?? null });
1308
- console.log(pc7.cyan("\n Provisioning complete.\n"));
1309
- console.log(pc7.dim(" Next steps:"));
1310
- console.log(pc7.cyan(" 1. npx wrangler dev"));
1311
- console.log(pc7.dim(" \u2192 start API + dashboard"));
1312
- console.log(pc7.dim(" 2. Open http://localhost:8789/admin"));
1313
- 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"));
1314
828
  }
1315
829
 
1316
830
  // src/commands/update.ts
1317
- import pc8 from "picocolors";
831
+ import pc7 from "picocolors";
1318
832
  import { spawnSync as spawnSync4 } from "node:child_process";
1319
833
  async function update(_args) {
1320
- console.log(pc8.cyan("\n beech update\n"));
1321
- 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"));
1322
836
  const installResult = spawnSync4(
1323
837
  "npm",
1324
838
  ["install", "@beechcms/api@latest", "@beechcms/core@latest"],
1325
839
  { stdio: "inherit", cwd: process.cwd(), shell: true }
1326
840
  );
1327
841
  if (installResult.status !== 0) {
1328
- console.log(pc8.red("\n \u2717 npm install failed\n"));
1329
- console.log(pc8.dim(" Check the output above for details."));
1330
- console.log(pc8.dim(" You may need to resolve version conflicts manually."));
1331
- 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"));
1332
846
  process.exit(1);
1333
847
  }
1334
- console.log(pc8.green("\n \u2713 Packages updated"));
1335
- 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"));
1336
850
  const initResult = spawnSync4(
1337
851
  "npx",
1338
852
  ["beech", "init", "--db", "--local"],
1339
853
  { stdio: "inherit", cwd: process.cwd(), shell: true }
1340
854
  );
1341
855
  if (initResult.status !== 0) {
1342
- console.log(pc8.yellow("\n \u26A0 Local DB update failed\n"));
1343
- console.log(pc8.dim(" Apply system migrations manually:"));
1344
- 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"));
1345
859
  } else {
1346
- console.log(pc8.green("\n \u2713 Local database updated"));
1347
- }
1348
- console.log(pc8.dim("\n Local update complete.\n"));
1349
- console.log(pc8.dim(" Next steps:"));
1350
- console.log(pc8.cyan(" 1. npx beech seed:load --local"));
1351
- console.log(pc8.dim(" \u2192 sync content schema to local DB"));
1352
- console.log(pc8.cyan(" 2. npm run deploy"));
1353
- console.log(pc8.dim(" \u2192 deploy updated API + dashboard"));
1354
- console.log(pc8.cyan(" 3. npx beech seed:load"));
1355
- 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"));
1356
866
  }
1357
867
 
1358
868
  // src/commands/reset.ts
1359
- import pc11 from "picocolors";
1360
- import { createInterface as createInterface3 } from "node:readline/promises";
869
+ import pc10 from "picocolors";
870
+ import { createInterface as createInterface2 } from "node:readline/promises";
1361
871
 
1362
872
  // src/commands/db-reset.ts
1363
- import pc9 from "picocolors";
873
+ import pc8 from "picocolors";
1364
874
  import { spawnSync as spawnSync5 } from "node:child_process";
1365
- import { existsSync as existsSync4, readFileSync as readFileSync5, rmSync as rmSync2 } from "node:fs";
1366
- 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";
1367
877
  async function dbReset(_args) {
1368
- 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"));
1369
879
  const cwd = process.cwd();
1370
- const apiDir = resolve4(cwd, "apps", "api");
880
+ const apiDir = resolve3(cwd, "apps", "api");
1371
881
  let dbResetSuccess = false;
1372
- if (existsSync4(resolve4(apiDir, "package.json"))) {
882
+ if (existsSync3(resolve3(apiDir, "package.json"))) {
1373
883
  const result = spawnSync5("npm", ["run", "db:reset:local"], {
1374
884
  stdio: "inherit",
1375
885
  cwd: apiDir,
1376
886
  shell: true
1377
887
  });
1378
888
  dbResetSuccess = result.status === 0;
1379
- } else if (existsSync4(resolve4(cwd, "package.json"))) {
1380
- 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"));
1381
891
  if (pkg.scripts?.["db:reset:local"]) {
1382
892
  const result = spawnSync5("npm", ["run", "db:reset:local"], {
1383
893
  stdio: "inherit",
@@ -1386,12 +896,12 @@ async function dbReset(_args) {
1386
896
  });
1387
897
  dbResetSuccess = result.status === 0;
1388
898
  } else {
1389
- const wranglerStateDir = resolve4(cwd, ".wrangler/state");
1390
- if (existsSync4(wranglerStateDir)) {
1391
- 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"));
1392
902
  rmSync2(wranglerStateDir, { recursive: true, force: true });
1393
903
  }
1394
- if (existsSync4(resolve4(cwd, "scripts", "bootstrap-d1.mjs"))) {
904
+ if (existsSync3(resolve3(cwd, "scripts", "bootstrap-d1.mjs"))) {
1395
905
  const result = spawnSync5("node", ["scripts/bootstrap-d1.mjs"], {
1396
906
  stdio: "inherit",
1397
907
  cwd,
@@ -1399,7 +909,7 @@ async function dbReset(_args) {
1399
909
  });
1400
910
  dbResetSuccess = result.status === 0;
1401
911
  } else {
1402
- console.log(pc9.yellow(" \u26A0 Could not find database reset script."));
912
+ console.log(pc8.yellow(" \u26A0 Could not find database reset script."));
1403
913
  const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1404
914
  try {
1405
915
  await init2({ initDb: true, local: true });
@@ -1410,24 +920,24 @@ async function dbReset(_args) {
1410
920
  }
1411
921
  }
1412
922
  } else {
1413
- const wranglerStateDir = resolve4(cwd, ".wrangler/state");
1414
- if (existsSync4(wranglerStateDir)) {
1415
- 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"));
1416
926
  rmSync2(wranglerStateDir, { recursive: true, force: true });
1417
927
  }
1418
928
  dbResetSuccess = true;
1419
929
  }
1420
930
  if (dbResetSuccess) {
1421
- console.log(pc9.green("\n \u2713 Local database reset completed."));
931
+ console.log(pc8.green("\n \u2713 Local database reset completed."));
1422
932
  } else {
1423
- console.log(pc9.red("\n \u2717 Database reset failed."));
933
+ console.log(pc8.red("\n \u2717 Database reset failed."));
1424
934
  process.exit(1);
1425
935
  return;
1426
936
  }
1427
937
  }
1428
938
 
1429
939
  // src/commands/dev-reset.ts
1430
- import pc10 from "picocolors";
940
+ import pc9 from "picocolors";
1431
941
  import { spawnSync as spawnSync6 } from "node:child_process";
1432
942
  function isDockerInstalled() {
1433
943
  try {
@@ -1446,27 +956,27 @@ function isDockerRunning() {
1446
956
  }
1447
957
  }
1448
958
  async function devReset() {
1449
- 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"));
1450
960
  if (!isDockerInstalled()) {
1451
- 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."));
1452
962
  process.exit(1);
1453
963
  return;
1454
964
  }
1455
965
  if (!isDockerRunning()) {
1456
- 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."));
1457
967
  process.exit(1);
1458
968
  return;
1459
969
  }
1460
- console.log(pc10.dim(" Resetting Docker containers and volumes\u2026\n"));
970
+ console.log(pc9.dim(" Resetting Docker containers and volumes\u2026\n"));
1461
971
  const result = spawnSync6("docker", ["compose", "-f", "docker/docker-compose.yml", "down", "-v"], {
1462
972
  stdio: "inherit",
1463
973
  cwd: process.cwd(),
1464
974
  shell: true
1465
975
  });
1466
976
  if (result.status === 0) {
1467
- 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."));
1468
978
  } else {
1469
- console.log(pc10.red("\n \u2717 Docker reset failed."));
979
+ console.log(pc9.red("\n \u2717 Docker reset failed."));
1470
980
  process.exit(1);
1471
981
  return;
1472
982
  }
@@ -1474,7 +984,7 @@ async function devReset() {
1474
984
 
1475
985
  // src/commands/reset.ts
1476
986
  async function reset(args) {
1477
- console.log(pc11.cyan("\n beech reset \u2014 cleanup environments\n"));
987
+ console.log(pc10.cyan("\n beech reset \u2014 cleanup environments\n"));
1478
988
  let resetDb = args.db || args.all;
1479
989
  let resetDocker = args.docker || args.all;
1480
990
  if (!args.db && !args.docker && !args.all) {
@@ -1482,23 +992,23 @@ async function reset(args) {
1482
992
  resetDb = true;
1483
993
  resetDocker = true;
1484
994
  } else if (process.stdin.isTTY) {
1485
- const rl = createInterface3({ input: process.stdin, output: process.stdout });
995
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
1486
996
  try {
1487
997
  const answer = (await rl.question(
1488
- 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): ")
1489
999
  )).trim().toLowerCase();
1490
1000
  if (answer === "y" || answer === "yes") {
1491
1001
  resetDb = true;
1492
1002
  resetDocker = true;
1493
1003
  } else {
1494
- 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"));
1495
1005
  return;
1496
1006
  }
1497
1007
  } finally {
1498
1008
  rl.close();
1499
1009
  }
1500
1010
  } else {
1501
- 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"));
1502
1012
  process.exit(1);
1503
1013
  }
1504
1014
  }
@@ -1512,237 +1022,170 @@ async function reset(args) {
1512
1022
 
1513
1023
  // src/commands/generate-types.ts
1514
1024
  init_wrangler();
1515
- import { writeFileSync as writeFileSync4, mkdirSync } from "node:fs";
1516
- import { dirname, resolve as resolve5 } from "node:path";
1517
- 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";
1518
1028
  import { generateSeedTypes } from "@beechcms/core";
1519
- function loadSeedsFromD1(db) {
1029
+ async function generateTypes(args = {}) {
1030
+ const isLocal = args.local !== false;
1520
1031
  const configPath = findWranglerConfig();
1521
- const options = { db, local: false, configPath };
1522
- const rows = queryD1(
1523
- `SELECT slug, definition FROM seeds WHERE status = 'active';`,
1524
- options
1525
- );
1526
- return rows.map((r) => JSON.parse(r.definition));
1527
- }
1528
- async function generateTypes(args) {
1529
- let seeds;
1530
- if (args.local) {
1531
- const registry = args.registry ?? {};
1532
- if (Object.keys(registry).length === 0) {
1533
- 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
+ );
1534
1039
  process.exit(1);
1535
1040
  }
1536
- seeds = Object.values(registry);
1537
- } else {
1538
- const db = args.db ?? resolveDbName(findWranglerConfig());
1539
- seeds = loadSeedsFromD1(db);
1540
- if (seeds.length === 0) {
1541
- console.log(pc12.red(`
1542
- \u2717 No active seeds in D1 (${db}). Run \`beech seed:load\` first.
1543
- `));
1544
- 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
+ );
1545
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);
1546
1086
  }
1547
1087
  const code = generateSeedTypes(seeds);
1548
- const outPath = resolve5(process.cwd(), args.out);
1549
- mkdirSync(dirname(outPath), { recursive: true });
1550
- writeFileSync4(outPath, code, "utf-8");
1551
- 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(`
1552
1093
  \u2713 Generated ${seeds.length} interface(s) \u2192 ${args.out}
1553
1094
  `));
1554
- }
1555
-
1556
- // src/commands/schema-diff.ts
1557
- init_wrangler();
1558
- import pc13 from "picocolors";
1559
- import { resolve as resolve6 } from "node:path";
1560
- import { SEED_REGISTRY as SEED_REGISTRY3, sortSeedsByDependencies as sortSeedsByDependencies2 } from "@beechcms/core";
1561
-
1562
- // src/lib/migration-writer.ts
1563
- import { readdirSync as readdirSync2, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "node:fs";
1564
- import { join as join2 } from "node:path";
1565
- import {
1566
- generateAddColumn,
1567
- generateIndexes as generateIndexes2,
1568
- planCreateSeed
1569
- } from "@beechcms/core";
1570
- var DESTRUCTIVE = /* @__PURE__ */ new Set([
1571
- "extra",
1572
- "type_mismatch",
1573
- "fk_mismatch"
1574
- ]);
1575
- function nextMigrationIndex(migrationsDir) {
1576
- if (!existsSync5(migrationsDir)) return "0000";
1577
- let max = -1;
1578
- for (const f of readdirSync2(migrationsDir)) {
1579
- const m = /^(\d{4})_/.exec(f);
1580
- if (m) max = Math.max(max, Number(m[1]));
1581
- }
1582
- return String(max + 1).padStart(4, "0");
1583
- }
1584
- function buildMigrationSql(diffs, registry) {
1585
- const lines = [];
1586
- let additiveCount = 0;
1587
- const destructiveSlugs = [];
1588
- for (const diff of diffs) {
1589
- const seed = registry[diff.slug];
1590
- if (!seed) continue;
1591
- if (!diff.tableExists) {
1592
- lines.push(`-- ${diff.slug}: create table from scratch`);
1593
- for (const stmt of planCreateSeed(seed)) {
1594
- lines.push(stmt);
1595
- additiveCount++;
1596
- }
1597
- lines.push("");
1598
- continue;
1599
- }
1600
- const missing = diff.columns.filter((c) => c.status === "missing");
1601
- const idxMissing = diff.columns.filter((c) => c.status === "index_missing");
1602
- const destructive = diff.columns.filter((c) => DESTRUCTIVE.has(c.status));
1603
- if (missing.length || idxMissing.length) {
1604
- lines.push(`-- ${diff.slug}: additive changes`);
1605
- for (const col of missing) {
1606
- const branch = seed.branches.find((b) => b.alias === col.name);
1607
- if (branch) {
1608
- lines.push(generateAddColumn(seed, branch));
1609
- additiveCount++;
1610
- }
1611
- }
1612
- if (idxMissing.length) {
1613
- for (const stmt of generateIndexes2(seed)) {
1614
- lines.push(stmt);
1615
- additiveCount++;
1616
- }
1617
- }
1618
- lines.push("");
1619
- }
1620
- if (destructive.length) {
1621
- destructiveSlugs.push(diff.slug);
1622
- lines.push(`-- \u26A0 ${diff.slug}: DESTRUCTIVE drift NOT auto-migrated \u2014 review manually:`);
1623
- for (const col of destructive) {
1624
- lines.push(`-- ${col.status}: ${col.name}` + (col.actualType ? ` (db: ${col.actualType})` : ""));
1625
- }
1626
- lines.push("");
1627
- }
1095
+ } else {
1096
+ process.stdout.write(code);
1628
1097
  }
1629
- return { sql: lines.join("\n").trimEnd() + "\n", additiveCount, destructiveSlugs };
1630
- }
1631
- function writeMigrationFile(migrationsDir, index, name, sql) {
1632
- if (!existsSync5(migrationsDir)) mkdirSync2(migrationsDir, { recursive: true });
1633
- const safe = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "schema_sync";
1634
- const file = join2(migrationsDir, `${index}_${safe}.sql`);
1635
- writeFileSync5(file, sql, "utf-8");
1636
- return file;
1637
1098
  }
1638
1099
 
1639
1100
  // src/commands/schema-diff.ts
1640
- function resolveMigrationsDir(override) {
1641
- if (override) return resolve6(process.cwd(), override);
1642
- return resolve6(process.cwd(), "apps", "api", "migrations");
1643
- }
1644
- async function schemaDiff(args) {
1645
- const registry = args.registry ?? SEED_REGISTRY3;
1646
- if (Object.keys(registry).length === 0) {
1647
- console.log(pc13.yellow("\n \u2717 No seeds found \u2014 nothing to diff.\n"));
1648
- return;
1649
- }
1650
- const configPath = findWranglerConfig();
1651
- const options = { db: args.db ?? resolveDbName(configPath), local: args.local, configPath };
1652
- const seeds = sortSeedsByDependencies2(Object.values(registry));
1653
- console.log(pc13.cyan(`
1654
- Diffing schema vs ${args.local ? "local" : "remote"} D1 (${options.db})\u2026
1655
- `));
1656
- const diffs = [];
1657
- let clean = true;
1658
- for (const seed of seeds) {
1659
- const d = await diffSeed(seed, options);
1660
- diffs.push(d);
1661
- renderSeedDiff(d);
1662
- if (!isSeedClean(d)) clean = false;
1663
- }
1664
- if (clean) {
1665
- console.log(pc13.green("\n Schema matches seeds. No migration needed.\n"));
1666
- return;
1667
- }
1668
- const plan = buildMigrationSql(diffs, registry);
1669
- if (!args.write) {
1670
- console.log(pc13.dim("\n -- proposed additive migration (preview):\n"));
1671
- console.log(plan.sql);
1672
- if (plan.destructiveSlugs.length) {
1673
- console.log(pc13.yellow(`
1674
- \u26A0 Destructive drift in: ${plan.destructiveSlugs.join(", ")} \u2014 not auto-migrated.`));
1675
- }
1676
- console.log(pc13.cyan("\n \u2192 Re-run with --write to save the migration file.\n"));
1677
- return;
1678
- }
1679
- if (plan.additiveCount === 0) {
1680
- console.log(pc13.yellow("\n \u26A0 Only destructive drift detected \u2014 no additive migration written."));
1681
- console.log(pc13.dim(" Author a reviewed migration by hand for renames/drops/type changes.\n"));
1682
- return;
1683
- }
1684
- const dir = resolveMigrationsDir(args.migrationsDir);
1685
- const index = nextMigrationIndex(dir);
1686
- const file = writeMigrationFile(dir, index, args.name ?? "schema_sync", plan.sql);
1687
- console.log(pc13.green(`
1688
- \u2713 Wrote ${file} (${plan.additiveCount} statement(s)).`));
1689
- console.log(pc13.dim(" Review, commit, then `wrangler d1 migrations apply --remote` in CI.\n"));
1690
- if (plan.destructiveSlugs.length) {
1691
- console.log(pc13.yellow(` \u26A0 Destructive drift in ${plan.destructiveSlugs.join(", ")} was NOT included.
1692
- `));
1693
- }
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"));
1694
1107
  }
1695
1108
 
1696
1109
  // src/commands/db-migrate.ts
1697
- import pc14 from "picocolors";
1110
+ import pc13 from "picocolors";
1698
1111
  import { spawnSync as spawnSync7 } from "node:child_process";
1699
- import { existsSync as existsSync6 } from "node:fs";
1700
- 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";
1701
1114
  async function dbMigrate(_args) {
1702
- 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"));
1703
1116
  const cwd = process.cwd();
1704
- const apiDir = resolve7(cwd, "apps", "api");
1705
- if (existsSync6(resolve7(apiDir, "package.json"))) {
1117
+ const apiDir = resolve5(cwd, "apps", "api");
1118
+ if (existsSync4(resolve5(apiDir, "package.json"))) {
1706
1119
  const result = spawnSync7("npm", ["run", "db:migrate:local"], {
1707
1120
  stdio: "inherit",
1708
1121
  cwd: apiDir,
1709
1122
  shell: true
1710
1123
  });
1711
1124
  if (result.status !== 0) {
1712
- console.log(pc14.red("\n \u2717 Failed to apply migrations."));
1125
+ console.log(pc13.red("\n \u2717 Failed to apply migrations."));
1713
1126
  process.exit(1);
1714
1127
  return;
1715
1128
  }
1716
- } 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"))) {
1717
1133
  const result = spawnSync7("node", ["scripts/bootstrap-d1.mjs"], {
1718
1134
  stdio: "inherit",
1719
1135
  cwd,
1720
1136
  shell: true
1721
1137
  });
1722
1138
  if (result.status !== 0) {
1723
- console.log(pc14.red("\n \u2717 Failed to apply migrations."));
1139
+ console.log(pc13.red("\n \u2717 Failed to apply migrations."));
1724
1140
  process.exit(1);
1725
1141
  return;
1726
1142
  }
1727
- } else {
1728
- console.log(pc14.yellow(" \u26A0 Could not find database migration script."));
1729
- process.exit(1);
1143
+ console.log(pc13.green("\n \u2713 Migrations applied successfully."));
1730
1144
  return;
1731
1145
  }
1732
- 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
+ }
1733
1176
  }
1734
1177
 
1735
1178
  // src/commands/dev.ts
1736
- import pc15 from "picocolors";
1179
+ import pc14 from "picocolors";
1737
1180
  import { spawnSync as spawnSync8 } from "node:child_process";
1738
- import { existsSync as existsSync7 } from "node:fs";
1739
- import { resolve as resolve8 } from "node:path";
1181
+ import { existsSync as existsSync5 } from "node:fs";
1182
+ import { resolve as resolve6 } from "node:path";
1740
1183
  async function dev(args) {
1741
- 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"));
1742
1185
  const cwd = process.cwd();
1743
- const devScript = resolve8(cwd, "scripts", "dev.mjs");
1744
- if (!existsSync7(devScript)) {
1745
- 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)."));
1746
1189
  process.exit(1);
1747
1190
  return;
1748
1191
  }
@@ -1763,7 +1206,7 @@ async function dev(args) {
1763
1206
  }
1764
1207
 
1765
1208
  // src/commands/dev-stop.ts
1766
- import pc16 from "picocolors";
1209
+ import pc15 from "picocolors";
1767
1210
  import { spawnSync as spawnSync9 } from "node:child_process";
1768
1211
  function isDockerInstalled2() {
1769
1212
  try {
@@ -1782,44 +1225,44 @@ function isDockerRunning2() {
1782
1225
  }
1783
1226
  }
1784
1227
  async function devStop() {
1785
- 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"));
1786
1229
  if (!isDockerInstalled2()) {
1787
- 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."));
1788
1231
  process.exit(1);
1789
1232
  return;
1790
1233
  }
1791
1234
  if (!isDockerRunning2()) {
1792
- 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."));
1793
1236
  process.exit(1);
1794
1237
  return;
1795
1238
  }
1796
- console.log(pc16.dim(" Stopping Docker containers\u2026\n"));
1239
+ console.log(pc15.dim(" Stopping Docker containers\u2026\n"));
1797
1240
  const result = spawnSync9("docker", ["compose", "-f", "docker/docker-compose.yml", "stop"], {
1798
1241
  stdio: "inherit",
1799
1242
  cwd: process.cwd(),
1800
1243
  shell: true
1801
1244
  });
1802
1245
  if (result.status === 0) {
1803
- console.log(pc16.green("\n \u2713 Docker containers stopped."));
1246
+ console.log(pc15.green("\n \u2713 Docker containers stopped."));
1804
1247
  } else {
1805
- console.log(pc16.red("\n \u2717 Failed to stop Docker containers."));
1248
+ console.log(pc15.red("\n \u2717 Failed to stop Docker containers."));
1806
1249
  process.exit(1);
1807
1250
  return;
1808
1251
  }
1809
1252
  }
1810
1253
 
1811
1254
  // src/commands/dev-tunnel.ts
1812
- import pc17 from "picocolors";
1255
+ import pc16 from "picocolors";
1813
1256
  import { spawnSync as spawnSync10 } from "node:child_process";
1814
1257
  async function devTunnel() {
1815
- 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"));
1816
1259
  const result = spawnSync10("docker", ["compose", "-f", "docker/docker-compose.yml", "logs", "tunnel"], {
1817
1260
  encoding: "utf-8",
1818
1261
  cwd: process.cwd(),
1819
1262
  shell: true
1820
1263
  });
1821
1264
  if (result.status !== 0) {
1822
- console.log(pc17.red(" \u2717 Failed to retrieve tunnel logs."));
1265
+ console.log(pc16.red(" \u2717 Failed to retrieve tunnel logs."));
1823
1266
  process.exit(1);
1824
1267
  return;
1825
1268
  }
@@ -1827,38 +1270,38 @@ async function devTunnel() {
1827
1270
  const match = logs2.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/g);
1828
1271
  if (match && match.length > 0) {
1829
1272
  const url = match[match.length - 1];
1830
- 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)}`));
1831
1274
  } else {
1832
- console.log(pc17.yellow(" \u26A0 No active Cloudflare Tunnel URL found in logs."));
1833
- 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`)."));
1834
1277
  }
1835
1278
  }
1836
1279
 
1837
1280
  // src/commands/mailpit-clear.ts
1838
- import pc18 from "picocolors";
1281
+ import pc17 from "picocolors";
1839
1282
  async function mailpitClear() {
1840
- 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"));
1841
1284
  try {
1842
1285
  const res = await fetch("http://localhost:8025/api/v1/messages", {
1843
1286
  method: "DELETE"
1844
1287
  });
1845
1288
  if (res.ok) {
1846
- console.log(pc18.green(" \u2713 Mailpit inbox cleared successfully."));
1289
+ console.log(pc17.green(" \u2713 Mailpit inbox cleared successfully."));
1847
1290
  } else {
1848
- 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}`));
1849
1292
  process.exit(1);
1850
1293
  return;
1851
1294
  }
1852
1295
  } catch (err) {
1853
- console.log(pc18.red(` \u2717 Error connecting to Mailpit: ${err.message}`));
1854
- 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)."));
1855
1298
  process.exit(1);
1856
1299
  return;
1857
1300
  }
1858
1301
  }
1859
1302
 
1860
1303
  // src/commands/logs.ts
1861
- import pc19 from "picocolors";
1304
+ import pc18 from "picocolors";
1862
1305
  import { spawnSync as spawnSync11 } from "node:child_process";
1863
1306
  var SERVICE_MAP = {
1864
1307
  mailpit: "mailpit",
@@ -1871,18 +1314,18 @@ var SERVICE_MAP = {
1871
1314
  async function logs(args) {
1872
1315
  const inputService = args.service?.toLowerCase();
1873
1316
  if (!inputService || !SERVICE_MAP[inputService]) {
1874
- console.log(pc19.red("\n \u2717 Error: Please specify a valid service name."));
1875
- console.log(pc19.dim("\n Accepted services:"));
1876
- console.log(` - ${pc19.cyan("mailpit")}`);
1877
- console.log(` - ${pc19.cyan("db")} / ${pc19.cyan("sqlite")}`);
1878
- console.log(` - ${pc19.cyan("tunnel")}`);
1879
- 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")}
1880
1323
  `);
1881
1324
  process.exit(1);
1882
1325
  return;
1883
1326
  }
1884
1327
  const service = SERVICE_MAP[inputService];
1885
- console.log(pc19.cyan(`
1328
+ console.log(pc18.cyan(`
1886
1329
  beech logs ${inputService} \u2014 streaming logs for ${service}\u2026
1887
1330
  `));
1888
1331
  const result = spawnSync11("docker", ["compose", "-f", "docker/docker-compose.yml", "logs", "-f", service], {
@@ -1897,22 +1340,22 @@ async function logs(args) {
1897
1340
  }
1898
1341
 
1899
1342
  // src/commands/test.ts
1900
- import pc20 from "picocolors";
1343
+ import pc19 from "picocolors";
1901
1344
  import { spawnSync as spawnSync12 } from "node:child_process";
1902
- import { existsSync as existsSync8 } from "node:fs";
1903
- import { resolve as resolve9 } from "node:path";
1345
+ import { existsSync as existsSync6 } from "node:fs";
1346
+ import { resolve as resolve7 } from "node:path";
1904
1347
  async function test(args) {
1905
- 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"));
1906
1349
  const cwd = process.cwd();
1907
1350
  let command = "turbo";
1908
1351
  let commandArgs = ["run", "test"];
1909
1352
  if (args.diff) {
1910
- const diffScript = resolve9(cwd, "scripts", "test-coverage-diff.mjs");
1911
- if (existsSync8(diffScript)) {
1353
+ const diffScript = resolve7(cwd, "scripts", "test-coverage-diff.mjs");
1354
+ if (existsSync6(diffScript)) {
1912
1355
  command = "node";
1913
1356
  commandArgs = ["scripts/test-coverage-diff.mjs"];
1914
1357
  } else {
1915
- 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)."));
1916
1359
  process.exit(1);
1917
1360
  return;
1918
1361
  }
@@ -1931,10 +1374,10 @@ async function test(args) {
1931
1374
  }
1932
1375
 
1933
1376
  // src/commands/lint.ts
1934
- import pc21 from "picocolors";
1377
+ import pc20 from "picocolors";
1935
1378
  import { spawnSync as spawnSync13 } from "node:child_process";
1936
1379
  async function lint() {
1937
- 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"));
1938
1381
  const result = spawnSync13("turbo", ["run", "lint"], {
1939
1382
  stdio: "inherit",
1940
1383
  cwd: process.cwd(),
@@ -1946,11 +1389,13 @@ async function lint() {
1946
1389
  }
1947
1390
 
1948
1391
  // src/commands/doctor.ts
1949
- import pc22 from "picocolors";
1392
+ import pc21 from "picocolors";
1950
1393
  import { spawnSync as spawnSync14 } from "node:child_process";
1951
1394
  async function doctor() {
1952
- console.log(pc22.cyan("\n beech doctor \u2014 React diagnostics\n"));
1953
- 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, {
1954
1399
  stdio: "inherit",
1955
1400
  cwd: process.cwd(),
1956
1401
  shell: true
@@ -1959,6 +1404,917 @@ async function doctor() {
1959
1404
  process.exit(result.status ?? 1);
1960
1405
  }
1961
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
+ }
1962
2318
  export {
1963
2319
  dbMigrate,
1964
2320
  dbReset,
@@ -1968,6 +2324,7 @@ export {
1968
2324
  devStop,
1969
2325
  devTunnel,
1970
2326
  doctor,
2327
+ forms,
1971
2328
  generateTypes,
1972
2329
  init,
1973
2330
  lint,
@@ -1978,6 +2335,7 @@ export {
1978
2335
  schemaDiff,
1979
2336
  seedCreate,
1980
2337
  seedLoad,
2338
+ setupCloudflare,
1981
2339
  test,
1982
2340
  update,
1983
2341
  validate,