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