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