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