@beechcms/cli 0.4.3 → 0.6.0-preview.1

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
@@ -6,7 +6,11 @@ import {
6
6
  generateDraftTable,
7
7
  generateIndexes,
8
8
  generateFtsTable,
9
- generateFtsTriggers
9
+ generateFtsTriggers,
10
+ generateJunctionTable,
11
+ generateJunctionIndexes,
12
+ generateJunctionDraftTable,
13
+ sortSeedsByDependencies
10
14
  } from "@beechcms/core";
11
15
 
12
16
  // src/lib/wrangler.ts
@@ -36,8 +40,18 @@ function executeD1File(sql, options) {
36
40
  }
37
41
  }
38
42
  function queryD1(sql, options) {
39
- const args = ["d1", "execute", options.db, "--command", sql, "--json", ...buildArgs(options)];
40
- const result = spawnSync("npx", ["wrangler", ...args], { encoding: "utf-8", cwd: process.cwd(), shell: true });
43
+ const tmpFile = join(tmpdir(), `beech-query-${Date.now()}.sql`);
44
+ let result;
45
+ try {
46
+ writeFileSync(tmpFile, sql, "utf-8");
47
+ const args = ["d1", "execute", options.db, "--file", tmpFile, "--json", ...buildArgs(options)];
48
+ result = spawnSync("npx", ["wrangler", ...args], { encoding: "utf-8", cwd: process.cwd(), shell: true });
49
+ } finally {
50
+ try {
51
+ rmSync(tmpFile);
52
+ } catch {
53
+ }
54
+ }
41
55
  if (result.status !== 0) {
42
56
  throw new Error(`wrangler d1 execute failed:
43
57
  ${result.stderr}`);
@@ -67,6 +81,9 @@ function findWranglerConfig() {
67
81
  }
68
82
  return null;
69
83
  }
84
+ function sqlQuote(value) {
85
+ return `'${value.replace(/'/g, "''")}'`;
86
+ }
70
87
  function resolveDbName(configPath) {
71
88
  if (!configPath) return "beech-db";
72
89
  try {
@@ -107,11 +124,11 @@ async function diffSeed(seed, options) {
107
124
  const expectedSet = new Set(expected.map((c) => c.name));
108
125
  const columns = [];
109
126
  for (const col of expected) {
110
- const actual2 = actualMap.get(col.name);
111
- if (!actual2) {
127
+ const actualRow = actualMap.get(col.name);
128
+ if (!actualRow) {
112
129
  columns.push({ name: col.name, status: "missing", expectedType: col.sqlType });
113
- } else if (actual2.type.toUpperCase() !== col.sqlType) {
114
- columns.push({ name: col.name, status: "type_mismatch", expectedType: col.sqlType, actualType: actual2.type });
130
+ } else if (actualRow.type.toUpperCase() !== col.sqlType) {
131
+ columns.push({ name: col.name, status: "type_mismatch", expectedType: col.sqlType, actualType: actualRow.type });
115
132
  } else {
116
133
  columns.push({ name: col.name, status: "ok" });
117
134
  }
@@ -121,37 +138,57 @@ async function diffSeed(seed, options) {
121
138
  columns.push({ name: row.name, status: "extra", actualType: row.type });
122
139
  }
123
140
  }
141
+ const relationBranches = seed.branches.filter((b) => b.type === "relation" && b.targetSeed);
142
+ if (relationBranches.length > 0) {
143
+ let fkList = [];
144
+ let indexList = [];
145
+ try {
146
+ fkList = queryD1(`PRAGMA foreign_key_list(${tableName})`, options);
147
+ indexList = queryD1(`PRAGMA index_list(${tableName})`, options);
148
+ } catch {
149
+ }
150
+ const fkByCol = /* @__PURE__ */ new Map();
151
+ for (const fk of fkList) {
152
+ fkByCol.set(fk.from, fk);
153
+ }
154
+ const indexNames = new Set(indexList.map((i) => i.name));
155
+ for (const branch of relationBranches) {
156
+ const expectedFkTable = `content_${branch.targetSeed}`;
157
+ const expectedOnDelete = (branch.onDelete ?? "SET NULL").toUpperCase();
158
+ const expectedIndexName = `idx_${seed.slug}_${branch.alias}`;
159
+ const colDiff = columns.find((c) => c.name === branch.alias);
160
+ if (!colDiff || colDiff.status === "missing") continue;
161
+ const fk = fkByCol.get(branch.alias);
162
+ if (!fk) {
163
+ colDiff.status = "fk_missing";
164
+ colDiff.expectedTarget = branch.targetSeed;
165
+ } else {
166
+ const actualTable = fk.table;
167
+ const actualOnDelete = fk.on_delete.toUpperCase();
168
+ if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
169
+ colDiff.status = "fk_mismatch";
170
+ colDiff.expected = `\u2192 ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`;
171
+ colDiff.actual = `\u2192 ${actualTable}(id) ON DELETE ${actualOnDelete}`;
172
+ colDiff.expectedTarget = branch.targetSeed;
173
+ }
174
+ }
175
+ if (!indexNames.has(expectedIndexName)) {
176
+ if (colDiff.status === "ok") {
177
+ colDiff.status = "index_missing";
178
+ } else {
179
+ columns.push({ name: branch.alias, status: "index_missing" });
180
+ }
181
+ }
182
+ }
183
+ }
124
184
  return { slug: seed.slug, tableExists: true, columns };
125
185
  }
126
186
 
127
187
  // src/commands/validate.ts
128
188
  import pc from "picocolors";
129
- import { SEED_REGISTRY } from "@beechcms/core";
189
+ import { SEED_REGISTRY, validateSeedDefinitions } from "@beechcms/core";
130
190
  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;
191
+ return validateSeedDefinitions(Object.values(registry));
155
192
  }
156
193
  async function validate(args) {
157
194
  const registry = args.registry ?? SEED_REGISTRY;
@@ -161,32 +198,57 @@ async function validate(args) {
161
198
  }
162
199
  console.log(pc.cyan("\n beech validate \u2014 checking seeds\n"));
163
200
  const errors = validateSeeds(registry);
164
- const errorMap = new Map(errors.map((e) => [e.slug, e.messages]));
165
- let totalIssues = 0;
201
+ const fatalErrors = errors.filter((e) => e.fatal);
202
+ const warnings = errors.filter((e) => !e.fatal);
203
+ for (const e of fatalErrors) {
204
+ console.log(pc.red(` \u2717 ${e.slug} (fatal)`));
205
+ for (const msg of e.messages) {
206
+ console.log(pc.red(` \u2192 ${msg}`));
207
+ }
208
+ }
209
+ const warningMap = new Map(warnings.map((e) => [e.slug, e.messages]));
210
+ const allWarningSlugsSeen = new Set(warnings.map((e) => e.slug));
166
211
  for (const seed of Object.values(registry)) {
167
- const msgs = errorMap.get(seed.slug);
212
+ const msgs = warningMap.get(seed.slug);
168
213
  if (!msgs) {
169
- console.log(pc.green(` \u2713 ${seed.slug}`));
214
+ if (!allWarningSlugsSeen.has(seed.slug)) {
215
+ const hasFatal = fatalErrors.some((e) => e.slug === seed.slug);
216
+ if (!hasFatal) console.log(pc.green(` \u2713 ${seed.slug}`));
217
+ }
170
218
  } else {
171
- totalIssues += msgs.length;
172
- console.log(pc.red(` \u2717 ${seed.slug}`));
219
+ console.log(pc.yellow(` \u26A0 ${seed.slug}`));
173
220
  for (const msg of msgs) {
174
- console.log(pc.red(` \u2192 ${msg}`));
221
+ console.log(pc.yellow(` \u2192 ${msg}`));
175
222
  }
176
223
  }
177
224
  }
178
225
  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.
226
+ const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
227
+ const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0);
228
+ if (totalFatal > 0) {
229
+ const s = totalFatal !== 1 ? "s" : "";
230
+ console.log(pc.red(` Found ${totalFatal} fatal error${s}. Fix before loading.
182
231
  `));
183
232
  process.exit(1);
233
+ } else if (totalWarnings > 0) {
234
+ const s = totalWarnings !== 1 ? "s" : "";
235
+ console.log(pc.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.
236
+ `));
184
237
  } else {
185
238
  console.log(pc.green(" All seeds valid.\n"));
186
239
  }
187
240
  }
188
241
 
189
242
  // src/commands/seed-load.ts
243
+ function buildSeedRegistrationSql(seed) {
244
+ const json = sqlQuote(JSON.stringify(seed));
245
+ return [
246
+ `INSERT INTO seeds (slug, definition, status, source, created_at, updated_at)`,
247
+ `VALUES (${sqlQuote(seed.slug)}, ${json}, 'active', 'code', unixepoch(), unixepoch())`,
248
+ `ON CONFLICT(slug) DO UPDATE SET definition = excluded.definition, status = 'active', updated_at = excluded.updated_at;`
249
+ ].join("\n");
250
+ }
251
+ var SEED_META_BUMP_SQL = `UPDATE seed_meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE id = 'registry_version';`;
190
252
  function buildStatements(seed) {
191
253
  const stmts = [generateCreateTable(seed), ...generateIndexes(seed)];
192
254
  const draft = generateDraftTable(seed);
@@ -195,10 +257,16 @@ function buildStatements(seed) {
195
257
  if (fts) {
196
258
  stmts.push(fts, ...generateFtsTriggers(seed));
197
259
  }
260
+ for (const branch of seed.branches) {
261
+ if (branch.type !== "relation" || branch.multiple !== true) continue;
262
+ stmts.push(generateJunctionTable(seed, branch), ...generateJunctionIndexes(seed, branch));
263
+ const draftJunction = generateJunctionDraftTable(seed, branch);
264
+ if (draftJunction) stmts.push(draftJunction);
265
+ }
198
266
  return stmts;
199
267
  }
200
268
  async function runDiff(options, registry) {
201
- const seeds = Object.values(registry);
269
+ const seeds = sortSeedsByDependencies(Object.values(registry));
202
270
  console.log(pc2.cyan("\n Diffing schema\u2026\n"));
203
271
  let allOk = true;
204
272
  for (const seed of seeds) {
@@ -223,6 +291,12 @@ async function runDiff(options, registry) {
223
291
  console.log(pc2.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) \u2014 exists in DB but not in seeds.ts`));
224
292
  } else if (col.status === "type_mismatch") {
225
293
  console.log(pc2.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
294
+ } else if (col.status === "fk_missing") {
295
+ console.log(pc2.red(` \u292C missing FK: ${col.name} \u2192 content_${col.expectedTarget}(id)`));
296
+ } else if (col.status === "fk_mismatch") {
297
+ console.log(pc2.yellow(` \u292C FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`));
298
+ } else if (col.status === "index_missing") {
299
+ console.log(pc2.yellow(` \u2298 missing index on ${col.name}`));
226
300
  }
227
301
  }
228
302
  }
@@ -234,7 +308,7 @@ async function runDiff(options, registry) {
234
308
  }
235
309
  }
236
310
  async function runLoad(options, dryRun, registry) {
237
- const seeds = Object.values(registry);
311
+ const seeds = sortSeedsByDependencies(Object.values(registry));
238
312
  if (dryRun) {
239
313
  console.log(pc2.cyan("\n -- dry-run: SQL that would be executed\n"));
240
314
  for (const seed of seeds) {
@@ -243,14 +317,18 @@ async function runLoad(options, dryRun, registry) {
243
317
  for (const stmt of stmts) {
244
318
  console.log(stmt + "\n");
245
319
  }
320
+ console.log(pc2.dim(` -- register ${seed.slug} in seeds table`));
321
+ console.log(buildSeedRegistrationSql(seed) + "\n");
246
322
  }
323
+ console.log(pc2.dim(" -- bump registry_version"));
324
+ console.log(SEED_META_BUMP_SQL + "\n");
247
325
  return;
248
326
  }
249
327
  console.log(pc2.cyan(`
250
328
  Loading seeds into ${options.local ? "local" : "remote"} D1 (${options.db})\u2026
251
329
  `));
252
330
  for (const seed of seeds) {
253
- const stmts = buildStatements(seed);
331
+ const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)];
254
332
  const sql = stmts.join("\n\n") + "\n";
255
333
  process.stdout.write(` ${pc2.dim("\u2192")} content_${seed.slug}\u2026 `);
256
334
  const ok = executeD1File(sql, options);
@@ -274,7 +352,10 @@ async function runLoad(options, dryRun, registry) {
274
352
  }
275
353
  console.log(pc2.green("done"));
276
354
  }
355
+ executeD1File(SEED_META_BUMP_SQL, options);
277
356
  console.log(pc2.green("\n All seeds loaded.\n"));
357
+ console.log(pc2.dim(" Definitions registered in the database."));
358
+ console.log(pc2.dim(" seed.ts is no longer required at runtime \u2014 you may keep it for code-first edits or delete it.\n"));
278
359
  }
279
360
  async function seedLoad(args) {
280
361
  const registry = args.registry ?? SEED_REGISTRY2;
@@ -285,8 +366,25 @@ async function seedLoad(args) {
285
366
  return;
286
367
  }
287
368
  const validationErrors = validateSeeds(registry);
288
- if (validationErrors.length > 0) {
289
- const total = validationErrors.reduce((n, e) => n + e.messages.length, 0);
369
+ const fatalErrors = validationErrors.filter((e) => e.fatal);
370
+ const warnings = validationErrors.filter((e) => !e.fatal);
371
+ if (fatalErrors.length > 0) {
372
+ const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
373
+ const s = total !== 1 ? "s" : "";
374
+ console.log(pc2.red(`
375
+ \u2717 Seed validation found ${total} fatal error${s}. Cannot load schema.
376
+ `));
377
+ for (const e of fatalErrors) {
378
+ console.log(pc2.red(` \u2717 ${e.slug}`));
379
+ for (const msg of e.messages) {
380
+ console.log(pc2.red(` \u2192 ${msg}`));
381
+ }
382
+ }
383
+ console.log("");
384
+ process.exit(1);
385
+ }
386
+ if (warnings.length > 0) {
387
+ const total = warnings.reduce((n, e) => n + e.messages.length, 0);
290
388
  const s = total !== 1 ? "s" : "";
291
389
  console.log(pc2.yellow(`
292
390
  \u26A0 Seed validation found ${total} issue${s}. Schema changes will still be applied.
@@ -300,6 +398,27 @@ async function seedLoad(args) {
300
398
  local: args.local,
301
399
  configPath
302
400
  };
401
+ if (!args.dryRun && !args.diff) {
402
+ try {
403
+ const rows = queryD1(
404
+ `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
405
+ options
406
+ );
407
+ if (rows.length < 2) {
408
+ console.log(pc2.red("\n \u2717 System tables not found (seeds, seed_meta)\n"));
409
+ console.log(pc2.dim(" Run `beech init --db` first to initialise the database."));
410
+ const flag = args.local ? " --local" : "";
411
+ console.log(pc2.cyan(`
412
+ \u2192 Run: npx beech init --db${flag}
413
+ `));
414
+ process.exit(1);
415
+ }
416
+ } catch {
417
+ console.log(pc2.red("\n \u2717 Could not query the database\n"));
418
+ console.log(pc2.dim(" Run `beech init --db` first to initialise the database."));
419
+ process.exit(1);
420
+ }
421
+ }
303
422
  if (args.diff) {
304
423
  await runDiff(options, registry);
305
424
  } else {
@@ -324,7 +443,10 @@ var SYSTEM_TABLES = [
324
443
  "notifications",
325
444
  "media_objects",
326
445
  "content_event_log",
327
- "automations"
446
+ "automations",
447
+ "seeds",
448
+ "seed_meta",
449
+ "site_settings"
328
450
  ];
329
451
  var BASE_SCHEMA_SQL = `
330
452
  CREATE TABLE IF NOT EXISTS users (
@@ -462,6 +584,31 @@ CREATE TABLE IF NOT EXISTS automations (
462
584
 
463
585
  CREATE INDEX IF NOT EXISTS idx_automations_seed_slug ON automations(seed_slug);
464
586
  CREATE INDEX IF NOT EXISTS idx_automations_enabled ON automations(enabled);
587
+
588
+ CREATE TABLE IF NOT EXISTS seeds (
589
+ slug TEXT NOT NULL PRIMARY KEY,
590
+ definition TEXT NOT NULL,
591
+ status TEXT NOT NULL DEFAULT 'active'
592
+ CHECK (status IN ('active', 'deleted')),
593
+ source TEXT NOT NULL DEFAULT 'runtime'
594
+ CHECK (source IN ('code', 'runtime')),
595
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
596
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
597
+ );
598
+
599
+ CREATE INDEX IF NOT EXISTS idx_seeds_status ON seeds(status);
600
+
601
+ CREATE TABLE IF NOT EXISTS seed_meta (
602
+ id TEXT NOT NULL PRIMARY KEY,
603
+ value TEXT NOT NULL
604
+ );
605
+
606
+ INSERT OR IGNORE INTO seed_meta (id, value) VALUES ('registry_version', '1');
607
+
608
+ CREATE TABLE IF NOT EXISTS site_settings (
609
+ key TEXT NOT NULL PRIMARY KEY,
610
+ value TEXT NOT NULL
611
+ );
465
612
  `.trim();
466
613
  var PLACEHOLDER_DB_IDS = [
467
614
  "INCOLLA_QUI_IL_TUO_ID_D1",
@@ -601,7 +748,7 @@ function checkFiles(cwd, checkDevVars) {
601
748
  }
602
749
  const seedsExists = existsSync2(resolve2(cwd, "seeds.ts")) || existsSync2(resolve2(cwd, "seeds.js")) || existsSync2(resolve2(cwd, "seed.ts")) || existsSync2(resolve2(cwd, "seed.js"));
603
750
  if (!seedsExists) {
604
- console.log(pc3.yellow(" \u26A0 seeds.ts \u2014 missing (create it, then run beech seed:load)"));
751
+ console.log(pc3.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)"));
605
752
  } else {
606
753
  console.log(pc3.green(" \u2713 seeds.ts"));
607
754
  }
@@ -618,7 +765,7 @@ function printNextSteps(local) {
618
765
  const localFlag = local ? " --local" : "";
619
766
  console.log(pc3.dim(" Next steps:"));
620
767
  console.log(pc3.cyan(` 1. npx beech seed:load${localFlag}`));
621
- console.log(pc3.dim(" \u2192 create content tables from seeds.ts"));
768
+ console.log(pc3.dim(" \u2192 create content tables and register seed definitions in D1"));
622
769
  console.log(pc3.cyan(" 2. npx wrangler dev"));
623
770
  console.log(pc3.dim(" \u2192 start API + dashboard"));
624
771
  console.log(pc3.dim(" 3. Open http://localhost:8789/admin\n"));
@@ -663,7 +810,7 @@ async function init(args) {
663
810
  for (const issue of placeholders) {
664
811
  console.log(pc3.yellow(` - ${issue}`));
665
812
  }
666
- if (process.stdin.isTTY) {
813
+ if (process.stdin.isTTY && !args.nonInteractive) {
667
814
  const rl = createInterface({ input: process.stdin, output: process.stdout });
668
815
  let autoCreate = false;
669
816
  try {
@@ -721,7 +868,14 @@ async function init(args) {
721
868
  printManualDbInstructions();
722
869
  process.exit(1);
723
870
  }
871
+ } else if (args.nonInteractive && args.local) {
872
+ console.log(pc3.dim(" --yes: proceeding with local mode (no remote resources needed)\n"));
724
873
  } else {
874
+ if (args.nonInteractive) {
875
+ console.log(pc3.red("\n \u2717 Cannot proceed non-interactively: remote database has a placeholder database_id\n"));
876
+ console.log(pc3.dim(" Set a real database_id in wrangler.jsonc, then retry."));
877
+ process.exit(1);
878
+ }
725
879
  printManualDbInstructions();
726
880
  process.exit(1);
727
881
  }
@@ -1039,50 +1193,65 @@ async function deploy(args) {
1039
1193
  }
1040
1194
  }
1041
1195
 
1042
- // src/commands/update.ts
1196
+ // src/commands/onboard.ts
1043
1197
  import pc6 from "picocolors";
1198
+ async function onboard(args) {
1199
+ console.log(pc6.cyan("\n beech onboard \u2014 full provisioning\n"));
1200
+ await init({ initDb: true, local: args.local, db: args.db, nonInteractive: args.yes });
1201
+ await seedLoad({ dryRun: false, diff: false, local: args.local, db: args.db, registry: args.registry ?? null });
1202
+ console.log(pc6.cyan("\n Provisioning complete.\n"));
1203
+ console.log(pc6.dim(" Next steps:"));
1204
+ console.log(pc6.cyan(" 1. npx wrangler dev"));
1205
+ console.log(pc6.dim(" \u2192 start API + dashboard"));
1206
+ console.log(pc6.dim(" 2. Open http://localhost:8789/admin"));
1207
+ console.log(pc6.dim(" \u2192 complete setup wizard to create admin user\n"));
1208
+ }
1209
+
1210
+ // src/commands/update.ts
1211
+ import pc7 from "picocolors";
1044
1212
  import { spawnSync as spawnSync4 } from "node:child_process";
1045
1213
  async function update(_args) {
1046
- console.log(pc6.cyan("\n beech update\n"));
1047
- console.log(pc6.dim(" [1/2] Installing latest BeechCMS packages\u2026\n"));
1214
+ console.log(pc7.cyan("\n beech update\n"));
1215
+ console.log(pc7.dim(" [1/2] Installing latest BeechCMS packages\u2026\n"));
1048
1216
  const installResult = spawnSync4(
1049
1217
  "npm",
1050
1218
  ["install", "@beechcms/api@latest", "@beechcms/core@latest"],
1051
1219
  { stdio: "inherit", cwd: process.cwd(), shell: true }
1052
1220
  );
1053
1221
  if (installResult.status !== 0) {
1054
- console.log(pc6.red("\n \u2717 npm install failed\n"));
1055
- console.log(pc6.dim(" Check the output above for details."));
1056
- console.log(pc6.dim(" You may need to resolve version conflicts manually."));
1057
- console.log(pc6.cyan("\n \u2192 Try: npm install --legacy-peer-deps\n"));
1222
+ console.log(pc7.red("\n \u2717 npm install failed\n"));
1223
+ console.log(pc7.dim(" Check the output above for details."));
1224
+ console.log(pc7.dim(" You may need to resolve version conflicts manually."));
1225
+ console.log(pc7.cyan("\n \u2192 Try: npm install --legacy-peer-deps\n"));
1058
1226
  process.exit(1);
1059
1227
  }
1060
- console.log(pc6.green("\n \u2713 Packages updated"));
1061
- console.log(pc6.dim("\n [2/2] Applying system migrations to local database\u2026\n"));
1228
+ console.log(pc7.green("\n \u2713 Packages updated"));
1229
+ console.log(pc7.dim("\n [2/2] Applying system migrations to local database\u2026\n"));
1062
1230
  const initResult = spawnSync4(
1063
1231
  "npx",
1064
1232
  ["beech", "init", "--db", "--local"],
1065
1233
  { stdio: "inherit", cwd: process.cwd(), shell: true }
1066
1234
  );
1067
1235
  if (initResult.status !== 0) {
1068
- console.log(pc6.yellow("\n \u26A0 Local DB update failed\n"));
1069
- console.log(pc6.dim(" Apply system migrations manually:"));
1070
- console.log(pc6.cyan(" \u2192 Run: npx beech init --db --local\n"));
1236
+ console.log(pc7.yellow("\n \u26A0 Local DB update failed\n"));
1237
+ console.log(pc7.dim(" Apply system migrations manually:"));
1238
+ console.log(pc7.cyan(" \u2192 Run: npx beech init --db --local\n"));
1071
1239
  } else {
1072
- console.log(pc6.green("\n \u2713 Local database updated"));
1240
+ console.log(pc7.green("\n \u2713 Local database updated"));
1073
1241
  }
1074
- console.log(pc6.dim("\n Local update complete.\n"));
1075
- console.log(pc6.dim(" Next steps:"));
1076
- console.log(pc6.cyan(" 1. npx beech seed:load --local"));
1077
- console.log(pc6.dim(" \u2192 sync content schema to local DB"));
1078
- console.log(pc6.cyan(" 2. npm run deploy"));
1079
- console.log(pc6.dim(" \u2192 deploy updated API + dashboard"));
1080
- console.log(pc6.cyan(" 3. npx beech seed:load"));
1081
- console.log(pc6.dim(" \u2192 sync remote schema\n"));
1242
+ console.log(pc7.dim("\n Local update complete.\n"));
1243
+ console.log(pc7.dim(" Next steps:"));
1244
+ console.log(pc7.cyan(" 1. npx beech seed:load --local"));
1245
+ console.log(pc7.dim(" \u2192 sync content schema to local DB"));
1246
+ console.log(pc7.cyan(" 2. npm run deploy"));
1247
+ console.log(pc7.dim(" \u2192 deploy updated API + dashboard"));
1248
+ console.log(pc7.cyan(" 3. npx beech seed:load"));
1249
+ console.log(pc7.dim(" \u2192 sync remote schema\n"));
1082
1250
  }
1083
1251
  export {
1084
1252
  deploy,
1085
1253
  init,
1254
+ onboard,
1086
1255
  seedCreate,
1087
1256
  seedLoad,
1088
1257
  update,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/cli",
3
- "version": "0.4.3",
3
+ "version": "0.6.0-preview.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -8,14 +8,18 @@
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc --noEmit && esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --outfile=dist/index.js",
11
- "dev": "esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --outfile=dist/index.js --watch"
11
+ "dev": "esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --outfile=dist/index.js --watch",
12
+ "test": "vitest run",
13
+ "test:coverage": "vitest run --coverage"
12
14
  },
13
15
  "dependencies": {
14
- "@beechcms/core": "^0.4.3",
16
+ "@beechcms/core": "^0.6.0-preview.1",
15
17
  "picocolors": "^1.1.1"
16
18
  },
17
19
  "devDependencies": {
18
20
  "esbuild": "^0.27.3",
19
- "typescript": "^5.9.3"
20
- }
21
+ "typescript": "^5.9.3",
22
+ "vitest": "^4.1.0"
23
+ },
24
+ "license": "MIT"
21
25
  }
@@ -1,3 +1,6 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
1
4
  import pc from 'picocolors'
2
5
  import { spawnSync } from 'node:child_process'
3
6
  import { readFileSync } from 'node:fs'
@@ -1,3 +1,6 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
1
4
  import pc from 'picocolors'
2
5
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3
6
  import { createInterface } from 'node:readline/promises'
@@ -19,6 +22,9 @@ const SYSTEM_TABLES = [
19
22
  'media_objects',
20
23
  'content_event_log',
21
24
  'automations',
25
+ 'seeds',
26
+ 'seed_meta',
27
+ 'site_settings',
22
28
  ]
23
29
 
24
30
  // Embedded copy of 0000_v040_base.sql — all DDL uses CREATE TABLE IF NOT EXISTS,
@@ -159,6 +165,31 @@ CREATE TABLE IF NOT EXISTS automations (
159
165
 
160
166
  CREATE INDEX IF NOT EXISTS idx_automations_seed_slug ON automations(seed_slug);
161
167
  CREATE INDEX IF NOT EXISTS idx_automations_enabled ON automations(enabled);
168
+
169
+ CREATE TABLE IF NOT EXISTS seeds (
170
+ slug TEXT NOT NULL PRIMARY KEY,
171
+ definition TEXT NOT NULL,
172
+ status TEXT NOT NULL DEFAULT 'active'
173
+ CHECK (status IN ('active', 'deleted')),
174
+ source TEXT NOT NULL DEFAULT 'runtime'
175
+ CHECK (source IN ('code', 'runtime')),
176
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
177
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
178
+ );
179
+
180
+ CREATE INDEX IF NOT EXISTS idx_seeds_status ON seeds(status);
181
+
182
+ CREATE TABLE IF NOT EXISTS seed_meta (
183
+ id TEXT NOT NULL PRIMARY KEY,
184
+ value TEXT NOT NULL
185
+ );
186
+
187
+ INSERT OR IGNORE INTO seed_meta (id, value) VALUES ('registry_version', '1');
188
+
189
+ CREATE TABLE IF NOT EXISTS site_settings (
190
+ key TEXT NOT NULL PRIMARY KEY,
191
+ value TEXT NOT NULL
192
+ );
162
193
  `.trim()
163
194
 
164
195
  const PLACEHOLDER_DB_IDS = [
@@ -171,6 +202,7 @@ export interface InitOptions {
171
202
  initDb: boolean
172
203
  local: boolean
173
204
  db?: string
205
+ nonInteractive?: boolean
174
206
  }
175
207
 
176
208
  function checkWranglerAuth(): boolean {
@@ -332,7 +364,7 @@ function checkFiles(cwd: string, checkDevVars: boolean): boolean {
332
364
  existsSync(resolve(cwd, 'seed.js'))
333
365
 
334
366
  if (!seedsExists) {
335
- console.log(pc.yellow(' ⚠ seeds.ts — missing (create it, then run beech seed:load)'))
367
+ console.log(pc.yellow(' ⚠ seeds.ts — not found (optional: needed only for the one-time code → DB load; after `beech seed:load`, the DB is canonical)'))
336
368
  } else {
337
369
  console.log(pc.green(' ✓ seeds.ts'))
338
370
  }
@@ -352,7 +384,7 @@ function printNextSteps(local: boolean): void {
352
384
  const localFlag = local ? ' --local' : ''
353
385
  console.log(pc.dim(' Next steps:'))
354
386
  console.log(pc.cyan(` 1. npx beech seed:load${localFlag}`))
355
- console.log(pc.dim(' → create content tables from seeds.ts'))
387
+ console.log(pc.dim(' → create content tables and register seed definitions in D1'))
356
388
  console.log(pc.cyan(' 2. npx wrangler dev'))
357
389
  console.log(pc.dim(' → start API + dashboard'))
358
390
  console.log(pc.dim(' 3. Open http://localhost:8789/admin\n'))
@@ -409,7 +441,7 @@ export async function init(args: InitOptions): Promise<void> {
409
441
  console.log(pc.yellow(` - ${issue}`))
410
442
  }
411
443
 
412
- if (process.stdin.isTTY) {
444
+ if (process.stdin.isTTY && !args.nonInteractive) {
413
445
  // Interactive: offer auto-creation of D1 + R2
414
446
  const rl = createInterface({ input: process.stdin, output: process.stdout })
415
447
  let autoCreate = false
@@ -471,7 +503,17 @@ export async function init(args: InitOptions): Promise<void> {
471
503
  printManualDbInstructions()
472
504
  process.exit(1)
473
505
  }
506
+ } else if (args.nonInteractive && args.local) {
507
+ // --yes + --local: local D1 needs no real database_id — just proceed
508
+ console.log(pc.dim(' --yes: proceeding with local mode (no remote resources needed)\n'))
509
+ // Fall through to DB initialization
474
510
  } else {
511
+ if (args.nonInteractive) {
512
+ // --yes + --remote + placeholder → cannot proceed non-interactively
513
+ console.log(pc.red('\n ✗ Cannot proceed non-interactively: remote database has a placeholder database_id\n'))
514
+ console.log(pc.dim(' Set a real database_id in wrangler.jsonc, then retry.'))
515
+ process.exit(1)
516
+ }
475
517
  printManualDbInstructions()
476
518
  process.exit(1)
477
519
  }
@@ -0,0 +1,32 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
4
+ import pc from 'picocolors'
5
+ import type { Seed } from '@beechcms/core'
6
+ import { init } from './init.js'
7
+ import { seedLoad } from './seed-load.js'
8
+
9
+ export interface OnboardOptions {
10
+ local: boolean
11
+ yes: boolean
12
+ db?: string
13
+ registry?: Record<string, Seed> | null
14
+ }
15
+
16
+ export async function onboard(args: OnboardOptions): Promise<void> {
17
+ console.log(pc.cyan('\n beech onboard — full provisioning\n'))
18
+
19
+ // Step 1: file check + DB init
20
+ await init({ initDb: true, local: args.local, db: args.db, nonInteractive: args.yes })
21
+
22
+ // Step 2: create content tables + register definitions + bump registry_version
23
+ await seedLoad({ dryRun: false, diff: false, local: args.local, db: args.db, registry: args.registry ?? null })
24
+
25
+ // Step 3: next steps
26
+ console.log(pc.cyan('\n Provisioning complete.\n'))
27
+ console.log(pc.dim(' Next steps:'))
28
+ console.log(pc.cyan(' 1. npx wrangler dev'))
29
+ console.log(pc.dim(' → start API + dashboard'))
30
+ console.log(pc.dim(' 2. Open http://localhost:8789/admin'))
31
+ console.log(pc.dim(' → complete setup wizard to create admin user\n'))
32
+ }