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