@beechcms/cli 0.6.0-preview.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
- // src/commands/seed-load.ts
2
- import pc3 from "picocolors";
3
- import {
4
- SEED_REGISTRY as SEED_REGISTRY2,
5
- generateCreateTable,
6
- generateDraftTable,
7
- generateIndexes,
8
- generateFtsTable,
9
- generateFtsTriggers,
10
- generateJunctionTable,
11
- generateJunctionIndexes,
12
- generateJunctionDraftTable,
13
- sortSeedsByDependencies
14
- } from "@beechcms/core";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
15
 
16
16
  // src/lib/wrangler.ts
17
17
  import { spawnSync } from "node:child_process";
@@ -105,363 +105,376 @@ function resolveDbName(configPath) {
105
105
  return "beech-db";
106
106
  }
107
107
  }
108
+ var init_wrangler = __esm({
109
+ "src/lib/wrangler.ts"() {
110
+ "use strict";
111
+ }
112
+ });
108
113
 
109
- // src/lib/schema-diff.ts
110
- import pc from "picocolors";
111
- import { getExpectedColumns } from "@beechcms/core";
112
- function isSeedClean(diff) {
113
- return diff.tableExists && diff.columns.every((c) => c.status === "ok");
114
+ // src/commands/init.ts
115
+ var init_exports = {};
116
+ __export(init_exports, {
117
+ init: () => init
118
+ });
119
+ import pc4 from "picocolors";
120
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
121
+ import { createInterface } from "node:readline/promises";
122
+ import { resolve as resolve2, basename } from "node:path";
123
+ import { spawnSync as spawnSync2 } from "node:child_process";
124
+ function checkWranglerAuth() {
125
+ const result = spawnSync2("npx", ["wrangler", "whoami", "--json"], {
126
+ encoding: "utf-8",
127
+ cwd: process.cwd(),
128
+ shell: true
129
+ });
130
+ return result.status === 0;
114
131
  }
115
- function renderSeedDiff(diff) {
116
- const table = `content_${diff.slug}`;
117
- if (!diff.tableExists) {
118
- console.log(pc.red(` \u2717 ${table} \u2014 table missing`));
119
- return;
120
- }
121
- const problems = diff.columns.filter((c) => c.status !== "ok");
122
- if (problems.length === 0) {
123
- console.log(pc.green(` \u2713 ${table}`));
124
- return;
125
- }
126
- console.log(pc.yellow(` \u26A0 ${table}`));
127
- for (const col of problems) {
128
- switch (col.status) {
129
- case "missing":
130
- console.log(pc.red(` + missing column: ${col.name} ${col.expectedType}`));
131
- break;
132
- case "extra":
133
- console.log(pc.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) \u2014 in DB, not in seeds.ts`));
134
- break;
135
- case "type_mismatch":
136
- console.log(pc.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
137
- break;
138
- case "fk_missing":
139
- console.log(pc.red(` \u292C missing FK: ${col.name} \u2192 content_${col.expectedTarget}(id)`));
140
- break;
141
- case "fk_mismatch":
142
- console.log(pc.yellow(` \u292C FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`));
143
- break;
144
- case "index_missing":
145
- console.log(pc.yellow(` \u2298 missing index on ${col.name}`));
146
- break;
132
+ function checkWranglerPlaceholders(configPath) {
133
+ try {
134
+ const raw = readFileSync2(configPath, "utf-8");
135
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
136
+ const parsed = JSON.parse(stripped);
137
+ const bindings = parsed?.d1_databases ?? [];
138
+ const issues = [];
139
+ for (const b of bindings) {
140
+ const id = b.database_id ?? "";
141
+ if (!id || PLACEHOLDER_DB_IDS.includes(id)) {
142
+ issues.push(`d1_databases[0].database_id is "${id || "(empty)"}"`);
143
+ }
147
144
  }
145
+ return issues;
146
+ } catch {
147
+ return [];
148
148
  }
149
149
  }
150
- async function diffSeed(seed, options) {
151
- const tableName = `content_${seed.slug}`;
152
- const expected = getExpectedColumns(seed);
153
- let actual;
150
+ function readProjectName(configPath) {
151
+ if (!configPath) return basename(process.cwd());
154
152
  try {
155
- actual = queryD1(`PRAGMA table_info(${tableName})`, options);
153
+ const raw = readFileSync2(configPath, "utf-8");
154
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
155
+ const parsed = JSON.parse(stripped);
156
+ return parsed?.name || basename(process.cwd());
156
157
  } catch {
157
- return { slug: seed.slug, tableExists: false, columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType })) };
158
+ return basename(process.cwd());
158
159
  }
159
- if (actual.length === 0) {
160
- return { slug: seed.slug, tableExists: false, columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType })) };
160
+ }
161
+ function readBucketName(configPath) {
162
+ if (!configPath) return null;
163
+ try {
164
+ const raw = readFileSync2(configPath, "utf-8");
165
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
166
+ const parsed = JSON.parse(stripped);
167
+ const buckets = parsed?.r2_buckets ?? [];
168
+ return buckets[0]?.bucket_name ?? null;
169
+ } catch {
170
+ return null;
161
171
  }
162
- const actualMap = new Map(actual.map((r) => [r.name, r]));
163
- const expectedSet = new Set(expected.map((c) => c.name));
164
- const columns = [];
165
- for (const col of expected) {
166
- const actualRow = actualMap.get(col.name);
167
- if (!actualRow) {
168
- columns.push({ name: col.name, status: "missing", expectedType: col.sqlType });
169
- } else if (actualRow.type.toUpperCase() !== col.sqlType) {
170
- columns.push({ name: col.name, status: "type_mismatch", expectedType: col.sqlType, actualType: actualRow.type });
171
- } else {
172
- columns.push({ name: col.name, status: "ok" });
173
- }
172
+ }
173
+ function createD1Database(dbName) {
174
+ const result = spawnSync2("npx", ["wrangler", "d1", "create", dbName, "--json"], {
175
+ encoding: "utf-8",
176
+ cwd: process.cwd(),
177
+ shell: true,
178
+ stdio: ["inherit", "pipe", "pipe"]
179
+ });
180
+ if (result.status !== 0) return null;
181
+ try {
182
+ const parsed = JSON.parse(result.stdout);
183
+ return parsed?.uuid ?? parsed?.database_id ?? null;
184
+ } catch {
185
+ return null;
174
186
  }
175
- for (const row of actual) {
176
- if (!expectedSet.has(row.name)) {
177
- columns.push({ name: row.name, status: "extra", actualType: row.type });
187
+ }
188
+ function createR2Bucket(bucketName) {
189
+ const result = spawnSync2("npx", ["wrangler", "r2", "bucket", "create", bucketName], {
190
+ stdio: "inherit",
191
+ cwd: process.cwd(),
192
+ shell: true
193
+ });
194
+ return result.status === 0;
195
+ }
196
+ function patchWranglerConfig(configPath, dbId) {
197
+ try {
198
+ let raw = readFileSync2(configPath, "utf-8");
199
+ for (const placeholder of PLACEHOLDER_DB_IDS) {
200
+ raw = raw.split(placeholder).join(dbId);
178
201
  }
202
+ raw = raw.replace(/"database_id"\s*:\s*""/g, `"database_id": "${dbId}"`);
203
+ writeFileSync2(configPath, raw, "utf-8");
204
+ return true;
205
+ } catch {
206
+ return false;
179
207
  }
180
- const relationBranches = seed.branches.filter((b) => b.type === "relation" && b.targetSeed);
181
- if (relationBranches.length > 0) {
182
- let fkList = [];
183
- let indexList = [];
184
- try {
185
- fkList = queryD1(`PRAGMA foreign_key_list(${tableName})`, options);
186
- indexList = queryD1(`PRAGMA index_list(${tableName})`, options);
187
- } catch {
188
- }
189
- const fkByCol = /* @__PURE__ */ new Map();
190
- for (const fk of fkList) {
191
- fkByCol.set(fk.from, fk);
208
+ }
209
+ function printManualDbInstructions() {
210
+ console.log(pc4.dim("\n Update your D1 database_id in wrangler.jsonc,"));
211
+ console.log(pc4.dim(" or create a new database with:"));
212
+ console.log(pc4.cyan("\n \u2192 Run: npx wrangler d1 create my-project-db"));
213
+ console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
214
+ }
215
+ function echoApiKeys(configPath) {
216
+ if (!configPath) return;
217
+ try {
218
+ const raw = readFileSync2(configPath, "utf-8");
219
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
220
+ const parsed = JSON.parse(stripped);
221
+ const vars = parsed?.vars ?? {};
222
+ const readKey = vars["PUBLIC_READ_API_KEY"];
223
+ const writeKey = vars["PUBLIC_WRITE_API_KEY"];
224
+ if (!readKey && !writeKey) return;
225
+ console.log(pc4.dim(" API keys detected in wrangler.jsonc:\n"));
226
+ if (readKey) {
227
+ const masked = readKey.length > 8 ? readKey.slice(0, 4) + "****" + readKey.slice(-4) : "****";
228
+ console.log(pc4.dim(` PUBLIC_READ_API_KEY = ${masked}`));
192
229
  }
193
- const indexNames = new Set(indexList.map((i) => i.name));
194
- for (const branch of relationBranches) {
195
- const expectedFkTable = `content_${branch.targetSeed}`;
196
- const expectedOnDelete = (branch.onDelete ?? "SET NULL").toUpperCase();
197
- const expectedIndexName = `idx_${seed.slug}_${branch.alias}`;
198
- const colDiff = columns.find((c) => c.name === branch.alias);
199
- if (!colDiff || colDiff.status === "missing") continue;
200
- const fk = fkByCol.get(branch.alias);
201
- if (!fk) {
202
- colDiff.status = "fk_missing";
203
- colDiff.expectedTarget = branch.targetSeed;
204
- } else {
205
- const actualTable = fk.table;
206
- const actualOnDelete = fk.on_delete.toUpperCase();
207
- if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
208
- colDiff.status = "fk_mismatch";
209
- colDiff.expected = `\u2192 ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`;
210
- colDiff.actual = `\u2192 ${actualTable}(id) ON DELETE ${actualOnDelete}`;
211
- colDiff.expectedTarget = branch.targetSeed;
212
- }
213
- }
214
- if (!indexNames.has(expectedIndexName)) {
215
- if (colDiff.status === "ok") {
216
- colDiff.status = "index_missing";
217
- } else {
218
- columns.push({ name: branch.alias, status: "index_missing" });
219
- }
220
- }
230
+ if (writeKey) {
231
+ const masked = writeKey.length > 8 ? writeKey.slice(0, 4) + "****" + writeKey.slice(-4) : "****";
232
+ console.log(pc4.dim(` PUBLIC_WRITE_API_KEY = ${masked}`));
221
233
  }
234
+ console.log(pc4.dim("\n Use these in your frontend as the X-API-Key header.\n"));
235
+ } catch {
222
236
  }
223
- return { slug: seed.slug, tableExists: true, columns };
224
- }
225
-
226
- // src/commands/validate.ts
227
- import pc2 from "picocolors";
228
- import { SEED_REGISTRY, validateSeedDefinitions } from "@beechcms/core";
229
- function validateSeeds(registry) {
230
- return validateSeedDefinitions(Object.values(registry));
231
237
  }
232
- async function validate(args) {
233
- const registry = args.registry ?? SEED_REGISTRY;
234
- if (Object.keys(registry).length === 0) {
235
- console.warn(pc2.yellow("\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n"));
236
- return;
237
- }
238
- console.log(pc2.cyan("\n beech validate \u2014 checking seeds\n"));
239
- const errors = validateSeeds(registry);
240
- const fatalErrors = errors.filter((e) => e.fatal);
241
- const warnings = errors.filter((e) => !e.fatal);
242
- for (const e of fatalErrors) {
243
- console.log(pc2.red(` \u2717 ${e.slug} (fatal)`));
244
- for (const msg of e.messages) {
245
- console.log(pc2.red(` \u2192 ${msg}`));
246
- }
238
+ function checkFiles(cwd, checkDevVars) {
239
+ let ok = true;
240
+ const workerExists = existsSync2(resolve2(cwd, "worker.ts")) || existsSync2(resolve2(cwd, "worker.js"));
241
+ if (!workerExists) {
242
+ console.log(pc4.red(" \u2717 worker.ts \u2014 missing (required)"));
243
+ ok = false;
244
+ } else {
245
+ console.log(pc4.green(" \u2713 worker.ts"));
247
246
  }
248
- const warningMap = new Map(warnings.map((e) => [e.slug, e.messages]));
249
- const allWarningSlugsSeen = new Set(warnings.map((e) => e.slug));
250
- for (const seed of Object.values(registry)) {
251
- const msgs = warningMap.get(seed.slug);
252
- if (!msgs) {
253
- if (!allWarningSlugsSeen.has(seed.slug)) {
254
- const hasFatal = fatalErrors.some((e) => e.slug === seed.slug);
255
- if (!hasFatal) console.log(pc2.green(` \u2713 ${seed.slug}`));
256
- }
257
- } else {
258
- console.log(pc2.yellow(` \u26A0 ${seed.slug}`));
259
- for (const msg of msgs) {
260
- console.log(pc2.yellow(` \u2192 ${msg}`));
261
- }
262
- }
247
+ const configPath = findWranglerConfig();
248
+ const configInCwd = configPath && (configPath === resolve2(cwd, "wrangler.jsonc") || configPath === resolve2(cwd, "wrangler.json") || configPath === resolve2(cwd, "wrangler.toml"));
249
+ if (!configInCwd) {
250
+ console.log(pc4.red(" \u2717 wrangler.jsonc \u2014 missing (required)"));
251
+ ok = false;
252
+ } else {
253
+ console.log(pc4.green(` \u2713 ${basename(configPath)}`));
263
254
  }
264
- console.log("");
265
- const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
266
- const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0);
267
- if (totalFatal > 0) {
268
- const s = totalFatal !== 1 ? "s" : "";
269
- console.log(pc2.red(` Found ${totalFatal} fatal error${s}. Fix before loading.
270
- `));
271
- process.exit(1);
272
- } else if (totalWarnings > 0) {
273
- const s = totalWarnings !== 1 ? "s" : "";
274
- console.log(pc2.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.
275
- `));
255
+ const seedsExists = existsSync2(resolve2(cwd, "seeds.ts")) || existsSync2(resolve2(cwd, "seeds.js")) || existsSync2(resolve2(cwd, "seed.ts")) || existsSync2(resolve2(cwd, "seed.js"));
256
+ if (!seedsExists) {
257
+ 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)"));
276
258
  } else {
277
- console.log(pc2.green(" All seeds valid.\n"));
259
+ console.log(pc4.green(" \u2713 seeds.ts"));
260
+ }
261
+ if (checkDevVars) {
262
+ if (!existsSync2(resolve2(cwd, ".dev.vars"))) {
263
+ console.log(pc4.dim(" \u25CB .dev.vars \u2014 not found (optional: only needed for production R2 credentials)"));
264
+ } else {
265
+ console.log(pc4.green(" \u2713 .dev.vars"));
266
+ }
278
267
  }
268
+ return ok;
279
269
  }
280
-
281
- // src/commands/seed-load.ts
282
- function buildSeedRegistrationSql(seed) {
283
- const json = sqlQuote(JSON.stringify(seed));
284
- return [
285
- `INSERT INTO seeds (slug, definition, status, source, created_at, updated_at)`,
286
- `VALUES (${sqlQuote(seed.slug)}, ${json}, 'active', 'code', unixepoch(), unixepoch())`,
287
- `ON CONFLICT(slug) DO UPDATE SET definition = excluded.definition, status = 'active', updated_at = excluded.updated_at;`
288
- ].join("\n");
270
+ function printNextSteps(local) {
271
+ const localFlag = local ? " --local" : "";
272
+ console.log(pc4.dim(" Next steps:"));
273
+ console.log(pc4.cyan(` 1. npx beech seed:load${localFlag}`));
274
+ console.log(pc4.dim(" \u2192 create content tables and register seed definitions in D1"));
275
+ console.log(pc4.cyan(" 2. npx wrangler dev"));
276
+ console.log(pc4.dim(" \u2192 start API + dashboard"));
277
+ console.log(pc4.dim(" 3. Open http://localhost:8789/admin\n"));
289
278
  }
290
- var SEED_META_BUMP_SQL = `UPDATE seed_meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE id = 'registry_version';`;
291
- function buildStatements(seed) {
292
- const stmts = [generateCreateTable(seed), ...generateIndexes(seed)];
293
- const draft = generateDraftTable(seed);
294
- if (draft) stmts.push(draft);
295
- const fts = generateFtsTable(seed);
296
- if (fts) {
297
- stmts.push(fts, ...generateFtsTriggers(seed));
298
- }
299
- for (const branch of seed.branches) {
300
- if (branch.type !== "relation" || branch.multiple !== true) continue;
301
- stmts.push(generateJunctionTable(seed, branch), ...generateJunctionIndexes(seed, branch));
302
- const draftJunction = generateJunctionDraftTable(seed, branch);
303
- if (draftJunction) stmts.push(draftJunction);
279
+ function getExistingTables(options) {
280
+ try {
281
+ const rows = queryD1(
282
+ `SELECT name FROM sqlite_master WHERE type='table'`,
283
+ options
284
+ );
285
+ return rows.map((r) => r.name);
286
+ } catch {
287
+ return null;
304
288
  }
305
- return stmts;
306
289
  }
307
- async function runDiff(options, registry) {
308
- const seeds = sortSeedsByDependencies(Object.values(registry));
309
- console.log(pc3.cyan("\n Diffing schema\u2026\n"));
310
- let allOk = true;
311
- for (const seed of seeds) {
312
- const result = await diffSeed(seed, options);
313
- renderSeedDiff(result);
314
- if (!isSeedClean(result)) allOk = false;
315
- }
316
- console.log("");
317
- if (allOk) {
318
- console.log(pc3.green(" Schema matches seeds. No action needed.\n"));
319
- } else {
320
- console.log(pc3.yellow(" Run `beech seed:load` to apply missing tables/columns.\n"));
290
+ async function init(args) {
291
+ const cwd = process.cwd();
292
+ console.log(pc4.cyan("\n beech init \u2014 project check\n"));
293
+ const filesOk = checkFiles(cwd, args.local);
294
+ if (!filesOk) {
295
+ console.log(pc4.red("\n \u2717 Required files missing\n"));
296
+ console.log(pc4.dim(" Fix the errors above before initialising the database."));
297
+ console.log(pc4.cyan("\n \u2192 See: https://beechcms.dev/docs/getting-started\n"));
298
+ process.exit(1);
321
299
  }
322
- }
323
- async function runLoad(options, dryRun, registry) {
324
- const seeds = sortSeedsByDependencies(Object.values(registry));
325
- if (dryRun) {
326
- console.log(pc3.cyan("\n -- dry-run: SQL that would be executed\n"));
327
- for (const seed of seeds) {
328
- const stmts = buildStatements(seed);
329
- console.log(pc3.dim(` -- content_${seed.slug}`));
330
- for (const stmt of stmts) {
331
- console.log(stmt + "\n");
332
- }
333
- console.log(pc3.dim(` -- register ${seed.slug} in seeds table`));
334
- console.log(buildSeedRegistrationSql(seed) + "\n");
335
- }
336
- console.log(pc3.dim(" -- bump registry_version"));
337
- console.log(SEED_META_BUMP_SQL + "\n");
300
+ console.log(pc4.green("\n All required files present.\n"));
301
+ if (!args.initDb) {
302
+ echoApiKeys(findWranglerConfig());
303
+ const localFlag = args.local ? " --local" : "";
304
+ console.log(pc4.dim(" Next steps:"));
305
+ console.log(pc4.dim(` 1. npx beech init --db${localFlag} # initialise D1 database`));
306
+ console.log(pc4.dim(` 2. npx beech seed:load${localFlag} # create content tables`));
307
+ console.log(pc4.dim(" 3. npx wrangler dev # start API + dashboard"));
308
+ console.log(pc4.dim(" 4. Open http://localhost:8789/admin\n"));
338
309
  return;
339
310
  }
340
- console.log(pc3.cyan(`
341
- Loading seeds into ${options.local ? "local" : "remote"} D1 (${options.db})\u2026
342
- `));
343
- for (const seed of seeds) {
344
- const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)];
345
- const sql = stmts.join("\n\n") + "\n";
346
- process.stdout.write(` ${pc3.dim("\u2192")} content_${seed.slug}\u2026 `);
347
- const ok = executeD1File(sql, options);
348
- if (!ok) {
349
- console.log(pc3.red("failed"));
350
- console.log(pc3.red(`
351
- \u2717 Failed to apply schema for content_${seed.slug}
311
+ const configPath = findWranglerConfig();
312
+ if (configPath) {
313
+ const placeholders = checkWranglerPlaceholders(configPath);
314
+ if (placeholders.length > 0) {
315
+ console.log(pc4.yellow(" \u26A0 wrangler.jsonc contains placeholder values:\n"));
316
+ for (const issue of placeholders) {
317
+ console.log(pc4.yellow(` - ${issue}`));
318
+ }
319
+ if (process.stdin.isTTY && !args.nonInteractive) {
320
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
321
+ let autoCreate = false;
322
+ try {
323
+ const answer = (await rl.question(
324
+ pc4.cyan("\n \u2192 Create a new D1 database (and R2 bucket) on Cloudflare automatically? (Y/n): ")
325
+ )).trim().toLowerCase();
326
+ autoCreate = !answer || answer === "y" || answer === "yes";
327
+ } finally {
328
+ rl.close();
329
+ }
330
+ if (autoCreate) {
331
+ const authed = checkWranglerAuth();
332
+ if (!authed) {
333
+ console.log(pc4.red("\n \u2717 Not logged in to Cloudflare\n"));
334
+ console.log(pc4.dim(" BeechCMS needs access to your Cloudflare account to create the database."));
335
+ console.log(pc4.cyan("\n \u2192 Run: npx wrangler login"));
336
+ console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
337
+ process.exit(1);
338
+ }
339
+ const projectName = readProjectName(configPath);
340
+ const dbName = `${projectName}-db`;
341
+ const bucketName = readBucketName(configPath) || `${projectName}-media`;
342
+ console.log(pc4.dim(`
343
+ Creating D1 database "${dbName}"\u2026`));
344
+ const dbId = createD1Database(dbName);
345
+ if (!dbId) {
346
+ console.log(pc4.red("\n \u2717 Failed to create D1 database\n"));
347
+ console.log(pc4.dim(" Create it manually and retry:"));
348
+ console.log(pc4.cyan(`
349
+ \u2192 Run: npx wrangler d1 create ${dbName}`));
350
+ console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
351
+ process.exit(1);
352
+ }
353
+ console.log(pc4.green(` \u2713 D1 database created (id: ${dbId})`));
354
+ console.log(pc4.dim(`
355
+ Creating R2 bucket "${bucketName}"\u2026`));
356
+ const r2Ok = createR2Bucket(bucketName);
357
+ if (r2Ok) {
358
+ console.log(pc4.green(` \u2713 R2 bucket "${bucketName}" created`));
359
+ } else {
360
+ console.log(pc4.yellow(` \u26A0 R2 bucket creation failed (may already exist \u2014 continuing)`));
361
+ }
362
+ console.log(pc4.dim("\n Updating wrangler.jsonc\u2026"));
363
+ const patched = patchWranglerConfig(configPath, dbId);
364
+ if (patched) {
365
+ console.log(pc4.green(" \u2713 wrangler.jsonc updated\n"));
366
+ } else {
367
+ console.log(pc4.yellow(` \u26A0 Could not update wrangler.jsonc automatically
352
368
  `));
353
- console.log(pc3.dim(" wrangler reported an error above."));
354
- console.log(pc3.dim(` Most likely causes:`));
355
- console.log(pc3.dim(` - Database "${options.db}" not found or wrong database_id`));
356
- if (!options.local) {
357
- console.log(pc3.dim(" - Not logged in to Cloudflare"));
358
- console.log(pc3.cyan("\n \u2192 Run: npx wrangler login"));
359
- console.log(pc3.cyan(" \u2192 Then: npx beech seed:load\n"));
369
+ console.log(pc4.dim(` Set database_id = "${dbId}" in wrangler.jsonc manually, then retry:`));
370
+ console.log(pc4.cyan(" \u2192 Run: npx beech init --db\n"));
371
+ process.exit(1);
372
+ }
373
+ } else {
374
+ printManualDbInstructions();
375
+ process.exit(1);
376
+ }
377
+ } else if (args.nonInteractive && args.local) {
378
+ console.log(pc4.dim(" --yes: proceeding with local mode (no remote resources needed)\n"));
360
379
  } else {
361
- console.log(pc3.cyan("\n \u2192 Run: npx beech init --db --local # re-initialise local DB"));
362
- console.log(pc3.cyan(" \u2192 Then: npx beech seed:load --local\n"));
380
+ if (args.nonInteractive) {
381
+ console.log(pc4.red("\n \u2717 Cannot proceed non-interactively: remote database has a placeholder database_id\n"));
382
+ console.log(pc4.dim(" Set a real database_id in wrangler.jsonc, then retry."));
383
+ process.exit(1);
384
+ }
385
+ printManualDbInstructions();
386
+ process.exit(1);
363
387
  }
364
- process.exit(1);
365
388
  }
366
- console.log(pc3.green("done"));
367
- }
368
- executeD1File(SEED_META_BUMP_SQL, options);
369
- console.log(pc3.green("\n All seeds loaded.\n"));
370
- console.log(pc3.dim(" Definitions registered in the database."));
371
- 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"));
372
- }
373
- async function seedLoad(args) {
374
- const registry = args.registry ?? SEED_REGISTRY2;
375
- if (Object.keys(registry).length === 0) {
376
- console.log(pc3.yellow("\n \u2717 No seeds found\n"));
377
- console.log(pc3.dim(" Create a seeds.ts file in your project root with at least one content type."));
378
- console.log(pc3.cyan("\n \u2192 Run: npx beech seed:create\n"));
379
- return;
380
389
  }
381
- const validationErrors = validateSeeds(registry);
382
- const fatalErrors = validationErrors.filter((e) => e.fatal);
383
- const warnings = validationErrors.filter((e) => !e.fatal);
384
- if (fatalErrors.length > 0) {
385
- const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
386
- const s = total !== 1 ? "s" : "";
387
- console.log(pc3.red(`
388
- \u2717 Seed validation found ${total} fatal error${s}. Cannot load schema.
389
- `));
390
- for (const e of fatalErrors) {
391
- console.log(pc3.red(` \u2717 ${e.slug}`));
392
- for (const msg of e.messages) {
393
- console.log(pc3.red(` \u2192 ${msg}`));
394
- }
390
+ if (!args.local) {
391
+ const authed = checkWranglerAuth();
392
+ if (!authed) {
393
+ console.log(pc4.red(" \u2717 Not logged in to Cloudflare\n"));
394
+ console.log(pc4.dim(" BeechCMS needs access to your Cloudflare account to manage the D1 database."));
395
+ console.log(pc4.cyan("\n \u2192 Run: npx wrangler login"));
396
+ console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
397
+ process.exit(1);
395
398
  }
396
- console.log("");
397
- process.exit(1);
398
- }
399
- if (warnings.length > 0) {
400
- const total = warnings.reduce((n, e) => n + e.messages.length, 0);
401
- const s = total !== 1 ? "s" : "";
402
- console.log(pc3.yellow(`
403
- \u26A0 Seed validation found ${total} issue${s}. Schema changes will still be applied.
404
- `));
405
- console.log(pc3.dim(' Run "npx beech validate" for details.\n'));
406
399
  }
407
- const configPath = findWranglerConfig();
408
400
  const db = args.db ?? resolveDbName(configPath);
409
- const options = {
410
- db,
411
- local: args.local,
412
- configPath
413
- };
414
- if (!args.dryRun && !args.diff) {
415
- try {
416
- const rows = queryD1(
417
- `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
418
- options
419
- );
420
- if (rows.length < 2) {
421
- console.log(pc3.red("\n \u2717 System tables not found (seeds, seed_meta)\n"));
422
- console.log(pc3.dim(" Run `beech init --db` first to initialise the database."));
423
- const flag = args.local ? " --local" : "";
424
- console.log(pc3.cyan(`
425
- \u2192 Run: npx beech init --db${flag}
401
+ const options = { db, local: args.local, configPath };
402
+ console.log(pc4.cyan(` Checking database "${db}" (${args.local ? "local" : "remote"})\u2026
426
403
  `));
427
- process.exit(1);
428
- }
429
- } catch {
430
- console.log(pc3.red("\n \u2717 Could not query the database\n"));
431
- console.log(pc3.dim(" Run `beech init --db` first to initialise the database."));
404
+ const existingTables = getExistingTables(options);
405
+ const missingTables = SYSTEM_TABLES.filter((t) => !existingTables?.includes(t));
406
+ if (!args.local) {
407
+ if (existingTables === null) {
408
+ console.log(pc4.red(" \u2717 Remote database unreachable\n"));
409
+ console.log(pc4.dim(" Most likely causes:"));
410
+ console.log(pc4.dim(" - Wrong database_id in wrangler.jsonc"));
411
+ console.log(pc4.dim(" - Worker not yet deployed"));
412
+ console.log(pc4.cyan("\n \u2192 Fix: Update d1_databases.database_id in wrangler.jsonc"));
413
+ console.log(pc4.cyan(" \u2192 Then: npm run deploy\n"));
432
414
  process.exit(1);
433
415
  }
416
+ if (missingTables.length > 0) {
417
+ console.log(pc4.yellow(` \u26A0 Missing system tables: ${missingTables.join(", ")}
418
+ `));
419
+ console.log(pc4.dim(" Most likely causes:"));
420
+ console.log(pc4.dim(" - Wrong database_id in wrangler.jsonc"));
421
+ console.log(pc4.dim(" - Migrations did not run during deploy"));
422
+ console.log(pc4.cyan("\n \u2192 Fix: Update d1_databases.database_id in wrangler.jsonc"));
423
+ console.log(pc4.cyan(" \u2192 Then: npm run deploy\n"));
424
+ process.exit(1);
425
+ }
426
+ for (const table of SYSTEM_TABLES) {
427
+ console.log(pc4.green(` \u2713 ${table}`));
428
+ }
429
+ console.log(pc4.green("\n All system tables present. Remote database is initialized.\n"));
430
+ return;
434
431
  }
435
- if (args.diff) {
436
- await runDiff(options, registry);
432
+ if (existingTables === null) {
433
+ console.log(pc4.yellow(" Database unreachable or not yet created \u2014 applying base schema\u2026\n"));
434
+ } else if (missingTables.length === 0) {
435
+ console.log(pc4.green(" \u2713 All system tables present. Database already initialised.\n"));
436
+ printNextSteps(args.local);
437
+ return;
437
438
  } else {
438
- await runLoad(options, args.dryRun, registry);
439
+ console.log(pc4.yellow(` Missing system tables: ${missingTables.join(", ")}`));
440
+ console.log(pc4.cyan("\n Applying base schema\u2026\n"));
439
441
  }
442
+ const ok = executeD1File(BASE_SCHEMA_SQL, options);
443
+ if (!ok) {
444
+ console.log(pc4.red("\n \u2717 Database initialisation failed\n"));
445
+ console.log(pc4.dim(" wrangler reported an error above."));
446
+ console.log(pc4.cyan("\n \u2192 Run: npx beech init --db --local\n"));
447
+ process.exit(1);
448
+ }
449
+ console.log(pc4.green("\n \u2713 worker.ts"));
450
+ console.log(pc4.green(` \u2713 ${configPath ? basename(configPath) : "wrangler.jsonc"}`));
451
+ console.log(pc4.green(" \u2713 seeds.ts"));
452
+ console.log(pc4.green(" \u2713 Local D1 system tables ready\n"));
453
+ echoApiKeys(configPath);
454
+ printNextSteps(args.local);
440
455
  }
441
-
442
- // src/commands/init.ts
443
- import pc4 from "picocolors";
444
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
445
- import { createInterface } from "node:readline/promises";
446
- import { resolve as resolve2, basename } from "node:path";
447
- import { spawnSync as spawnSync2 } from "node:child_process";
448
- var SYSTEM_TABLES = [
449
- "users",
450
- "refresh_tokens",
451
- "password_reset_tokens",
452
- "public_idempotency_keys",
453
- "analytics",
454
- "system_stats",
455
- "activity_logs",
456
- "notifications",
457
- "media_objects",
458
- "content_event_log",
459
- "automations",
460
- "seeds",
461
- "seed_meta",
462
- "site_settings"
463
- ];
464
- var BASE_SCHEMA_SQL = `
456
+ var SYSTEM_TABLES, BASE_SCHEMA_SQL, PLACEHOLDER_DB_IDS;
457
+ var init_init = __esm({
458
+ "src/commands/init.ts"() {
459
+ "use strict";
460
+ init_wrangler();
461
+ SYSTEM_TABLES = [
462
+ "users",
463
+ "refresh_tokens",
464
+ "password_reset_tokens",
465
+ "public_idempotency_keys",
466
+ "analytics",
467
+ "system_stats",
468
+ "activity_logs",
469
+ "notifications",
470
+ "media_objects",
471
+ "content_event_log",
472
+ "automations",
473
+ "seeds",
474
+ "seed_meta",
475
+ "site_settings"
476
+ ];
477
+ BASE_SCHEMA_SQL = `
465
478
  CREATE TABLE IF NOT EXISTS users (
466
479
  id TEXT NOT NULL PRIMARY KEY,
467
480
  email TEXT NOT NULL UNIQUE,
@@ -623,344 +636,367 @@ CREATE TABLE IF NOT EXISTS site_settings (
623
636
  value TEXT NOT NULL
624
637
  );
625
638
  `.trim();
626
- var PLACEHOLDER_DB_IDS = [
627
- "INCOLLA_QUI_IL_TUO_ID_D1",
628
- "FILL_IN_YOUR_D1_DATABASE_ID",
629
- "YOUR_D1_DATABASE_ID"
630
- ];
631
- function checkWranglerAuth() {
632
- const result = spawnSync2("npx", ["wrangler", "whoami", "--json"], {
633
- encoding: "utf-8",
634
- cwd: process.cwd(),
635
- shell: true
636
- });
637
- return result.status === 0;
639
+ PLACEHOLDER_DB_IDS = [
640
+ "INCOLLA_QUI_IL_TUO_ID_D1",
641
+ "FILL_IN_YOUR_D1_DATABASE_ID",
642
+ "YOUR_D1_DATABASE_ID"
643
+ ];
644
+ }
645
+ });
646
+
647
+ // src/commands/seed-load.ts
648
+ init_wrangler();
649
+ import pc3 from "picocolors";
650
+ import {
651
+ SEED_REGISTRY as SEED_REGISTRY2,
652
+ generateCreateTable,
653
+ generateDraftTable,
654
+ generateIndexes,
655
+ generateFtsTable,
656
+ generateFtsTriggers,
657
+ generateJunctionTable,
658
+ generateJunctionIndexes,
659
+ generateJunctionDraftTable,
660
+ sortSeedsByDependencies
661
+ } from "@beechcms/core";
662
+
663
+ // src/lib/schema-diff.ts
664
+ init_wrangler();
665
+ import pc from "picocolors";
666
+ import { getExpectedColumns } from "@beechcms/core";
667
+ function isSeedClean(diff) {
668
+ return diff.tableExists && diff.columns.every((c) => c.status === "ok");
638
669
  }
639
- function checkWranglerPlaceholders(configPath) {
640
- try {
641
- const raw = readFileSync2(configPath, "utf-8");
642
- const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
643
- const parsed = JSON.parse(stripped);
644
- const bindings = parsed?.d1_databases ?? [];
645
- const issues = [];
646
- for (const b of bindings) {
647
- const id = b.database_id ?? "";
648
- if (!id || PLACEHOLDER_DB_IDS.includes(id)) {
649
- issues.push(`d1_databases[0].database_id is "${id || "(empty)"}"`);
650
- }
670
+ function renderSeedDiff(diff) {
671
+ const table = `content_${diff.slug}`;
672
+ if (!diff.tableExists) {
673
+ console.log(pc.red(` \u2717 ${table} \u2014 table missing`));
674
+ return;
675
+ }
676
+ const problems = diff.columns.filter((c) => c.status !== "ok");
677
+ if (problems.length === 0) {
678
+ console.log(pc.green(` \u2713 ${table}`));
679
+ return;
680
+ }
681
+ console.log(pc.yellow(` \u26A0 ${table}`));
682
+ for (const col of problems) {
683
+ switch (col.status) {
684
+ case "missing":
685
+ console.log(pc.red(` + missing column: ${col.name} ${col.expectedType}`));
686
+ break;
687
+ case "extra":
688
+ console.log(pc.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) \u2014 in DB, not in seeds.ts`));
689
+ break;
690
+ case "type_mismatch":
691
+ console.log(pc.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
692
+ break;
693
+ case "fk_missing":
694
+ console.log(pc.red(` \u292C missing FK: ${col.name} \u2192 content_${col.expectedTarget}(id)`));
695
+ break;
696
+ case "fk_mismatch":
697
+ console.log(pc.yellow(` \u292C FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`));
698
+ break;
699
+ case "index_missing":
700
+ console.log(pc.yellow(` \u2298 missing index on ${col.name}`));
701
+ break;
651
702
  }
652
- return issues;
653
- } catch {
654
- return [];
655
703
  }
656
704
  }
657
- function readProjectName(configPath) {
658
- if (!configPath) return basename(process.cwd());
705
+ async function diffSeed(seed, options) {
706
+ const tableName = `content_${seed.slug}`;
707
+ const expected = getExpectedColumns(seed);
708
+ let actual;
659
709
  try {
660
- const raw = readFileSync2(configPath, "utf-8");
661
- const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
662
- const parsed = JSON.parse(stripped);
663
- return parsed?.name || basename(process.cwd());
710
+ actual = queryD1(`PRAGMA table_info(${tableName})`, options);
664
711
  } catch {
665
- return basename(process.cwd());
712
+ return { slug: seed.slug, tableExists: false, columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType })) };
666
713
  }
667
- }
668
- function readBucketName(configPath) {
669
- if (!configPath) return null;
670
- try {
671
- const raw = readFileSync2(configPath, "utf-8");
672
- const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
673
- const parsed = JSON.parse(stripped);
674
- const buckets = parsed?.r2_buckets ?? [];
675
- return buckets[0]?.bucket_name ?? null;
676
- } catch {
677
- return null;
714
+ if (actual.length === 0) {
715
+ return { slug: seed.slug, tableExists: false, columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType })) };
678
716
  }
679
- }
680
- function createD1Database(dbName) {
681
- const result = spawnSync2("npx", ["wrangler", "d1", "create", dbName, "--json"], {
682
- encoding: "utf-8",
683
- cwd: process.cwd(),
684
- shell: true,
685
- stdio: ["inherit", "pipe", "pipe"]
686
- });
687
- if (result.status !== 0) return null;
688
- try {
689
- const parsed = JSON.parse(result.stdout);
690
- return parsed?.uuid ?? parsed?.database_id ?? null;
691
- } catch {
692
- return null;
717
+ const actualMap = new Map(actual.map((r) => [r.name, r]));
718
+ const expectedSet = new Set(expected.map((c) => c.name));
719
+ const columns = [];
720
+ for (const col of expected) {
721
+ const actualRow = actualMap.get(col.name);
722
+ if (!actualRow) {
723
+ columns.push({ name: col.name, status: "missing", expectedType: col.sqlType });
724
+ } else if (actualRow.type.toUpperCase() !== col.sqlType) {
725
+ columns.push({ name: col.name, status: "type_mismatch", expectedType: col.sqlType, actualType: actualRow.type });
726
+ } else {
727
+ columns.push({ name: col.name, status: "ok" });
728
+ }
693
729
  }
694
- }
695
- function createR2Bucket(bucketName) {
696
- const result = spawnSync2("npx", ["wrangler", "r2", "bucket", "create", bucketName], {
697
- stdio: "inherit",
698
- cwd: process.cwd(),
699
- shell: true
700
- });
701
- return result.status === 0;
702
- }
703
- function patchWranglerConfig(configPath, dbId) {
704
- try {
705
- let raw = readFileSync2(configPath, "utf-8");
706
- for (const placeholder of PLACEHOLDER_DB_IDS) {
707
- raw = raw.split(placeholder).join(dbId);
730
+ for (const row of actual) {
731
+ if (!expectedSet.has(row.name)) {
732
+ columns.push({ name: row.name, status: "extra", actualType: row.type });
733
+ }
734
+ }
735
+ const relationBranches = seed.branches.filter((b) => b.type === "relation" && b.targetSeed);
736
+ if (relationBranches.length > 0) {
737
+ let fkList = [];
738
+ let indexList = [];
739
+ try {
740
+ fkList = queryD1(`PRAGMA foreign_key_list(${tableName})`, options);
741
+ indexList = queryD1(`PRAGMA index_list(${tableName})`, options);
742
+ } catch {
743
+ }
744
+ const fkByCol = /* @__PURE__ */ new Map();
745
+ for (const fk of fkList) {
746
+ fkByCol.set(fk.from, fk);
747
+ }
748
+ const indexNames = new Set(indexList.map((i) => i.name));
749
+ for (const branch of relationBranches) {
750
+ const expectedFkTable = `content_${branch.targetSeed}`;
751
+ const expectedOnDelete = (branch.onDelete ?? "SET NULL").toUpperCase();
752
+ const expectedIndexName = `idx_${seed.slug}_${branch.alias}`;
753
+ const colDiff = columns.find((c) => c.name === branch.alias);
754
+ if (!colDiff || colDiff.status === "missing") continue;
755
+ const fk = fkByCol.get(branch.alias);
756
+ if (!fk) {
757
+ colDiff.status = "fk_missing";
758
+ colDiff.expectedTarget = branch.targetSeed;
759
+ } else {
760
+ const actualTable = fk.table;
761
+ const actualOnDelete = fk.on_delete.toUpperCase();
762
+ if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
763
+ colDiff.status = "fk_mismatch";
764
+ colDiff.expected = `\u2192 ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`;
765
+ colDiff.actual = `\u2192 ${actualTable}(id) ON DELETE ${actualOnDelete}`;
766
+ colDiff.expectedTarget = branch.targetSeed;
767
+ }
768
+ }
769
+ if (!indexNames.has(expectedIndexName)) {
770
+ if (colDiff.status === "ok") {
771
+ colDiff.status = "index_missing";
772
+ } else {
773
+ columns.push({ name: branch.alias, status: "index_missing" });
774
+ }
775
+ }
708
776
  }
709
- raw = raw.replace(/"database_id"\s*:\s*""/g, `"database_id": "${dbId}"`);
710
- writeFileSync2(configPath, raw, "utf-8");
711
- return true;
712
- } catch {
713
- return false;
714
777
  }
778
+ return { slug: seed.slug, tableExists: true, columns };
715
779
  }
716
- function printManualDbInstructions() {
717
- console.log(pc4.dim("\n Update your D1 database_id in wrangler.jsonc,"));
718
- console.log(pc4.dim(" or create a new database with:"));
719
- console.log(pc4.cyan("\n \u2192 Run: npx wrangler d1 create my-project-db"));
720
- console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
780
+
781
+ // src/commands/validate.ts
782
+ import pc2 from "picocolors";
783
+ import { SEED_REGISTRY, validateSeedDefinitions } from "@beechcms/core";
784
+ function validateSeeds(registry) {
785
+ return validateSeedDefinitions(Object.values(registry));
721
786
  }
722
- function echoApiKeys(configPath) {
723
- if (!configPath) return;
724
- try {
725
- const raw = readFileSync2(configPath, "utf-8");
726
- const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
727
- const parsed = JSON.parse(stripped);
728
- const vars = parsed?.vars ?? {};
729
- const readKey = vars["PUBLIC_READ_API_KEY"];
730
- const writeKey = vars["PUBLIC_WRITE_API_KEY"];
731
- if (!readKey && !writeKey) return;
732
- console.log(pc4.dim(" API keys detected in wrangler.jsonc:\n"));
733
- if (readKey) {
734
- const masked = readKey.length > 8 ? readKey.slice(0, 4) + "****" + readKey.slice(-4) : "****";
735
- console.log(pc4.dim(` PUBLIC_READ_API_KEY = ${masked}`));
787
+ async function validate(args) {
788
+ const registry = args.registry ?? SEED_REGISTRY;
789
+ if (Object.keys(registry).length === 0) {
790
+ console.warn(pc2.yellow("\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n"));
791
+ return;
792
+ }
793
+ console.log(pc2.cyan("\n beech validate \u2014 checking seeds\n"));
794
+ const errors = validateSeeds(registry);
795
+ const fatalErrors = errors.filter((e) => e.fatal);
796
+ const warnings = errors.filter((e) => !e.fatal);
797
+ for (const e of fatalErrors) {
798
+ console.log(pc2.red(` \u2717 ${e.slug} (fatal)`));
799
+ for (const msg of e.messages) {
800
+ console.log(pc2.red(` \u2192 ${msg}`));
736
801
  }
737
- if (writeKey) {
738
- const masked = writeKey.length > 8 ? writeKey.slice(0, 4) + "****" + writeKey.slice(-4) : "****";
739
- console.log(pc4.dim(` PUBLIC_WRITE_API_KEY = ${masked}`));
802
+ }
803
+ const warningMap = new Map(warnings.map((e) => [e.slug, e.messages]));
804
+ const allWarningSlugsSeen = new Set(warnings.map((e) => e.slug));
805
+ for (const seed of Object.values(registry)) {
806
+ const msgs = warningMap.get(seed.slug);
807
+ if (!msgs) {
808
+ if (!allWarningSlugsSeen.has(seed.slug)) {
809
+ const hasFatal = fatalErrors.some((e) => e.slug === seed.slug);
810
+ if (!hasFatal) console.log(pc2.green(` \u2713 ${seed.slug}`));
811
+ }
812
+ } else {
813
+ console.log(pc2.yellow(` \u26A0 ${seed.slug}`));
814
+ for (const msg of msgs) {
815
+ console.log(pc2.yellow(` \u2192 ${msg}`));
816
+ }
740
817
  }
741
- console.log(pc4.dim("\n Use these in your frontend as the X-API-Key header.\n"));
742
- } catch {
743
818
  }
744
- }
745
- function checkFiles(cwd, checkDevVars) {
746
- let ok = true;
747
- const workerExists = existsSync2(resolve2(cwd, "worker.ts")) || existsSync2(resolve2(cwd, "worker.js"));
748
- if (!workerExists) {
749
- console.log(pc4.red(" \u2717 worker.ts \u2014 missing (required)"));
750
- ok = false;
819
+ console.log("");
820
+ const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
821
+ const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0);
822
+ if (totalFatal > 0) {
823
+ const s = totalFatal !== 1 ? "s" : "";
824
+ console.log(pc2.red(` Found ${totalFatal} fatal error${s}. Fix before loading.
825
+ `));
826
+ process.exit(1);
827
+ } else if (totalWarnings > 0) {
828
+ const s = totalWarnings !== 1 ? "s" : "";
829
+ console.log(pc2.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.
830
+ `));
751
831
  } else {
752
- console.log(pc4.green(" \u2713 worker.ts"));
832
+ console.log(pc2.green(" All seeds valid.\n"));
753
833
  }
754
- const configPath = findWranglerConfig();
755
- const configInCwd = configPath && (configPath === resolve2(cwd, "wrangler.jsonc") || configPath === resolve2(cwd, "wrangler.json") || configPath === resolve2(cwd, "wrangler.toml"));
756
- if (!configInCwd) {
757
- console.log(pc4.red(" \u2717 wrangler.jsonc \u2014 missing (required)"));
758
- ok = false;
759
- } else {
760
- console.log(pc4.green(` \u2713 ${basename(configPath)}`));
834
+ }
835
+
836
+ // src/commands/seed-load.ts
837
+ function buildSeedRegistrationSql(seed) {
838
+ const json = sqlQuote(JSON.stringify(seed));
839
+ return [
840
+ `INSERT INTO seeds (slug, definition, status, source, created_at, updated_at)`,
841
+ `VALUES (${sqlQuote(seed.slug)}, ${json}, 'active', 'code', unixepoch(), unixepoch())`,
842
+ `ON CONFLICT(slug) DO UPDATE SET definition = excluded.definition, status = 'active', updated_at = excluded.updated_at;`
843
+ ].join("\n");
844
+ }
845
+ var SEED_META_BUMP_SQL = `UPDATE seed_meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE id = 'registry_version';`;
846
+ function buildStatements(seed) {
847
+ const stmts = [generateCreateTable(seed), ...generateIndexes(seed)];
848
+ const draft = generateDraftTable(seed);
849
+ if (draft) stmts.push(draft);
850
+ const fts = generateFtsTable(seed);
851
+ if (fts) {
852
+ stmts.push(fts, ...generateFtsTriggers(seed));
853
+ }
854
+ for (const branch of seed.branches) {
855
+ if (branch.type !== "relation" || branch.multiple !== true) continue;
856
+ stmts.push(generateJunctionTable(seed, branch), ...generateJunctionIndexes(seed, branch));
857
+ const draftJunction = generateJunctionDraftTable(seed, branch);
858
+ if (draftJunction) stmts.push(draftJunction);
859
+ }
860
+ return stmts;
861
+ }
862
+ async function runDiff(options, registry) {
863
+ const seeds = sortSeedsByDependencies(Object.values(registry));
864
+ console.log(pc3.cyan("\n Diffing schema\u2026\n"));
865
+ let allOk = true;
866
+ for (const seed of seeds) {
867
+ const result = await diffSeed(seed, options);
868
+ renderSeedDiff(result);
869
+ if (!isSeedClean(result)) allOk = false;
761
870
  }
762
- const seedsExists = existsSync2(resolve2(cwd, "seeds.ts")) || existsSync2(resolve2(cwd, "seeds.js")) || existsSync2(resolve2(cwd, "seed.ts")) || existsSync2(resolve2(cwd, "seed.js"));
763
- if (!seedsExists) {
764
- 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)"));
871
+ console.log("");
872
+ if (allOk) {
873
+ console.log(pc3.green(" Schema matches seeds. No action needed.\n"));
765
874
  } else {
766
- console.log(pc4.green(" \u2713 seeds.ts"));
875
+ console.log(pc3.yellow(" Run `beech seed:load` to apply missing tables/columns.\n"));
767
876
  }
768
- if (checkDevVars) {
769
- if (!existsSync2(resolve2(cwd, ".dev.vars"))) {
770
- console.log(pc4.dim(" \u25CB .dev.vars \u2014 not found (optional: only needed for production R2 credentials)"));
771
- } else {
772
- console.log(pc4.green(" \u2713 .dev.vars"));
877
+ }
878
+ async function runLoad(options, dryRun, registry) {
879
+ const seeds = sortSeedsByDependencies(Object.values(registry));
880
+ if (dryRun) {
881
+ console.log(pc3.cyan("\n -- dry-run: SQL that would be executed\n"));
882
+ for (const seed of seeds) {
883
+ const stmts = buildStatements(seed);
884
+ console.log(pc3.dim(` -- content_${seed.slug}`));
885
+ for (const stmt of stmts) {
886
+ console.log(stmt + "\n");
887
+ }
888
+ console.log(pc3.dim(` -- register ${seed.slug} in seeds table`));
889
+ console.log(buildSeedRegistrationSql(seed) + "\n");
773
890
  }
891
+ console.log(pc3.dim(" -- bump registry_version"));
892
+ console.log(SEED_META_BUMP_SQL + "\n");
893
+ return;
774
894
  }
775
- return ok;
776
- }
777
- function printNextSteps(local) {
778
- const localFlag = local ? " --local" : "";
779
- console.log(pc4.dim(" Next steps:"));
780
- console.log(pc4.cyan(` 1. npx beech seed:load${localFlag}`));
781
- console.log(pc4.dim(" \u2192 create content tables and register seed definitions in D1"));
782
- console.log(pc4.cyan(" 2. npx wrangler dev"));
783
- console.log(pc4.dim(" \u2192 start API + dashboard"));
784
- console.log(pc4.dim(" 3. Open http://localhost:8789/admin\n"));
785
- }
786
- function getExistingTables(options) {
787
- try {
788
- const rows = queryD1(
789
- `SELECT name FROM sqlite_master WHERE type='table'`,
790
- options
791
- );
792
- return rows.map((r) => r.name);
793
- } catch {
794
- return null;
895
+ console.log(pc3.cyan(`
896
+ Loading seeds into ${options.local ? "local" : "remote"} D1 (${options.db})\u2026
897
+ `));
898
+ for (const seed of seeds) {
899
+ const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)];
900
+ const sql = stmts.join("\n\n") + "\n";
901
+ process.stdout.write(` ${pc3.dim("\u2192")} content_${seed.slug}\u2026 `);
902
+ const ok = executeD1File(sql, options);
903
+ if (!ok) {
904
+ console.log(pc3.red("failed"));
905
+ console.log(pc3.red(`
906
+ \u2717 Failed to apply schema for content_${seed.slug}
907
+ `));
908
+ console.log(pc3.dim(" wrangler reported an error above."));
909
+ console.log(pc3.dim(` Most likely causes:`));
910
+ console.log(pc3.dim(` - Database "${options.db}" not found or wrong database_id`));
911
+ if (!options.local) {
912
+ console.log(pc3.dim(" - Not logged in to Cloudflare"));
913
+ console.log(pc3.cyan("\n \u2192 Run: npx wrangler login"));
914
+ console.log(pc3.cyan(" \u2192 Then: npx beech seed:load\n"));
915
+ } else {
916
+ console.log(pc3.cyan("\n \u2192 Run: npx beech init --db --local # re-initialise local DB"));
917
+ console.log(pc3.cyan(" \u2192 Then: npx beech seed:load --local\n"));
918
+ }
919
+ process.exit(1);
920
+ }
921
+ console.log(pc3.green("done"));
795
922
  }
923
+ executeD1File(SEED_META_BUMP_SQL, options);
924
+ console.log(pc3.green("\n All seeds loaded.\n"));
925
+ console.log(pc3.dim(" Definitions registered in the database."));
926
+ 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"));
796
927
  }
797
- async function init(args) {
798
- const cwd = process.cwd();
799
- console.log(pc4.cyan("\n beech init \u2014 project check\n"));
800
- const filesOk = checkFiles(cwd, args.local);
801
- if (!filesOk) {
802
- console.log(pc4.red("\n \u2717 Required files missing\n"));
803
- console.log(pc4.dim(" Fix the errors above before initialising the database."));
804
- console.log(pc4.cyan("\n \u2192 See: https://beechcms.dev/docs/getting-started\n"));
805
- process.exit(1);
806
- }
807
- console.log(pc4.green("\n All required files present.\n"));
808
- if (!args.initDb) {
809
- echoApiKeys(findWranglerConfig());
810
- const localFlag = args.local ? " --local" : "";
811
- console.log(pc4.dim(" Next steps:"));
812
- console.log(pc4.dim(` 1. npx beech init --db${localFlag} # initialise D1 database`));
813
- console.log(pc4.dim(` 2. npx beech seed:load${localFlag} # create content tables`));
814
- console.log(pc4.dim(" 3. npx wrangler dev # start API + dashboard"));
815
- console.log(pc4.dim(" 4. Open http://localhost:8789/admin\n"));
928
+ async function seedLoad(args) {
929
+ const registry = args.registry ?? SEED_REGISTRY2;
930
+ if (Object.keys(registry).length === 0) {
931
+ console.log(pc3.yellow("\n \u2717 No seeds found\n"));
932
+ console.log(pc3.dim(" Create a seeds.ts file in your project root with at least one content type."));
933
+ console.log(pc3.cyan("\n \u2192 Run: npx beech seed:create\n"));
816
934
  return;
817
935
  }
818
- const configPath = findWranglerConfig();
819
- if (configPath) {
820
- const placeholders = checkWranglerPlaceholders(configPath);
821
- if (placeholders.length > 0) {
822
- console.log(pc4.yellow(" \u26A0 wrangler.jsonc contains placeholder values:\n"));
823
- for (const issue of placeholders) {
824
- console.log(pc4.yellow(` - ${issue}`));
825
- }
826
- if (process.stdin.isTTY && !args.nonInteractive) {
827
- const rl = createInterface({ input: process.stdin, output: process.stdout });
828
- let autoCreate = false;
829
- try {
830
- const answer = (await rl.question(
831
- pc4.cyan("\n \u2192 Create a new D1 database (and R2 bucket) on Cloudflare automatically? (Y/n): ")
832
- )).trim().toLowerCase();
833
- autoCreate = !answer || answer === "y" || answer === "yes";
834
- } finally {
835
- rl.close();
836
- }
837
- if (autoCreate) {
838
- const authed = checkWranglerAuth();
839
- if (!authed) {
840
- console.log(pc4.red("\n \u2717 Not logged in to Cloudflare\n"));
841
- console.log(pc4.dim(" BeechCMS needs access to your Cloudflare account to create the database."));
842
- console.log(pc4.cyan("\n \u2192 Run: npx wrangler login"));
843
- console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
844
- process.exit(1);
845
- }
846
- const projectName = readProjectName(configPath);
847
- const dbName = `${projectName}-db`;
848
- const bucketName = readBucketName(configPath) || `${projectName}-media`;
849
- console.log(pc4.dim(`
850
- Creating D1 database "${dbName}"\u2026`));
851
- const dbId = createD1Database(dbName);
852
- if (!dbId) {
853
- console.log(pc4.red("\n \u2717 Failed to create D1 database\n"));
854
- console.log(pc4.dim(" Create it manually and retry:"));
855
- console.log(pc4.cyan(`
856
- \u2192 Run: npx wrangler d1 create ${dbName}`));
857
- console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
858
- process.exit(1);
859
- }
860
- console.log(pc4.green(` \u2713 D1 database created (id: ${dbId})`));
861
- console.log(pc4.dim(`
862
- Creating R2 bucket "${bucketName}"\u2026`));
863
- const r2Ok = createR2Bucket(bucketName);
864
- if (r2Ok) {
865
- console.log(pc4.green(` \u2713 R2 bucket "${bucketName}" created`));
866
- } else {
867
- console.log(pc4.yellow(` \u26A0 R2 bucket creation failed (may already exist \u2014 continuing)`));
868
- }
869
- console.log(pc4.dim("\n Updating wrangler.jsonc\u2026"));
870
- const patched = patchWranglerConfig(configPath, dbId);
871
- if (patched) {
872
- console.log(pc4.green(" \u2713 wrangler.jsonc updated\n"));
873
- } else {
874
- console.log(pc4.yellow(` \u26A0 Could not update wrangler.jsonc automatically
936
+ const validationErrors = validateSeeds(registry);
937
+ const fatalErrors = validationErrors.filter((e) => e.fatal);
938
+ const warnings = validationErrors.filter((e) => !e.fatal);
939
+ if (fatalErrors.length > 0) {
940
+ const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
941
+ const s = total !== 1 ? "s" : "";
942
+ console.log(pc3.red(`
943
+ \u2717 Seed validation found ${total} fatal error${s}. Cannot load schema.
875
944
  `));
876
- console.log(pc4.dim(` Set database_id = "${dbId}" in wrangler.jsonc manually, then retry:`));
877
- console.log(pc4.cyan(" \u2192 Run: npx beech init --db\n"));
878
- process.exit(1);
879
- }
880
- } else {
881
- printManualDbInstructions();
882
- process.exit(1);
883
- }
884
- } else if (args.nonInteractive && args.local) {
885
- console.log(pc4.dim(" --yes: proceeding with local mode (no remote resources needed)\n"));
886
- } else {
887
- if (args.nonInteractive) {
888
- console.log(pc4.red("\n \u2717 Cannot proceed non-interactively: remote database has a placeholder database_id\n"));
889
- console.log(pc4.dim(" Set a real database_id in wrangler.jsonc, then retry."));
890
- process.exit(1);
891
- }
892
- printManualDbInstructions();
893
- process.exit(1);
945
+ for (const e of fatalErrors) {
946
+ console.log(pc3.red(` \u2717 ${e.slug}`));
947
+ for (const msg of e.messages) {
948
+ console.log(pc3.red(` \u2192 ${msg}`));
894
949
  }
895
950
  }
951
+ console.log("");
952
+ process.exit(1);
896
953
  }
897
- if (!args.local) {
898
- const authed = checkWranglerAuth();
899
- if (!authed) {
900
- console.log(pc4.red(" \u2717 Not logged in to Cloudflare\n"));
901
- console.log(pc4.dim(" BeechCMS needs access to your Cloudflare account to manage the D1 database."));
902
- console.log(pc4.cyan("\n \u2192 Run: npx wrangler login"));
903
- console.log(pc4.cyan(" \u2192 Then: npx beech init --db\n"));
904
- process.exit(1);
905
- }
954
+ if (warnings.length > 0) {
955
+ const total = warnings.reduce((n, e) => n + e.messages.length, 0);
956
+ const s = total !== 1 ? "s" : "";
957
+ console.log(pc3.yellow(`
958
+ \u26A0 Seed validation found ${total} issue${s}. Schema changes will still be applied.
959
+ `));
960
+ console.log(pc3.dim(' Run "npx beech validate" for details.\n'));
906
961
  }
962
+ const configPath = findWranglerConfig();
907
963
  const db = args.db ?? resolveDbName(configPath);
908
- const options = { db, local: args.local, configPath };
909
- console.log(pc4.cyan(` Checking database "${db}" (${args.local ? "local" : "remote"})\u2026
910
- `));
911
- const existingTables = getExistingTables(options);
912
- const missingTables = SYSTEM_TABLES.filter((t) => !existingTables?.includes(t));
913
- if (!args.local) {
914
- if (existingTables === null) {
915
- console.log(pc4.red(" \u2717 Remote database unreachable\n"));
916
- console.log(pc4.dim(" Most likely causes:"));
917
- console.log(pc4.dim(" - Wrong database_id in wrangler.jsonc"));
918
- console.log(pc4.dim(" - Worker not yet deployed"));
919
- console.log(pc4.cyan("\n \u2192 Fix: Update d1_databases.database_id in wrangler.jsonc"));
920
- console.log(pc4.cyan(" \u2192 Then: npm run deploy\n"));
921
- process.exit(1);
922
- }
923
- if (missingTables.length > 0) {
924
- console.log(pc4.yellow(` \u26A0 Missing system tables: ${missingTables.join(", ")}
964
+ const options = {
965
+ db,
966
+ local: args.local,
967
+ configPath
968
+ };
969
+ if (!args.dryRun && !args.diff) {
970
+ try {
971
+ const rows = queryD1(
972
+ `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
973
+ options
974
+ );
975
+ if (rows.length < 2) {
976
+ console.log(pc3.red("\n \u2717 System tables not found (seeds, seed_meta)\n"));
977
+ console.log(pc3.dim(" Run `beech init --db` first to initialise the database."));
978
+ const flag = args.local ? " --local" : "";
979
+ console.log(pc3.cyan(`
980
+ \u2192 Run: npx beech init --db${flag}
925
981
  `));
926
- console.log(pc4.dim(" Most likely causes:"));
927
- console.log(pc4.dim(" - Wrong database_id in wrangler.jsonc"));
928
- console.log(pc4.dim(" - Migrations did not run during deploy"));
929
- console.log(pc4.cyan("\n \u2192 Fix: Update d1_databases.database_id in wrangler.jsonc"));
930
- console.log(pc4.cyan(" \u2192 Then: npm run deploy\n"));
982
+ process.exit(1);
983
+ }
984
+ } catch {
985
+ console.log(pc3.red("\n \u2717 Could not query the database\n"));
986
+ console.log(pc3.dim(" Run `beech init --db` first to initialise the database."));
931
987
  process.exit(1);
932
988
  }
933
- for (const table of SYSTEM_TABLES) {
934
- console.log(pc4.green(` \u2713 ${table}`));
935
- }
936
- console.log(pc4.green("\n All system tables present. Remote database is initialized.\n"));
937
- return;
938
989
  }
939
- if (existingTables === null) {
940
- console.log(pc4.yellow(" Database unreachable or not yet created \u2014 applying base schema\u2026\n"));
941
- } else if (missingTables.length === 0) {
942
- console.log(pc4.green(" \u2713 All system tables present. Database already initialised.\n"));
943
- printNextSteps(args.local);
944
- return;
990
+ if (args.diff) {
991
+ await runDiff(options, registry);
945
992
  } else {
946
- console.log(pc4.yellow(` Missing system tables: ${missingTables.join(", ")}`));
947
- console.log(pc4.cyan("\n Applying base schema\u2026\n"));
948
- }
949
- const ok = executeD1File(BASE_SCHEMA_SQL, options);
950
- if (!ok) {
951
- console.log(pc4.red("\n \u2717 Database initialisation failed\n"));
952
- console.log(pc4.dim(" wrangler reported an error above."));
953
- console.log(pc4.cyan("\n \u2192 Run: npx beech init --db --local\n"));
954
- process.exit(1);
993
+ await runLoad(options, args.dryRun, registry);
955
994
  }
956
- console.log(pc4.green("\n \u2713 worker.ts"));
957
- console.log(pc4.green(` \u2713 ${configPath ? basename(configPath) : "wrangler.jsonc"}`));
958
- console.log(pc4.green(" \u2713 seeds.ts"));
959
- console.log(pc4.green(" \u2713 Local D1 system tables ready\n"));
960
- echoApiKeys(configPath);
961
- printNextSteps(args.local);
962
995
  }
963
996
 
997
+ // src/index.ts
998
+ init_init();
999
+
964
1000
  // src/commands/seed-create.ts
965
1001
  import pc5 from "picocolors";
966
1002
  import { createInterface as createInterface2 } from "node:readline/promises";
@@ -1110,6 +1146,7 @@ async function seedCreate(_args) {
1110
1146
  }
1111
1147
 
1112
1148
  // src/commands/deploy.ts
1149
+ init_wrangler();
1113
1150
  import pc6 from "picocolors";
1114
1151
  import { spawnSync as spawnSync3 } from "node:child_process";
1115
1152
  import { readFileSync as readFileSync4 } from "node:fs";
@@ -1207,6 +1244,7 @@ async function deploy(args) {
1207
1244
  }
1208
1245
 
1209
1246
  // src/commands/onboard.ts
1247
+ init_init();
1210
1248
  import pc7 from "picocolors";
1211
1249
  async function onboard(args) {
1212
1250
  console.log(pc7.cyan("\n beech onboard \u2014 full provisioning\n"));
@@ -1263,14 +1301,82 @@ async function update(_args) {
1263
1301
  }
1264
1302
 
1265
1303
  // src/commands/reset.ts
1304
+ import pc11 from "picocolors";
1305
+ import { createInterface as createInterface3 } from "node:readline/promises";
1306
+
1307
+ // src/commands/db-reset.ts
1266
1308
  import pc9 from "picocolors";
1267
1309
  import { spawnSync as spawnSync5 } from "node:child_process";
1268
1310
  import { existsSync as existsSync4, readFileSync as readFileSync5, rmSync as rmSync2 } from "node:fs";
1269
1311
  import { resolve as resolve4 } from "node:path";
1270
- import { createInterface as createInterface3 } from "node:readline/promises";
1312
+ async function dbReset(_args) {
1313
+ console.log(pc9.cyan("\n beech db:reset \u2014 reset local database\n"));
1314
+ const cwd = process.cwd();
1315
+ const apiDir = resolve4(cwd, "apps", "api");
1316
+ let dbResetSuccess = false;
1317
+ if (existsSync4(resolve4(apiDir, "package.json"))) {
1318
+ const result = spawnSync5("npm", ["run", "db:reset:local"], {
1319
+ stdio: "inherit",
1320
+ cwd: apiDir,
1321
+ shell: true
1322
+ });
1323
+ dbResetSuccess = result.status === 0;
1324
+ } else if (existsSync4(resolve4(cwd, "package.json"))) {
1325
+ const pkg = JSON.parse(readFileSync5(resolve4(cwd, "package.json"), "utf-8"));
1326
+ if (pkg.scripts?.["db:reset:local"]) {
1327
+ const result = spawnSync5("npm", ["run", "db:reset:local"], {
1328
+ stdio: "inherit",
1329
+ cwd,
1330
+ shell: true
1331
+ });
1332
+ dbResetSuccess = result.status === 0;
1333
+ } else {
1334
+ const wranglerStateDir = resolve4(cwd, ".wrangler/state");
1335
+ if (existsSync4(wranglerStateDir)) {
1336
+ console.log(pc9.dim(" Removing .wrangler/state\u2026"));
1337
+ rmSync2(wranglerStateDir, { recursive: true, force: true });
1338
+ }
1339
+ if (existsSync4(resolve4(cwd, "scripts", "bootstrap-d1.mjs"))) {
1340
+ const result = spawnSync5("node", ["scripts/bootstrap-d1.mjs"], {
1341
+ stdio: "inherit",
1342
+ cwd,
1343
+ shell: true
1344
+ });
1345
+ dbResetSuccess = result.status === 0;
1346
+ } else {
1347
+ console.log(pc9.yellow(" \u26A0 Could not find database reset script."));
1348
+ const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1349
+ try {
1350
+ await init2({ initDb: true, local: true });
1351
+ dbResetSuccess = true;
1352
+ } catch {
1353
+ dbResetSuccess = false;
1354
+ }
1355
+ }
1356
+ }
1357
+ } else {
1358
+ const wranglerStateDir = resolve4(cwd, ".wrangler/state");
1359
+ if (existsSync4(wranglerStateDir)) {
1360
+ console.log(pc9.dim(" Removing .wrangler/state\u2026"));
1361
+ rmSync2(wranglerStateDir, { recursive: true, force: true });
1362
+ }
1363
+ dbResetSuccess = true;
1364
+ }
1365
+ if (dbResetSuccess) {
1366
+ console.log(pc9.green("\n \u2713 Local database reset completed."));
1367
+ } else {
1368
+ console.log(pc9.red("\n \u2717 Database reset failed."));
1369
+ process.exit(1);
1370
+ return;
1371
+ }
1372
+ }
1373
+
1374
+ // src/commands/dev-reset.ts
1375
+ import pc10 from "picocolors";
1376
+ import { spawnSync as spawnSync6 } from "node:child_process";
1271
1377
  function isDockerInstalled() {
1272
1378
  try {
1273
- const result = spawnSync5("docker", ["--version"], { stdio: "ignore", shell: true });
1379
+ const result = spawnSync6("docker", ["--version"], { stdio: "ignore", shell: true });
1274
1380
  return result.status === 0;
1275
1381
  } catch {
1276
1382
  return false;
@@ -1278,130 +1384,82 @@ function isDockerInstalled() {
1278
1384
  }
1279
1385
  function isDockerRunning() {
1280
1386
  try {
1281
- const result = spawnSync5("docker", ["info"], { stdio: "ignore", shell: true });
1387
+ const result = spawnSync6("docker", ["info"], { stdio: "ignore", shell: true });
1282
1388
  return result.status === 0;
1283
1389
  } catch {
1284
1390
  return false;
1285
1391
  }
1286
1392
  }
1393
+ async function devReset() {
1394
+ console.log(pc10.cyan("\n beech dev:reset \u2014 reset Docker environment\n"));
1395
+ if (!isDockerInstalled()) {
1396
+ console.log(pc10.red(" \u2717 Docker is not installed or not found in your PATH."));
1397
+ process.exit(1);
1398
+ return;
1399
+ }
1400
+ if (!isDockerRunning()) {
1401
+ console.log(pc10.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
1402
+ process.exit(1);
1403
+ return;
1404
+ }
1405
+ console.log(pc10.dim(" Resetting Docker containers and volumes\u2026\n"));
1406
+ const result = spawnSync6("docker", ["compose", "-f", "docker/docker-compose.yml", "down", "-v"], {
1407
+ stdio: "inherit",
1408
+ cwd: process.cwd(),
1409
+ shell: true
1410
+ });
1411
+ if (result.status === 0) {
1412
+ console.log(pc10.green("\n \u2713 Docker containers stopped and volumes removed."));
1413
+ } else {
1414
+ console.log(pc10.red("\n \u2717 Docker reset failed."));
1415
+ process.exit(1);
1416
+ return;
1417
+ }
1418
+ }
1419
+
1420
+ // src/commands/reset.ts
1287
1421
  async function reset(args) {
1288
- console.log(pc9.cyan("\n beech reset \u2014 cleanup environments\n"));
1422
+ console.log(pc11.cyan("\n beech reset \u2014 cleanup environments\n"));
1289
1423
  let resetDb = args.db || args.all;
1290
1424
  let resetDocker = args.docker || args.all;
1291
1425
  if (!args.db && !args.docker && !args.all) {
1292
- if (process.stdin.isTTY) {
1426
+ if (args.yes) {
1427
+ resetDb = true;
1428
+ resetDocker = true;
1429
+ } else if (process.stdin.isTTY) {
1293
1430
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
1294
1431
  try {
1295
1432
  const answer = (await rl.question(
1296
- pc9.cyan(" \u2192 No options provided. Would you like to reset everything (DB & Docker)? (y/N): ")
1433
+ pc11.cyan(" \u2192 No options provided. Would you like to reset everything (DB & Docker)? (y/N): ")
1297
1434
  )).trim().toLowerCase();
1298
1435
  if (answer === "y" || answer === "yes") {
1299
1436
  resetDb = true;
1300
1437
  resetDocker = true;
1301
1438
  } else {
1302
- console.log(pc9.dim("\n Reset cancelled. Use --db, --docker, or --all.\n"));
1439
+ console.log(pc11.dim("\n Reset cancelled. Use --db, --docker, or --all.\n"));
1303
1440
  return;
1304
1441
  }
1305
1442
  } finally {
1306
1443
  rl.close();
1307
1444
  }
1308
1445
  } else {
1309
- console.log(pc9.red("\n \u2717 Error: Please specify what to reset using --db, --docker, or --all.\n"));
1446
+ console.log(pc11.red("\n \u2717 Error: Please specify what to reset using --db, --docker, or --all.\n"));
1310
1447
  process.exit(1);
1311
1448
  }
1312
1449
  }
1313
- const cwd = process.cwd();
1314
1450
  if (resetDocker) {
1315
- if (!isDockerInstalled()) {
1316
- console.log(pc9.red(" \u2717 Docker is not installed or not found in your PATH."));
1317
- console.log(pc9.dim(" Please install Docker to reset Docker containers and volumes.\n"));
1318
- if (!args.all) {
1319
- process.exit(1);
1320
- }
1321
- } else if (!isDockerRunning()) {
1322
- console.log(pc9.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
1323
- console.log(pc9.dim(" Please start Docker Desktop or your Docker daemon to reset containers.\n"));
1324
- if (!args.all) {
1325
- process.exit(1);
1326
- }
1327
- } else {
1328
- console.log(pc9.dim(" Resetting Docker containers and volumes\u2026\n"));
1329
- const result = spawnSync5("docker", ["compose", "down", "-v"], {
1330
- stdio: "inherit",
1331
- cwd,
1332
- shell: true
1333
- });
1334
- if (result.status === 0) {
1335
- console.log(pc9.green("\n \u2713 Docker containers stopped and volumes removed."));
1336
- } else {
1337
- console.log(pc9.red("\n \u2717 Docker reset failed."));
1338
- }
1339
- }
1451
+ await devReset();
1340
1452
  }
1341
1453
  if (resetDb) {
1342
- console.log(pc9.dim("\n Resetting local database\u2026\n"));
1343
- let dbResetSuccess = false;
1344
- const apiDir = resolve4(cwd, "apps", "api");
1345
- if (existsSync4(resolve4(apiDir, "package.json"))) {
1346
- const result = spawnSync5("npm", ["run", "db:reset:local"], {
1347
- stdio: "inherit",
1348
- cwd: apiDir,
1349
- shell: true
1350
- });
1351
- dbResetSuccess = result.status === 0;
1352
- } else if (existsSync4(resolve4(cwd, "package.json"))) {
1353
- const pkg = JSON.parse(readFileSync5(resolve4(cwd, "package.json"), "utf-8"));
1354
- if (pkg.scripts?.["db:reset:local"]) {
1355
- const result = spawnSync5("npm", ["run", "db:reset:local"], {
1356
- stdio: "inherit",
1357
- cwd,
1358
- shell: true
1359
- });
1360
- dbResetSuccess = result.status === 0;
1361
- } else {
1362
- const wranglerStateDir = resolve4(cwd, ".wrangler/state");
1363
- if (existsSync4(wranglerStateDir)) {
1364
- console.log(pc9.dim(" Removing .wrangler/state\u2026"));
1365
- rmSync2(wranglerStateDir, { recursive: true, force: true });
1366
- }
1367
- if (existsSync4(resolve4(cwd, "scripts", "bootstrap-d1.mjs"))) {
1368
- const result = spawnSync5("node", ["scripts/bootstrap-d1.mjs"], {
1369
- stdio: "inherit",
1370
- cwd,
1371
- shell: true
1372
- });
1373
- dbResetSuccess = result.status === 0;
1374
- } else {
1375
- console.log(pc9.yellow(" \u26A0 Could not find database reset script."));
1376
- const initResult = spawnSync5("npx", ["beech", "init", "--db", "--local"], {
1377
- stdio: "inherit",
1378
- cwd,
1379
- shell: true
1380
- });
1381
- dbResetSuccess = initResult.status === 0;
1382
- }
1383
- }
1384
- } else {
1385
- const wranglerStateDir = resolve4(cwd, ".wrangler/state");
1386
- if (existsSync4(wranglerStateDir)) {
1387
- console.log(pc9.dim(" Removing .wrangler/state\u2026"));
1388
- rmSync2(wranglerStateDir, { recursive: true, force: true });
1389
- }
1390
- dbResetSuccess = true;
1391
- }
1392
- if (dbResetSuccess) {
1393
- console.log(pc9.green("\n \u2713 Local database reset completed."));
1394
- } else {
1395
- console.log(pc9.red("\n \u2717 Database reset failed."));
1396
- }
1454
+ await dbReset({});
1397
1455
  }
1398
- console.log(pc9.dim("\n Reset process finished.\n"));
1399
1456
  }
1400
1457
 
1401
1458
  // src/commands/generate-types.ts
1459
+ init_wrangler();
1402
1460
  import { writeFileSync as writeFileSync4, mkdirSync } from "node:fs";
1403
1461
  import { dirname, resolve as resolve5 } from "node:path";
1404
- import pc10 from "picocolors";
1462
+ import pc12 from "picocolors";
1405
1463
  import { generateSeedTypes } from "@beechcms/core";
1406
1464
  function loadSeedsFromD1(db) {
1407
1465
  const configPath = findWranglerConfig();
@@ -1417,7 +1475,7 @@ async function generateTypes(args) {
1417
1475
  if (args.local) {
1418
1476
  const registry = args.registry ?? {};
1419
1477
  if (Object.keys(registry).length === 0) {
1420
- console.log(pc10.red("\n \u2717 No seeds found (seeds.ts empty or missing).\n"));
1478
+ console.log(pc12.red("\n \u2717 No seeds found (seeds.ts empty or missing).\n"));
1421
1479
  process.exit(1);
1422
1480
  }
1423
1481
  seeds = Object.values(registry);
@@ -1425,7 +1483,7 @@ async function generateTypes(args) {
1425
1483
  const db = args.db ?? resolveDbName(findWranglerConfig());
1426
1484
  seeds = loadSeedsFromD1(db);
1427
1485
  if (seeds.length === 0) {
1428
- console.log(pc10.red(`
1486
+ console.log(pc12.red(`
1429
1487
  \u2717 No active seeds in D1 (${db}). Run \`beech seed:load\` first.
1430
1488
  `));
1431
1489
  process.exit(1);
@@ -1435,13 +1493,14 @@ async function generateTypes(args) {
1435
1493
  const outPath = resolve5(process.cwd(), args.out);
1436
1494
  mkdirSync(dirname(outPath), { recursive: true });
1437
1495
  writeFileSync4(outPath, code, "utf-8");
1438
- console.log(pc10.green(`
1496
+ console.log(pc12.green(`
1439
1497
  \u2713 Generated ${seeds.length} interface(s) \u2192 ${args.out}
1440
1498
  `));
1441
1499
  }
1442
1500
 
1443
1501
  // src/commands/schema-diff.ts
1444
- import pc11 from "picocolors";
1502
+ init_wrangler();
1503
+ import pc13 from "picocolors";
1445
1504
  import { resolve as resolve6 } from "node:path";
1446
1505
  import { SEED_REGISTRY as SEED_REGISTRY3, sortSeedsByDependencies as sortSeedsByDependencies2 } from "@beechcms/core";
1447
1506
 
@@ -1530,13 +1589,13 @@ function resolveMigrationsDir(override) {
1530
1589
  async function schemaDiff(args) {
1531
1590
  const registry = args.registry ?? SEED_REGISTRY3;
1532
1591
  if (Object.keys(registry).length === 0) {
1533
- console.log(pc11.yellow("\n \u2717 No seeds found \u2014 nothing to diff.\n"));
1592
+ console.log(pc13.yellow("\n \u2717 No seeds found \u2014 nothing to diff.\n"));
1534
1593
  return;
1535
1594
  }
1536
1595
  const configPath = findWranglerConfig();
1537
1596
  const options = { db: args.db ?? resolveDbName(configPath), local: args.local, configPath };
1538
1597
  const seeds = sortSeedsByDependencies2(Object.values(registry));
1539
- console.log(pc11.cyan(`
1598
+ console.log(pc13.cyan(`
1540
1599
  Diffing schema vs ${args.local ? "local" : "remote"} D1 (${options.db})\u2026
1541
1600
  `));
1542
1601
  const diffs = [];
@@ -1548,45 +1607,323 @@ async function schemaDiff(args) {
1548
1607
  if (!isSeedClean(d)) clean = false;
1549
1608
  }
1550
1609
  if (clean) {
1551
- console.log(pc11.green("\n Schema matches seeds. No migration needed.\n"));
1610
+ console.log(pc13.green("\n Schema matches seeds. No migration needed.\n"));
1552
1611
  return;
1553
1612
  }
1554
1613
  const plan = buildMigrationSql(diffs, registry);
1555
1614
  if (!args.write) {
1556
- console.log(pc11.dim("\n -- proposed additive migration (preview):\n"));
1615
+ console.log(pc13.dim("\n -- proposed additive migration (preview):\n"));
1557
1616
  console.log(plan.sql);
1558
1617
  if (plan.destructiveSlugs.length) {
1559
- console.log(pc11.yellow(`
1618
+ console.log(pc13.yellow(`
1560
1619
  \u26A0 Destructive drift in: ${plan.destructiveSlugs.join(", ")} \u2014 not auto-migrated.`));
1561
1620
  }
1562
- console.log(pc11.cyan("\n \u2192 Re-run with --write to save the migration file.\n"));
1621
+ console.log(pc13.cyan("\n \u2192 Re-run with --write to save the migration file.\n"));
1563
1622
  return;
1564
1623
  }
1565
1624
  if (plan.additiveCount === 0) {
1566
- console.log(pc11.yellow("\n \u26A0 Only destructive drift detected \u2014 no additive migration written."));
1567
- console.log(pc11.dim(" Author a reviewed migration by hand for renames/drops/type changes.\n"));
1625
+ console.log(pc13.yellow("\n \u26A0 Only destructive drift detected \u2014 no additive migration written."));
1626
+ console.log(pc13.dim(" Author a reviewed migration by hand for renames/drops/type changes.\n"));
1568
1627
  return;
1569
1628
  }
1570
1629
  const dir = resolveMigrationsDir(args.migrationsDir);
1571
1630
  const index = nextMigrationIndex(dir);
1572
1631
  const file = writeMigrationFile(dir, index, args.name ?? "schema_sync", plan.sql);
1573
- console.log(pc11.green(`
1632
+ console.log(pc13.green(`
1574
1633
  \u2713 Wrote ${file} (${plan.additiveCount} statement(s)).`));
1575
- console.log(pc11.dim(" Review, commit, then `wrangler d1 migrations apply --remote` in CI.\n"));
1634
+ console.log(pc13.dim(" Review, commit, then `wrangler d1 migrations apply --remote` in CI.\n"));
1576
1635
  if (plan.destructiveSlugs.length) {
1577
- console.log(pc11.yellow(` \u26A0 Destructive drift in ${plan.destructiveSlugs.join(", ")} was NOT included.
1636
+ console.log(pc13.yellow(` \u26A0 Destructive drift in ${plan.destructiveSlugs.join(", ")} was NOT included.
1637
+ `));
1638
+ }
1639
+ }
1640
+
1641
+ // src/commands/db-migrate.ts
1642
+ import pc14 from "picocolors";
1643
+ import { spawnSync as spawnSync7 } from "node:child_process";
1644
+ import { existsSync as existsSync6 } from "node:fs";
1645
+ import { resolve as resolve7 } from "node:path";
1646
+ async function dbMigrate(_args) {
1647
+ console.log(pc14.cyan("\n beech db:migrate \u2014 apply migrations\n"));
1648
+ const cwd = process.cwd();
1649
+ const apiDir = resolve7(cwd, "apps", "api");
1650
+ if (existsSync6(resolve7(apiDir, "package.json"))) {
1651
+ const result = spawnSync7("npm", ["run", "db:migrate:local"], {
1652
+ stdio: "inherit",
1653
+ cwd: apiDir,
1654
+ shell: true
1655
+ });
1656
+ if (result.status !== 0) {
1657
+ console.log(pc14.red("\n \u2717 Failed to apply migrations."));
1658
+ process.exit(1);
1659
+ return;
1660
+ }
1661
+ } else if (existsSync6(resolve7(cwd, "scripts", "bootstrap-d1.mjs"))) {
1662
+ const result = spawnSync7("node", ["scripts/bootstrap-d1.mjs"], {
1663
+ stdio: "inherit",
1664
+ cwd,
1665
+ shell: true
1666
+ });
1667
+ if (result.status !== 0) {
1668
+ console.log(pc14.red("\n \u2717 Failed to apply migrations."));
1669
+ process.exit(1);
1670
+ return;
1671
+ }
1672
+ } else {
1673
+ console.log(pc14.yellow(" \u26A0 Could not find database migration script."));
1674
+ process.exit(1);
1675
+ return;
1676
+ }
1677
+ console.log(pc14.green("\n \u2713 Migrations applied successfully."));
1678
+ }
1679
+
1680
+ // src/commands/dev.ts
1681
+ import pc15 from "picocolors";
1682
+ import { spawnSync as spawnSync8 } from "node:child_process";
1683
+ import { existsSync as existsSync7 } from "node:fs";
1684
+ import { resolve as resolve8 } from "node:path";
1685
+ async function dev(args) {
1686
+ console.log(pc15.cyan("\n beech dev \u2014 start development environment\n"));
1687
+ const cwd = process.cwd();
1688
+ const devScript = resolve8(cwd, "scripts", "dev.mjs");
1689
+ if (!existsSync7(devScript)) {
1690
+ console.log(pc15.red(" \u2717 Could not find development script (scripts/dev.mjs)."));
1691
+ process.exit(1);
1692
+ return;
1693
+ }
1694
+ const env = { ...process.env };
1695
+ if (args.plain) {
1696
+ env.BEECH_DEV_PLAIN = "1";
1697
+ }
1698
+ const result = spawnSync8("node", ["scripts/dev.mjs"], {
1699
+ stdio: "inherit",
1700
+ cwd,
1701
+ env,
1702
+ shell: true
1703
+ });
1704
+ if (result.status !== 0) {
1705
+ process.exit(result.status ?? 1);
1706
+ return;
1707
+ }
1708
+ }
1709
+
1710
+ // src/commands/dev-stop.ts
1711
+ import pc16 from "picocolors";
1712
+ import { spawnSync as spawnSync9 } from "node:child_process";
1713
+ function isDockerInstalled2() {
1714
+ try {
1715
+ const result = spawnSync9("docker", ["--version"], { stdio: "ignore", shell: true });
1716
+ return result.status === 0;
1717
+ } catch {
1718
+ return false;
1719
+ }
1720
+ }
1721
+ function isDockerRunning2() {
1722
+ try {
1723
+ const result = spawnSync9("docker", ["info"], { stdio: "ignore", shell: true });
1724
+ return result.status === 0;
1725
+ } catch {
1726
+ return false;
1727
+ }
1728
+ }
1729
+ async function devStop() {
1730
+ console.log(pc16.cyan("\n beech dev:stop \u2014 stop Docker environment\n"));
1731
+ if (!isDockerInstalled2()) {
1732
+ console.log(pc16.red(" \u2717 Docker is not installed or not found in your PATH."));
1733
+ process.exit(1);
1734
+ return;
1735
+ }
1736
+ if (!isDockerRunning2()) {
1737
+ console.log(pc16.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
1738
+ process.exit(1);
1739
+ return;
1740
+ }
1741
+ console.log(pc16.dim(" Stopping Docker containers\u2026\n"));
1742
+ const result = spawnSync9("docker", ["compose", "-f", "docker/docker-compose.yml", "stop"], {
1743
+ stdio: "inherit",
1744
+ cwd: process.cwd(),
1745
+ shell: true
1746
+ });
1747
+ if (result.status === 0) {
1748
+ console.log(pc16.green("\n \u2713 Docker containers stopped."));
1749
+ } else {
1750
+ console.log(pc16.red("\n \u2717 Failed to stop Docker containers."));
1751
+ process.exit(1);
1752
+ return;
1753
+ }
1754
+ }
1755
+
1756
+ // src/commands/dev-tunnel.ts
1757
+ import pc17 from "picocolors";
1758
+ import { spawnSync as spawnSync10 } from "node:child_process";
1759
+ async function devTunnel() {
1760
+ console.log(pc17.cyan("\n beech dev:tunnel \u2014 get Cloudflare Tunnel URL\n"));
1761
+ const result = spawnSync10("docker", ["compose", "-f", "docker/docker-compose.yml", "logs", "tunnel"], {
1762
+ encoding: "utf-8",
1763
+ cwd: process.cwd(),
1764
+ shell: true
1765
+ });
1766
+ if (result.status !== 0) {
1767
+ console.log(pc17.red(" \u2717 Failed to retrieve tunnel logs."));
1768
+ process.exit(1);
1769
+ return;
1770
+ }
1771
+ const logs2 = result.stdout + (result.stderr || "");
1772
+ const match = logs2.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/g);
1773
+ if (match && match.length > 0) {
1774
+ const url = match[match.length - 1];
1775
+ console.log(pc17.green(` \u2713 Active Cloudflare Tunnel URL: ${pc17.bold(url)}`));
1776
+ } else {
1777
+ console.log(pc17.yellow(" \u26A0 No active Cloudflare Tunnel URL found in logs."));
1778
+ console.log(pc17.dim(" Make sure the dev server is running with Docker (`pnpm beech dev`)."));
1779
+ }
1780
+ }
1781
+
1782
+ // src/commands/mailpit-clear.ts
1783
+ import pc18 from "picocolors";
1784
+ async function mailpitClear() {
1785
+ console.log(pc18.cyan("\n beech mailpit:clear \u2014 clear test emails\n"));
1786
+ try {
1787
+ const res = await fetch("http://localhost:8025/api/v1/messages", {
1788
+ method: "DELETE"
1789
+ });
1790
+ if (res.ok) {
1791
+ console.log(pc18.green(" \u2713 Mailpit inbox cleared successfully."));
1792
+ } else {
1793
+ console.log(pc18.red(` \u2717 Failed to clear Mailpit inbox: ${res.statusText}`));
1794
+ process.exit(1);
1795
+ return;
1796
+ }
1797
+ } catch (err) {
1798
+ console.log(pc18.red(` \u2717 Error connecting to Mailpit: ${err.message}`));
1799
+ console.log(pc18.dim(" Make sure Mailpit is running (default port 8025)."));
1800
+ process.exit(1);
1801
+ return;
1802
+ }
1803
+ }
1804
+
1805
+ // src/commands/logs.ts
1806
+ import pc19 from "picocolors";
1807
+ import { spawnSync as spawnSync11 } from "node:child_process";
1808
+ var SERVICE_MAP = {
1809
+ mailpit: "mailpit",
1810
+ db: "sqlite-web",
1811
+ sqlite: "sqlite-web",
1812
+ tunnel: "tunnel",
1813
+ storage: "minio",
1814
+ minio: "minio"
1815
+ };
1816
+ async function logs(args) {
1817
+ const inputService = args.service?.toLowerCase();
1818
+ if (!inputService || !SERVICE_MAP[inputService]) {
1819
+ console.log(pc19.red("\n \u2717 Error: Please specify a valid service name."));
1820
+ console.log(pc19.dim("\n Accepted services:"));
1821
+ console.log(` - ${pc19.cyan("mailpit")}`);
1822
+ console.log(` - ${pc19.cyan("db")} / ${pc19.cyan("sqlite")}`);
1823
+ console.log(` - ${pc19.cyan("tunnel")}`);
1824
+ console.log(` - ${pc19.cyan("storage")} / ${pc19.cyan("minio")}
1825
+ `);
1826
+ process.exit(1);
1827
+ return;
1828
+ }
1829
+ const service = SERVICE_MAP[inputService];
1830
+ console.log(pc19.cyan(`
1831
+ beech logs ${inputService} \u2014 streaming logs for ${service}\u2026
1578
1832
  `));
1833
+ const result = spawnSync11("docker", ["compose", "-f", "docker/docker-compose.yml", "logs", "-f", service], {
1834
+ stdio: "inherit",
1835
+ cwd: process.cwd(),
1836
+ shell: true
1837
+ });
1838
+ if (result.status !== 0) {
1839
+ process.exit(result.status ?? 1);
1840
+ return;
1841
+ }
1842
+ }
1843
+
1844
+ // src/commands/test.ts
1845
+ import pc20 from "picocolors";
1846
+ import { spawnSync as spawnSync12 } from "node:child_process";
1847
+ import { existsSync as existsSync8 } from "node:fs";
1848
+ import { resolve as resolve9 } from "node:path";
1849
+ async function test(args) {
1850
+ console.log(pc20.cyan("\n beech test \u2014 run test suite\n"));
1851
+ const cwd = process.cwd();
1852
+ let command = "turbo";
1853
+ let commandArgs = ["run", "test"];
1854
+ if (args.diff) {
1855
+ const diffScript = resolve9(cwd, "scripts", "test-coverage-diff.mjs");
1856
+ if (existsSync8(diffScript)) {
1857
+ command = "node";
1858
+ commandArgs = ["scripts/test-coverage-diff.mjs"];
1859
+ } else {
1860
+ console.log(pc20.red(" \u2717 Coverage diff script not found (scripts/test-coverage-diff.mjs)."));
1861
+ process.exit(1);
1862
+ return;
1863
+ }
1864
+ } else if (args.coverage) {
1865
+ commandArgs = ["run", "test:coverage"];
1866
+ }
1867
+ const result = spawnSync12(command, commandArgs, {
1868
+ stdio: "inherit",
1869
+ cwd,
1870
+ shell: true
1871
+ });
1872
+ if (result.status !== 0) {
1873
+ process.exit(result.status ?? 1);
1874
+ return;
1875
+ }
1876
+ }
1877
+
1878
+ // src/commands/lint.ts
1879
+ import pc21 from "picocolors";
1880
+ import { spawnSync as spawnSync13 } from "node:child_process";
1881
+ async function lint() {
1882
+ console.log(pc21.cyan("\n beech lint \u2014 check code style\n"));
1883
+ const result = spawnSync13("turbo", ["run", "lint"], {
1884
+ stdio: "inherit",
1885
+ cwd: process.cwd(),
1886
+ shell: true
1887
+ });
1888
+ if (result.status !== 0) {
1889
+ process.exit(result.status ?? 1);
1890
+ }
1891
+ }
1892
+
1893
+ // src/commands/doctor.ts
1894
+ import pc22 from "picocolors";
1895
+ import { spawnSync as spawnSync14 } from "node:child_process";
1896
+ async function doctor() {
1897
+ console.log(pc22.cyan("\n beech doctor \u2014 React diagnostics\n"));
1898
+ const result = spawnSync14("pnpm", ["dlx", "react-doctor@latest"], {
1899
+ stdio: "inherit",
1900
+ cwd: process.cwd(),
1901
+ shell: true
1902
+ });
1903
+ if (result.status !== 0) {
1904
+ process.exit(result.status ?? 1);
1579
1905
  }
1580
1906
  }
1581
1907
  export {
1908
+ dbMigrate,
1909
+ dbReset,
1582
1910
  deploy,
1911
+ dev,
1912
+ devReset,
1913
+ devStop,
1914
+ devTunnel,
1915
+ doctor,
1583
1916
  generateTypes,
1584
1917
  init,
1918
+ lint,
1919
+ logs,
1920
+ mailpitClear,
1585
1921
  onboard,
1586
1922
  reset,
1587
1923
  schemaDiff,
1588
1924
  seedCreate,
1589
1925
  seedLoad,
1926
+ test,
1590
1927
  update,
1591
1928
  validate,
1592
1929
  validateSeeds