@beechcms/cli 0.8.0 → 0.9.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
@@ -17,8 +17,15 @@ var __export = (target, all) => {
17
17
  import { spawnSync } from "node:child_process";
18
18
  import { writeFileSync, rmSync, existsSync, readFileSync, readdirSync } from "node:fs";
19
19
  import { tmpdir } from "node:os";
20
- import { join, resolve } from "node:path";
20
+ import { dirname, join, resolve } from "node:path";
21
21
  import { DatabaseSync } from "node:sqlite";
22
+ import { createRequire } from "node:module";
23
+ function validateDbName(name) {
24
+ if (typeof name !== "string" || !/^[A-Za-z0-9_-]+$/.test(name)) {
25
+ throw new Error(`Invalid D1 database name: ${name}`);
26
+ }
27
+ return name;
28
+ }
22
29
  function buildArgs(options) {
23
30
  const args = [];
24
31
  if (options.configPath) args.push("--config", options.configPath);
@@ -50,6 +57,7 @@ function getLocalD1SqlitePath(startDir = process.cwd()) {
50
57
  return null;
51
58
  }
52
59
  function executeD1File(sql, options) {
60
+ validateDbName(options.db);
53
61
  if (options.local) {
54
62
  const sqlitePath = getLocalD1SqlitePath();
55
63
  if (sqlitePath) {
@@ -65,10 +73,10 @@ function executeD1File(sql, options) {
65
73
  try {
66
74
  writeFileSync(tmpFile, sql, "utf-8");
67
75
  const args = ["d1", "execute", options.db, "--file", tmpFile, ...buildArgs(options)];
68
- const result = spawnSync("npx", ["wrangler", ...args], {
76
+ const result = spawnSync(process.execPath, [join(dirname(require2.resolve("wrangler/package.json")), "bin", "wrangler.js"), ...args], {
69
77
  stdio: "inherit",
70
78
  cwd: process.cwd(),
71
- shell: true,
79
+ shell: false,
72
80
  env: { ...process.env, WRANGLER_SEND_METRICS: "false" }
73
81
  });
74
82
  return result.status === 0;
@@ -80,6 +88,7 @@ function executeD1File(sql, options) {
80
88
  }
81
89
  }
82
90
  function queryD1(sql, options) {
91
+ validateDbName(options.db);
83
92
  if (options.local) {
84
93
  const sqlitePath = getLocalD1SqlitePath();
85
94
  if (sqlitePath) {
@@ -95,10 +104,10 @@ function queryD1(sql, options) {
95
104
  try {
96
105
  writeFileSync(tmpFile, sql, "utf-8");
97
106
  const args = ["d1", "execute", options.db, "--file", tmpFile, "--json", ...buildArgs(options)];
98
- result = spawnSync("npx", ["wrangler", ...args], {
107
+ result = spawnSync(process.execPath, [join(dirname(require2.resolve("wrangler/package.json")), "bin", "wrangler.js"), ...args], {
99
108
  encoding: "utf-8",
100
109
  cwd: process.cwd(),
101
- shell: true,
110
+ shell: false,
102
111
  env: { ...process.env, WRANGLER_SEND_METRICS: "false" }
103
112
  });
104
113
  } finally {
@@ -123,12 +132,12 @@ function findWranglerConfig() {
123
132
  let dir = process.cwd();
124
133
  while (true) {
125
134
  for (const name of ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]) {
126
- const p3 = resolve(dir, name);
127
- if (existsSync(p3)) return p3;
135
+ const p4 = resolve(dir, name);
136
+ if (existsSync(p4)) return p4;
128
137
  }
129
138
  for (const name of ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]) {
130
- const p3 = resolve(dir, "apps", "api", name);
131
- if (existsSync(p3)) return p3;
139
+ const p4 = resolve(dir, "apps", "api", name);
140
+ if (existsSync(p4)) return p4;
132
141
  }
133
142
  const parent = resolve(dir, "..");
134
143
  if (parent === dir) break;
@@ -138,6 +147,7 @@ function findWranglerConfig() {
138
147
  }
139
148
  function resolveDbName(configPath) {
140
149
  if (!configPath) return "beech-db";
150
+ let name = "beech-db";
141
151
  try {
142
152
  const raw = readFileSync(configPath, "utf-8");
143
153
  if (configPath.endsWith(".toml")) {
@@ -145,21 +155,24 @@ function resolveDbName(configPath) {
145
155
  if (d1SectionMatch) {
146
156
  const section = d1SectionMatch[0];
147
157
  const dbNameMatch = section.match(/database_name\s*=\s*["'](.+?)["']/);
148
- if (dbNameMatch) return dbNameMatch[1];
158
+ if (dbNameMatch) name = dbNameMatch[1];
149
159
  }
150
- return "beech-db";
160
+ } else {
161
+ const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
162
+ const parsed = JSON.parse(stripped);
163
+ const bindings = parsed?.d1_databases ?? [];
164
+ name = bindings[0]?.database_name ?? "beech-db";
151
165
  }
152
- const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
153
- const parsed = JSON.parse(stripped);
154
- const bindings = parsed?.d1_databases ?? [];
155
- return bindings[0]?.database_name ?? "beech-db";
156
166
  } catch {
157
167
  return "beech-db";
158
168
  }
169
+ return validateDbName(name);
159
170
  }
171
+ var require2;
160
172
  var init_wrangler = __esm({
161
173
  "src/lib/wrangler.ts"() {
162
174
  "use strict";
175
+ require2 = createRequire(import.meta.url);
163
176
  }
164
177
  });
165
178
 
@@ -169,10 +182,11 @@ __export(init_exports, {
169
182
  init: () => init
170
183
  });
171
184
  import pc2 from "picocolors";
172
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
185
+ import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
173
186
  import { createInterface } from "node:readline/promises";
174
187
  import { resolve as resolve2, basename } from "node:path";
175
188
  import { spawnSync as spawnSync2 } from "node:child_process";
189
+ import { randomBytes } from "node:crypto";
176
190
  function checkWranglerAuth() {
177
191
  const result = spawnSync2("npx", ["wrangler", "whoami", "--json"], {
178
192
  encoding: "utf-8",
@@ -287,6 +301,24 @@ function echoApiKeys(configPath) {
287
301
  } catch {
288
302
  }
289
303
  }
304
+ function ensurePrivacyMasterKey(devVarsPath) {
305
+ const content = readFileSync2(devVarsPath, "utf8");
306
+ const lines = content.split(/\r?\n/);
307
+ const index = lines.findIndex((line) => /^PRIVACY_MASTER_KEY=/.test(line));
308
+ if (index !== -1 && lines[index].slice("PRIVACY_MASTER_KEY=".length).trim() !== "") return false;
309
+ const entry = `PRIVACY_MASTER_KEY=${randomBytes(32).toString("hex")}`;
310
+ if (index !== -1) {
311
+ lines[index] = entry;
312
+ writeFileSync2(devVarsPath, lines.join("\n"), "utf8");
313
+ } else {
314
+ const separator = content === "" || content.endsWith("\n") ? "" : "\n";
315
+ appendFileSync(devVarsPath, `${separator}
316
+ # Master key for protected fields (AES-GCM + blind index). Do NOT change once data is stored.
317
+ ${entry}
318
+ `, "utf8");
319
+ }
320
+ return true;
321
+ }
290
322
  function checkFiles(cwd, checkDevVars) {
291
323
  let ok = true;
292
324
  const workerExists = existsSync2(resolve2(cwd, "worker.ts")) || existsSync2(resolve2(cwd, "worker.js"));
@@ -309,6 +341,9 @@ function checkFiles(cwd, checkDevVars) {
309
341
  console.log(pc2.dim(" \u25CB .dev.vars \u2014 not found (optional: only needed for production R2 credentials)"));
310
342
  } else {
311
343
  console.log(pc2.green(" \u2713 .dev.vars"));
344
+ if (ensurePrivacyMasterKey(resolve2(cwd, ".dev.vars"))) {
345
+ console.log(pc2.green(" \u2713 PRIVACY_MASTER_KEY \u2014 generated in .dev.vars"));
346
+ }
312
347
  }
313
348
  }
314
349
  return ok;
@@ -1021,132 +1056,1291 @@ async function reset(args) {
1021
1056
  }
1022
1057
 
1023
1058
  // src/commands/generate-types.ts
1024
- init_wrangler();
1025
1059
  import { writeFileSync as writeFileSync3, mkdirSync } from "node:fs";
1026
- import { dirname, resolve as resolve4 } from "node:path";
1060
+ import { dirname as dirname2, resolve as resolve4 } from "node:path";
1061
+ import pc12 from "picocolors";
1062
+ import { computeSchemaFingerprint, generateSeedTypes } from "@beechcms/core";
1063
+
1064
+ // src/lib/d1-context.ts
1065
+ init_wrangler();
1027
1066
  import pc11 from "picocolors";
1028
- import { generateSeedTypes } from "@beechcms/core";
1029
- async function generateTypes(args = {}) {
1030
- const isLocal = args.local !== false;
1067
+ import { introspectSeedDefinitions } from "@beechcms/core";
1068
+
1069
+ // src/lib/d1-executor.ts
1070
+ init_wrangler();
1071
+ function createWranglerExecutor(options) {
1072
+ return {
1073
+ all(sql) {
1074
+ return Promise.resolve(queryD1(sql, options));
1075
+ }
1076
+ };
1077
+ }
1078
+
1079
+ // src/lib/d1-context.ts
1080
+ var CliError = class extends Error {
1081
+ constructor(message, hint, cause) {
1082
+ super(message);
1083
+ this.hint = hint;
1084
+ this.cause = cause;
1085
+ this.name = "CliError";
1086
+ }
1087
+ hint;
1088
+ cause;
1089
+ };
1090
+ function createD1Context(args = {}) {
1091
+ const local = args.local !== false;
1031
1092
  const configPath = findWranglerConfig();
1032
1093
  const db = args.db ?? resolveDbName(configPath);
1033
- if (isLocal) {
1034
- const sqlitePath = getLocalD1SqlitePath();
1035
- if (!sqlitePath) {
1036
- console.error(
1037
- pc11.red("\n \u2717 Local D1 database state not found.") + pc11.gray("\n Start your local development environment with `beech dev` or initialize with `beech init --db`.\n")
1094
+ if (local && !getLocalD1SqlitePath()) {
1095
+ throw new CliError(
1096
+ "Local D1 database state not found.",
1097
+ "Start the local environment with `beech dev`, or initialize it with `beech init --db`."
1098
+ );
1099
+ }
1100
+ const options = { db, local, configPath };
1101
+ return { executor: createWranglerExecutor(options), options };
1102
+ }
1103
+ async function loadLiveSeeds(context) {
1104
+ let seeds;
1105
+ try {
1106
+ seeds = await introspectSeedDefinitions(context.executor);
1107
+ } catch (error) {
1108
+ const detail = describeCause(error);
1109
+ if (detail.includes("no such table: seeds")) {
1110
+ throw new CliError(
1111
+ "System table `seeds` not found in the database.",
1112
+ "Run `beech init --db` or `beech onboard` to create the system tables.",
1113
+ error
1038
1114
  );
1039
- process.exit(1);
1040
1115
  }
1116
+ throw new CliError(
1117
+ `Failed to introspect D1 database (${context.options.db}): ${detail}`,
1118
+ void 0,
1119
+ error
1120
+ );
1041
1121
  }
1042
- const options = {
1043
- db,
1044
- local: isLocal,
1045
- configPath
1046
- };
1047
- let rows;
1122
+ if (seeds.length === 0) {
1123
+ throw new CliError(
1124
+ `No active seeds found in D1 database (${context.options.db}).`,
1125
+ "Create a content type in the dashboard (/admin) or through POST /api/seeds."
1126
+ );
1127
+ }
1128
+ return seeds;
1129
+ }
1130
+ function describeCause(error) {
1131
+ const parts = [];
1132
+ let current = error;
1133
+ for (let depth = 0; current instanceof Error && depth < 5; depth++) {
1134
+ parts.push(current.message);
1135
+ current = current.cause;
1136
+ }
1137
+ if (parts.length === 0) parts.push(String(error));
1138
+ return parts.join(" \u2014 ");
1139
+ }
1140
+ function exitWithError(error) {
1141
+ if (error instanceof CliError) {
1142
+ console.error(pc11.red(`
1143
+ \u2717 ${error.message}`) + (error.hint ? pc11.gray(`
1144
+ ${error.hint}
1145
+ `) : "\n"));
1146
+ } else {
1147
+ console.error(pc11.red(`
1148
+ \u2717 ${error instanceof Error ? error.message : String(error)}
1149
+ `));
1150
+ }
1151
+ process.exit(1);
1152
+ }
1153
+
1154
+ // src/commands/generate-types.ts
1155
+ var DEFAULT_TYPES_PATH = "beech.generated.ts";
1156
+ async function generateTypes(args = {}) {
1048
1157
  try {
1049
- rows = queryD1(
1050
- `SELECT slug, definition FROM seeds WHERE status = 'active' ORDER BY slug ASC;`,
1051
- options
1158
+ const context = createD1Context(args);
1159
+ const seeds = await loadLiveSeeds(context);
1160
+ const fingerprint = await computeSchemaFingerprint(seeds);
1161
+ const code = generateSeedTypes(seeds, { fingerprint });
1162
+ if (args.out === null) {
1163
+ process.stdout.write(code);
1164
+ return;
1165
+ }
1166
+ const target = args.out ?? DEFAULT_TYPES_PATH;
1167
+ const outPath = resolve4(process.cwd(), target);
1168
+ mkdirSync(dirname2(outPath), { recursive: true });
1169
+ writeFileSync3(outPath, code, "utf-8");
1170
+ console.log(
1171
+ pc12.green(`
1172
+ \u2713 Generated ${seeds.length} interface(s) \u2192 ${target}`) + pc12.gray(`
1173
+ schema fingerprint ${fingerprint}
1174
+ `)
1052
1175
  );
1053
1176
  } catch (error) {
1054
- const errMsg = error?.message || String(error);
1055
- if (errMsg.includes("no such table: seeds")) {
1056
- console.error(
1057
- pc11.red("\n \u2717 System table `seeds` not found in database.") + pc11.gray("\n Run `beech init --db` or `beech onboard` to initialize system tables.\n")
1177
+ exitWithError(error);
1178
+ }
1179
+ }
1180
+
1181
+ // src/commands/types-check.ts
1182
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
1183
+ import { resolve as resolve5 } from "node:path";
1184
+ import pc13 from "picocolors";
1185
+ import { computeSchemaFingerprint as computeSchemaFingerprint2, generateSeedTypes as generateSeedTypes2 } from "@beechcms/core";
1186
+ async function typesCheck(args = {}) {
1187
+ try {
1188
+ const context = createD1Context(args);
1189
+ const seeds = await loadLiveSeeds(context);
1190
+ const fingerprint = await computeSchemaFingerprint2(seeds);
1191
+ const fresh = generateSeedTypes2(seeds, { fingerprint });
1192
+ const target = args.out ?? DEFAULT_TYPES_PATH;
1193
+ const outPath = resolve5(process.cwd(), target);
1194
+ if (!existsSync4(outPath)) {
1195
+ console.log(
1196
+ pc13.yellow(`
1197
+ \u26A0 ${target} not found.`) + pc13.gray(`
1198
+ Run \`beech types generate\` to create it.
1199
+ `)
1058
1200
  );
1059
- } else {
1060
- console.error(
1061
- pc11.red(`
1062
- \u2717 Failed to introspect D1 database (${db}):`) + pc11.gray(`
1063
- ${errMsg}
1201
+ process.exit(1);
1202
+ }
1203
+ const committed = readFileSync5(outPath, "utf-8");
1204
+ if (normalizeEol(committed) !== normalizeEol(fresh)) {
1205
+ console.log(
1206
+ pc13.yellow(`
1207
+ \u26A0 ${target} is stale \u2014 it does not match the live D1 schema.`) + pc13.gray(`
1208
+ Run \`beech types generate\` and commit the result.
1064
1209
  `)
1065
1210
  );
1211
+ process.exit(1);
1066
1212
  }
1067
- process.exit(1);
1213
+ console.log(
1214
+ pc13.green(`
1215
+ \u2713 ${target} matches live D1.`) + pc13.gray(`
1216
+ schema fingerprint ${fingerprint}
1217
+ `)
1218
+ );
1219
+ } catch (error) {
1220
+ exitWithError(error);
1221
+ }
1222
+ }
1223
+ function normalizeEol(text3) {
1224
+ return text3.replace(/\r\n/g, "\n");
1225
+ }
1226
+
1227
+ // src/commands/schema-diff.ts
1228
+ import { existsSync as existsSync6 } from "node:fs";
1229
+ import { resolve as resolve7 } from "node:path";
1230
+ import pc16 from "picocolors";
1231
+
1232
+ // src/lib/manifest-loader.ts
1233
+ import { existsSync as existsSync5 } from "node:fs";
1234
+ import { resolve as resolve6 } from "node:path";
1235
+ import { pathToFileURL } from "node:url";
1236
+ import { MANIFEST_VERSION, validateManifest } from "@beechcms/core/schema";
1237
+ var DEFAULT_MANIFEST_PATH = "beech.schema.ts";
1238
+ var ManifestLoadError = class extends Error {
1239
+ constructor(message, issues = [], cause) {
1240
+ super(message);
1241
+ this.issues = issues;
1242
+ this.cause = cause;
1243
+ this.name = "ManifestLoadError";
1244
+ }
1245
+ issues;
1246
+ cause;
1247
+ };
1248
+ function interpretManifestModule(module, path) {
1249
+ const candidate = module?.default;
1250
+ if (candidate === void 0) {
1251
+ throw new ManifestLoadError(
1252
+ `${path} has no default export. A manifest ends with \`export default defineSchema({ seeds: [...] })\`.`
1253
+ );
1068
1254
  }
1069
- if (!rows || rows.length === 0) {
1070
- console.error(
1071
- pc11.red(`
1072
- \u2717 No active seeds found in D1 database (${db}).`) + pc11.gray("\n Create or activate seeds via the dashboard (/admin) or REST API (/api/seeds).\n")
1255
+ if (typeof candidate !== "object" || candidate === null) {
1256
+ throw new ManifestLoadError(`${path} default-exports a ${typeof candidate}, not a manifest object.`);
1257
+ }
1258
+ const manifest = candidate;
1259
+ if (manifest.version !== MANIFEST_VERSION) {
1260
+ throw new ManifestLoadError(
1261
+ `${path} declares manifest version ${String(manifest.version)}; this build understands ${MANIFEST_VERSION}.`
1073
1262
  );
1074
- process.exit(1);
1075
1263
  }
1076
- let seeds;
1264
+ if (!Array.isArray(manifest.seeds)) {
1265
+ throw new ManifestLoadError(`${path} is missing a \`seeds\` array.`);
1266
+ }
1267
+ const complete = manifest;
1268
+ const issues = validateManifest(complete);
1269
+ const fatal = issues.filter((issue) => issue.fatal);
1270
+ if (fatal.length > 0) {
1271
+ throw new ManifestLoadError(
1272
+ `${path} is not a valid schema: ${fatal.flatMap((issue) => issue.messages).join("; ")}`,
1273
+ issues
1274
+ );
1275
+ }
1276
+ return complete;
1277
+ }
1278
+ async function loadManifest(path = DEFAULT_MANIFEST_PATH) {
1279
+ const absolute = resolve6(process.cwd(), path);
1280
+ if (!existsSync5(absolute)) {
1281
+ throw new ManifestLoadError(
1282
+ `No manifest at ${path}. Produce one from the live database with \`beech schema export\`.`
1283
+ );
1284
+ }
1285
+ let module;
1077
1286
  try {
1078
- seeds = rows.map((r) => JSON.parse(r.definition));
1079
- } catch (err) {
1080
- console.error(
1081
- pc11.red("\n \u2717 Failed to parse seed definitions from database:") + pc11.gray(`
1082
- ${err?.message || err}
1083
- `)
1287
+ module = await import(pathToFileURL(absolute).href);
1288
+ } catch (error) {
1289
+ const code = error.code;
1290
+ if (code === "ERR_UNKNOWN_FILE_EXTENSION" || code === "ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING") {
1291
+ throw new ManifestLoadError(
1292
+ `This Node build cannot import ${path} directly (TypeScript type stripping unavailable).`,
1293
+ [],
1294
+ error
1295
+ );
1296
+ }
1297
+ throw new ManifestLoadError(`${path} failed to load: ${error.message}`, [], error);
1298
+ }
1299
+ return interpretManifestModule(module, path);
1300
+ }
1301
+
1302
+ // src/lib/manifest-compare.ts
1303
+ import pc14 from "picocolors";
1304
+ import { normalizeSeedDefinition } from "@beechcms/core";
1305
+ import { MANIFEST_VERSION as MANIFEST_VERSION2, seedsToManifest, toCanonicalJson } from "@beechcms/core/schema";
1306
+ function canonicalize(seed) {
1307
+ const normalized = normalizeSeedDefinition(seed);
1308
+ return toCanonicalJson({ version: MANIFEST_VERSION2, seeds: [normalized] });
1309
+ }
1310
+ function compareManifest(manifest, liveSeeds) {
1311
+ const authored = new Map(manifest.seeds.map((seed) => [seed.slug, canonicalize(seed)]));
1312
+ const deployed = new Map(seedsToManifest(liveSeeds).seeds.map((seed) => [seed.slug, canonicalize(seed)]));
1313
+ const slugs = [.../* @__PURE__ */ new Set([...authored.keys(), ...deployed.keys()])].sort((a, b) => a.localeCompare(b));
1314
+ const seeds = slugs.map((slug) => {
1315
+ const left = authored.get(slug);
1316
+ const right = deployed.get(slug);
1317
+ if (left === void 0) return { slug, status: "only_in_database" };
1318
+ if (right === void 0) return { slug, status: "only_in_manifest" };
1319
+ return { slug, status: left === right ? "in_sync" : "differs" };
1320
+ });
1321
+ return { seeds, inSync: seeds.every((seed) => seed.status === "in_sync") };
1322
+ }
1323
+ function renderManifestDrift(drift, manifestPath) {
1324
+ if (drift.inSync) {
1325
+ console.log(pc14.green(` \u2713 ${manifestPath} matches the deployed schema`));
1326
+ return;
1327
+ }
1328
+ for (const seed of drift.seeds) {
1329
+ switch (seed.status) {
1330
+ case "in_sync":
1331
+ console.log(pc14.green(` \u2713 ${seed.slug}`));
1332
+ break;
1333
+ case "only_in_manifest":
1334
+ console.log(pc14.yellow(` + ${seed.slug} \u2014 in the manifest, not deployed (apply would create it)`));
1335
+ break;
1336
+ case "only_in_database":
1337
+ console.log(pc14.yellow(` - ${seed.slug} \u2014 deployed, absent from the manifest (re-export to refresh)`));
1338
+ break;
1339
+ case "differs":
1340
+ console.log(pc14.red(` \u2260 ${seed.slug} \u2014 manifest and deployed definition disagree`));
1341
+ break;
1342
+ }
1343
+ }
1344
+ }
1345
+
1346
+ // src/lib/schema-diff.ts
1347
+ import pc15 from "picocolors";
1348
+ import { getExpectedColumns, introspectTable } from "@beechcms/core";
1349
+ function isSeedClean(diff) {
1350
+ return diff.tableExists && diff.columns.every((c) => c.status === "ok");
1351
+ }
1352
+ function renderSeedDiff(diff) {
1353
+ const table = `content_${diff.slug}`;
1354
+ if (!diff.tableExists) {
1355
+ console.log(pc15.red(` \u2717 ${table} \u2014 table missing`));
1356
+ return;
1357
+ }
1358
+ const problems = diff.columns.filter((c) => c.status !== "ok");
1359
+ if (problems.length === 0) {
1360
+ console.log(pc15.green(` \u2713 ${table}`));
1361
+ return;
1362
+ }
1363
+ console.log(pc15.yellow(` \u26A0 ${table}`));
1364
+ for (const col of problems) {
1365
+ switch (col.status) {
1366
+ case "missing":
1367
+ console.log(pc15.red(` + missing column: ${col.name} ${col.expectedType}`));
1368
+ break;
1369
+ case "extra":
1370
+ console.log(pc15.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) \u2014 in DB, not in schema definition`));
1371
+ break;
1372
+ case "type_mismatch":
1373
+ console.log(pc15.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
1374
+ break;
1375
+ case "fk_missing":
1376
+ console.log(pc15.red(` \u292C missing FK: ${col.name} \u2192 content_${col.expectedTarget}(id)`));
1377
+ break;
1378
+ case "fk_mismatch":
1379
+ console.log(pc15.yellow(` \u292C FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`));
1380
+ break;
1381
+ case "index_missing":
1382
+ console.log(pc15.yellow(` \u2298 missing index on ${col.name}`));
1383
+ break;
1384
+ }
1385
+ }
1386
+ }
1387
+ async function diffSeed(seed, executor) {
1388
+ const live = await introspectTable(executor, `content_${seed.slug}`);
1389
+ const expected = getExpectedColumns(seed);
1390
+ if (!live.exists) {
1391
+ return {
1392
+ slug: seed.slug,
1393
+ tableExists: false,
1394
+ columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType }))
1395
+ };
1396
+ }
1397
+ const actualMap = new Map(live.columns.map((c) => [c.name, c]));
1398
+ const expectedSet = new Set(expected.map((c) => c.name));
1399
+ const columns = [];
1400
+ for (const col of expected) {
1401
+ const actual = actualMap.get(col.name);
1402
+ if (!actual) {
1403
+ columns.push({ name: col.name, status: "missing", expectedType: col.sqlType });
1404
+ } else if (actual.sqlType !== col.sqlType) {
1405
+ columns.push({ name: col.name, status: "type_mismatch", expectedType: col.sqlType, actualType: actual.sqlType });
1406
+ } else {
1407
+ columns.push({ name: col.name, status: "ok" });
1408
+ }
1409
+ }
1410
+ for (const actual of live.columns) {
1411
+ if (!expectedSet.has(actual.name)) {
1412
+ columns.push({ name: actual.name, status: "extra", actualType: actual.sqlType });
1413
+ }
1414
+ }
1415
+ const relationBranches = seed.branches.filter((b) => b.type === "relation" && b.targetSeed);
1416
+ if (relationBranches.length > 0) {
1417
+ const fkByColumn = new Map(live.foreignKeys.map((fk) => [fk.column, fk]));
1418
+ const indexNames = new Set(live.indexes.map((i) => i.name));
1419
+ for (const branch of relationBranches) {
1420
+ const expectedFkTable = `content_${branch.targetSeed}`;
1421
+ const expectedOnDelete = (branch.onDelete ?? "SET NULL").toUpperCase();
1422
+ const expectedIndexName = `idx_${seed.slug}_${branch.alias}`;
1423
+ const colDiff = columns.find((c) => c.name === branch.alias);
1424
+ if (!colDiff || colDiff.status === "missing") continue;
1425
+ const fk = fkByColumn.get(branch.alias);
1426
+ if (!fk) {
1427
+ colDiff.status = "fk_missing";
1428
+ colDiff.expectedTarget = branch.targetSeed;
1429
+ } else if (fk.targetTable !== expectedFkTable || fk.onDelete !== expectedOnDelete) {
1430
+ colDiff.status = "fk_mismatch";
1431
+ colDiff.expected = `\u2192 ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`;
1432
+ colDiff.actual = `\u2192 ${fk.targetTable}(id) ON DELETE ${fk.onDelete}`;
1433
+ colDiff.expectedTarget = branch.targetSeed;
1434
+ }
1435
+ if (!indexNames.has(expectedIndexName)) {
1436
+ if (colDiff.status === "ok") colDiff.status = "index_missing";
1437
+ else columns.push({ name: branch.alias, status: "index_missing" });
1438
+ }
1439
+ }
1440
+ }
1441
+ return { slug: seed.slug, tableExists: true, columns };
1442
+ }
1443
+
1444
+ // src/commands/schema-diff.ts
1445
+ async function schemaDiff(args = {}) {
1446
+ try {
1447
+ const context = createD1Context(args);
1448
+ const liveSeeds = await loadLiveSeeds(context);
1449
+ const manifestPath = args.manifest ?? DEFAULT_MANIFEST_PATH;
1450
+ let manifestDrifted = false;
1451
+ console.log(pc16.cyan(`
1452
+ Manifest vs deployed definitions (${context.options.db})
1453
+ `));
1454
+ if (existsSync6(resolve7(process.cwd(), manifestPath))) {
1455
+ const manifest = await loadManifest(manifestPath);
1456
+ const drift = compareManifest(manifest, liveSeeds);
1457
+ renderManifestDrift(drift, manifestPath);
1458
+ manifestDrifted = !drift.inSync;
1459
+ } else {
1460
+ console.log(pc16.dim(` \u2298 no ${manifestPath} \u2014 run \`beech schema export\` to create one`));
1461
+ }
1462
+ console.log(pc16.cyan("\n Deployed definitions vs physical tables\n"));
1463
+ let physicalDrifted = false;
1464
+ for (const seed of liveSeeds) {
1465
+ const diff = await diffSeed(seed, context.executor);
1466
+ renderSeedDiff(diff);
1467
+ if (!isSeedClean(diff)) physicalDrifted = true;
1468
+ }
1469
+ console.log("");
1470
+ if (manifestDrifted || physicalDrifted) {
1471
+ console.log(pc16.yellow(" \u26A0 Schema drift detected.\n"));
1472
+ process.exit(1);
1473
+ }
1474
+ console.log(pc16.green(" \u2713 No drift.\n"));
1475
+ } catch (error) {
1476
+ exitWithError(error);
1477
+ }
1478
+ }
1479
+
1480
+ // src/commands/schema-migrate.ts
1481
+ import pc17 from "picocolors";
1482
+ import {
1483
+ findTagOrphans,
1484
+ manualTagSteps,
1485
+ planTagMigration,
1486
+ tagMigrationStatements
1487
+ } from "@beechcms/core";
1488
+ init_wrangler();
1489
+ function renderTagMigrationPlan(plan) {
1490
+ for (const step of plan.steps) {
1491
+ if (step.action === "manual") {
1492
+ console.log(pc17.red(` \u2717 ${step.seed}: ${step.summary}`));
1493
+ } else {
1494
+ console.log(pc17.green(` \u2713 ${step.seed}: ${step.summary}`));
1495
+ }
1496
+ for (const warning of step.warnings) console.log(pc17.yellow(` \u26A0 ${warning}`));
1497
+ }
1498
+ }
1499
+ async function schemaMigrate(args = {}) {
1500
+ try {
1501
+ const context = createD1Context(args);
1502
+ const seeds = await loadLiveSeeds(context);
1503
+ const plan = planTagMigration(seeds);
1504
+ console.log(pc17.cyan(`
1505
+ Tags contract migration (${context.options.db})
1506
+ `));
1507
+ if (plan.steps.length === 0) {
1508
+ console.log(pc17.green(" \u2713 Every seed already uses the closed-vocabulary tags contract. Nothing to migrate.\n"));
1509
+ return;
1510
+ }
1511
+ renderTagMigrationPlan(plan);
1512
+ const orphans = await findTagOrphans(context.executor, plan, seeds);
1513
+ for (const orphan of orphans) {
1514
+ console.log(pc17.yellow(
1515
+ ` \u26A0 ${orphan.seed}.${orphan.alias}: ${orphan.rows} row(s) hold '${orphan.value}', which is not an option. Left untouched.`
1516
+ ));
1517
+ }
1518
+ const manual = manualTagSteps(plan);
1519
+ const statements = tagMigrationStatements(plan, seeds);
1520
+ if (!args.apply) {
1521
+ console.log(pc17.dim(`
1522
+ Dry run: nothing was changed. ${statements.length > 0 ? "Re-run with --apply to migrate." : ""}`));
1523
+ if (manual.length > 0) console.log(pc17.red(` ${manual.length} step(s) need a manual edit of the seed.`));
1524
+ console.log("");
1525
+ process.exit(1);
1526
+ return;
1527
+ }
1528
+ if (statements.length === 0) {
1529
+ console.log(pc17.red(`
1530
+ \u2717 Nothing can be migrated automatically: ${manual.length} step(s) need a manual edit.
1531
+ `));
1532
+ process.exit(1);
1533
+ return;
1534
+ }
1535
+ if (!executeD1File(statements.join("\n"), context.options)) {
1536
+ throw new CliError(
1537
+ "The migration statements failed to execute.",
1538
+ "They are idempotent: fix the reported error and run `beech schema migrate --apply` again."
1539
+ );
1540
+ }
1541
+ const remaining = planTagMigration(await loadLiveSeeds(context));
1542
+ console.log(pc17.green(`
1543
+ \u2713 Migrated ${plan.seeds.length} seed(s).`));
1544
+ if (remaining.steps.length > 0) {
1545
+ console.log(pc17.yellow(` \u26A0 ${remaining.steps.length} step(s) still need attention; run \`beech schema audit\`.`));
1546
+ console.log("");
1547
+ process.exit(1);
1548
+ return;
1549
+ }
1550
+ console.log(pc17.dim(" Restart running Workers or wait for the registry cache to refresh, then run `beech types generate`.\n"));
1551
+ } catch (error) {
1552
+ exitWithError(error);
1553
+ }
1554
+ }
1555
+
1556
+ // src/commands/schema-audit.ts
1557
+ import pc18 from "picocolors";
1558
+ import { auditRelationStorage, planTagMigration as planTagMigration2 } from "@beechcms/core";
1559
+ async function schemaAudit(args = {}) {
1560
+ try {
1561
+ const context = createD1Context(args);
1562
+ const seeds = await loadLiveSeeds(context);
1563
+ const findings = await auditRelationStorage(context.executor, seeds);
1564
+ console.log(pc18.cyan(`
1565
+ Relation storage audit (${context.options.db})
1566
+ `));
1567
+ if (findings.length === 0) {
1568
+ console.log(pc18.green(" \u2713 Every relation branch matches its physical storage.\n"));
1569
+ }
1570
+ for (const finding of findings) {
1571
+ console.log(pc18.yellow(` \u26A0 ${finding.seed}.${finding.alias} \u2014 ${finding.issue}`));
1572
+ console.log(pc18.dim(` expected: ${finding.expected}`));
1573
+ console.log(pc18.dim(` actual: ${finding.actual}`));
1574
+ if (finding.strandedRows > 0) {
1575
+ console.log(pc18.red(` ${finding.strandedRows} stranded value(s) NOT read by the API; do not drop this storage`));
1576
+ }
1577
+ }
1578
+ if (findings.length > 0) {
1579
+ console.log(pc18.yellow(`
1580
+ \u26A0 ${findings.length} relation storage mismatch(es). Nothing was changed.
1581
+ `));
1582
+ }
1583
+ const tagPlan = planTagMigration2(seeds);
1584
+ console.log(pc18.cyan(" Tags contract audit\n"));
1585
+ if (tagPlan.steps.length === 0) {
1586
+ console.log(pc18.green(" \u2713 Every seed uses the closed-vocabulary tags contract.\n"));
1587
+ } else {
1588
+ renderTagMigrationPlan(tagPlan);
1589
+ console.log(pc18.yellow("\n \u26A0 Legacy tag/choice definitions found. Run `beech schema migrate` to review and apply the fix.\n"));
1590
+ }
1591
+ if (findings.length > 0 || tagPlan.steps.length > 0) process.exit(1);
1592
+ } catch (error) {
1593
+ exitWithError(error);
1594
+ }
1595
+ }
1596
+
1597
+ // src/commands/schema-export.ts
1598
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2 } from "node:fs";
1599
+ import { dirname as dirname3, resolve as resolve8 } from "node:path";
1600
+ import pc19 from "picocolors";
1601
+ import { emitManifestModule, seedsToManifest as seedsToManifest2 } from "@beechcms/core/schema";
1602
+ async function schemaExport(args = {}) {
1603
+ try {
1604
+ const context = createD1Context(args);
1605
+ const seeds = await loadLiveSeeds(context);
1606
+ const source = emitManifestModule(seedsToManifest2(seeds));
1607
+ if (args.out === null) {
1608
+ process.stdout.write(source);
1609
+ return;
1610
+ }
1611
+ const target = args.out ?? DEFAULT_MANIFEST_PATH;
1612
+ const outPath = resolve8(process.cwd(), target);
1613
+ mkdirSync2(dirname3(outPath), { recursive: true });
1614
+ writeFileSync4(outPath, source, "utf-8");
1615
+ console.log(pc19.green(`
1616
+ \u2713 Exported ${seeds.length} seed(s) \u2192 ${target}
1617
+ `));
1618
+ } catch (error) {
1619
+ exitWithError(error);
1620
+ }
1621
+ }
1622
+
1623
+ // src/commands/schema-plan.ts
1624
+ import pc20 from "picocolors";
1625
+
1626
+ // ../api-client/dist/index.js
1627
+ import { existsSync as existsSync22, readFileSync as readFileSync22 } from "node:fs";
1628
+ import { join as join2 } from "node:path";
1629
+ import { createServer } from "node:http";
1630
+ import { spawn } from "node:child_process";
1631
+ import { randomBytes as randomBytes2 } from "node:crypto";
1632
+ import { deriveCodeChallenge } from "@beechcms/core";
1633
+ import { chmodSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync, unlinkSync, writeFileSync as writeFileSync5 } from "node:fs";
1634
+ import { homedir } from "node:os";
1635
+ import { dirname as dirname4, join as join3 } from "node:path";
1636
+ var BeechClientError = class extends Error {
1637
+ constructor(message, status, problem) {
1638
+ super(message);
1639
+ this.status = status;
1640
+ this.problem = problem;
1641
+ this.name = "BeechClientError";
1642
+ }
1643
+ status;
1644
+ problem;
1645
+ };
1646
+ function base64Url(bytes) {
1647
+ return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1648
+ }
1649
+ async function createPkcePair() {
1650
+ const verifier = base64Url(randomBytes2(32));
1651
+ const challenge = await deriveCodeChallenge(verifier);
1652
+ return { verifier, challenge };
1653
+ }
1654
+ async function exchangeToken(apiUrl, body) {
1655
+ const response = await fetch(`${apiUrl}/oauth/token`, {
1656
+ method: "POST",
1657
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1658
+ body
1659
+ });
1660
+ if (response.status === 429) {
1661
+ const retryAfter = response.headers.get("Retry-After");
1662
+ throw new BeechClientError(`OAuth token request rate-limited.${retryAfter ? ` Retry after ${retryAfter}s.` : ""}`);
1663
+ }
1664
+ if (!response.ok) {
1665
+ const problem = await response.json();
1666
+ throw new BeechClientError(`OAuth token request failed (${problem.error}): ${problem.error_description ?? ""}`);
1667
+ }
1668
+ const data = await response.json();
1669
+ return {
1670
+ accessToken: data.access_token,
1671
+ refreshToken: data.refresh_token,
1672
+ scope: data.scope,
1673
+ expiresAt: Date.now() + data.expires_in * 1e3
1674
+ };
1675
+ }
1676
+ function renderClosePage() {
1677
+ return `<!doctype html>
1678
+ <html lang="en">
1679
+ <head>
1680
+ <meta charset="utf-8">
1681
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1682
+ <title>Authorization Successful - BeechCMS</title>
1683
+ <style>
1684
+ :root {
1685
+ --bg: #f8fafc;
1686
+ --card-bg: #ffffff;
1687
+ --card-border: #e2e8f0;
1688
+ --text-primary: #0f172a;
1689
+ --text-muted: #64748b;
1690
+ --success-bg: #dcfce7;
1691
+ --success-border: #bbf7d0;
1692
+ --success-text: #15803d;
1693
+ }
1694
+ @media (prefers-color-scheme: dark) {
1695
+ :root {
1696
+ --bg: #09090b;
1697
+ --card-bg: #121215;
1698
+ --card-border: #27272a;
1699
+ --text-primary: #f4f4f5;
1700
+ --text-muted: #a1a1aa;
1701
+ --success-bg: rgba(34, 197, 94, 0.15);
1702
+ --success-border: rgba(34, 197, 94, 0.3);
1703
+ --success-text: #4ade80;
1704
+ }
1705
+ }
1706
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1707
+ body {
1708
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
1709
+ background-color: var(--bg);
1710
+ color: var(--text-primary);
1711
+ min-height: 100vh;
1712
+ display: flex;
1713
+ align-items: center;
1714
+ justify-content: center;
1715
+ padding: 1.5rem;
1716
+ -webkit-font-smoothing: antialiased;
1717
+ }
1718
+ .card {
1719
+ background-color: var(--card-bg);
1720
+ border: 1px solid var(--card-border);
1721
+ border-radius: 1rem;
1722
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.05);
1723
+ max-width: 380px;
1724
+ width: 100%;
1725
+ padding: 2.5rem 2rem;
1726
+ text-align: center;
1727
+ display: flex;
1728
+ flex-direction: column;
1729
+ align-items: center;
1730
+ justify-content: center;
1731
+ }
1732
+ .icon-badge {
1733
+ width: 3.5rem;
1734
+ height: 3.5rem;
1735
+ border-radius: 50%;
1736
+ background-color: var(--success-bg);
1737
+ border: 1px solid var(--success-border);
1738
+ color: var(--success-text);
1739
+ display: flex;
1740
+ align-items: center;
1741
+ justify-content: center;
1742
+ margin-bottom: 1.25rem;
1743
+ }
1744
+ .icon-badge svg {
1745
+ width: 1.75rem;
1746
+ height: 1.75rem;
1747
+ }
1748
+ h1 {
1749
+ font-size: 1.25rem;
1750
+ font-weight: 600;
1751
+ letter-spacing: -0.02em;
1752
+ margin-bottom: 0.5rem;
1753
+ }
1754
+ p {
1755
+ color: var(--text-muted);
1756
+ font-size: 0.875rem;
1757
+ line-height: 1.5;
1758
+ margin: 0;
1759
+ }
1760
+ </style>
1761
+ </head>
1762
+ <body>
1763
+ <div class="card">
1764
+ <div class="icon-badge">
1765
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
1766
+ <polyline points="20 6 9 17 4 12"></polyline>
1767
+ </svg>
1768
+ </div>
1769
+ <h1>Authorization Successful</h1>
1770
+ <p>You can safely close this tab and return to your terminal or editor.</p>
1771
+ </div>
1772
+ <script>try { window.close(); } catch (_) {}</script>
1773
+ </body>
1774
+ </html>`;
1775
+ }
1776
+ function waitForCallback(state, timeoutMs, callbackPath) {
1777
+ let resolvePort;
1778
+ const redirectUriPromise = new Promise((resolve15) => {
1779
+ resolvePort = resolve15;
1780
+ });
1781
+ const result = new Promise((resolve15, reject) => {
1782
+ const server = createServer((req, res) => {
1783
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
1784
+ if (url.pathname !== callbackPath) {
1785
+ res.writeHead(404);
1786
+ res.end();
1787
+ return;
1788
+ }
1789
+ const callbackState = url.searchParams.get("state");
1790
+ const error = url.searchParams.get("error");
1791
+ const code = url.searchParams.get("code");
1792
+ if (callbackState !== state) {
1793
+ res.writeHead(400, { "Content-Type": "text/plain" });
1794
+ res.end("state mismatch");
1795
+ finish(() => reject(new BeechClientError("OAuth callback state mismatch. Aborting for safety.")));
1796
+ return;
1797
+ }
1798
+ if (error) {
1799
+ res.writeHead(400, { "Content-Type": "text/plain" });
1800
+ res.end("authorization error");
1801
+ finish(() => reject(new BeechClientError(url.searchParams.get("error_description") ?? error)));
1802
+ return;
1803
+ }
1804
+ if (code) {
1805
+ res.writeHead(200, { "Content-Type": "text/html" });
1806
+ res.end(renderClosePage());
1807
+ finish(() => resolve15({ code }));
1808
+ return;
1809
+ }
1810
+ res.writeHead(404);
1811
+ res.end();
1812
+ });
1813
+ let timer;
1814
+ let finished = false;
1815
+ function finish(action) {
1816
+ if (finished) return;
1817
+ finished = true;
1818
+ clearTimeout(timer);
1819
+ server.close(() => action());
1820
+ }
1821
+ server.listen(0, "127.0.0.1", () => {
1822
+ const address = server.address();
1823
+ const port = typeof address === "object" && address ? address.port : 0;
1824
+ resolvePort(`http://127.0.0.1:${port}${callbackPath}`);
1825
+ });
1826
+ timer = setTimeout(() => {
1827
+ finish(() => reject(new BeechClientError(`Authorization timed out after ${timeoutMs / 1e3}s. Re-run the tool and complete the consent in the browser.`)));
1828
+ }, timeoutMs);
1829
+ });
1830
+ return { redirectUriPromise, result };
1831
+ }
1832
+ function openBrowser(url) {
1833
+ const platform = process.platform;
1834
+ const child = platform === "darwin" ? spawn("open", [url], { detached: true, stdio: "ignore" }) : platform === "win32" ? spawn("cmd", ["/d", "/c", "start", '""', `"${url}"`], { detached: true, stdio: "ignore", windowsVerbatimArguments: true }) : spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
1835
+ child.on("error", () => {
1836
+ process.stderr.write(`Open this URL to continue: ${url}
1837
+ `);
1838
+ });
1839
+ process.stderr.write(`Open this URL to continue: ${url}
1840
+ `);
1841
+ child.unref();
1842
+ }
1843
+ async function authorize(config) {
1844
+ const { verifier, challenge } = await createPkcePair();
1845
+ const state = base64Url(randomBytes2(16));
1846
+ const callbackPath = config.callbackPath ?? "/oauth/callback";
1847
+ const { redirectUriPromise, result } = waitForCallback(state, config.timeoutMs, callbackPath);
1848
+ const redirectUri = await redirectUriPromise;
1849
+ const authorizeUrl = new URL(`${config.authUrl}/oauth/authorize`);
1850
+ authorizeUrl.searchParams.set("response_type", "code");
1851
+ authorizeUrl.searchParams.set("client_id", config.clientId);
1852
+ authorizeUrl.searchParams.set("redirect_uri", redirectUri);
1853
+ authorizeUrl.searchParams.set("scope", config.scope);
1854
+ authorizeUrl.searchParams.set("state", state);
1855
+ authorizeUrl.searchParams.set("code_challenge", challenge);
1856
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
1857
+ openBrowser(authorizeUrl.toString());
1858
+ const { code } = await result;
1859
+ const body = new URLSearchParams({
1860
+ grant_type: "authorization_code",
1861
+ code,
1862
+ redirect_uri: redirectUri,
1863
+ client_id: config.clientId,
1864
+ code_verifier: verifier
1865
+ });
1866
+ return exchangeToken(config.apiUrl, body);
1867
+ }
1868
+ async function refresh(config, refreshToken) {
1869
+ const body = new URLSearchParams({
1870
+ grant_type: "refresh_token",
1871
+ refresh_token: refreshToken,
1872
+ client_id: config.clientId
1873
+ });
1874
+ return exchangeToken(config.apiUrl, body);
1875
+ }
1876
+ function cachePath() {
1877
+ return process.env.BEECH_TOKEN_CACHE ?? join3(homedir(), ".beechcms", "mcp-tokens.json");
1878
+ }
1879
+ function cacheKey(apiUrl, clientId) {
1880
+ return `${apiUrl}|${clientId}`;
1881
+ }
1882
+ function isValidGrant(value) {
1883
+ if (!value || typeof value !== "object") return false;
1884
+ const grant = value;
1885
+ return typeof grant.accessToken === "string" && grant.accessToken.length > 0 && typeof grant.refreshToken === "string" && grant.refreshToken.length > 0 && typeof grant.scope === "string" && grant.scope.length > 0 && typeof grant.expiresAt === "number" && Number.isFinite(grant.expiresAt);
1886
+ }
1887
+ function readCacheFile() {
1888
+ const path = cachePath();
1889
+ if (!existsSync7(path)) return {};
1890
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
1891
+ if (!parsed || typeof parsed !== "object") return {};
1892
+ return parsed;
1893
+ }
1894
+ function readGrant(apiUrl, clientId) {
1895
+ try {
1896
+ const entry = readCacheFile()[cacheKey(apiUrl, clientId)];
1897
+ return isValidGrant(entry) ? entry : void 0;
1898
+ } catch {
1899
+ return void 0;
1900
+ }
1901
+ }
1902
+ function writeGrant(apiUrl, clientId, grant) {
1903
+ try {
1904
+ const path = cachePath();
1905
+ mkdirSync3(dirname4(path), { recursive: true, mode: 448 });
1906
+ let cache;
1907
+ try {
1908
+ cache = readCacheFile();
1909
+ } catch {
1910
+ cache = {};
1911
+ }
1912
+ cache[cacheKey(apiUrl, clientId)] = grant;
1913
+ const tmpPath = `${path}.tmp`;
1914
+ writeFileSync5(tmpPath, JSON.stringify(cache));
1915
+ chmodSync(tmpPath, 384);
1916
+ renameSync(tmpPath, path);
1917
+ } catch {
1918
+ }
1919
+ }
1920
+ function clearGrant(apiUrl, clientId) {
1921
+ try {
1922
+ const path = cachePath();
1923
+ if (!existsSync7(path)) return;
1924
+ const cache = readCacheFile();
1925
+ delete cache[cacheKey(apiUrl, clientId)];
1926
+ if (Object.keys(cache).length === 0) {
1927
+ unlinkSync(path);
1928
+ return;
1929
+ }
1930
+ const tmpPath = `${path}.tmp`;
1931
+ writeFileSync5(tmpPath, JSON.stringify(cache));
1932
+ chmodSync(tmpPath, 384);
1933
+ renameSync(tmpPath, path);
1934
+ } catch {
1935
+ }
1936
+ }
1937
+ var EXPIRY_SKEW_MS = 3e4;
1938
+ var MAX_TRANSIENT_RETRIES = 2;
1939
+ var RETRY_BASE_DELAY_MS = 100;
1940
+ function readDevVarsApiUrl() {
1941
+ const devVarsPath = join2(process.cwd(), ".dev.vars");
1942
+ if (!existsSync22(devVarsPath)) return void 0;
1943
+ const match = readFileSync22(devVarsPath, "utf8").match(/^BEECH_API_URL=(.*)$/m);
1944
+ return match ? match[1].trim() : void 0;
1945
+ }
1946
+ function resolveApiConfig(overrides = {}) {
1947
+ const baseUrl = overrides.baseUrl ?? process.env.BEECH_API_URL ?? readDevVarsApiUrl() ?? "http://localhost:8789";
1948
+ return {
1949
+ baseUrl,
1950
+ oauth: {
1951
+ apiUrl: baseUrl,
1952
+ authUrl: process.env.BEECH_AUTH_URL ?? baseUrl,
1953
+ clientId: overrides.clientId ?? process.env.BEECH_OAUTH_CLIENT_ID ?? "beech-mcp",
1954
+ scope: overrides.scope ?? process.env.BEECH_OAUTH_SCOPE ?? "schema:read schema:write",
1955
+ timeoutMs: process.env.BEECH_OAUTH_TIMEOUT_MS ? Number(process.env.BEECH_OAUTH_TIMEOUT_MS) : 18e4,
1956
+ callbackPath: overrides.callbackPath
1957
+ }
1958
+ };
1959
+ }
1960
+ function isRetryableStatus(status) {
1961
+ return status === 502 || status === 503 || status === 504;
1962
+ }
1963
+ function sleep(ms) {
1964
+ return new Promise((resolve15) => setTimeout(resolve15, ms));
1965
+ }
1966
+ function createApiClient(config) {
1967
+ let grant;
1968
+ let inFlightAuth;
1969
+ async function getAccessToken(forceRefresh = false) {
1970
+ if (grant === void 0) grant = readGrant(config.baseUrl, config.oauth.clientId);
1971
+ if (grant && !forceRefresh && grant.expiresAt - Date.now() > EXPIRY_SKEW_MS) return grant.accessToken;
1972
+ if (inFlightAuth) return (await inFlightAuth).accessToken;
1973
+ const authPromise = (async () => {
1974
+ if (grant?.refreshToken) {
1975
+ try {
1976
+ const refreshed = await refresh(config.oauth, grant.refreshToken);
1977
+ writeGrant(config.baseUrl, config.oauth.clientId, refreshed);
1978
+ grant = refreshed;
1979
+ return refreshed;
1980
+ } catch {
1981
+ clearGrant(config.baseUrl, config.oauth.clientId);
1982
+ grant = void 0;
1983
+ }
1984
+ }
1985
+ const authorized = await authorize(config.oauth);
1986
+ writeGrant(config.baseUrl, config.oauth.clientId, authorized);
1987
+ grant = authorized;
1988
+ return authorized;
1989
+ })();
1990
+ inFlightAuth = authPromise;
1991
+ try {
1992
+ return (await authPromise).accessToken;
1993
+ } finally {
1994
+ inFlightAuth = void 0;
1995
+ }
1996
+ }
1997
+ async function rawFetch(method, path, accessToken, body) {
1998
+ try {
1999
+ return await fetch(`${config.baseUrl}${path}`, {
2000
+ method,
2001
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
2002
+ body: body !== void 0 ? JSON.stringify(body) : void 0
2003
+ });
2004
+ } catch (error) {
2005
+ if (error instanceof TypeError || error?.code === "ECONNREFUSED") {
2006
+ throw new BeechClientError(`Cannot reach the BeechCMS API at ${config.baseUrl}. Start the local stack with: pnpm beech dev`);
2007
+ }
2008
+ throw error;
2009
+ }
2010
+ }
2011
+ async function request(method, path, body) {
2012
+ let accessToken = await getAccessToken();
2013
+ let response = await rawFetch(method, path, accessToken, body);
2014
+ if (response.status === 401) {
2015
+ accessToken = await getAccessToken(true);
2016
+ response = await rawFetch(method, path, accessToken, body);
2017
+ if (response.status === 401) {
2018
+ throw new BeechClientError(
2019
+ "Authorization failed. Run any Beech tool again to re-authorize in the browser, or revoke and re-grant the client from Settings \u2192 Connected apps.",
2020
+ 401
2021
+ );
2022
+ }
2023
+ }
2024
+ for (let attempt = 0; isRetryableStatus(response.status) && attempt < MAX_TRANSIENT_RETRIES; attempt++) {
2025
+ await sleep(RETRY_BASE_DELAY_MS * 2 ** attempt + Math.random() * RETRY_BASE_DELAY_MS);
2026
+ response = await rawFetch(method, path, accessToken, body);
2027
+ }
2028
+ if (response.status === 403) {
2029
+ let errorBody = {};
2030
+ try {
2031
+ errorBody = await response.json();
2032
+ } catch {
2033
+ }
2034
+ if (errorBody.error === "insufficient_scope") {
2035
+ const scope = errorBody.error_description?.match(/'([^']+)'/)?.[1] ?? "";
2036
+ throw new BeechClientError(
2037
+ `Token lacks the '${scope}' scope. Revoke 'BeechCMS MCP Server' under Settings \u2192 Connected apps and re-authorize.`,
2038
+ 403
2039
+ );
2040
+ }
2041
+ throw new BeechClientError(
2042
+ JSON.stringify({ status: 403, title: errorBody.error ?? response.statusText, detail: errorBody.error_description ?? "" }),
2043
+ 403
2044
+ );
2045
+ }
2046
+ if (!response.ok) {
2047
+ let problem = {};
2048
+ try {
2049
+ problem = await response.json();
2050
+ } catch {
2051
+ }
2052
+ throw new BeechClientError(
2053
+ JSON.stringify({ status: problem.status ?? response.status, title: problem.title ?? response.statusText, detail: problem.detail ?? "" }),
2054
+ problem.status ?? response.status,
2055
+ problem
2056
+ );
2057
+ }
2058
+ return { data: await response.json(), headers: response.headers };
2059
+ }
2060
+ return { request, baseUrl: config.baseUrl };
2061
+ }
2062
+
2063
+ // src/lib/control-plane.ts
2064
+ var CLI_OAUTH_CLIENT_ID = "beech-mcp-cli";
2065
+ var CLI_CALLBACK_PATH = "/callback";
2066
+ function toCliError(error, slug) {
2067
+ if (!(error instanceof BeechClientError)) {
2068
+ return new CliError(error instanceof Error ? error.message : String(error), void 0, error);
2069
+ }
2070
+ const detail = error.problem?.detail ?? error.message;
2071
+ if (error.status === 401 || error.status === 403) {
2072
+ return new CliError(`Not authorized to change schema (${slug}).`, error.message, error);
2073
+ }
2074
+ if (error.status === 409) {
2075
+ return new CliError(
2076
+ `The schema registry moved while '${slug}' was being applied. Nothing was written.`,
2077
+ "Re-run `beech schema plan` and apply again.",
2078
+ error
1084
2079
  );
1085
- process.exit(1);
1086
2080
  }
1087
- const code = generateSeedTypes(seeds);
1088
- if (args.out) {
1089
- const outPath = resolve4(process.cwd(), args.out);
1090
- mkdirSync(dirname(outPath), { recursive: true });
1091
- writeFileSync3(outPath, code, "utf-8");
1092
- console.log(pc11.green(`
1093
- \u2713 Generated ${seeds.length} interface(s) \u2192 ${args.out}
2081
+ if (error.status === 422) {
2082
+ return new CliError(`Server refused the change to '${slug}': ${detail}`, void 0, error);
2083
+ }
2084
+ return new CliError(`Control plane request failed for '${slug}': ${detail}`, void 0, error);
2085
+ }
2086
+ function createControlPlane(options = {}) {
2087
+ const config = resolveApiConfig({
2088
+ baseUrl: options.apiUrl,
2089
+ clientId: CLI_OAUTH_CLIENT_ID,
2090
+ scope: "schema:read schema:write",
2091
+ callbackPath: CLI_CALLBACK_PATH
2092
+ });
2093
+ const client = createApiClient(config);
2094
+ return {
2095
+ baseUrl: config.baseUrl,
2096
+ async plan(slug, candidate) {
2097
+ try {
2098
+ const { data } = await client.request("POST", `/api/seeds/${slug}/mcp-plan`, { candidate });
2099
+ return data;
2100
+ } catch (error) {
2101
+ throw toCliError(error, slug);
2102
+ }
2103
+ },
2104
+ async apply({ slug, candidate, expectedVersion, planId }) {
2105
+ try {
2106
+ const { data } = await client.request("POST", `/api/seeds/${slug}/mcp-apply`, {
2107
+ candidate,
2108
+ expectedVersion,
2109
+ planId,
2110
+ source: "code"
2111
+ });
2112
+ return data;
2113
+ } catch (error) {
2114
+ throw toCliError(error, slug);
2115
+ }
2116
+ }
2117
+ };
2118
+ }
2119
+
2120
+ // src/lib/manifest-order.ts
2121
+ function manifestTargets(seed, slugs) {
2122
+ const targets = /* @__PURE__ */ new Set();
2123
+ for (const branch of seed.branches ?? []) {
2124
+ if (branch.type === "relation" && branch.targetSeed && branch.targetSeed !== seed.slug && slugs.has(branch.targetSeed)) {
2125
+ targets.add(branch.targetSeed);
2126
+ }
2127
+ }
2128
+ return [...targets].sort((a, b) => a.localeCompare(b));
2129
+ }
2130
+ function orderSeedsForApply(seeds) {
2131
+ const bySlug = new Map(seeds.map((seed) => [seed.slug, seed]));
2132
+ const slugs = new Set(bySlug.keys());
2133
+ const sorted = [...seeds].sort((a, b) => a.slug.localeCompare(b.slug));
2134
+ const ordered = [];
2135
+ const state = /* @__PURE__ */ new Map();
2136
+ const cycles = [];
2137
+ function visit(seed, stack) {
2138
+ const mark = state.get(seed.slug);
2139
+ if (mark === "done") return;
2140
+ if (mark === "visiting") {
2141
+ cycles.push([...stack.slice(stack.indexOf(seed.slug)), seed.slug]);
2142
+ return;
2143
+ }
2144
+ state.set(seed.slug, "visiting");
2145
+ for (const target of manifestTargets(seed, slugs)) {
2146
+ const next = bySlug.get(target);
2147
+ if (next) visit(next, [...stack, seed.slug]);
2148
+ }
2149
+ state.set(seed.slug, "done");
2150
+ ordered.push(seed);
2151
+ }
2152
+ for (const seed of sorted) visit(seed, []);
2153
+ return { ordered, cycles };
2154
+ }
2155
+
2156
+ // src/commands/schema-plan.ts
2157
+ async function schemaPlan(args = {}) {
2158
+ try {
2159
+ const manifestPath = args.manifest ?? DEFAULT_MANIFEST_PATH;
2160
+ const manifest = await loadManifest(manifestPath);
2161
+ const { ordered, cycles } = orderSeedsForApply(manifest.seeds);
2162
+ if (cycles.length > 0) {
2163
+ throw new CliError(
2164
+ `Relation cycle in ${manifestPath}: ${cycles.map((cycle) => cycle.join(" \u2192 ")).join("; ")}.`,
2165
+ "Apply one of these seeds without its relation branch first, then add the branch and apply again."
2166
+ );
2167
+ }
2168
+ const controlPlane = createControlPlane({ apiUrl: args.apiUrl });
2169
+ console.log(pc20.cyan(`
2170
+ Plan for ${manifestPath} against ${controlPlane.baseUrl}
2171
+ `));
2172
+ const plans = [];
2173
+ for (const seed of ordered) {
2174
+ const plan = await controlPlane.plan(seed.slug, seed);
2175
+ plans.push(plan);
2176
+ renderPlan(plan);
2177
+ }
2178
+ const blocked = plans.filter((plan) => !plan.applicable);
2179
+ console.log("");
2180
+ if (blocked.length > 0) {
2181
+ console.log(pc20.yellow(` \u26A0 ${blocked.length} seed(s) cannot be applied: ${blocked.map((p4) => p4.slug).join(", ")}
1094
2182
  `));
2183
+ process.exit(1);
2184
+ }
2185
+ const changing = plans.filter((plan) => plan.definitionChanged);
2186
+ if (changing.length === 0) {
2187
+ console.log(pc20.green(" \u2713 Nothing to apply \u2014 the deployed schema already matches the manifest.\n"));
2188
+ return;
2189
+ }
2190
+ console.log(pc20.green(` \u2713 ${changing.length} seed(s) ready to apply. Run \`beech schema apply\`.
2191
+ `));
2192
+ } catch (error) {
2193
+ exitWithError(error);
2194
+ }
2195
+ }
2196
+ function renderPlan(plan) {
2197
+ const label = plan.classification === "create" ? pc20.green("create") : plan.classification === "additive" ? pc20.cyan("additive") : pc20.red("destructive");
2198
+ const owner = plan.source === null ? "new" : plan.source === "code" ? "manifest-owned" : "dashboard-owned";
2199
+ console.log(` ${pc20.bold(plan.slug)} \u2014 ${label} (${owner})`);
2200
+ if (plan.source === "runtime") {
2201
+ console.log(pc20.dim(" note: stays dashboard-editable \u2014 applying does not transfer ownership"));
2202
+ }
2203
+ for (const issue of plan.issues) {
2204
+ for (const message of issue.messages) {
2205
+ console.log(issue.fatal ? pc20.red(` \u2717 ${message}`) : pc20.yellow(` ! ${message}`));
2206
+ }
2207
+ }
2208
+ for (const reason of plan.blockedReasons) console.log(pc20.red(` \u2717 ${reason}`));
2209
+ if (plan.applicable && !plan.definitionChanged) {
2210
+ console.log(pc20.dim(" (no change)"));
1095
2211
  } else {
1096
- process.stdout.write(code);
2212
+ for (const statement of plan.statements) console.log(pc20.dim(` ${statement}`));
2213
+ for (const alias of plan.changedBranches) console.log(pc20.yellow(` ~ ${alias} \u2014 definition changed`));
1097
2214
  }
2215
+ if (plan.ftsRebuildNeeded) console.log(pc20.yellow(" ! full-text index will be rebuilt after apply"));
1098
2216
  }
1099
2217
 
1100
- // src/commands/schema-diff.ts
1101
- import pc12 from "picocolors";
1102
- async function schemaDiff(_args = {}) {
1103
- console.log(pc12.yellow('\n \u26A0 "beech schema:diff" is deprecated'));
1104
- console.log(pc12.dim(" Cloudflare D1 is the canonical authority for schema definitions."));
1105
- console.log(pc12.dim(" Runtime schema mutations are handled automatically by the Botanical Engine."));
1106
- console.log(pc12.cyan("\n \u2192 Schema diffing from static files is no longer supported.\n"));
2218
+ // src/commands/schema-apply.ts
2219
+ import { randomUUID } from "node:crypto";
2220
+ import * as p from "@clack/prompts";
2221
+ import pc21 from "picocolors";
2222
+ async function schemaApply(args = {}) {
2223
+ try {
2224
+ const manifestPath = args.manifest ?? DEFAULT_MANIFEST_PATH;
2225
+ const manifest = await loadManifest(manifestPath);
2226
+ const { ordered, cycles } = orderSeedsForApply(manifest.seeds);
2227
+ if (cycles.length > 0) {
2228
+ throw new CliError(
2229
+ `Relation cycle in ${manifestPath}: ${cycles.map((cycle) => cycle.join(" \u2192 ")).join("; ")}.`,
2230
+ "Apply one of these seeds without its relation branch first, then add the branch and apply again."
2231
+ );
2232
+ }
2233
+ const controlPlane = createControlPlane({ apiUrl: args.apiUrl });
2234
+ console.log(pc21.cyan(`
2235
+ Plan for ${manifestPath} against ${controlPlane.baseUrl}
2236
+ `));
2237
+ const previews = /* @__PURE__ */ new Map();
2238
+ for (const seed of ordered) {
2239
+ const plan = await controlPlane.plan(seed.slug, seed);
2240
+ previews.set(seed.slug, plan);
2241
+ renderPlan(plan);
2242
+ }
2243
+ const blocked = [...previews.values()].filter((plan) => !plan.applicable);
2244
+ if (blocked.length > 0) {
2245
+ throw new CliError(
2246
+ `${blocked.length} seed(s) cannot be applied: ${blocked.map((plan) => plan.slug).join(", ")}. Nothing was written.`,
2247
+ "Fix the manifest, or perform the flagged changes through the endpoints named above."
2248
+ );
2249
+ }
2250
+ const pending = ordered.filter((seed) => previews.get(seed.slug)?.definitionChanged === true);
2251
+ if (pending.length === 0) {
2252
+ console.log(pc21.green("\n \u2713 Nothing to apply \u2014 the deployed schema already matches the manifest.\n"));
2253
+ return;
2254
+ }
2255
+ await confirmApply(pending, args.yes === true);
2256
+ const planId = randomUUID();
2257
+ console.log("");
2258
+ for (const seed of pending) {
2259
+ const result = await applyOne(controlPlane, seed, previews.get(seed.slug), planId);
2260
+ console.log(pc21.green(` \u2713 ${seed.slug} \u2014 registry version ${result.newVersion}`));
2261
+ if (result.warning) console.log(pc21.yellow(` ! ${result.warning}`));
2262
+ }
2263
+ console.log(pc21.green(`
2264
+ \u2713 Applied ${pending.length} seed(s).`));
2265
+ console.log(pc21.dim(" Regenerate client types with `beech types generate`.\n"));
2266
+ } catch (error) {
2267
+ exitWithError(error);
2268
+ }
2269
+ }
2270
+ async function applyOne(controlPlane, seed, preview, planId) {
2271
+ const fresh = await controlPlane.plan(seed.slug, seed);
2272
+ if (!fresh.applicable) {
2273
+ throw new CliError(
2274
+ `'${seed.slug}' became unappliable between plan and apply: ${fresh.blockedReasons.join("; ")}`,
2275
+ "The deployed schema changed under this run. Re-run `beech schema plan`."
2276
+ );
2277
+ }
2278
+ if (fresh.statements.join("\n") !== preview.statements.join("\n") || fresh.changedBranches.join("\n") !== preview.changedBranches.join("\n")) {
2279
+ throw new CliError(
2280
+ `The plan for '${seed.slug}' changed between review and apply. Nothing was written for it.`,
2281
+ "Someone else changed the schema during this run. Re-run `beech schema plan`."
2282
+ );
2283
+ }
2284
+ return controlPlane.apply({ slug: seed.slug, candidate: seed, expectedVersion: fresh.expectedVersion, planId });
2285
+ }
2286
+ async function confirmApply(pending, skip) {
2287
+ if (skip) return;
2288
+ if (!process.stdout.isTTY) {
2289
+ throw new CliError(
2290
+ "Refusing to apply schema changes without confirmation in a non-interactive shell.",
2291
+ "Re-run with --yes once the plan above has been reviewed."
2292
+ );
2293
+ }
2294
+ const answer = await p.confirm({
2295
+ message: `Apply ${pending.length} seed(s) to the deployed schema? (${pending.map((seed) => seed.slug).join(", ")})`,
2296
+ initialValue: false
2297
+ });
2298
+ if (p.isCancel(answer) || !answer) {
2299
+ throw new CliError("Aborted. Nothing was written.");
2300
+ }
1107
2301
  }
1108
2302
 
1109
2303
  // src/commands/db-migrate.ts
1110
- import pc13 from "picocolors";
2304
+ import pc22 from "picocolors";
1111
2305
  import { spawnSync as spawnSync7 } from "node:child_process";
1112
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
1113
- import { resolve as resolve5 } from "node:path";
2306
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
2307
+ import { resolve as resolve9 } from "node:path";
1114
2308
  async function dbMigrate(_args) {
1115
- console.log(pc13.cyan("\n beech db:migrate \u2014 apply migrations\n"));
2309
+ console.log(pc22.cyan("\n beech db:migrate \u2014 apply migrations\n"));
1116
2310
  const cwd = process.cwd();
1117
- const apiDir = resolve5(cwd, "apps", "api");
1118
- if (existsSync4(resolve5(apiDir, "package.json"))) {
2311
+ const apiDir = resolve9(cwd, "apps", "api");
2312
+ if (existsSync8(resolve9(apiDir, "package.json"))) {
1119
2313
  const result = spawnSync7("npm", ["run", "db:migrate:local"], {
1120
2314
  stdio: "inherit",
1121
2315
  cwd: apiDir,
1122
2316
  shell: true
1123
2317
  });
1124
2318
  if (result.status !== 0) {
1125
- console.log(pc13.red("\n \u2717 Failed to apply migrations."));
2319
+ console.log(pc22.red("\n \u2717 Failed to apply migrations."));
1126
2320
  process.exit(1);
1127
2321
  return;
1128
2322
  }
1129
- console.log(pc13.green("\n \u2713 Migrations applied successfully."));
2323
+ console.log(pc22.green("\n \u2713 Migrations applied successfully."));
1130
2324
  return;
1131
2325
  }
1132
- if (existsSync4(resolve5(cwd, "scripts", "bootstrap-d1.mjs"))) {
2326
+ if (existsSync8(resolve9(cwd, "scripts", "bootstrap-d1.mjs"))) {
1133
2327
  const result = spawnSync7("node", ["scripts/bootstrap-d1.mjs"], {
1134
2328
  stdio: "inherit",
1135
2329
  cwd,
1136
2330
  shell: true
1137
2331
  });
1138
2332
  if (result.status !== 0) {
1139
- console.log(pc13.red("\n \u2717 Failed to apply migrations."));
2333
+ console.log(pc22.red("\n \u2717 Failed to apply migrations."));
1140
2334
  process.exit(1);
1141
2335
  return;
1142
2336
  }
1143
- console.log(pc13.green("\n \u2713 Migrations applied successfully."));
2337
+ console.log(pc22.green("\n \u2713 Migrations applied successfully."));
1144
2338
  return;
1145
2339
  }
1146
- const rootPkgPath = resolve5(cwd, "package.json");
1147
- if (existsSync4(rootPkgPath)) {
2340
+ const rootPkgPath = resolve9(cwd, "package.json");
2341
+ if (existsSync8(rootPkgPath)) {
1148
2342
  try {
1149
- const pkg = JSON.parse(readFileSync5(rootPkgPath, "utf-8"));
2343
+ const pkg = JSON.parse(readFileSync7(rootPkgPath, "utf-8"));
1150
2344
  if (pkg.scripts?.["db:migrate:local"]) {
1151
2345
  const result = spawnSync7("npm", ["run", "db:migrate:local"], {
1152
2346
  stdio: "inherit",
@@ -1154,38 +2348,38 @@ async function dbMigrate(_args) {
1154
2348
  shell: true
1155
2349
  });
1156
2350
  if (result.status !== 0) {
1157
- console.log(pc13.red("\n \u2717 Failed to apply migrations."));
2351
+ console.log(pc22.red("\n \u2717 Failed to apply migrations."));
1158
2352
  process.exit(1);
1159
2353
  return;
1160
2354
  }
1161
- console.log(pc13.green("\n \u2713 Migrations applied successfully."));
2355
+ console.log(pc22.green("\n \u2713 Migrations applied successfully."));
1162
2356
  return;
1163
2357
  }
1164
2358
  } catch {
1165
2359
  }
1166
2360
  }
1167
- console.log(pc13.dim(" No migration script found \u2014 initialising database via beech init --db...\n"));
2361
+ console.log(pc22.dim(" No migration script found \u2014 initialising database via beech init --db...\n"));
1168
2362
  const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1169
2363
  try {
1170
2364
  await init2({ initDb: true, local: true, nonInteractive: true });
1171
2365
  } catch {
1172
- console.log(pc13.red("\n \u2717 Database initialisation failed."));
1173
- console.log(pc13.dim(" Run: npm run db:migrate:local or npx beech init --db\n"));
2366
+ console.log(pc22.red("\n \u2717 Database initialisation failed."));
2367
+ console.log(pc22.dim(" Run: npm run db:migrate:local or npx beech init --db\n"));
1174
2368
  process.exit(1);
1175
2369
  }
1176
2370
  }
1177
2371
 
1178
2372
  // src/commands/dev.ts
1179
- import pc14 from "picocolors";
2373
+ import pc23 from "picocolors";
1180
2374
  import { spawnSync as spawnSync8 } from "node:child_process";
1181
- import { existsSync as existsSync5 } from "node:fs";
1182
- import { resolve as resolve6 } from "node:path";
2375
+ import { existsSync as existsSync9 } from "node:fs";
2376
+ import { resolve as resolve10 } from "node:path";
1183
2377
  async function dev(args) {
1184
- console.log(pc14.cyan("\n beech dev \u2014 start development environment\n"));
2378
+ console.log(pc23.cyan("\n beech dev \u2014 start development environment\n"));
1185
2379
  const cwd = process.cwd();
1186
- const devScript = resolve6(cwd, "scripts", "dev.mjs");
1187
- if (!existsSync5(devScript)) {
1188
- console.log(pc14.red(" \u2717 Could not find development script (scripts/dev.mjs)."));
2380
+ const devScript = resolve10(cwd, "scripts", "dev.mjs");
2381
+ if (!existsSync9(devScript)) {
2382
+ console.log(pc23.red(" \u2717 Could not find development script (scripts/dev.mjs)."));
1189
2383
  process.exit(1);
1190
2384
  return;
1191
2385
  }
@@ -1206,7 +2400,7 @@ async function dev(args) {
1206
2400
  }
1207
2401
 
1208
2402
  // src/commands/dev-stop.ts
1209
- import pc15 from "picocolors";
2403
+ import pc24 from "picocolors";
1210
2404
  import { spawnSync as spawnSync9 } from "node:child_process";
1211
2405
  function isDockerInstalled2() {
1212
2406
  try {
@@ -1225,44 +2419,44 @@ function isDockerRunning2() {
1225
2419
  }
1226
2420
  }
1227
2421
  async function devStop() {
1228
- console.log(pc15.cyan("\n beech dev:stop \u2014 stop Docker environment\n"));
2422
+ console.log(pc24.cyan("\n beech dev:stop \u2014 stop Docker environment\n"));
1229
2423
  if (!isDockerInstalled2()) {
1230
- console.log(pc15.red(" \u2717 Docker is not installed or not found in your PATH."));
2424
+ console.log(pc24.red(" \u2717 Docker is not installed or not found in your PATH."));
1231
2425
  process.exit(1);
1232
2426
  return;
1233
2427
  }
1234
2428
  if (!isDockerRunning2()) {
1235
- console.log(pc15.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
2429
+ console.log(pc24.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
1236
2430
  process.exit(1);
1237
2431
  return;
1238
2432
  }
1239
- console.log(pc15.dim(" Stopping Docker containers\u2026\n"));
2433
+ console.log(pc24.dim(" Stopping Docker containers\u2026\n"));
1240
2434
  const result = spawnSync9("docker", ["compose", "-f", "docker/docker-compose.yml", "stop"], {
1241
2435
  stdio: "inherit",
1242
2436
  cwd: process.cwd(),
1243
2437
  shell: true
1244
2438
  });
1245
2439
  if (result.status === 0) {
1246
- console.log(pc15.green("\n \u2713 Docker containers stopped."));
2440
+ console.log(pc24.green("\n \u2713 Docker containers stopped."));
1247
2441
  } else {
1248
- console.log(pc15.red("\n \u2717 Failed to stop Docker containers."));
2442
+ console.log(pc24.red("\n \u2717 Failed to stop Docker containers."));
1249
2443
  process.exit(1);
1250
2444
  return;
1251
2445
  }
1252
2446
  }
1253
2447
 
1254
2448
  // src/commands/dev-tunnel.ts
1255
- import pc16 from "picocolors";
2449
+ import pc25 from "picocolors";
1256
2450
  import { spawnSync as spawnSync10 } from "node:child_process";
1257
2451
  async function devTunnel() {
1258
- console.log(pc16.cyan("\n beech dev:tunnel \u2014 get Cloudflare Tunnel URL\n"));
2452
+ console.log(pc25.cyan("\n beech dev:tunnel \u2014 get Cloudflare Tunnel URL\n"));
1259
2453
  const result = spawnSync10("docker", ["compose", "-f", "docker/docker-compose.yml", "logs", "tunnel"], {
1260
2454
  encoding: "utf-8",
1261
2455
  cwd: process.cwd(),
1262
2456
  shell: true
1263
2457
  });
1264
2458
  if (result.status !== 0) {
1265
- console.log(pc16.red(" \u2717 Failed to retrieve tunnel logs."));
2459
+ console.log(pc25.red(" \u2717 Failed to retrieve tunnel logs."));
1266
2460
  process.exit(1);
1267
2461
  return;
1268
2462
  }
@@ -1270,38 +2464,38 @@ async function devTunnel() {
1270
2464
  const match = logs2.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/g);
1271
2465
  if (match && match.length > 0) {
1272
2466
  const url = match[match.length - 1];
1273
- console.log(pc16.green(` \u2713 Active Cloudflare Tunnel URL: ${pc16.bold(url)}`));
2467
+ console.log(pc25.green(` \u2713 Active Cloudflare Tunnel URL: ${pc25.bold(url)}`));
1274
2468
  } else {
1275
- console.log(pc16.yellow(" \u26A0 No active Cloudflare Tunnel URL found in logs."));
1276
- console.log(pc16.dim(" Make sure the dev server is running with Docker (`pnpm beech dev`)."));
2469
+ console.log(pc25.yellow(" \u26A0 No active Cloudflare Tunnel URL found in logs."));
2470
+ console.log(pc25.dim(" Make sure the dev server is running with Docker (`pnpm beech dev`)."));
1277
2471
  }
1278
2472
  }
1279
2473
 
1280
2474
  // src/commands/mailpit-clear.ts
1281
- import pc17 from "picocolors";
2475
+ import pc26 from "picocolors";
1282
2476
  async function mailpitClear() {
1283
- console.log(pc17.cyan("\n beech mailpit:clear \u2014 clear test emails\n"));
2477
+ console.log(pc26.cyan("\n beech mailpit:clear \u2014 clear test emails\n"));
1284
2478
  try {
1285
2479
  const res = await fetch("http://localhost:8025/api/v1/messages", {
1286
2480
  method: "DELETE"
1287
2481
  });
1288
2482
  if (res.ok) {
1289
- console.log(pc17.green(" \u2713 Mailpit inbox cleared successfully."));
2483
+ console.log(pc26.green(" \u2713 Mailpit inbox cleared successfully."));
1290
2484
  } else {
1291
- console.log(pc17.red(` \u2717 Failed to clear Mailpit inbox: ${res.statusText}`));
2485
+ console.log(pc26.red(` \u2717 Failed to clear Mailpit inbox: ${res.statusText}`));
1292
2486
  process.exit(1);
1293
2487
  return;
1294
2488
  }
1295
2489
  } catch (err) {
1296
- console.log(pc17.red(` \u2717 Error connecting to Mailpit: ${err.message}`));
1297
- console.log(pc17.dim(" Make sure Mailpit is running (default port 8025)."));
2490
+ console.log(pc26.red(` \u2717 Error connecting to Mailpit: ${err.message}`));
2491
+ console.log(pc26.dim(" Make sure Mailpit is running (default port 8025)."));
1298
2492
  process.exit(1);
1299
2493
  return;
1300
2494
  }
1301
2495
  }
1302
2496
 
1303
2497
  // src/commands/logs.ts
1304
- import pc18 from "picocolors";
2498
+ import pc27 from "picocolors";
1305
2499
  import { spawnSync as spawnSync11 } from "node:child_process";
1306
2500
  var SERVICE_MAP = {
1307
2501
  mailpit: "mailpit",
@@ -1314,18 +2508,18 @@ var SERVICE_MAP = {
1314
2508
  async function logs(args) {
1315
2509
  const inputService = args.service?.toLowerCase();
1316
2510
  if (!inputService || !SERVICE_MAP[inputService]) {
1317
- console.log(pc18.red("\n \u2717 Error: Please specify a valid service name."));
1318
- console.log(pc18.dim("\n Accepted services:"));
1319
- console.log(` - ${pc18.cyan("mailpit")}`);
1320
- console.log(` - ${pc18.cyan("db")} / ${pc18.cyan("sqlite")}`);
1321
- console.log(` - ${pc18.cyan("tunnel")}`);
1322
- console.log(` - ${pc18.cyan("storage")} / ${pc18.cyan("minio")}
2511
+ console.log(pc27.red("\n \u2717 Error: Please specify a valid service name."));
2512
+ console.log(pc27.dim("\n Accepted services:"));
2513
+ console.log(` - ${pc27.cyan("mailpit")}`);
2514
+ console.log(` - ${pc27.cyan("db")} / ${pc27.cyan("sqlite")}`);
2515
+ console.log(` - ${pc27.cyan("tunnel")}`);
2516
+ console.log(` - ${pc27.cyan("storage")} / ${pc27.cyan("minio")}
1323
2517
  `);
1324
2518
  process.exit(1);
1325
2519
  return;
1326
2520
  }
1327
2521
  const service = SERVICE_MAP[inputService];
1328
- console.log(pc18.cyan(`
2522
+ console.log(pc27.cyan(`
1329
2523
  beech logs ${inputService} \u2014 streaming logs for ${service}\u2026
1330
2524
  `));
1331
2525
  const result = spawnSync11("docker", ["compose", "-f", "docker/docker-compose.yml", "logs", "-f", service], {
@@ -1340,33 +2534,97 @@ async function logs(args) {
1340
2534
  }
1341
2535
 
1342
2536
  // src/commands/test.ts
1343
- import pc19 from "picocolors";
2537
+ import pc28 from "picocolors";
1344
2538
  import { spawnSync as spawnSync12 } from "node:child_process";
1345
- import { existsSync as existsSync6 } from "node:fs";
1346
- import { resolve as resolve7 } from "node:path";
2539
+ import { existsSync as existsSync11 } from "node:fs";
2540
+ import { resolve as resolve12 } from "node:path";
2541
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
2542
+
2543
+ // src/lib/cpu-slot.ts
2544
+ import { existsSync as existsSync10 } from "node:fs";
2545
+ import { resolve as resolve11 } from "node:path";
2546
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
2547
+ async function loadCpuSlot(cwd) {
2548
+ const modulePath = resolve11(cwd, "scripts", "lib", "cpu-slot.mjs");
2549
+ if (!existsSync10(modulePath)) return null;
2550
+ return await import(pathToFileURL2(modulePath).href);
2551
+ }
2552
+
2553
+ // src/commands/test.ts
2554
+ var RUNNABLE_TIERS = ["unit", "flow", "integration", "e2e", "scale"];
2555
+ async function loadTestResources(cwd) {
2556
+ const modulePath = resolve12(cwd, "scripts", "lib", "test-resources.mjs");
2557
+ if (!existsSync11(modulePath)) return { env: {}, turboArgs: [] };
2558
+ try {
2559
+ const mod = await import(pathToFileURL3(modulePath).href);
2560
+ const resources = mod.resolveTestResources();
2561
+ console.log(pc28.dim(` ${mod.describeTestResources(resources)}
2562
+ `));
2563
+ if (!resources) return { env: {}, turboArgs: [] };
2564
+ mod.lowerProcessPriority();
2565
+ return { env: mod.testResourceEnv(resources), turboArgs: [`--concurrency=${resources.turboConcurrency}`] };
2566
+ } catch {
2567
+ return { env: {}, turboArgs: [] };
2568
+ }
2569
+ }
1347
2570
  async function test(args) {
1348
- console.log(pc19.cyan("\n beech test \u2014 run test suite\n"));
2571
+ console.log(pc28.cyan("\n beech test \u2014 run test suite\n"));
1349
2572
  const cwd = process.cwd();
2573
+ const tiers = (args.tier ?? "").split(",").map((t) => t.trim()).filter(Boolean);
2574
+ const invalid = tiers.filter((t) => !RUNNABLE_TIERS.includes(t));
2575
+ if (invalid.length > 0) {
2576
+ console.log(pc28.red(` \u2717 Unknown tier(s): ${invalid.join(", ")}. Valid tiers: ${RUNNABLE_TIERS.join(", ")}.`));
2577
+ process.exit(1);
2578
+ return;
2579
+ }
2580
+ if (args.diff && (tiers.includes("e2e") || tiers.includes("scale"))) {
2581
+ console.log(pc28.red(" \u2717 The e2e and scale tiers are never selected by --diff."));
2582
+ process.exit(1);
2583
+ return;
2584
+ }
1350
2585
  let command = "turbo";
1351
2586
  let commandArgs = ["run", "test"];
1352
2587
  if (args.diff) {
1353
- const diffScript = resolve7(cwd, "scripts", "test-coverage-diff.mjs");
1354
- if (existsSync6(diffScript)) {
1355
- command = "node";
1356
- commandArgs = ["scripts/test-coverage-diff.mjs"];
1357
- } else {
1358
- console.log(pc19.red(" \u2717 Coverage diff script not found (scripts/test-coverage-diff.mjs)."));
2588
+ const diffScript = resolve12(cwd, "scripts", "test-coverage-diff.mjs");
2589
+ if (!existsSync11(diffScript)) {
2590
+ console.log(pc28.red(" \u2717 Coverage diff script not found (scripts/test-coverage-diff.mjs)."));
1359
2591
  process.exit(1);
1360
2592
  return;
1361
2593
  }
2594
+ command = "node";
2595
+ commandArgs = ["scripts/test-coverage-diff.mjs"];
2596
+ if (tiers.length > 0) commandArgs.push("--tier", tiers.join(","));
2597
+ } else if (tiers.length > 0) {
2598
+ commandArgs = ["run", ...tiers.map((t) => `test:${t}`)];
1362
2599
  } else if (args.coverage) {
1363
2600
  commandArgs = ["run", "test:coverage"];
1364
2601
  }
2602
+ let releaseSlot = () => {
2603
+ };
2604
+ const slotEnv = {};
2605
+ const cpuSlot = command === "turbo" ? await loadCpuSlot(cwd) : null;
2606
+ if (cpuSlot) {
2607
+ const exclusive = tiers.length === 0 || tiers.includes("flow");
2608
+ try {
2609
+ releaseSlot = await cpuSlot.acquireCpuSlot({ exclusive, label: `beech test: turbo ${commandArgs.join(" ")}` });
2610
+ } catch (err) {
2611
+ if (err.code !== "SLOT_TIMEOUT") throw err;
2612
+ console.log(pc28.red(` \u2717 ${err.message} The tests did NOT run.`));
2613
+ process.exit(cpuSlot.EXIT_SLOT_TIMEOUT);
2614
+ return;
2615
+ }
2616
+ cpuSlot.releaseOnExit(releaseSlot);
2617
+ slotEnv[cpuSlot.HELD_ENV] = "1";
2618
+ }
2619
+ const resources = await loadTestResources(cwd);
2620
+ if (command === "turbo") commandArgs.push(...resources.turboArgs);
1365
2621
  const result = spawnSync12(command, commandArgs, {
1366
2622
  stdio: "inherit",
1367
2623
  cwd,
1368
- shell: true
2624
+ shell: true,
2625
+ env: { ...process.env, ...resources.env, ...slotEnv }
1369
2626
  });
2627
+ releaseSlot();
1370
2628
  if (result.status !== 0) {
1371
2629
  process.exit(result.status ?? 1);
1372
2630
  return;
@@ -1374,10 +2632,17 @@ async function test(args) {
1374
2632
  }
1375
2633
 
1376
2634
  // src/commands/lint.ts
1377
- import pc20 from "picocolors";
2635
+ import pc29 from "picocolors";
1378
2636
  import { spawnSync as spawnSync13 } from "node:child_process";
1379
2637
  async function lint() {
1380
- console.log(pc20.cyan("\n beech lint \u2014 check code style\n"));
2638
+ console.log(pc29.cyan("\n beech lint \u2014 check code style\n"));
2639
+ const placement = spawnSync13("node", ["scripts/check-test-placement.mjs"], {
2640
+ stdio: "inherit",
2641
+ cwd: process.cwd()
2642
+ });
2643
+ if (placement.status !== 0) {
2644
+ process.exit(placement.status ?? 1);
2645
+ }
1381
2646
  const result = spawnSync13("turbo", ["run", "lint"], {
1382
2647
  stdio: "inherit",
1383
2648
  cwd: process.cwd(),
@@ -1389,10 +2654,10 @@ async function lint() {
1389
2654
  }
1390
2655
 
1391
2656
  // src/commands/doctor.ts
1392
- import pc21 from "picocolors";
2657
+ import pc30 from "picocolors";
1393
2658
  import { spawnSync as spawnSync14 } from "node:child_process";
1394
2659
  async function doctor() {
1395
- console.log(pc21.cyan("\n beech doctor \u2014 React diagnostics\n"));
2660
+ console.log(pc30.cyan("\n beech doctor \u2014 React diagnostics\n"));
1396
2661
  const cmd = process.env.npm_config_user_agent?.includes("pnpm") ? "pnpm" : "npx";
1397
2662
  const args = cmd === "pnpm" ? ["dlx", "react-doctor@latest"] : ["--yes", "react-doctor@latest"];
1398
2663
  const result = spawnSync14(cmd, args, {
@@ -1406,10 +2671,10 @@ async function doctor() {
1406
2671
  }
1407
2672
 
1408
2673
  // src/commands/forms.ts
1409
- import * as p from "@clack/prompts";
1410
- import pc22 from "picocolors";
1411
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "node:fs";
1412
- import { dirname as dirname2, resolve as resolve8 } from "node:path";
2674
+ import * as p2 from "@clack/prompts";
2675
+ import pc31 from "picocolors";
2676
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
2677
+ import { dirname as dirname5, resolve as resolve13 } from "node:path";
1413
2678
  function getReactTemplate(seedSlug, mode) {
1414
2679
  if (mode === "headless") {
1415
2680
  return `import React, { useState, useEffect } from 'react'
@@ -2042,9 +3307,9 @@ export { BeechFormElement }
2042
3307
  async function forms(options = {}) {
2043
3308
  let { framework, seed, mode, out, yes, json } = options;
2044
3309
  if (!yes && !json) {
2045
- p.intro(pc22.bgCyan(pc22.black(" \u{1F332} BeechCMS Form Generator ")));
3310
+ p2.intro(pc31.bgCyan(pc31.black(" \u{1F332} BeechCMS Form Generator ")));
2046
3311
  if (!framework) {
2047
- const selected = await p.select({
3312
+ const selected = await p2.select({
2048
3313
  message: "Which framework are you using?",
2049
3314
  options: [
2050
3315
  { value: "react", label: "React", hint: "Next.js / Vite / Remix (.tsx)" },
@@ -2054,14 +3319,14 @@ async function forms(options = {}) {
2054
3319
  ],
2055
3320
  initialValue: "react"
2056
3321
  });
2057
- if (p.isCancel(selected)) {
2058
- p.cancel("Operation cancelled.");
3322
+ if (p2.isCancel(selected)) {
3323
+ p2.cancel("Operation cancelled.");
2059
3324
  process.exit(0);
2060
3325
  }
2061
3326
  framework = selected;
2062
3327
  }
2063
3328
  if (!seed) {
2064
- const seedInput = await p.text({
3329
+ const seedInput = await p2.text({
2065
3330
  message: "Which Seed do you want to bind this form to?",
2066
3331
  placeholder: "clienti",
2067
3332
  defaultValue: "clienti",
@@ -2069,14 +3334,14 @@ async function forms(options = {}) {
2069
3334
  if (!value.trim()) return "Seed slug cannot be empty";
2070
3335
  }
2071
3336
  });
2072
- if (p.isCancel(seedInput)) {
2073
- p.cancel("Operation cancelled.");
3337
+ if (p2.isCancel(seedInput)) {
3338
+ p2.cancel("Operation cancelled.");
2074
3339
  process.exit(0);
2075
3340
  }
2076
3341
  seed = seedInput.trim();
2077
3342
  }
2078
3343
  if (!mode && framework !== "vanilla") {
2079
- const modeSelected = await p.select({
3344
+ const modeSelected = await p2.select({
2080
3345
  message: "Choose styling preset:",
2081
3346
  options: [
2082
3347
  { value: "styled", label: "Tailwind CSS", hint: "Full responsive ready-to-use component" },
@@ -2084,8 +3349,8 @@ async function forms(options = {}) {
2084
3349
  ],
2085
3350
  initialValue: "styled"
2086
3351
  });
2087
- if (p.isCancel(modeSelected)) {
2088
- p.cancel("Operation cancelled.");
3352
+ if (p2.isCancel(modeSelected)) {
3353
+ p2.cancel("Operation cancelled.");
2089
3354
  process.exit(0);
2090
3355
  }
2091
3356
  mode = modeSelected;
@@ -2096,7 +3361,7 @@ async function forms(options = {}) {
2096
3361
  mode = mode || "styled";
2097
3362
  const ext = framework === "react" ? "tsx" : framework === "vue" ? "vue" : framework === "svelte" ? "svelte" : "js";
2098
3363
  const filename = `BeechForm.${ext}`;
2099
- const targetPath = out ? resolve8(process.cwd(), out) : resolve8(process.cwd(), "src", "components", filename);
3364
+ const targetPath = out ? resolve13(process.cwd(), out) : resolve13(process.cwd(), "src", "components", filename);
2100
3365
  let content = "";
2101
3366
  if (framework === "react") {
2102
3367
  content = getReactTemplate(seed, mode);
@@ -2107,8 +3372,8 @@ async function forms(options = {}) {
2107
3372
  } else {
2108
3373
  content = getVanillaTemplate(seed);
2109
3374
  }
2110
- mkdirSync2(dirname2(targetPath), { recursive: true });
2111
- writeFileSync4(targetPath, content, "utf-8");
3375
+ mkdirSync4(dirname5(targetPath), { recursive: true });
3376
+ writeFileSync6(targetPath, content, "utf-8");
2112
3377
  if (json) {
2113
3378
  console.log(
2114
3379
  JSON.stringify(
@@ -2127,24 +3392,24 @@ async function forms(options = {}) {
2127
3392
  return;
2128
3393
  }
2129
3394
  if (!yes) {
2130
- p.outro(pc22.green(`\u2714 Form component created at ${out ? out : `src/components/${filename}`}`));
3395
+ p2.outro(pc31.green(`\u2714 Form component created at ${out ? out : `src/components/${filename}`}`));
2131
3396
  console.log(`
2132
- ${pc22.bold("Next steps:")}`);
3397
+ ${pc31.bold("Next steps:")}`);
2133
3398
  if (framework === "react") {
2134
- console.log(pc22.cyan(` import { BeechForm } from './components/BeechForm'`));
2135
- console.log(pc22.cyan(` <BeechForm seed="${seed}" />
3399
+ console.log(pc31.cyan(` import { BeechForm } from './components/BeechForm'`));
3400
+ console.log(pc31.cyan(` <BeechForm seed="${seed}" />
2136
3401
  `));
2137
3402
  } else if (framework === "vue") {
2138
- console.log(pc22.cyan(` import BeechForm from './components/BeechForm.vue'`));
2139
- console.log(pc22.cyan(` <BeechForm seed="${seed}" />
3403
+ console.log(pc31.cyan(` import BeechForm from './components/BeechForm.vue'`));
3404
+ console.log(pc31.cyan(` <BeechForm seed="${seed}" />
2140
3405
  `));
2141
3406
  } else if (framework === "svelte") {
2142
- console.log(pc22.cyan(` import BeechForm from './components/BeechForm.svelte'`));
2143
- console.log(pc22.cyan(` <BeechForm seed="${seed}" />
3407
+ console.log(pc31.cyan(` import BeechForm from './components/BeechForm.svelte'`));
3408
+ console.log(pc31.cyan(` <BeechForm seed="${seed}" />
2144
3409
  `));
2145
3410
  } else {
2146
- console.log(pc22.cyan(` <script type="module" src="./src/components/BeechForm.js"></script>`));
2147
- console.log(pc22.cyan(` <beech-form seed="${seed}"></beech-form>
3411
+ console.log(pc31.cyan(` <script type="module" src="./src/components/BeechForm.js"></script>`));
3412
+ console.log(pc31.cyan(` <beech-form seed="${seed}"></beech-form>
2148
3413
  `));
2149
3414
  }
2150
3415
  } else {
@@ -2154,19 +3419,19 @@ async function forms(options = {}) {
2154
3419
 
2155
3420
  // src/commands/setup-cloudflare.ts
2156
3421
  init_wrangler();
2157
- import pc23 from "picocolors";
2158
- import * as p2 from "@clack/prompts";
3422
+ import pc32 from "picocolors";
3423
+ import * as p3 from "@clack/prompts";
2159
3424
  import { spawnSync as spawnSync15 } from "node:child_process";
2160
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync8 } from "node:fs";
2161
- import { resolve as resolve9 } from "node:path";
3425
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync13 } from "node:fs";
3426
+ import { resolve as resolve14 } from "node:path";
2162
3427
  function parseWranglerConfig(configPath) {
2163
- const raw = readFileSync6(configPath, "utf-8");
3428
+ const raw = readFileSync8(configPath, "utf-8");
2164
3429
  const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
2165
3430
  const parsed = JSON.parse(stripped);
2166
3431
  return { name: parsed.name || "my-beech-project", raw, parsed };
2167
3432
  }
2168
3433
  async function setupCloudflare(options = {}) {
2169
- p2.intro(pc23.cyan("\u26A1 BeechCMS \u2014 Automated Cloudflare Provisioning"));
3434
+ p3.intro(pc32.cyan("\u26A1 BeechCMS \u2014 Automated Cloudflare Provisioning"));
2170
3435
  const configPath = findWranglerConfig();
2171
3436
  let projectName = options.projectName;
2172
3437
  if (!projectName && configPath) {
@@ -2180,7 +3445,7 @@ async function setupCloudflare(options = {}) {
2180
3445
  if (options.nonInteractive) {
2181
3446
  projectName = "my-beech-project";
2182
3447
  } else {
2183
- const input = await p2.text({
3448
+ const input = await p3.text({
2184
3449
  message: "Project name",
2185
3450
  placeholder: "my-website",
2186
3451
  validate: (v) => {
@@ -2188,8 +3453,8 @@ async function setupCloudflare(options = {}) {
2188
3453
  if (!/^[a-z0-9][a-z0-9-]*$/.test(v.trim())) return "Lowercase letters, numbers and hyphens only";
2189
3454
  }
2190
3455
  });
2191
- if (p2.isCancel(input)) {
2192
- p2.cancel("Setup cancelled");
3456
+ if (p3.isCancel(input)) {
3457
+ p3.cancel("Setup cancelled");
2193
3458
  return;
2194
3459
  }
2195
3460
  projectName = input.trim();
@@ -2197,8 +3462,8 @@ async function setupCloudflare(options = {}) {
2197
3462
  }
2198
3463
  const d1Name = `${projectName}-db`;
2199
3464
  const r2Bucket = `${projectName}-media`;
2200
- const s = p2.spinner();
2201
- s.start(`Creating Cloudflare D1 database: ${pc23.bold(d1Name)}\u2026`);
3465
+ const s = p3.spinner();
3466
+ s.start(`Creating Cloudflare D1 database: ${pc32.bold(d1Name)}\u2026`);
2202
3467
  const d1Result = spawnSync15("npx", ["wrangler", "d1", "create", d1Name], {
2203
3468
  encoding: "utf-8",
2204
3469
  shell: true,
@@ -2209,13 +3474,13 @@ async function setupCloudflare(options = {}) {
2209
3474
  const idMatch = d1Output.match(/database_id\s*=\s*["']?([a-f0-9-]{36})["']?/i) || d1Output.match(/"database_id":\s*"([a-f0-9-]{36})"/i) || d1Output.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i);
2210
3475
  if (idMatch) {
2211
3476
  databaseId = idMatch[1];
2212
- s.stop(pc23.green(`\u2713 D1 database ready: ${pc23.bold(d1Name)} (${databaseId})`));
3477
+ s.stop(pc32.green(`\u2713 D1 database ready: ${pc32.bold(d1Name)} (${databaseId})`));
2213
3478
  } else if (d1Output.includes("already exists") || d1Result.status === 0) {
2214
- s.stop(pc23.yellow(`\u2139 D1 database ${pc23.bold(d1Name)} already exists or created.`));
3479
+ s.stop(pc32.yellow(`\u2139 D1 database ${pc32.bold(d1Name)} already exists or created.`));
2215
3480
  } else {
2216
- s.stop(pc23.yellow(`\u26A0 Note on D1 create: ${d1Output.trim().slice(0, 150)}`));
3481
+ s.stop(pc32.yellow(`\u26A0 Note on D1 create: ${d1Output.trim().slice(0, 150)}`));
2217
3482
  }
2218
- s.start(`Creating Cloudflare R2 bucket: ${pc23.bold(r2Bucket)}\u2026`);
3483
+ s.start(`Creating Cloudflare R2 bucket: ${pc32.bold(r2Bucket)}\u2026`);
2219
3484
  const r2Result = spawnSync15("npx", ["wrangler", "r2", "bucket", "create", r2Bucket], {
2220
3485
  encoding: "utf-8",
2221
3486
  shell: true,
@@ -2223,26 +3488,26 @@ async function setupCloudflare(options = {}) {
2223
3488
  });
2224
3489
  const r2Output = (r2Result.stdout || "") + (r2Result.stderr || "");
2225
3490
  if (r2Result.status === 0 || r2Output.includes("already exists")) {
2226
- s.stop(pc23.green(`\u2713 R2 bucket ready: ${pc23.bold(r2Bucket)}`));
3491
+ s.stop(pc32.green(`\u2713 R2 bucket ready: ${pc32.bold(r2Bucket)}`));
2227
3492
  } else {
2228
- s.stop(pc23.yellow(`\u26A0 Note on R2 create: ${r2Output.trim().slice(0, 150)}`));
3493
+ s.stop(pc32.yellow(`\u26A0 Note on R2 create: ${r2Output.trim().slice(0, 150)}`));
2229
3494
  }
2230
- if (configPath && existsSync8(configPath)) {
3495
+ if (configPath && existsSync13(configPath)) {
2231
3496
  try {
2232
- let content = readFileSync6(configPath, "utf-8");
3497
+ let content = readFileSync8(configPath, "utf-8");
2233
3498
  content = content.replace(/"database_name":\s*"[^"]*"/, `"database_name": "${d1Name}"`);
2234
3499
  if (databaseId) {
2235
3500
  content = content.replace(/"database_id":\s*"[^"]*"/, `"database_id": "${databaseId}"`);
2236
3501
  }
2237
3502
  content = content.replace(/"bucket_name":\s*"[^"]*"/, `"bucket_name": "${r2Bucket}"`);
2238
- writeFileSync5(configPath, content, "utf-8");
2239
- p2.log.success(pc23.green(`Updated ${pc23.bold(configPath)} with database_id and bucket_name.`));
3503
+ writeFileSync7(configPath, content, "utf-8");
3504
+ p3.log.success(pc32.green(`Updated ${pc32.bold(configPath)} with database_id and bucket_name.`));
2240
3505
  } catch (err) {
2241
- p2.log.warn(`Could not update wrangler config automatically: ${err.message}`);
3506
+ p3.log.warn(`Could not update wrangler config automatically: ${err.message}`);
2242
3507
  }
2243
3508
  }
2244
3509
  if (!options.nonInteractive) {
2245
- p2.note(
3510
+ p3.note(
2246
3511
  [
2247
3512
  "Direct uploads via Presigned URLs (SigV4) stream directly from browser to R2 with zero Worker CPU/RAM.",
2248
3513
  "To configure Presigned uploads:",
@@ -2252,33 +3517,33 @@ async function setupCloudflare(options = {}) {
2252
3517
  ].join("\n"),
2253
3518
  "R2 S3 Credentials Setup"
2254
3519
  );
2255
- const configureSecrets = await p2.confirm({
3520
+ const configureSecrets = await p3.confirm({
2256
3521
  message: "Do you have your R2 API Token ready to configure now?",
2257
3522
  initialValue: true
2258
3523
  });
2259
- if (!p2.isCancel(configureSecrets) && configureSecrets) {
2260
- const accountId = await p2.text({
3524
+ if (!p3.isCancel(configureSecrets) && configureSecrets) {
3525
+ const accountId = await p3.text({
2261
3526
  message: "Cloudflare Account ID (from dash.cloudflare.com)",
2262
3527
  placeholder: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
2263
3528
  validate: (v) => {
2264
3529
  if (!v.trim()) return "Required";
2265
3530
  }
2266
3531
  });
2267
- const accessKeyId = !p2.isCancel(accountId) ? await p2.text({
3532
+ const accessKeyId = !p3.isCancel(accountId) ? await p3.text({
2268
3533
  message: "R2 Access Key ID",
2269
3534
  validate: (v) => {
2270
3535
  if (!v.trim()) return "Required";
2271
3536
  }
2272
3537
  }) : null;
2273
- const secretAccessKey = !p2.isCancel(accessKeyId) && accessKeyId ? await p2.password({
3538
+ const secretAccessKey = !p3.isCancel(accessKeyId) && accessKeyId ? await p3.password({
2274
3539
  message: "R2 Secret Access Key",
2275
3540
  validate: (v) => {
2276
3541
  if (!v.trim()) return "Required";
2277
3542
  }
2278
3543
  }) : null;
2279
- if (!p2.isCancel(accountId) && !p2.isCancel(accessKeyId) && !p2.isCancel(secretAccessKey) && accountId && accessKeyId && secretAccessKey) {
3544
+ if (!p3.isCancel(accountId) && !p3.isCancel(accessKeyId) && !p3.isCancel(secretAccessKey) && accountId && accessKeyId && secretAccessKey) {
2280
3545
  const endpoint = `https://${accountId.trim()}.r2.cloudflarestorage.com`;
2281
- const devVarsPath = resolve9(process.cwd(), ".dev.vars");
3546
+ const devVarsPath = resolve14(process.cwd(), ".dev.vars");
2282
3547
  const devVarsContent = [
2283
3548
  "# Cloudflare R2 S3 credentials (for direct Presigned URL uploads)",
2284
3549
  `R2_ACCESS_KEY_ID=${accessKeyId.trim()}`,
@@ -2286,8 +3551,8 @@ async function setupCloudflare(options = {}) {
2286
3551
  `R2_ENDPOINT=${endpoint}`,
2287
3552
  `R2_BUCKET_NAME=${r2Bucket}`
2288
3553
  ].join("\n") + "\n";
2289
- writeFileSync5(devVarsPath, devVarsContent, "utf-8");
2290
- p2.log.success(pc23.green(`Created ${pc23.bold(".dev.vars")} with local development credentials.`));
3554
+ writeFileSync7(devVarsPath, devVarsContent, "utf-8");
3555
+ p3.log.success(pc32.green(`Created ${pc32.bold(".dev.vars")} with local development credentials.`));
2291
3556
  s.start("Setting production secrets on Cloudflare Worker\u2026");
2292
3557
  spawnSync15("npx", ["wrangler", "secret", "put", "R2_ACCESS_KEY_ID"], {
2293
3558
  input: accessKeyId.trim(),
@@ -2309,11 +3574,11 @@ async function setupCloudflare(options = {}) {
2309
3574
  encoding: "utf-8",
2310
3575
  shell: true
2311
3576
  });
2312
- s.stop(pc23.green("\u2713 Production secrets configured on Cloudflare Worker."));
3577
+ s.stop(pc32.green("\u2713 Production secrets configured on Cloudflare Worker."));
2313
3578
  }
2314
3579
  }
2315
3580
  }
2316
- p2.outro(pc23.green("\u2728 Cloudflare infrastructure setup complete! You are ready to run `npx beech deploy`."));
3581
+ p3.outro(pc32.green("\u2728 Cloudflare infrastructure setup complete! You are ready to run `npx beech deploy`."));
2317
3582
  }
2318
3583
  export {
2319
3584
  dbMigrate,
@@ -2332,11 +3597,17 @@ export {
2332
3597
  mailpitClear,
2333
3598
  onboard,
2334
3599
  reset,
3600
+ schemaApply,
3601
+ schemaAudit,
2335
3602
  schemaDiff,
3603
+ schemaExport,
3604
+ schemaMigrate,
3605
+ schemaPlan,
2336
3606
  seedCreate,
2337
3607
  seedLoad,
2338
3608
  setupCloudflare,
2339
3609
  test,
3610
+ typesCheck,
2340
3611
  update,
2341
3612
  validate,
2342
3613
  validateSeeds