@beechcms/cli 0.4.0-preview.11 → 0.4.0-preview.13

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
@@ -1,7 +1,7 @@
1
1
  // src/commands/seed-load.ts
2
- import pc from "picocolors";
2
+ import pc2 from "picocolors";
3
3
  import {
4
- SEED_REGISTRY,
4
+ SEED_REGISTRY as SEED_REGISTRY2,
5
5
  generateCreateTable,
6
6
  generateDraftTable,
7
7
  generateIndexes,
@@ -126,6 +126,68 @@ async function diffSeed(seed, options) {
126
126
  return { slug: seed.slug, tableExists: true, columns };
127
127
  }
128
128
 
129
+ // src/commands/validate.ts
130
+ import pc from "picocolors";
131
+ import { SEED_REGISTRY } from "@beechcms/core";
132
+ function validateSeeds(registry) {
133
+ const result = [];
134
+ const slugsSeen = /* @__PURE__ */ new Set();
135
+ for (const seed of Object.values(registry)) {
136
+ const messages = [];
137
+ if (slugsSeen.has(seed.slug)) {
138
+ messages.push(`duplicate slug "${seed.slug}" \u2014 each seed must have a unique slug`);
139
+ }
140
+ slugsSeen.add(seed.slug);
141
+ const aliasesSeen = /* @__PURE__ */ new Set();
142
+ for (const branch of seed.branches) {
143
+ if (aliasesSeen.has(branch.alias)) {
144
+ messages.push(`duplicate branch alias "${branch.alias}"`);
145
+ }
146
+ aliasesSeen.add(branch.alias);
147
+ }
148
+ const allAliases = new Set(seed.branches.map((b) => b.alias));
149
+ if (!allAliases.has(seed.displayNameAlias)) {
150
+ messages.push(`displayNameAlias "${seed.displayNameAlias}" not found in branches`);
151
+ }
152
+ if (messages.length > 0) {
153
+ result.push({ slug: seed.slug, messages });
154
+ }
155
+ }
156
+ return result;
157
+ }
158
+ async function validate(args) {
159
+ const registry = args.registry ?? SEED_REGISTRY;
160
+ if (Object.keys(registry).length === 0) {
161
+ console.warn(pc.yellow("\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n"));
162
+ return;
163
+ }
164
+ console.log(pc.cyan("\n beech validate \u2014 checking seeds\n"));
165
+ const errors = validateSeeds(registry);
166
+ const errorMap = new Map(errors.map((e) => [e.slug, e.messages]));
167
+ let totalIssues = 0;
168
+ for (const seed of Object.values(registry)) {
169
+ const msgs = errorMap.get(seed.slug);
170
+ if (!msgs) {
171
+ console.log(pc.green(` \u2713 ${seed.slug}`));
172
+ } else {
173
+ totalIssues += msgs.length;
174
+ console.log(pc.red(` \u2717 ${seed.slug}`));
175
+ for (const msg of msgs) {
176
+ console.log(pc.red(` \u2192 ${msg}`));
177
+ }
178
+ }
179
+ }
180
+ console.log("");
181
+ if (totalIssues > 0) {
182
+ const s = totalIssues !== 1 ? "s" : "";
183
+ console.log(pc.red(` Found ${totalIssues} issue${s}. Fix the seeds above before loading.
184
+ `));
185
+ process.exit(1);
186
+ } else {
187
+ console.log(pc.green(" All seeds valid.\n"));
188
+ }
189
+ }
190
+
129
191
  // src/commands/seed-load.ts
130
192
  function buildStatements(seed) {
131
193
  const stmts = [generateCreateTable(seed), ...generateIndexes(seed)];
@@ -139,71 +201,80 @@ function buildStatements(seed) {
139
201
  }
140
202
  async function runDiff(options, registry) {
141
203
  const seeds = Object.values(registry);
142
- console.log(pc.cyan("\n Diffing schema\u2026\n"));
204
+ console.log(pc2.cyan("\n Diffing schema\u2026\n"));
143
205
  let allOk = true;
144
206
  for (const seed of seeds) {
145
207
  const result = await diffSeed(seed, options);
146
208
  const tableName = `content_${seed.slug}`;
147
209
  if (!result.tableExists) {
148
- console.log(pc.red(` \u2717 ${tableName} \u2014 table missing`));
210
+ console.log(pc2.red(` \u2717 ${tableName} \u2014 table missing`));
149
211
  allOk = false;
150
212
  continue;
151
213
  }
152
214
  const problems = result.columns.filter((c) => c.status !== "ok");
153
215
  if (problems.length === 0) {
154
- console.log(pc.green(` \u2713 ${tableName}`));
216
+ console.log(pc2.green(` \u2713 ${tableName}`));
155
217
  continue;
156
218
  }
157
219
  allOk = false;
158
- console.log(pc.yellow(` \u26A0 ${tableName}`));
220
+ console.log(pc2.yellow(` \u26A0 ${tableName}`));
159
221
  for (const col of problems) {
160
222
  if (col.status === "missing") {
161
- console.log(pc.red(` + missing column: ${col.name} ${col.expectedType}`));
223
+ console.log(pc2.red(` + missing column: ${col.name} ${col.expectedType}`));
162
224
  } else if (col.status === "extra") {
163
- console.log(pc.dim(` ~ extra column: ${col.name} ${col.actualType}`));
225
+ console.log(pc2.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) \u2014 exists in DB but not in seeds.ts`));
164
226
  } else if (col.status === "type_mismatch") {
165
- console.log(pc.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
227
+ console.log(pc2.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
166
228
  }
167
229
  }
168
230
  }
169
231
  console.log("");
170
232
  if (allOk) {
171
- console.log(pc.green(" Schema matches seeds. No action needed.\n"));
233
+ console.log(pc2.green(" Schema matches seeds. No action needed.\n"));
172
234
  } else {
173
- console.log(pc.yellow(" Run `beech seed:load` to apply missing tables/columns.\n"));
235
+ console.log(pc2.yellow(" Run `beech seed:load` to apply missing tables/columns.\n"));
174
236
  }
175
237
  }
176
238
  async function runLoad(options, dryRun, registry) {
177
239
  const seeds = Object.values(registry);
178
240
  if (dryRun) {
179
- console.log(pc.cyan("\n -- dry-run: SQL that would be executed\n"));
241
+ console.log(pc2.cyan("\n -- dry-run: SQL that would be executed\n"));
180
242
  for (const seed of seeds) {
181
243
  const stmts = buildStatements(seed);
182
- console.log(pc.dim(` -- content_${seed.slug}`));
244
+ console.log(pc2.dim(` -- content_${seed.slug}`));
183
245
  for (const stmt of stmts) {
184
246
  console.log(stmt + "\n");
185
247
  }
186
248
  }
187
249
  return;
188
250
  }
189
- console.log(pc.cyan(`
251
+ console.log(pc2.cyan(`
190
252
  Loading seeds into ${options.local ? "local" : "remote"} D1 (${options.db})\u2026
191
253
  `));
192
254
  for (const seed of seeds) {
193
255
  const stmts = buildStatements(seed);
194
256
  const sql = stmts.join("\n\n") + "\n";
195
- process.stdout.write(` ${pc.dim("\u2192")} content_${seed.slug}\u2026 `);
257
+ process.stdout.write(` ${pc2.dim("\u2192")} content_${seed.slug}\u2026 `);
196
258
  executeD1File(sql, options);
197
- console.log(pc.green("done"));
259
+ console.log(pc2.green("done"));
198
260
  }
199
- console.log(pc.green("\n All seeds loaded.\n"));
261
+ console.log(pc2.green("\n All seeds loaded.\n"));
200
262
  }
201
263
  async function seedLoad(args) {
202
- const registry = args.registry ?? SEED_REGISTRY;
264
+ const registry = args.registry ?? SEED_REGISTRY2;
203
265
  if (Object.keys(registry).length === 0) {
204
- console.warn(pc.yellow("\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n"));
266
+ console.warn(pc2.yellow("\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n"));
205
267
  return;
206
268
  }
269
+ const validationErrors = validateSeeds(registry);
270
+ if (validationErrors.length > 0) {
271
+ const total = validationErrors.reduce((n, e) => n + e.messages.length, 0);
272
+ const s = total !== 1 ? "s" : "";
273
+ console.log(pc2.yellow(`
274
+ \u26A0 Seed validation found ${total} issue${s}. Schema changes will still be applied.
275
+ `));
276
+ console.log(pc2.dim(' Run "npx beech validate" for details.\n'));
277
+ }
207
278
  const configPath = findWranglerConfig();
208
279
  const db = args.db ?? resolveDbName(configPath);
209
280
  const options = {
@@ -217,6 +288,596 @@ async function seedLoad(args) {
217
288
  await runLoad(options, args.dryRun, registry);
218
289
  }
219
290
  }
291
+
292
+ // src/commands/init.ts
293
+ import pc3 from "picocolors";
294
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
295
+ import { resolve as resolve2, basename } from "node:path";
296
+ import { spawnSync as spawnSync2 } from "node:child_process";
297
+ var SYSTEM_TABLES = [
298
+ "users",
299
+ "refresh_tokens",
300
+ "password_reset_tokens",
301
+ "public_idempotency_keys",
302
+ "analytics",
303
+ "system_stats",
304
+ "activity_logs",
305
+ "notifications",
306
+ "media_objects",
307
+ "content_event_log"
308
+ ];
309
+ var BASE_SCHEMA_SQL = `
310
+ CREATE TABLE IF NOT EXISTS users (
311
+ id TEXT NOT NULL PRIMARY KEY,
312
+ email TEXT NOT NULL UNIQUE,
313
+ password_hash TEXT NOT NULL,
314
+ role TEXT NOT NULL DEFAULT 'editor' CHECK (role IN ('admin', 'editor')),
315
+ name TEXT,
316
+ avatar_url TEXT,
317
+ notification_prefs TEXT NOT NULL DEFAULT '{}',
318
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
319
+ );
320
+
321
+ CREATE TABLE IF NOT EXISTS refresh_tokens (
322
+ id TEXT NOT NULL PRIMARY KEY,
323
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
324
+ token_hash TEXT NOT NULL,
325
+ expires_at INTEGER NOT NULL,
326
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
327
+ revoked_at INTEGER DEFAULT NULL
328
+ );
329
+
330
+ CREATE INDEX IF NOT EXISTS idx_refresh_user ON refresh_tokens(user_id);
331
+ CREATE INDEX IF NOT EXISTS idx_refresh_hash ON refresh_tokens(token_hash);
332
+ CREATE INDEX IF NOT EXISTS idx_refresh_expires ON refresh_tokens(expires_at);
333
+
334
+ CREATE TABLE IF NOT EXISTS password_reset_tokens (
335
+ id TEXT NOT NULL PRIMARY KEY,
336
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
337
+ token_hash TEXT NOT NULL,
338
+ expires_at INTEGER NOT NULL,
339
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
340
+ used_at INTEGER DEFAULT NULL
341
+ );
342
+
343
+ CREATE INDEX IF NOT EXISTS idx_prt_hash ON password_reset_tokens(token_hash);
344
+ CREATE INDEX IF NOT EXISTS idx_prt_user ON password_reset_tokens(user_id);
345
+
346
+ CREATE TABLE IF NOT EXISTS public_idempotency_keys (
347
+ idempotency_key TEXT NOT NULL PRIMARY KEY,
348
+ request_fingerprint TEXT NOT NULL,
349
+ response_status INTEGER NOT NULL,
350
+ response_body TEXT NOT NULL,
351
+ created_at INTEGER NOT NULL,
352
+ expires_at INTEGER NOT NULL
353
+ );
354
+
355
+ CREATE INDEX IF NOT EXISTS idx_idempotency_expires ON public_idempotency_keys(expires_at);
356
+
357
+ CREATE TABLE IF NOT EXISTS analytics (
358
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
359
+ day_ts INTEGER NOT NULL,
360
+ metric TEXT NOT NULL,
361
+ seed TEXT NOT NULL DEFAULT '',
362
+ value INTEGER NOT NULL DEFAULT 0,
363
+ UNIQUE(day_ts, metric, seed)
364
+ );
365
+
366
+ CREATE INDEX IF NOT EXISTS idx_analytics_day ON analytics(day_ts);
367
+ CREATE INDEX IF NOT EXISTS idx_analytics_seed ON analytics(seed, day_ts);
368
+
369
+ CREATE TABLE IF NOT EXISTS system_stats (
370
+ id TEXT NOT NULL PRIMARY KEY,
371
+ value TEXT NOT NULL
372
+ );
373
+
374
+ INSERT OR IGNORE INTO system_stats (id, value) VALUES ('total_storage_bytes', '0');
375
+
376
+ CREATE TABLE IF NOT EXISTS activity_logs (
377
+ id TEXT NOT NULL PRIMARY KEY,
378
+ user_id TEXT NOT NULL,
379
+ user_email TEXT NOT NULL,
380
+ user_name TEXT,
381
+ action TEXT NOT NULL,
382
+ entity_type TEXT NOT NULL,
383
+ entity_id TEXT NOT NULL,
384
+ entity_slug TEXT,
385
+ details TEXT,
386
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
387
+ );
388
+
389
+ CREATE INDEX IF NOT EXISTS idx_activity_user ON activity_logs(user_id);
390
+ CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_logs(created_at);
391
+
392
+ CREATE TABLE IF NOT EXISTS notifications (
393
+ id TEXT NOT NULL PRIMARY KEY,
394
+ title TEXT NOT NULL,
395
+ message TEXT NOT NULL,
396
+ type TEXT NOT NULL DEFAULT 'info' CHECK (type IN ('info', 'warning', 'error')),
397
+ is_read INTEGER NOT NULL DEFAULT 0 CHECK (is_read IN (0, 1)),
398
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
399
+ );
400
+
401
+ CREATE INDEX IF NOT EXISTS idx_notifications_created ON notifications(created_at);
402
+ CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(is_read);
403
+
404
+ CREATE TABLE IF NOT EXISTS media_objects (
405
+ key TEXT NOT NULL PRIMARY KEY,
406
+ filename TEXT NOT NULL,
407
+ mime_type TEXT NOT NULL,
408
+ size_bytes INTEGER NOT NULL,
409
+ uploaded_by TEXT NOT NULL DEFAULT '',
410
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
411
+ );
412
+
413
+ CREATE INDEX IF NOT EXISTS idx_media_user ON media_objects(uploaded_by);
414
+ CREATE INDEX IF NOT EXISTS idx_media_created ON media_objects(created_at DESC);
415
+
416
+ CREATE TABLE IF NOT EXISTS content_event_log (
417
+ id TEXT NOT NULL PRIMARY KEY,
418
+ schema_slug TEXT NOT NULL,
419
+ entry_id TEXT NOT NULL,
420
+ action TEXT NOT NULL CHECK (action IN ('create', 'update', 'delete')),
421
+ user_id TEXT,
422
+ details TEXT,
423
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
424
+ );
425
+
426
+ CREATE INDEX IF NOT EXISTS idx_event_log_schema_slug ON content_event_log(schema_slug);
427
+ CREATE INDEX IF NOT EXISTS idx_event_log_created_at ON content_event_log(created_at DESC);
428
+ CREATE INDEX IF NOT EXISTS idx_event_log_entry_id ON content_event_log(entry_id);
429
+ `.trim();
430
+ var PLACEHOLDER_DB_IDS = [
431
+ "INCOLLA_QUI_IL_TUO_ID_D1",
432
+ "FILL_IN_YOUR_D1_DATABASE_ID",
433
+ "YOUR_D1_DATABASE_ID"
434
+ ];
435
+ function checkWranglerAuth() {
436
+ const result = spawnSync2("npx", ["wrangler", "whoami", "--json"], {
437
+ encoding: "utf-8",
438
+ cwd: process.cwd(),
439
+ shell: true
440
+ });
441
+ return result.status === 0;
442
+ }
443
+ function checkWranglerPlaceholders(configPath) {
444
+ try {
445
+ const raw = readFileSync2(configPath, "utf-8");
446
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
447
+ const parsed = JSON.parse(stripped);
448
+ const bindings = parsed?.d1_databases ?? [];
449
+ const issues = [];
450
+ for (const b of bindings) {
451
+ const id = b.database_id ?? "";
452
+ if (!id || PLACEHOLDER_DB_IDS.includes(id)) {
453
+ issues.push(`d1_databases[0].database_id is "${id || "(empty)"}"`);
454
+ }
455
+ }
456
+ return issues;
457
+ } catch {
458
+ return [];
459
+ }
460
+ }
461
+ function echoApiKeys(configPath) {
462
+ if (!configPath) return;
463
+ try {
464
+ const raw = readFileSync2(configPath, "utf-8");
465
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
466
+ const parsed = JSON.parse(stripped);
467
+ const vars = parsed?.vars ?? {};
468
+ const readKey = vars["PUBLIC_READ_API_KEY"];
469
+ const writeKey = vars["PUBLIC_WRITE_API_KEY"];
470
+ if (!readKey && !writeKey) return;
471
+ console.log(pc3.dim(" API keys detected in wrangler.jsonc:\n"));
472
+ if (readKey) {
473
+ const masked = readKey.length > 8 ? readKey.slice(0, 4) + "****" + readKey.slice(-4) : "****";
474
+ console.log(pc3.dim(` PUBLIC_READ_API_KEY = ${masked}`));
475
+ }
476
+ if (writeKey) {
477
+ const masked = writeKey.length > 8 ? writeKey.slice(0, 4) + "****" + writeKey.slice(-4) : "****";
478
+ console.log(pc3.dim(` PUBLIC_WRITE_API_KEY = ${masked}`));
479
+ }
480
+ console.log(pc3.dim("\n Use these in your frontend as the X-API-Key header.\n"));
481
+ } catch {
482
+ }
483
+ }
484
+ function checkFiles(cwd, checkDevVars) {
485
+ let ok = true;
486
+ const workerExists = existsSync2(resolve2(cwd, "worker.ts")) || existsSync2(resolve2(cwd, "worker.js"));
487
+ if (!workerExists) {
488
+ console.log(pc3.red(" \u2717 worker.ts \u2014 missing (required)"));
489
+ ok = false;
490
+ } else {
491
+ console.log(pc3.green(" \u2713 worker.ts"));
492
+ }
493
+ const configPath = findWranglerConfig();
494
+ const configInCwd = configPath && (configPath === resolve2(cwd, "wrangler.jsonc") || configPath === resolve2(cwd, "wrangler.json") || configPath === resolve2(cwd, "wrangler.toml"));
495
+ if (!configInCwd) {
496
+ console.log(pc3.red(" \u2717 wrangler.jsonc \u2014 missing (required)"));
497
+ ok = false;
498
+ } else {
499
+ console.log(pc3.green(` \u2713 ${basename(configPath)}`));
500
+ }
501
+ const seedsExists = existsSync2(resolve2(cwd, "seeds.ts")) || existsSync2(resolve2(cwd, "seeds.js")) || existsSync2(resolve2(cwd, "seed.ts")) || existsSync2(resolve2(cwd, "seed.js"));
502
+ if (!seedsExists) {
503
+ console.log(pc3.yellow(" \u26A0 seeds.ts \u2014 missing (create it, then run beech seed:load)"));
504
+ } else {
505
+ console.log(pc3.green(" \u2713 seeds.ts"));
506
+ }
507
+ if (checkDevVars) {
508
+ if (!existsSync2(resolve2(cwd, ".dev.vars"))) {
509
+ console.log(pc3.dim(" \u25CB .dev.vars \u2014 not found (optional: only needed for production R2 credentials)"));
510
+ } else {
511
+ console.log(pc3.green(" \u2713 .dev.vars"));
512
+ }
513
+ }
514
+ return ok;
515
+ }
516
+ function printNextSteps(local) {
517
+ const localFlag = local ? " --local" : "";
518
+ console.log(pc3.dim(" Next steps:"));
519
+ console.log(pc3.cyan(` 1. npx beech seed:load${localFlag}`));
520
+ console.log(pc3.dim(" \u2192 create content tables from seeds.ts"));
521
+ console.log(pc3.cyan(" 2. npx wrangler dev"));
522
+ console.log(pc3.dim(" \u2192 start API + dashboard"));
523
+ console.log(pc3.dim(" 3. Open http://localhost:8789/admin\n"));
524
+ }
525
+ function getExistingTables(options) {
526
+ try {
527
+ const rows = queryD1(
528
+ `SELECT name FROM sqlite_master WHERE type='table'`,
529
+ options
530
+ );
531
+ return rows.map((r) => r.name);
532
+ } catch {
533
+ return null;
534
+ }
535
+ }
536
+ async function init(args) {
537
+ const cwd = process.cwd();
538
+ console.log(pc3.cyan("\n beech init \u2014 project check\n"));
539
+ const filesOk = checkFiles(cwd, args.local);
540
+ if (!filesOk) {
541
+ console.log(pc3.red("\n \u2717 Required files missing. Fix the errors above before initialising the database.\n"));
542
+ process.exit(1);
543
+ }
544
+ console.log(pc3.green("\n All required files present.\n"));
545
+ if (!args.initDb) {
546
+ echoApiKeys(findWranglerConfig());
547
+ const localFlag = args.local ? " --local" : "";
548
+ console.log(pc3.dim(" Next steps:"));
549
+ console.log(pc3.dim(` 1. npx beech init --db${localFlag} # initialise D1 database`));
550
+ console.log(pc3.dim(` 2. npx beech seed:load${localFlag} # create content tables`));
551
+ console.log(pc3.dim(" 3. npx wrangler dev # start API + dashboard"));
552
+ console.log(pc3.dim(" 4. Open http://localhost:8789/admin\n"));
553
+ return;
554
+ }
555
+ const configPath = findWranglerConfig();
556
+ if (configPath) {
557
+ const placeholders = checkWranglerPlaceholders(configPath);
558
+ if (placeholders.length > 0) {
559
+ console.log(pc3.yellow(" \u26A0 wrangler.jsonc contains placeholder values:\n"));
560
+ for (const issue of placeholders) {
561
+ console.log(pc3.yellow(` - ${issue}`));
562
+ }
563
+ console.log(pc3.dim("\n Update your D1 database_id in wrangler.jsonc,"));
564
+ console.log(pc3.dim(" or create a new database with:"));
565
+ console.log(pc3.cyan("\n npx wrangler d1 create my-project-db\n"));
566
+ console.log(pc3.dim(" Then retry: npx beech init --db\n"));
567
+ process.exit(1);
568
+ }
569
+ }
570
+ if (!args.local) {
571
+ const authed = checkWranglerAuth();
572
+ if (!authed) {
573
+ console.log(pc3.red(" \u2717 Not logged in to Cloudflare\n"));
574
+ console.log(pc3.dim(" BeechCMS needs access to your Cloudflare account to manage the D1 database.\n"));
575
+ console.log(pc3.cyan(" \u2192 Run: npx wrangler login"));
576
+ console.log(pc3.cyan(" \u2192 Then: npx beech init --db\n"));
577
+ process.exit(1);
578
+ }
579
+ }
580
+ const db = args.db ?? resolveDbName(configPath);
581
+ const options = { db, local: args.local, configPath };
582
+ console.log(pc3.cyan(` Checking database "${db}" (${args.local ? "local" : "remote"})\u2026
583
+ `));
584
+ const existingTables = getExistingTables(options);
585
+ const missingTables = SYSTEM_TABLES.filter((t) => !existingTables?.includes(t));
586
+ if (!args.local) {
587
+ if (existingTables === null) {
588
+ console.log(pc3.red(" \u2717 Remote database unreachable\n"));
589
+ console.log(pc3.dim(" Most likely causes:"));
590
+ console.log(pc3.dim(" - Wrong database_id in wrangler.jsonc"));
591
+ console.log(pc3.dim(" - Worker not yet deployed\n"));
592
+ console.log(pc3.dim(" Fix the configuration and re-deploy:\n"));
593
+ console.log(pc3.cyan(" 1. Update d1_databases.database_id in wrangler.jsonc"));
594
+ console.log(pc3.cyan(" 2. npm run deploy\n"));
595
+ process.exit(1);
596
+ }
597
+ if (missingTables.length > 0) {
598
+ console.log(pc3.yellow(` \u26A0 Missing system tables: ${missingTables.join(", ")}
599
+ `));
600
+ console.log(pc3.dim(" Most likely causes:"));
601
+ console.log(pc3.dim(" - Wrong database_id in wrangler.jsonc"));
602
+ console.log(pc3.dim(" - Migrations did not run during deploy\n"));
603
+ console.log(pc3.dim(" Fix the configuration and re-deploy:\n"));
604
+ console.log(pc3.cyan(" 1. Update d1_databases.database_id in wrangler.jsonc"));
605
+ console.log(pc3.cyan(" 2. npm run deploy\n"));
606
+ process.exit(1);
607
+ }
608
+ for (const table of SYSTEM_TABLES) {
609
+ console.log(pc3.green(` \u2713 ${table}`));
610
+ }
611
+ console.log(pc3.green("\n All system tables present. Remote database is initialized.\n"));
612
+ return;
613
+ }
614
+ if (existingTables === null) {
615
+ console.log(pc3.yellow(" Database unreachable or not yet created \u2014 applying base schema\u2026\n"));
616
+ } else if (missingTables.length === 0) {
617
+ console.log(pc3.green(" \u2713 All system tables present. Database already initialised.\n"));
618
+ printNextSteps(args.local);
619
+ return;
620
+ } else {
621
+ console.log(pc3.yellow(` Missing system tables: ${missingTables.join(", ")}`));
622
+ console.log(pc3.cyan("\n Applying base schema\u2026\n"));
623
+ }
624
+ executeD1File(BASE_SCHEMA_SQL, options);
625
+ console.log(pc3.green("\n \u2713 worker.ts"));
626
+ console.log(pc3.green(` \u2713 ${configPath ? basename(configPath) : "wrangler.jsonc"}`));
627
+ console.log(pc3.green(" \u2713 seeds.ts"));
628
+ console.log(pc3.green(" \u2713 Local D1 system tables ready\n"));
629
+ echoApiKeys(configPath);
630
+ printNextSteps(args.local);
631
+ }
632
+
633
+ // src/commands/seed-create.ts
634
+ import pc4 from "picocolors";
635
+ import { createInterface } from "node:readline/promises";
636
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
637
+ import { resolve as resolve3 } from "node:path";
638
+ var BRANCH_TYPES = ["text", "number", "boolean", "date", "richtext", "file", "tags"];
639
+ function slugify(str) {
640
+ return str.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
641
+ }
642
+ function toConstName(slug) {
643
+ return slug.replace(/-/g, "_").toUpperCase() + "_SEED";
644
+ }
645
+ function toLabel(alias) {
646
+ return alias.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
647
+ }
648
+ function generateSeedBlock(slug, label, labelPlural, branches) {
649
+ const displayAlias = branches.find((b) => b.type === "text")?.alias ?? branches[0]?.alias ?? "name";
650
+ const cName = toConstName(slug);
651
+ const branchLines = branches.map((b) => {
652
+ const parts = [
653
+ `alias: '${b.alias}'`,
654
+ `label: '${b.label}'`,
655
+ `type: '${b.type}'`
656
+ ];
657
+ if (b.required) parts.push("requiredOnCreate: true");
658
+ return ` { ${parts.join(", ")} },`;
659
+ });
660
+ return [
661
+ "",
662
+ `export const ${cName} = defineSeed({`,
663
+ ` slug: '${slug}',`,
664
+ ` label: '${label}',`,
665
+ ` labelPlural: '${labelPlural}',`,
666
+ ` displayNameAlias: '${displayAlias}',`,
667
+ " branches: [",
668
+ ...branchLines,
669
+ " ],",
670
+ " dashboard: {",
671
+ " icon: 'Folder',",
672
+ " group: 'Content',",
673
+ " },",
674
+ "})",
675
+ ""
676
+ ].join("\n");
677
+ }
678
+ function ensureDefineSeedImport(content) {
679
+ if (content.includes("defineSeed")) return content;
680
+ return `import { defineSeed } from '@beechcms/core'
681
+ ` + content;
682
+ }
683
+ function tryInsertRegistryEntry(content, slug, cName) {
684
+ const match = content.match(/(SEED_REGISTRY[^{]*\{)([\s\S]*?)(\n\})/m);
685
+ if (!match) return null;
686
+ const [full, open, inner, close] = match;
687
+ const newEntry = `
688
+ ${slug}: ${cName},`;
689
+ return content.replace(full, open + inner + newEntry + close);
690
+ }
691
+ function findSeedsFile() {
692
+ const cwd = process.cwd();
693
+ const searchDirs = [cwd, resolve3(cwd, "apps", "api")];
694
+ for (const dir of searchDirs) {
695
+ for (const name of ["seeds.ts", "seed.ts"]) {
696
+ const p = resolve3(dir, name);
697
+ if (existsSync3(p)) return p;
698
+ }
699
+ }
700
+ return null;
701
+ }
702
+ async function seedCreate(_args) {
703
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
704
+ const ask = async (q, fallback = "") => {
705
+ const hint = fallback ? pc4.dim(` [${fallback}]`) : "";
706
+ const answer = await rl.question(` ${q}${hint}: `);
707
+ return answer.trim() || fallback;
708
+ };
709
+ const askYN = async (q, defaultYes = true) => {
710
+ const hint = defaultYes ? pc4.dim(" (Y/n)") : pc4.dim(" (y/N)");
711
+ const answer = (await rl.question(` ${q}${hint}: `)).trim().toLowerCase();
712
+ if (!answer) return defaultYes;
713
+ return answer === "y" || answer === "yes";
714
+ };
715
+ console.log(pc4.cyan("\n beech seed:create \u2014 new content type wizard\n"));
716
+ try {
717
+ const label = await ask('Content type name (singular, e.g. "Article")');
718
+ if (!label) {
719
+ rl.close();
720
+ console.log(pc4.red("\n \u2717 Name required.\n"));
721
+ process.exit(1);
722
+ }
723
+ const defaultSlug = slugify(label) + "s";
724
+ const slug = slugify(await ask("Slug (plural, used in URL + table name)", defaultSlug)) || defaultSlug;
725
+ const labelPlural = await ask("Plural label", label + "s") || label + "s";
726
+ const branches = [];
727
+ console.log(pc4.dim("\n Now define the fields. Press Enter to accept defaults.\n"));
728
+ let addMore = true;
729
+ while (addMore) {
730
+ console.log(pc4.dim(` \u2500\u2500\u2500 Field ${branches.length + 1} \u2500\u2500\u2500`));
731
+ const alias = await ask(' Alias (camelCase, e.g. "title", "publishedAt")');
732
+ if (!alias) {
733
+ console.log(pc4.yellow(" Alias required \u2014 skipping."));
734
+ addMore = await askYN("\n Add a field?");
735
+ continue;
736
+ }
737
+ const fieldLabel = await ask(" Label", toLabel(alias)) || toLabel(alias);
738
+ const typeList = BRANCH_TYPES.join(" | ");
739
+ const rawType = (await ask(` Type (${typeList})`, "text")).toLowerCase();
740
+ const type = BRANCH_TYPES.includes(rawType) ? rawType : "text";
741
+ const required = await askYN(" Required on create?", false);
742
+ branches.push({ alias, label: fieldLabel, type, required });
743
+ addMore = await askYN("\n Add another field?");
744
+ }
745
+ rl.close();
746
+ if (branches.length === 0) {
747
+ console.log(pc4.yellow("\n No fields defined \u2014 seed not created.\n"));
748
+ process.exit(0);
749
+ }
750
+ const seedBlock = generateSeedBlock(slug, label, labelPlural, branches);
751
+ const cName = toConstName(slug);
752
+ const seedsPath = findSeedsFile();
753
+ if (!seedsPath) {
754
+ console.log(pc4.yellow("\n Could not find seeds.ts. Add this to your seeds file manually:\n"));
755
+ console.log(seedBlock);
756
+ process.exit(0);
757
+ }
758
+ let content = readFileSync3(seedsPath, "utf-8");
759
+ content = ensureDefineSeedImport(content);
760
+ const withSeed = content + seedBlock;
761
+ const withRegistry = tryInsertRegistryEntry(withSeed, slug, cName);
762
+ writeFileSync2(seedsPath, withRegistry ?? withSeed, "utf-8");
763
+ console.log(pc4.green(`
764
+ \u2713 Seed "${slug}" appended to ${seedsPath}
765
+ `));
766
+ if (!withRegistry) {
767
+ console.log(pc4.yellow(` \u26A0 Could not auto-update SEED_REGISTRY \u2014 add this entry manually:
768
+ `));
769
+ console.log(pc4.cyan(` ${slug}: ${cName},
770
+ `));
771
+ }
772
+ console.log(pc4.dim(" Next steps:"));
773
+ console.log(pc4.cyan(" npx beech seed:load --local"));
774
+ console.log(pc4.dim(" \u2192 create the new content table in your local D1 database\n"));
775
+ } catch (err) {
776
+ rl.close();
777
+ throw err;
778
+ }
779
+ }
780
+
781
+ // src/commands/deploy.ts
782
+ import pc5 from "picocolors";
783
+ import { spawnSync as spawnSync3 } from "node:child_process";
784
+ import { readFileSync as readFileSync4 } from "node:fs";
785
+ function readWorkerName(configPath) {
786
+ if (!configPath) return null;
787
+ try {
788
+ const raw = readFileSync4(configPath, "utf-8");
789
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
790
+ const parsed = JSON.parse(stripped);
791
+ return parsed?.name ?? null;
792
+ } catch {
793
+ return null;
794
+ }
795
+ }
796
+ function extractWorkerUrl(output) {
797
+ const match = output.match(/https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.workers\.dev\b/);
798
+ return match?.[0] ?? null;
799
+ }
800
+ async function checkAdmin(url) {
801
+ try {
802
+ const res = await fetch(`${url}/admin`, {
803
+ method: "HEAD",
804
+ redirect: "follow",
805
+ signal: AbortSignal.timeout(12e3)
806
+ });
807
+ return { ok: res.status < 500, status: res.status };
808
+ } catch {
809
+ return { ok: false, status: null };
810
+ }
811
+ }
812
+ async function deploy(args) {
813
+ console.log(pc5.cyan("\n beech deploy\n"));
814
+ console.log(pc5.dim(" [1/3] Deploying Worker\u2026\n"));
815
+ const deployResult = spawnSync3("npm", ["run", "deploy"], {
816
+ stdio: ["inherit", "pipe", "inherit"],
817
+ encoding: "utf-8",
818
+ cwd: process.cwd(),
819
+ shell: true
820
+ });
821
+ const deployStdout = deployResult.stdout ?? "";
822
+ if (deployStdout) process.stdout.write(deployStdout);
823
+ if (deployResult.status !== 0) {
824
+ console.log(pc5.red("\n \u2717 Worker deploy failed\n"));
825
+ console.log(pc5.dim(" Check the output above for wrangler errors."));
826
+ console.log(pc5.dim(" Common causes:"));
827
+ console.log(pc5.dim(" - Not logged in to Cloudflare \u2192 npx wrangler login"));
828
+ console.log(pc5.dim(" - Wrong database_id in wrangler.jsonc\n"));
829
+ process.exit(1);
830
+ }
831
+ const deployedUrl = extractWorkerUrl(deployStdout);
832
+ console.log(pc5.green("\n \u2713 Worker deployed"));
833
+ if (args.skipSeed) {
834
+ console.log(pc5.dim("\n [2/3] Skipping seed:load (--skip-seed)"));
835
+ } else {
836
+ console.log(pc5.dim("\n [2/3] Syncing content schema to remote D1\u2026\n"));
837
+ const seedResult = spawnSync3("npx", ["beech", "seed:load"], {
838
+ stdio: "inherit",
839
+ cwd: process.cwd(),
840
+ shell: true
841
+ });
842
+ if (seedResult.status !== 0) {
843
+ console.log(pc5.yellow("\n \u26A0 seed:load failed \u2014 run manually: npx beech seed:load"));
844
+ } else {
845
+ console.log(pc5.green("\n \u2713 Content schema synced"));
846
+ }
847
+ }
848
+ if (args.skipCheck) {
849
+ console.log(pc5.dim("\n [3/3] Skipping admin check (--skip-check)\n"));
850
+ return;
851
+ }
852
+ const adminBase = deployedUrl ?? (() => {
853
+ const workerName = readWorkerName(findWranglerConfig());
854
+ return workerName ? `https://${workerName}.workers.dev` : null;
855
+ })();
856
+ if (!adminBase) {
857
+ console.log(pc5.dim("\n [3/3] Could not determine worker URL \u2014 skipping admin check\n"));
858
+ console.log(pc5.dim(" The deployed URL is printed by wrangler above. Open <url>/admin to verify.\n"));
859
+ return;
860
+ }
861
+ console.log(pc5.dim(`
862
+ [3/3] Checking ${adminBase}/admin\u2026
863
+ `));
864
+ const { ok, status } = await checkAdmin(adminBase);
865
+ if (ok) {
866
+ console.log(pc5.green(` \u2713 Admin reachable at: ${adminBase}/admin
867
+ `));
868
+ } else {
869
+ const statusStr = status != null ? ` (HTTP ${status})` : "";
870
+ console.log(pc5.yellow(` \u26A0 Admin returned an error${statusStr} at: ${adminBase}/admin
871
+ `));
872
+ console.log(pc5.dim(" The database may not be fully initialized."));
873
+ console.log(pc5.cyan(" \u2192 Run: npx beech init --db --remote\n"));
874
+ }
875
+ }
220
876
  export {
221
- seedLoad
877
+ deploy,
878
+ init,
879
+ seedCreate,
880
+ seedLoad,
881
+ validate,
882
+ validateSeeds
222
883
  };