@beechcms/cli 0.6.0-preview.3 → 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 +1298 -765
- package/package.json +8 -3
- package/.turbo/turbo-build.log +0 -5
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -116
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -116
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov-report/validate.ts.html +0 -325
- package/coverage/lcov.info +0 -80
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/validate.ts.html +0 -325
- package/dist/commands/seed-load.d.ts +0 -8
- package/dist/commands/seed-load.d.ts.map +0 -1
- package/dist/commands/seed-load.js +0 -89
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/schema-diff.d.ts +0 -15
- package/dist/lib/schema-diff.d.ts.map +0 -1
- package/dist/lib/schema-diff.js +0 -37
- package/dist/lib/wrangler.d.ts +0 -17
- package/dist/lib/wrangler.d.ts.map +0 -1
- package/dist/lib/wrangler.js +0 -65
- package/src/commands/deploy.ts +0 -126
- package/src/commands/init.ts +0 -599
- package/src/commands/onboard.ts +0 -32
- package/src/commands/reset.ts +0 -157
- package/src/commands/seed-create.ts +0 -192
- package/src/commands/seed-load.ts +0 -235
- package/src/commands/update.ts +0 -54
- package/src/commands/validate.ts +0 -80
- package/src/index.ts +0 -20
- package/src/lib/schema-diff.ts +0 -150
- package/src/lib/wrangler.ts +0 -129
- package/src/test/reset.test.ts +0 -128
- 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,350 +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
|
-
|
|
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;
|
|
131
|
+
}
|
|
132
|
+
function checkWranglerPlaceholders(configPath) {
|
|
115
133
|
try {
|
|
116
|
-
|
|
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
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return issues;
|
|
117
146
|
} catch {
|
|
118
|
-
return
|
|
147
|
+
return [];
|
|
119
148
|
}
|
|
120
|
-
|
|
121
|
-
|
|
149
|
+
}
|
|
150
|
+
function readProjectName(configPath) {
|
|
151
|
+
if (!configPath) return basename(process.cwd());
|
|
152
|
+
try {
|
|
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());
|
|
157
|
+
} catch {
|
|
158
|
+
return basename(process.cwd());
|
|
122
159
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
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;
|
|
135
171
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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;
|
|
140
186
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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);
|
|
149
201
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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;
|
|
207
|
+
}
|
|
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}`));
|
|
153
229
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const expectedOnDelete = (branch.onDelete ?? "SET NULL").toUpperCase();
|
|
158
|
-
const expectedIndexName = `idx_${seed.slug}_${branch.alias}`;
|
|
159
|
-
const colDiff = columns.find((c) => c.name === branch.alias);
|
|
160
|
-
if (!colDiff || colDiff.status === "missing") continue;
|
|
161
|
-
const fk = fkByCol.get(branch.alias);
|
|
162
|
-
if (!fk) {
|
|
163
|
-
colDiff.status = "fk_missing";
|
|
164
|
-
colDiff.expectedTarget = branch.targetSeed;
|
|
165
|
-
} else {
|
|
166
|
-
const actualTable = fk.table;
|
|
167
|
-
const actualOnDelete = fk.on_delete.toUpperCase();
|
|
168
|
-
if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
|
|
169
|
-
colDiff.status = "fk_mismatch";
|
|
170
|
-
colDiff.expected = `\u2192 ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`;
|
|
171
|
-
colDiff.actual = `\u2192 ${actualTable}(id) ON DELETE ${actualOnDelete}`;
|
|
172
|
-
colDiff.expectedTarget = branch.targetSeed;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
if (!indexNames.has(expectedIndexName)) {
|
|
176
|
-
if (colDiff.status === "ok") {
|
|
177
|
-
colDiff.status = "index_missing";
|
|
178
|
-
} else {
|
|
179
|
-
columns.push({ name: branch.alias, status: "index_missing" });
|
|
180
|
-
}
|
|
181
|
-
}
|
|
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}`));
|
|
182
233
|
}
|
|
234
|
+
console.log(pc4.dim("\n Use these in your frontend as the X-API-Key header.\n"));
|
|
235
|
+
} catch {
|
|
183
236
|
}
|
|
184
|
-
return { slug: seed.slug, tableExists: true, columns };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// src/commands/validate.ts
|
|
188
|
-
import pc from "picocolors";
|
|
189
|
-
import { SEED_REGISTRY, validateSeedDefinitions } from "@beechcms/core";
|
|
190
|
-
function validateSeeds(registry) {
|
|
191
|
-
return validateSeedDefinitions(Object.values(registry));
|
|
192
237
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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"));
|
|
198
246
|
}
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
console.log(pc.red(` \u2192 ${msg}`));
|
|
207
|
-
}
|
|
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)}`));
|
|
208
254
|
}
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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)"));
|
|
258
|
+
} else {
|
|
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)"));
|
|
218
264
|
} else {
|
|
219
|
-
console.log(
|
|
220
|
-
for (const msg of msgs) {
|
|
221
|
-
console.log(pc.yellow(` \u2192 ${msg}`));
|
|
222
|
-
}
|
|
265
|
+
console.log(pc4.green(" \u2713 .dev.vars"));
|
|
223
266
|
}
|
|
224
267
|
}
|
|
225
|
-
|
|
226
|
-
const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0);
|
|
227
|
-
const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0);
|
|
228
|
-
if (totalFatal > 0) {
|
|
229
|
-
const s = totalFatal !== 1 ? "s" : "";
|
|
230
|
-
console.log(pc.red(` Found ${totalFatal} fatal error${s}. Fix before loading.
|
|
231
|
-
`));
|
|
232
|
-
process.exit(1);
|
|
233
|
-
} else if (totalWarnings > 0) {
|
|
234
|
-
const s = totalWarnings !== 1 ? "s" : "";
|
|
235
|
-
console.log(pc.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.
|
|
236
|
-
`));
|
|
237
|
-
} else {
|
|
238
|
-
console.log(pc.green(" All seeds valid.\n"));
|
|
239
|
-
}
|
|
268
|
+
return ok;
|
|
240
269
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
].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"));
|
|
250
278
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
for (const branch of seed.branches) {
|
|
261
|
-
if (branch.type !== "relation" || branch.multiple !== true) continue;
|
|
262
|
-
stmts.push(generateJunctionTable(seed, branch), ...generateJunctionIndexes(seed, branch));
|
|
263
|
-
const draftJunction = generateJunctionDraftTable(seed, branch);
|
|
264
|
-
if (draftJunction) stmts.push(draftJunction);
|
|
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;
|
|
265
288
|
}
|
|
266
|
-
return stmts;
|
|
267
289
|
}
|
|
268
|
-
async function
|
|
269
|
-
const
|
|
270
|
-
console.log(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
allOk = false;
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
const problems = result.columns.filter((c) => c.status !== "ok");
|
|
281
|
-
if (problems.length === 0) {
|
|
282
|
-
console.log(pc2.green(` \u2713 ${tableName}`));
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
allOk = false;
|
|
286
|
-
console.log(pc2.yellow(` \u26A0 ${tableName}`));
|
|
287
|
-
for (const col of problems) {
|
|
288
|
-
if (col.status === "missing") {
|
|
289
|
-
console.log(pc2.red(` + missing column: ${col.name} ${col.expectedType}`));
|
|
290
|
-
} else if (col.status === "extra") {
|
|
291
|
-
console.log(pc2.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) \u2014 exists in DB but not in seeds.ts`));
|
|
292
|
-
} else if (col.status === "type_mismatch") {
|
|
293
|
-
console.log(pc2.red(` \u2260 type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`));
|
|
294
|
-
} else if (col.status === "fk_missing") {
|
|
295
|
-
console.log(pc2.red(` \u292C missing FK: ${col.name} \u2192 content_${col.expectedTarget}(id)`));
|
|
296
|
-
} else if (col.status === "fk_mismatch") {
|
|
297
|
-
console.log(pc2.yellow(` \u292C FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`));
|
|
298
|
-
} else if (col.status === "index_missing") {
|
|
299
|
-
console.log(pc2.yellow(` \u2298 missing index on ${col.name}`));
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
console.log("");
|
|
304
|
-
if (allOk) {
|
|
305
|
-
console.log(pc2.green(" Schema matches seeds. No action needed.\n"));
|
|
306
|
-
} else {
|
|
307
|
-
console.log(pc2.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);
|
|
308
299
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
console.log(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
console.log(stmt + "\n");
|
|
319
|
-
}
|
|
320
|
-
console.log(pc2.dim(` -- register ${seed.slug} in seeds table`));
|
|
321
|
-
console.log(buildSeedRegistrationSql(seed) + "\n");
|
|
322
|
-
}
|
|
323
|
-
console.log(pc2.dim(" -- bump registry_version"));
|
|
324
|
-
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"));
|
|
325
309
|
return;
|
|
326
310
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
339
368
|
`));
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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"));
|
|
347
379
|
} else {
|
|
348
|
-
|
|
349
|
-
|
|
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);
|
|
350
387
|
}
|
|
351
|
-
process.exit(1);
|
|
352
388
|
}
|
|
353
|
-
console.log(pc2.green("done"));
|
|
354
|
-
}
|
|
355
|
-
executeD1File(SEED_META_BUMP_SQL, options);
|
|
356
|
-
console.log(pc2.green("\n All seeds loaded.\n"));
|
|
357
|
-
console.log(pc2.dim(" Definitions registered in the database."));
|
|
358
|
-
console.log(pc2.dim(" seed.ts is no longer required at runtime \u2014 you may keep it for code-first edits or delete it.\n"));
|
|
359
|
-
}
|
|
360
|
-
async function seedLoad(args) {
|
|
361
|
-
const registry = args.registry ?? SEED_REGISTRY2;
|
|
362
|
-
if (Object.keys(registry).length === 0) {
|
|
363
|
-
console.log(pc2.yellow("\n \u2717 No seeds found\n"));
|
|
364
|
-
console.log(pc2.dim(" Create a seeds.ts file in your project root with at least one content type."));
|
|
365
|
-
console.log(pc2.cyan("\n \u2192 Run: npx beech seed:create\n"));
|
|
366
|
-
return;
|
|
367
389
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
`));
|
|
377
|
-
for (const e of fatalErrors) {
|
|
378
|
-
console.log(pc2.red(` \u2717 ${e.slug}`));
|
|
379
|
-
for (const msg of e.messages) {
|
|
380
|
-
console.log(pc2.red(` \u2192 ${msg}`));
|
|
381
|
-
}
|
|
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);
|
|
382
398
|
}
|
|
383
|
-
console.log("");
|
|
384
|
-
process.exit(1);
|
|
385
|
-
}
|
|
386
|
-
if (warnings.length > 0) {
|
|
387
|
-
const total = warnings.reduce((n, e) => n + e.messages.length, 0);
|
|
388
|
-
const s = total !== 1 ? "s" : "";
|
|
389
|
-
console.log(pc2.yellow(`
|
|
390
|
-
\u26A0 Seed validation found ${total} issue${s}. Schema changes will still be applied.
|
|
391
|
-
`));
|
|
392
|
-
console.log(pc2.dim(' Run "npx beech validate" for details.\n'));
|
|
393
399
|
}
|
|
394
|
-
const configPath = findWranglerConfig();
|
|
395
400
|
const db = args.db ?? resolveDbName(configPath);
|
|
396
|
-
const options = {
|
|
397
|
-
|
|
398
|
-
local: args.local,
|
|
399
|
-
configPath
|
|
400
|
-
};
|
|
401
|
-
if (!args.dryRun && !args.diff) {
|
|
402
|
-
try {
|
|
403
|
-
const rows = queryD1(
|
|
404
|
-
`SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
|
|
405
|
-
options
|
|
406
|
-
);
|
|
407
|
-
if (rows.length < 2) {
|
|
408
|
-
console.log(pc2.red("\n \u2717 System tables not found (seeds, seed_meta)\n"));
|
|
409
|
-
console.log(pc2.dim(" Run `beech init --db` first to initialise the database."));
|
|
410
|
-
const flag = args.local ? " --local" : "";
|
|
411
|
-
console.log(pc2.cyan(`
|
|
412
|
-
\u2192 Run: npx beech init --db${flag}
|
|
401
|
+
const options = { db, local: args.local, configPath };
|
|
402
|
+
console.log(pc4.cyan(` Checking database "${db}" (${args.local ? "local" : "remote"})\u2026
|
|
413
403
|
`));
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
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"));
|
|
414
|
+
process.exit(1);
|
|
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"));
|
|
419
424
|
process.exit(1);
|
|
420
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;
|
|
421
431
|
}
|
|
422
|
-
if (
|
|
423
|
-
|
|
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;
|
|
424
438
|
} else {
|
|
425
|
-
|
|
439
|
+
console.log(pc4.yellow(` Missing system tables: ${missingTables.join(", ")}`));
|
|
440
|
+
console.log(pc4.cyan("\n Applying base schema\u2026\n"));
|
|
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);
|
|
426
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);
|
|
427
455
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
];
|
|
451
|
-
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 = `
|
|
452
478
|
CREATE TABLE IF NOT EXISTS users (
|
|
453
479
|
id TEXT NOT NULL PRIMARY KEY,
|
|
454
480
|
email TEXT NOT NULL UNIQUE,
|
|
@@ -610,346 +636,369 @@ CREATE TABLE IF NOT EXISTS site_settings (
|
|
|
610
636
|
value TEXT NOT NULL
|
|
611
637
|
);
|
|
612
638
|
`.trim();
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
];
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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");
|
|
625
669
|
}
|
|
626
|
-
function
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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;
|
|
638
702
|
}
|
|
639
|
-
return issues;
|
|
640
|
-
} catch {
|
|
641
|
-
return [];
|
|
642
703
|
}
|
|
643
704
|
}
|
|
644
|
-
function
|
|
645
|
-
|
|
705
|
+
async function diffSeed(seed, options) {
|
|
706
|
+
const tableName = `content_${seed.slug}`;
|
|
707
|
+
const expected = getExpectedColumns(seed);
|
|
708
|
+
let actual;
|
|
646
709
|
try {
|
|
647
|
-
|
|
648
|
-
const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
649
|
-
const parsed = JSON.parse(stripped);
|
|
650
|
-
return parsed?.name || basename(process.cwd());
|
|
710
|
+
actual = queryD1(`PRAGMA table_info(${tableName})`, options);
|
|
651
711
|
} catch {
|
|
652
|
-
return
|
|
712
|
+
return { slug: seed.slug, tableExists: false, columns: expected.map((c) => ({ name: c.name, status: "missing", expectedType: c.sqlType })) };
|
|
653
713
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (!configPath) return null;
|
|
657
|
-
try {
|
|
658
|
-
const raw = readFileSync2(configPath, "utf-8");
|
|
659
|
-
const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
660
|
-
const parsed = JSON.parse(stripped);
|
|
661
|
-
const buckets = parsed?.r2_buckets ?? [];
|
|
662
|
-
return buckets[0]?.bucket_name ?? null;
|
|
663
|
-
} catch {
|
|
664
|
-
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 })) };
|
|
665
716
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
} catch {
|
|
679
|
-
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
|
+
}
|
|
680
729
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
stdio: "inherit",
|
|
685
|
-
cwd: process.cwd(),
|
|
686
|
-
shell: true
|
|
687
|
-
});
|
|
688
|
-
return result.status === 0;
|
|
689
|
-
}
|
|
690
|
-
function patchWranglerConfig(configPath, dbId) {
|
|
691
|
-
try {
|
|
692
|
-
let raw = readFileSync2(configPath, "utf-8");
|
|
693
|
-
for (const placeholder of PLACEHOLDER_DB_IDS) {
|
|
694
|
-
raw = raw.split(placeholder).join(dbId);
|
|
730
|
+
for (const row of actual) {
|
|
731
|
+
if (!expectedSet.has(row.name)) {
|
|
732
|
+
columns.push({ name: row.name, status: "extra", actualType: row.type });
|
|
695
733
|
}
|
|
696
|
-
raw = raw.replace(/"database_id"\s*:\s*""/g, `"database_id": "${dbId}"`);
|
|
697
|
-
writeFileSync2(configPath, raw, "utf-8");
|
|
698
|
-
return true;
|
|
699
|
-
} catch {
|
|
700
|
-
return false;
|
|
701
734
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (!configPath) return;
|
|
711
|
-
try {
|
|
712
|
-
const raw = readFileSync2(configPath, "utf-8");
|
|
713
|
-
const stripped = raw.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
714
|
-
const parsed = JSON.parse(stripped);
|
|
715
|
-
const vars = parsed?.vars ?? {};
|
|
716
|
-
const readKey = vars["PUBLIC_READ_API_KEY"];
|
|
717
|
-
const writeKey = vars["PUBLIC_WRITE_API_KEY"];
|
|
718
|
-
if (!readKey && !writeKey) return;
|
|
719
|
-
console.log(pc3.dim(" API keys detected in wrangler.jsonc:\n"));
|
|
720
|
-
if (readKey) {
|
|
721
|
-
const masked = readKey.length > 8 ? readKey.slice(0, 4) + "****" + readKey.slice(-4) : "****";
|
|
722
|
-
console.log(pc3.dim(` PUBLIC_READ_API_KEY = ${masked}`));
|
|
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 {
|
|
723
743
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
+
}
|
|
727
776
|
}
|
|
728
|
-
console.log(pc3.dim("\n Use these in your frontend as the X-API-Key header.\n"));
|
|
729
|
-
} catch {
|
|
730
777
|
}
|
|
778
|
+
return { slug: seed.slug, tableExists: true, columns };
|
|
731
779
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
console.log(pc3.red(" \u2717 wrangler.jsonc \u2014 missing (required)"));
|
|
745
|
-
ok = false;
|
|
746
|
-
} else {
|
|
747
|
-
console.log(pc3.green(` \u2713 ${basename(configPath)}`));
|
|
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));
|
|
786
|
+
}
|
|
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;
|
|
748
792
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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}`));
|
|
801
|
+
}
|
|
754
802
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
+
}
|
|
758
812
|
} else {
|
|
759
|
-
console.log(
|
|
813
|
+
console.log(pc2.yellow(` \u26A0 ${seed.slug}`));
|
|
814
|
+
for (const msg of msgs) {
|
|
815
|
+
console.log(pc2.yellow(` \u2192 ${msg}`));
|
|
816
|
+
}
|
|
760
817
|
}
|
|
761
818
|
}
|
|
762
|
-
|
|
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
|
+
`));
|
|
831
|
+
} else {
|
|
832
|
+
console.log(pc2.green(" All seeds valid.\n"));
|
|
833
|
+
}
|
|
763
834
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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");
|
|
772
844
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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);
|
|
782
859
|
}
|
|
860
|
+
return stmts;
|
|
783
861
|
}
|
|
784
|
-
async function
|
|
785
|
-
const
|
|
786
|
-
console.log(pc3.cyan("\n
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
process.exit(1);
|
|
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;
|
|
793
870
|
}
|
|
794
|
-
console.log(
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
console.log(pc3.
|
|
799
|
-
console.log(pc3.dim(` 1. npx beech init --db${localFlag} # initialise D1 database`));
|
|
800
|
-
console.log(pc3.dim(` 2. npx beech seed:load${localFlag} # create content tables`));
|
|
801
|
-
console.log(pc3.dim(" 3. npx wrangler dev # start API + dashboard"));
|
|
802
|
-
console.log(pc3.dim(" 4. Open http://localhost:8789/admin\n"));
|
|
803
|
-
return;
|
|
871
|
+
console.log("");
|
|
872
|
+
if (allOk) {
|
|
873
|
+
console.log(pc3.green(" Schema matches seeds. No action needed.\n"));
|
|
874
|
+
} else {
|
|
875
|
+
console.log(pc3.yellow(" Run `beech seed:load` to apply missing tables/columns.\n"));
|
|
804
876
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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");
|
|
812
887
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
rl.close();
|
|
823
|
-
}
|
|
824
|
-
if (autoCreate) {
|
|
825
|
-
const authed = checkWranglerAuth();
|
|
826
|
-
if (!authed) {
|
|
827
|
-
console.log(pc3.red("\n \u2717 Not logged in to Cloudflare\n"));
|
|
828
|
-
console.log(pc3.dim(" BeechCMS needs access to your Cloudflare account to create the database."));
|
|
829
|
-
console.log(pc3.cyan("\n \u2192 Run: npx wrangler login"));
|
|
830
|
-
console.log(pc3.cyan(" \u2192 Then: npx beech init --db\n"));
|
|
831
|
-
process.exit(1);
|
|
832
|
-
}
|
|
833
|
-
const projectName = readProjectName(configPath);
|
|
834
|
-
const dbName = `${projectName}-db`;
|
|
835
|
-
const bucketName = readBucketName(configPath) || `${projectName}-media`;
|
|
836
|
-
console.log(pc3.dim(`
|
|
837
|
-
Creating D1 database "${dbName}"\u2026`));
|
|
838
|
-
const dbId = createD1Database(dbName);
|
|
839
|
-
if (!dbId) {
|
|
840
|
-
console.log(pc3.red("\n \u2717 Failed to create D1 database\n"));
|
|
841
|
-
console.log(pc3.dim(" Create it manually and retry:"));
|
|
842
|
-
console.log(pc3.cyan(`
|
|
843
|
-
\u2192 Run: npx wrangler d1 create ${dbName}`));
|
|
844
|
-
console.log(pc3.cyan(" \u2192 Then: npx beech init --db\n"));
|
|
845
|
-
process.exit(1);
|
|
846
|
-
}
|
|
847
|
-
console.log(pc3.green(` \u2713 D1 database created (id: ${dbId})`));
|
|
848
|
-
console.log(pc3.dim(`
|
|
849
|
-
Creating R2 bucket "${bucketName}"\u2026`));
|
|
850
|
-
const r2Ok = createR2Bucket(bucketName);
|
|
851
|
-
if (r2Ok) {
|
|
852
|
-
console.log(pc3.green(` \u2713 R2 bucket "${bucketName}" created`));
|
|
853
|
-
} else {
|
|
854
|
-
console.log(pc3.yellow(` \u26A0 R2 bucket creation failed (may already exist \u2014 continuing)`));
|
|
855
|
-
}
|
|
856
|
-
console.log(pc3.dim("\n Updating wrangler.jsonc\u2026"));
|
|
857
|
-
const patched = patchWranglerConfig(configPath, dbId);
|
|
858
|
-
if (patched) {
|
|
859
|
-
console.log(pc3.green(" \u2713 wrangler.jsonc updated\n"));
|
|
860
|
-
} else {
|
|
861
|
-
console.log(pc3.yellow(` \u26A0 Could not update wrangler.jsonc automatically
|
|
888
|
+
console.log(pc3.dim(` -- register ${seed.slug} in seeds table`));
|
|
889
|
+
console.log(buildSeedRegistrationSql(seed) + "\n");
|
|
890
|
+
}
|
|
891
|
+
console.log(pc3.dim(" -- bump registry_version"));
|
|
892
|
+
console.log(SEED_META_BUMP_SQL + "\n");
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
console.log(pc3.cyan(`
|
|
896
|
+
Loading seeds into ${options.local ? "local" : "remote"} D1 (${options.db})\u2026
|
|
862
897
|
`));
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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"));
|
|
873
915
|
} else {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
console.log(pc3.dim(" Set a real database_id in wrangler.jsonc, then retry."));
|
|
877
|
-
process.exit(1);
|
|
878
|
-
}
|
|
879
|
-
printManualDbInstructions();
|
|
880
|
-
process.exit(1);
|
|
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"));
|
|
881
918
|
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
if (!args.local) {
|
|
885
|
-
const authed = checkWranglerAuth();
|
|
886
|
-
if (!authed) {
|
|
887
|
-
console.log(pc3.red(" \u2717 Not logged in to Cloudflare\n"));
|
|
888
|
-
console.log(pc3.dim(" BeechCMS needs access to your Cloudflare account to manage the D1 database."));
|
|
889
|
-
console.log(pc3.cyan("\n \u2192 Run: npx wrangler login"));
|
|
890
|
-
console.log(pc3.cyan(" \u2192 Then: npx beech init --db\n"));
|
|
891
919
|
process.exit(1);
|
|
892
920
|
}
|
|
921
|
+
console.log(pc3.green("done"));
|
|
893
922
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
console.log(pc3.
|
|
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"));
|
|
927
|
+
}
|
|
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"));
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
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.
|
|
897
944
|
`));
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
console.log(pc3.dim(" Most likely causes:"));
|
|
904
|
-
console.log(pc3.dim(" - Wrong database_id in wrangler.jsonc"));
|
|
905
|
-
console.log(pc3.dim(" - Worker not yet deployed"));
|
|
906
|
-
console.log(pc3.cyan("\n \u2192 Fix: Update d1_databases.database_id in wrangler.jsonc"));
|
|
907
|
-
console.log(pc3.cyan(" \u2192 Then: npm run deploy\n"));
|
|
908
|
-
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}`));
|
|
949
|
+
}
|
|
909
950
|
}
|
|
910
|
-
|
|
911
|
-
|
|
951
|
+
console.log("");
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
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'));
|
|
961
|
+
}
|
|
962
|
+
const configPath = findWranglerConfig();
|
|
963
|
+
const db = args.db ?? resolveDbName(configPath);
|
|
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}
|
|
912
981
|
`));
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
console.log(pc3.
|
|
917
|
-
console.log(pc3.
|
|
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."));
|
|
918
987
|
process.exit(1);
|
|
919
988
|
}
|
|
920
|
-
for (const table of SYSTEM_TABLES) {
|
|
921
|
-
console.log(pc3.green(` \u2713 ${table}`));
|
|
922
|
-
}
|
|
923
|
-
console.log(pc3.green("\n All system tables present. Remote database is initialized.\n"));
|
|
924
|
-
return;
|
|
925
989
|
}
|
|
926
|
-
if (
|
|
927
|
-
|
|
928
|
-
} else if (missingTables.length === 0) {
|
|
929
|
-
console.log(pc3.green(" \u2713 All system tables present. Database already initialised.\n"));
|
|
930
|
-
printNextSteps(args.local);
|
|
931
|
-
return;
|
|
990
|
+
if (args.diff) {
|
|
991
|
+
await runDiff(options, registry);
|
|
932
992
|
} else {
|
|
933
|
-
|
|
934
|
-
console.log(pc3.cyan("\n Applying base schema\u2026\n"));
|
|
935
|
-
}
|
|
936
|
-
const ok = executeD1File(BASE_SCHEMA_SQL, options);
|
|
937
|
-
if (!ok) {
|
|
938
|
-
console.log(pc3.red("\n \u2717 Database initialisation failed\n"));
|
|
939
|
-
console.log(pc3.dim(" wrangler reported an error above."));
|
|
940
|
-
console.log(pc3.cyan("\n \u2192 Run: npx beech init --db --local\n"));
|
|
941
|
-
process.exit(1);
|
|
993
|
+
await runLoad(options, args.dryRun, registry);
|
|
942
994
|
}
|
|
943
|
-
console.log(pc3.green("\n \u2713 worker.ts"));
|
|
944
|
-
console.log(pc3.green(` \u2713 ${configPath ? basename(configPath) : "wrangler.jsonc"}`));
|
|
945
|
-
console.log(pc3.green(" \u2713 seeds.ts"));
|
|
946
|
-
console.log(pc3.green(" \u2713 Local D1 system tables ready\n"));
|
|
947
|
-
echoApiKeys(configPath);
|
|
948
|
-
printNextSteps(args.local);
|
|
949
995
|
}
|
|
950
996
|
|
|
997
|
+
// src/index.ts
|
|
998
|
+
init_init();
|
|
999
|
+
|
|
951
1000
|
// src/commands/seed-create.ts
|
|
952
|
-
import
|
|
1001
|
+
import pc5 from "picocolors";
|
|
953
1002
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
954
1003
|
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
955
1004
|
import { resolve as resolve3 } from "node:path";
|
|
@@ -1020,35 +1069,35 @@ function findSeedsFile() {
|
|
|
1020
1069
|
async function seedCreate(_args) {
|
|
1021
1070
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1022
1071
|
const ask = async (q, fallback = "") => {
|
|
1023
|
-
const hint = fallback ?
|
|
1072
|
+
const hint = fallback ? pc5.dim(` [${fallback}]`) : "";
|
|
1024
1073
|
const answer = await rl.question(` ${q}${hint}: `);
|
|
1025
1074
|
return answer.trim() || fallback;
|
|
1026
1075
|
};
|
|
1027
1076
|
const askYN = async (q, defaultYes = true) => {
|
|
1028
|
-
const hint = defaultYes ?
|
|
1077
|
+
const hint = defaultYes ? pc5.dim(" (Y/n)") : pc5.dim(" (y/N)");
|
|
1029
1078
|
const answer = (await rl.question(` ${q}${hint}: `)).trim().toLowerCase();
|
|
1030
1079
|
if (!answer) return defaultYes;
|
|
1031
1080
|
return answer === "y" || answer === "yes";
|
|
1032
1081
|
};
|
|
1033
|
-
console.log(
|
|
1082
|
+
console.log(pc5.cyan("\n beech seed:create \u2014 new content type wizard\n"));
|
|
1034
1083
|
try {
|
|
1035
1084
|
const label = await ask('Content type name (singular, e.g. "Article")');
|
|
1036
1085
|
if (!label) {
|
|
1037
1086
|
rl.close();
|
|
1038
|
-
console.log(
|
|
1087
|
+
console.log(pc5.red("\n \u2717 Name required.\n"));
|
|
1039
1088
|
process.exit(1);
|
|
1040
1089
|
}
|
|
1041
1090
|
const defaultSlug = slugify(label) + "s";
|
|
1042
1091
|
const slug = slugify(await ask("Slug (plural, used in URL + table name)", defaultSlug)) || defaultSlug;
|
|
1043
1092
|
const labelPlural = await ask("Plural label", label + "s") || label + "s";
|
|
1044
1093
|
const branches = [];
|
|
1045
|
-
console.log(
|
|
1094
|
+
console.log(pc5.dim("\n Now define the fields. Press Enter to accept defaults.\n"));
|
|
1046
1095
|
let addMore = true;
|
|
1047
1096
|
while (addMore) {
|
|
1048
|
-
console.log(
|
|
1097
|
+
console.log(pc5.dim(` \u2500\u2500\u2500 Field ${branches.length + 1} \u2500\u2500\u2500`));
|
|
1049
1098
|
const alias = await ask(' Alias (camelCase, e.g. "title", "publishedAt")');
|
|
1050
1099
|
if (!alias) {
|
|
1051
|
-
console.log(
|
|
1100
|
+
console.log(pc5.yellow(" Alias required \u2014 skipping."));
|
|
1052
1101
|
addMore = await askYN("\n Add a field?");
|
|
1053
1102
|
continue;
|
|
1054
1103
|
}
|
|
@@ -1062,14 +1111,14 @@ async function seedCreate(_args) {
|
|
|
1062
1111
|
}
|
|
1063
1112
|
rl.close();
|
|
1064
1113
|
if (branches.length === 0) {
|
|
1065
|
-
console.log(
|
|
1114
|
+
console.log(pc5.yellow("\n No fields defined \u2014 seed not created.\n"));
|
|
1066
1115
|
process.exit(0);
|
|
1067
1116
|
}
|
|
1068
1117
|
const seedBlock = generateSeedBlock(slug, label, labelPlural, branches);
|
|
1069
1118
|
const cName = toConstName(slug);
|
|
1070
1119
|
const seedsPath = findSeedsFile();
|
|
1071
1120
|
if (!seedsPath) {
|
|
1072
|
-
console.log(
|
|
1121
|
+
console.log(pc5.yellow("\n Could not find seeds.ts. Add this to your seeds file manually:\n"));
|
|
1073
1122
|
console.log(seedBlock);
|
|
1074
1123
|
process.exit(0);
|
|
1075
1124
|
}
|
|
@@ -1078,18 +1127,18 @@ async function seedCreate(_args) {
|
|
|
1078
1127
|
const withSeed = content + seedBlock;
|
|
1079
1128
|
const withRegistry = tryInsertRegistryEntry(withSeed, slug, cName);
|
|
1080
1129
|
writeFileSync3(seedsPath, withRegistry ?? withSeed, "utf-8");
|
|
1081
|
-
console.log(
|
|
1130
|
+
console.log(pc5.green(`
|
|
1082
1131
|
\u2713 Seed "${slug}" appended to ${seedsPath}
|
|
1083
1132
|
`));
|
|
1084
1133
|
if (!withRegistry) {
|
|
1085
|
-
console.log(
|
|
1134
|
+
console.log(pc5.yellow(` \u26A0 Could not auto-update SEED_REGISTRY \u2014 add this entry manually:
|
|
1086
1135
|
`));
|
|
1087
|
-
console.log(
|
|
1136
|
+
console.log(pc5.cyan(` ${slug}: ${cName},
|
|
1088
1137
|
`));
|
|
1089
1138
|
}
|
|
1090
|
-
console.log(
|
|
1091
|
-
console.log(
|
|
1092
|
-
console.log(
|
|
1139
|
+
console.log(pc5.dim(" Next steps:"));
|
|
1140
|
+
console.log(pc5.cyan(" npx beech seed:load --local"));
|
|
1141
|
+
console.log(pc5.dim(" \u2192 create the new content table in your local D1 database\n"));
|
|
1093
1142
|
} catch (err) {
|
|
1094
1143
|
rl.close();
|
|
1095
1144
|
throw err;
|
|
@@ -1097,7 +1146,8 @@ async function seedCreate(_args) {
|
|
|
1097
1146
|
}
|
|
1098
1147
|
|
|
1099
1148
|
// src/commands/deploy.ts
|
|
1100
|
-
|
|
1149
|
+
init_wrangler();
|
|
1150
|
+
import pc6 from "picocolors";
|
|
1101
1151
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1102
1152
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1103
1153
|
function readWorkerName(configPath) {
|
|
@@ -1128,8 +1178,8 @@ async function checkAdmin(url) {
|
|
|
1128
1178
|
}
|
|
1129
1179
|
}
|
|
1130
1180
|
async function deploy(args) {
|
|
1131
|
-
console.log(
|
|
1132
|
-
console.log(
|
|
1181
|
+
console.log(pc6.cyan("\n beech deploy\n"));
|
|
1182
|
+
console.log(pc6.dim(" [1/3] Deploying Worker\u2026\n"));
|
|
1133
1183
|
const deployResult = spawnSync3("npm", ["run", "deploy"], {
|
|
1134
1184
|
stdio: ["inherit", "pipe", "inherit"],
|
|
1135
1185
|
encoding: "utf-8",
|
|
@@ -1139,33 +1189,33 @@ async function deploy(args) {
|
|
|
1139
1189
|
const deployStdout = deployResult.stdout ?? "";
|
|
1140
1190
|
if (deployStdout) process.stdout.write(deployStdout);
|
|
1141
1191
|
if (deployResult.status !== 0) {
|
|
1142
|
-
console.log(
|
|
1143
|
-
console.log(
|
|
1144
|
-
console.log(
|
|
1145
|
-
console.log(
|
|
1192
|
+
console.log(pc6.red("\n \u2717 Worker deploy failed\n"));
|
|
1193
|
+
console.log(pc6.dim(" Check the wrangler output above for details."));
|
|
1194
|
+
console.log(pc6.cyan("\n \u2192 Run: npx wrangler login # if not authenticated"));
|
|
1195
|
+
console.log(pc6.cyan(" \u2192 Or: Update wrangler.jsonc # if database_id is wrong\n"));
|
|
1146
1196
|
process.exit(1);
|
|
1147
1197
|
}
|
|
1148
1198
|
const deployedUrl = extractWorkerUrl(deployStdout);
|
|
1149
|
-
console.log(
|
|
1199
|
+
console.log(pc6.green("\n \u2713 Worker deployed"));
|
|
1150
1200
|
if (args.skipSeed) {
|
|
1151
|
-
console.log(
|
|
1201
|
+
console.log(pc6.dim("\n [2/3] Skipping seed:load (--skip-seed)"));
|
|
1152
1202
|
} else {
|
|
1153
|
-
console.log(
|
|
1203
|
+
console.log(pc6.dim("\n [2/3] Syncing content schema to remote D1\u2026\n"));
|
|
1154
1204
|
const seedResult = spawnSync3("npx", ["beech", "seed:load"], {
|
|
1155
1205
|
stdio: "inherit",
|
|
1156
1206
|
cwd: process.cwd(),
|
|
1157
1207
|
shell: true
|
|
1158
1208
|
});
|
|
1159
1209
|
if (seedResult.status !== 0) {
|
|
1160
|
-
console.log(
|
|
1161
|
-
console.log(
|
|
1162
|
-
console.log(
|
|
1210
|
+
console.log(pc6.yellow("\n \u26A0 seed:load failed\n"));
|
|
1211
|
+
console.log(pc6.dim(" Sync the remote content schema manually:"));
|
|
1212
|
+
console.log(pc6.cyan(" \u2192 Run: npx beech seed:load\n"));
|
|
1163
1213
|
} else {
|
|
1164
|
-
console.log(
|
|
1214
|
+
console.log(pc6.green("\n \u2713 Content schema synced"));
|
|
1165
1215
|
}
|
|
1166
1216
|
}
|
|
1167
1217
|
if (args.skipCheck) {
|
|
1168
|
-
console.log(
|
|
1218
|
+
console.log(pc6.dim("\n [3/3] Skipping admin check (--skip-check)\n"));
|
|
1169
1219
|
return;
|
|
1170
1220
|
}
|
|
1171
1221
|
const adminBase = deployedUrl ?? (() => {
|
|
@@ -1173,91 +1223,160 @@ async function deploy(args) {
|
|
|
1173
1223
|
return workerName ? `https://${workerName}.workers.dev` : null;
|
|
1174
1224
|
})();
|
|
1175
1225
|
if (!adminBase) {
|
|
1176
|
-
console.log(
|
|
1177
|
-
console.log(
|
|
1226
|
+
console.log(pc6.dim("\n [3/3] Could not determine worker URL \u2014 skipping admin check\n"));
|
|
1227
|
+
console.log(pc6.dim(" The deployed URL is printed by wrangler above. Open <url>/admin to verify.\n"));
|
|
1178
1228
|
return;
|
|
1179
1229
|
}
|
|
1180
|
-
console.log(
|
|
1230
|
+
console.log(pc6.dim(`
|
|
1181
1231
|
[3/3] Checking ${adminBase}/admin\u2026
|
|
1182
1232
|
`));
|
|
1183
1233
|
const { ok, status } = await checkAdmin(adminBase);
|
|
1184
1234
|
if (ok) {
|
|
1185
|
-
console.log(
|
|
1235
|
+
console.log(pc6.green(` \u2713 Admin reachable at: ${adminBase}/admin
|
|
1186
1236
|
`));
|
|
1187
1237
|
} else {
|
|
1188
1238
|
const statusStr = status != null ? ` (HTTP ${status})` : "";
|
|
1189
|
-
console.log(
|
|
1239
|
+
console.log(pc6.yellow(` \u26A0 Admin returned an error${statusStr} at: ${adminBase}/admin
|
|
1190
1240
|
`));
|
|
1191
|
-
console.log(
|
|
1192
|
-
console.log(
|
|
1241
|
+
console.log(pc6.dim(" The database may not be fully initialized."));
|
|
1242
|
+
console.log(pc6.cyan(" \u2192 Run: npx beech init --db --remote\n"));
|
|
1193
1243
|
}
|
|
1194
1244
|
}
|
|
1195
1245
|
|
|
1196
1246
|
// src/commands/onboard.ts
|
|
1197
|
-
|
|
1247
|
+
init_init();
|
|
1248
|
+
import pc7 from "picocolors";
|
|
1198
1249
|
async function onboard(args) {
|
|
1199
|
-
console.log(
|
|
1250
|
+
console.log(pc7.cyan("\n beech onboard \u2014 full provisioning\n"));
|
|
1200
1251
|
await init({ initDb: true, local: args.local, db: args.db, nonInteractive: args.yes });
|
|
1201
1252
|
await seedLoad({ dryRun: false, diff: false, local: args.local, db: args.db, registry: args.registry ?? null });
|
|
1202
|
-
console.log(
|
|
1203
|
-
console.log(
|
|
1204
|
-
console.log(
|
|
1205
|
-
console.log(
|
|
1206
|
-
console.log(
|
|
1207
|
-
console.log(
|
|
1253
|
+
console.log(pc7.cyan("\n Provisioning complete.\n"));
|
|
1254
|
+
console.log(pc7.dim(" Next steps:"));
|
|
1255
|
+
console.log(pc7.cyan(" 1. npx wrangler dev"));
|
|
1256
|
+
console.log(pc7.dim(" \u2192 start API + dashboard"));
|
|
1257
|
+
console.log(pc7.dim(" 2. Open http://localhost:8789/admin"));
|
|
1258
|
+
console.log(pc7.dim(" \u2192 complete setup wizard to create admin user\n"));
|
|
1208
1259
|
}
|
|
1209
1260
|
|
|
1210
1261
|
// src/commands/update.ts
|
|
1211
|
-
import
|
|
1262
|
+
import pc8 from "picocolors";
|
|
1212
1263
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1213
1264
|
async function update(_args) {
|
|
1214
|
-
console.log(
|
|
1215
|
-
console.log(
|
|
1265
|
+
console.log(pc8.cyan("\n beech update\n"));
|
|
1266
|
+
console.log(pc8.dim(" [1/2] Installing latest BeechCMS packages\u2026\n"));
|
|
1216
1267
|
const installResult = spawnSync4(
|
|
1217
1268
|
"npm",
|
|
1218
1269
|
["install", "@beechcms/api@latest", "@beechcms/core@latest"],
|
|
1219
1270
|
{ stdio: "inherit", cwd: process.cwd(), shell: true }
|
|
1220
1271
|
);
|
|
1221
1272
|
if (installResult.status !== 0) {
|
|
1222
|
-
console.log(
|
|
1223
|
-
console.log(
|
|
1224
|
-
console.log(
|
|
1225
|
-
console.log(
|
|
1273
|
+
console.log(pc8.red("\n \u2717 npm install failed\n"));
|
|
1274
|
+
console.log(pc8.dim(" Check the output above for details."));
|
|
1275
|
+
console.log(pc8.dim(" You may need to resolve version conflicts manually."));
|
|
1276
|
+
console.log(pc8.cyan("\n \u2192 Try: npm install --legacy-peer-deps\n"));
|
|
1226
1277
|
process.exit(1);
|
|
1227
1278
|
}
|
|
1228
|
-
console.log(
|
|
1229
|
-
console.log(
|
|
1279
|
+
console.log(pc8.green("\n \u2713 Packages updated"));
|
|
1280
|
+
console.log(pc8.dim("\n [2/2] Applying system migrations to local database\u2026\n"));
|
|
1230
1281
|
const initResult = spawnSync4(
|
|
1231
1282
|
"npx",
|
|
1232
1283
|
["beech", "init", "--db", "--local"],
|
|
1233
1284
|
{ stdio: "inherit", cwd: process.cwd(), shell: true }
|
|
1234
1285
|
);
|
|
1235
1286
|
if (initResult.status !== 0) {
|
|
1236
|
-
console.log(
|
|
1237
|
-
console.log(
|
|
1238
|
-
console.log(
|
|
1287
|
+
console.log(pc8.yellow("\n \u26A0 Local DB update failed\n"));
|
|
1288
|
+
console.log(pc8.dim(" Apply system migrations manually:"));
|
|
1289
|
+
console.log(pc8.cyan(" \u2192 Run: npx beech init --db --local\n"));
|
|
1239
1290
|
} else {
|
|
1240
|
-
console.log(
|
|
1291
|
+
console.log(pc8.green("\n \u2713 Local database updated"));
|
|
1241
1292
|
}
|
|
1242
|
-
console.log(
|
|
1243
|
-
console.log(
|
|
1244
|
-
console.log(
|
|
1245
|
-
console.log(
|
|
1246
|
-
console.log(
|
|
1247
|
-
console.log(
|
|
1248
|
-
console.log(
|
|
1249
|
-
console.log(
|
|
1293
|
+
console.log(pc8.dim("\n Local update complete.\n"));
|
|
1294
|
+
console.log(pc8.dim(" Next steps:"));
|
|
1295
|
+
console.log(pc8.cyan(" 1. npx beech seed:load --local"));
|
|
1296
|
+
console.log(pc8.dim(" \u2192 sync content schema to local DB"));
|
|
1297
|
+
console.log(pc8.cyan(" 2. npm run deploy"));
|
|
1298
|
+
console.log(pc8.dim(" \u2192 deploy updated API + dashboard"));
|
|
1299
|
+
console.log(pc8.cyan(" 3. npx beech seed:load"));
|
|
1300
|
+
console.log(pc8.dim(" \u2192 sync remote schema\n"));
|
|
1250
1301
|
}
|
|
1251
1302
|
|
|
1252
1303
|
// src/commands/reset.ts
|
|
1253
|
-
import
|
|
1304
|
+
import pc11 from "picocolors";
|
|
1305
|
+
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
1306
|
+
|
|
1307
|
+
// src/commands/db-reset.ts
|
|
1308
|
+
import pc9 from "picocolors";
|
|
1254
1309
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1255
1310
|
import { existsSync as existsSync4, readFileSync as readFileSync5, rmSync as rmSync2 } from "node:fs";
|
|
1256
1311
|
import { resolve as resolve4 } from "node:path";
|
|
1257
|
-
|
|
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";
|
|
1258
1377
|
function isDockerInstalled() {
|
|
1259
1378
|
try {
|
|
1260
|
-
const result =
|
|
1379
|
+
const result = spawnSync6("docker", ["--version"], { stdio: "ignore", shell: true });
|
|
1261
1380
|
return result.status === 0;
|
|
1262
1381
|
} catch {
|
|
1263
1382
|
return false;
|
|
@@ -1265,132 +1384,546 @@ function isDockerInstalled() {
|
|
|
1265
1384
|
}
|
|
1266
1385
|
function isDockerRunning() {
|
|
1267
1386
|
try {
|
|
1268
|
-
const result =
|
|
1387
|
+
const result = spawnSync6("docker", ["info"], { stdio: "ignore", shell: true });
|
|
1269
1388
|
return result.status === 0;
|
|
1270
1389
|
} catch {
|
|
1271
1390
|
return false;
|
|
1272
1391
|
}
|
|
1273
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
|
|
1274
1421
|
async function reset(args) {
|
|
1275
|
-
console.log(
|
|
1422
|
+
console.log(pc11.cyan("\n beech reset \u2014 cleanup environments\n"));
|
|
1276
1423
|
let resetDb = args.db || args.all;
|
|
1277
1424
|
let resetDocker = args.docker || args.all;
|
|
1278
1425
|
if (!args.db && !args.docker && !args.all) {
|
|
1279
|
-
if (
|
|
1426
|
+
if (args.yes) {
|
|
1427
|
+
resetDb = true;
|
|
1428
|
+
resetDocker = true;
|
|
1429
|
+
} else if (process.stdin.isTTY) {
|
|
1280
1430
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
1281
1431
|
try {
|
|
1282
1432
|
const answer = (await rl.question(
|
|
1283
|
-
|
|
1433
|
+
pc11.cyan(" \u2192 No options provided. Would you like to reset everything (DB & Docker)? (y/N): ")
|
|
1284
1434
|
)).trim().toLowerCase();
|
|
1285
1435
|
if (answer === "y" || answer === "yes") {
|
|
1286
1436
|
resetDb = true;
|
|
1287
1437
|
resetDocker = true;
|
|
1288
1438
|
} else {
|
|
1289
|
-
console.log(
|
|
1439
|
+
console.log(pc11.dim("\n Reset cancelled. Use --db, --docker, or --all.\n"));
|
|
1290
1440
|
return;
|
|
1291
1441
|
}
|
|
1292
1442
|
} finally {
|
|
1293
1443
|
rl.close();
|
|
1294
1444
|
}
|
|
1295
1445
|
} else {
|
|
1296
|
-
console.log(
|
|
1446
|
+
console.log(pc11.red("\n \u2717 Error: Please specify what to reset using --db, --docker, or --all.\n"));
|
|
1297
1447
|
process.exit(1);
|
|
1298
1448
|
}
|
|
1299
1449
|
}
|
|
1300
|
-
const cwd = process.cwd();
|
|
1301
1450
|
if (resetDocker) {
|
|
1302
|
-
|
|
1303
|
-
console.log(pc8.red(" \u2717 Docker is not installed or not found in your PATH."));
|
|
1304
|
-
console.log(pc8.dim(" Please install Docker to reset Docker containers and volumes.\n"));
|
|
1305
|
-
if (!args.all) {
|
|
1306
|
-
process.exit(1);
|
|
1307
|
-
}
|
|
1308
|
-
} else if (!isDockerRunning()) {
|
|
1309
|
-
console.log(pc8.yellow(" \u26A0 Docker is installed, but the Docker daemon is NOT running."));
|
|
1310
|
-
console.log(pc8.dim(" Please start Docker Desktop or your Docker daemon to reset containers.\n"));
|
|
1311
|
-
if (!args.all) {
|
|
1312
|
-
process.exit(1);
|
|
1313
|
-
}
|
|
1314
|
-
} else {
|
|
1315
|
-
console.log(pc8.dim(" Resetting Docker containers and volumes\u2026\n"));
|
|
1316
|
-
const result = spawnSync5("docker", ["compose", "down", "-v"], {
|
|
1317
|
-
stdio: "inherit",
|
|
1318
|
-
cwd,
|
|
1319
|
-
shell: true
|
|
1320
|
-
});
|
|
1321
|
-
if (result.status === 0) {
|
|
1322
|
-
console.log(pc8.green("\n \u2713 Docker containers stopped and volumes removed."));
|
|
1323
|
-
} else {
|
|
1324
|
-
console.log(pc8.red("\n \u2717 Docker reset failed."));
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1451
|
+
await devReset();
|
|
1327
1452
|
}
|
|
1328
1453
|
if (resetDb) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1454
|
+
await dbReset({});
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/commands/generate-types.ts
|
|
1459
|
+
init_wrangler();
|
|
1460
|
+
import { writeFileSync as writeFileSync4, mkdirSync } from "node:fs";
|
|
1461
|
+
import { dirname, resolve as resolve5 } from "node:path";
|
|
1462
|
+
import pc12 from "picocolors";
|
|
1463
|
+
import { generateSeedTypes } from "@beechcms/core";
|
|
1464
|
+
function loadSeedsFromD1(db) {
|
|
1465
|
+
const configPath = findWranglerConfig();
|
|
1466
|
+
const options = { db, local: false, configPath };
|
|
1467
|
+
const rows = queryD1(
|
|
1468
|
+
`SELECT slug, definition FROM seeds WHERE status = 'active';`,
|
|
1469
|
+
options
|
|
1470
|
+
);
|
|
1471
|
+
return rows.map((r) => JSON.parse(r.definition));
|
|
1472
|
+
}
|
|
1473
|
+
async function generateTypes(args) {
|
|
1474
|
+
let seeds;
|
|
1475
|
+
if (args.local) {
|
|
1476
|
+
const registry = args.registry ?? {};
|
|
1477
|
+
if (Object.keys(registry).length === 0) {
|
|
1478
|
+
console.log(pc12.red("\n \u2717 No seeds found (seeds.ts empty or missing).\n"));
|
|
1479
|
+
process.exit(1);
|
|
1480
|
+
}
|
|
1481
|
+
seeds = Object.values(registry);
|
|
1482
|
+
} else {
|
|
1483
|
+
const db = args.db ?? resolveDbName(findWranglerConfig());
|
|
1484
|
+
seeds = loadSeedsFromD1(db);
|
|
1485
|
+
if (seeds.length === 0) {
|
|
1486
|
+
console.log(pc12.red(`
|
|
1487
|
+
\u2717 No active seeds in D1 (${db}). Run \`beech seed:load\` first.
|
|
1488
|
+
`));
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const code = generateSeedTypes(seeds);
|
|
1493
|
+
const outPath = resolve5(process.cwd(), args.out);
|
|
1494
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
1495
|
+
writeFileSync4(outPath, code, "utf-8");
|
|
1496
|
+
console.log(pc12.green(`
|
|
1497
|
+
\u2713 Generated ${seeds.length} interface(s) \u2192 ${args.out}
|
|
1498
|
+
`));
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/commands/schema-diff.ts
|
|
1502
|
+
init_wrangler();
|
|
1503
|
+
import pc13 from "picocolors";
|
|
1504
|
+
import { resolve as resolve6 } from "node:path";
|
|
1505
|
+
import { SEED_REGISTRY as SEED_REGISTRY3, sortSeedsByDependencies as sortSeedsByDependencies2 } from "@beechcms/core";
|
|
1506
|
+
|
|
1507
|
+
// src/lib/migration-writer.ts
|
|
1508
|
+
import { readdirSync, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "node:fs";
|
|
1509
|
+
import { join as join2 } from "node:path";
|
|
1510
|
+
import {
|
|
1511
|
+
generateAddColumn,
|
|
1512
|
+
generateIndexes as generateIndexes2,
|
|
1513
|
+
planCreateSeed
|
|
1514
|
+
} from "@beechcms/core";
|
|
1515
|
+
var DESTRUCTIVE = /* @__PURE__ */ new Set([
|
|
1516
|
+
"extra",
|
|
1517
|
+
"type_mismatch",
|
|
1518
|
+
"fk_mismatch"
|
|
1519
|
+
]);
|
|
1520
|
+
function nextMigrationIndex(migrationsDir) {
|
|
1521
|
+
if (!existsSync5(migrationsDir)) return "0000";
|
|
1522
|
+
let max = -1;
|
|
1523
|
+
for (const f of readdirSync(migrationsDir)) {
|
|
1524
|
+
const m = /^(\d{4})_/.exec(f);
|
|
1525
|
+
if (m) max = Math.max(max, Number(m[1]));
|
|
1526
|
+
}
|
|
1527
|
+
return String(max + 1).padStart(4, "0");
|
|
1528
|
+
}
|
|
1529
|
+
function buildMigrationSql(diffs, registry) {
|
|
1530
|
+
const lines = [];
|
|
1531
|
+
let additiveCount = 0;
|
|
1532
|
+
const destructiveSlugs = [];
|
|
1533
|
+
for (const diff of diffs) {
|
|
1534
|
+
const seed = registry[diff.slug];
|
|
1535
|
+
if (!seed) continue;
|
|
1536
|
+
if (!diff.tableExists) {
|
|
1537
|
+
lines.push(`-- ${diff.slug}: create table from scratch`);
|
|
1538
|
+
for (const stmt of planCreateSeed(seed)) {
|
|
1539
|
+
lines.push(stmt);
|
|
1540
|
+
additiveCount++;
|
|
1541
|
+
}
|
|
1542
|
+
lines.push("");
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
const missing = diff.columns.filter((c) => c.status === "missing");
|
|
1546
|
+
const idxMissing = diff.columns.filter((c) => c.status === "index_missing");
|
|
1547
|
+
const destructive = diff.columns.filter((c) => DESTRUCTIVE.has(c.status));
|
|
1548
|
+
if (missing.length || idxMissing.length) {
|
|
1549
|
+
lines.push(`-- ${diff.slug}: additive changes`);
|
|
1550
|
+
for (const col of missing) {
|
|
1551
|
+
const branch = seed.branches.find((b) => b.alias === col.name);
|
|
1552
|
+
if (branch) {
|
|
1553
|
+
lines.push(generateAddColumn(seed, branch));
|
|
1554
|
+
additiveCount++;
|
|
1353
1555
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
});
|
|
1360
|
-
dbResetSuccess = result.status === 0;
|
|
1361
|
-
} else {
|
|
1362
|
-
console.log(pc8.yellow(" \u26A0 Could not find database reset script."));
|
|
1363
|
-
const initResult = spawnSync5("npx", ["beech", "init", "--db", "--local"], {
|
|
1364
|
-
stdio: "inherit",
|
|
1365
|
-
cwd,
|
|
1366
|
-
shell: true
|
|
1367
|
-
});
|
|
1368
|
-
dbResetSuccess = initResult.status === 0;
|
|
1556
|
+
}
|
|
1557
|
+
if (idxMissing.length) {
|
|
1558
|
+
for (const stmt of generateIndexes2(seed)) {
|
|
1559
|
+
lines.push(stmt);
|
|
1560
|
+
additiveCount++;
|
|
1369
1561
|
}
|
|
1370
1562
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1563
|
+
lines.push("");
|
|
1564
|
+
}
|
|
1565
|
+
if (destructive.length) {
|
|
1566
|
+
destructiveSlugs.push(diff.slug);
|
|
1567
|
+
lines.push(`-- \u26A0 ${diff.slug}: DESTRUCTIVE drift NOT auto-migrated \u2014 review manually:`);
|
|
1568
|
+
for (const col of destructive) {
|
|
1569
|
+
lines.push(`-- ${col.status}: ${col.name}` + (col.actualType ? ` (db: ${col.actualType})` : ""));
|
|
1376
1570
|
}
|
|
1377
|
-
|
|
1571
|
+
lines.push("");
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return { sql: lines.join("\n").trimEnd() + "\n", additiveCount, destructiveSlugs };
|
|
1575
|
+
}
|
|
1576
|
+
function writeMigrationFile(migrationsDir, index, name, sql) {
|
|
1577
|
+
if (!existsSync5(migrationsDir)) mkdirSync2(migrationsDir, { recursive: true });
|
|
1578
|
+
const safe = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "schema_sync";
|
|
1579
|
+
const file = join2(migrationsDir, `${index}_${safe}.sql`);
|
|
1580
|
+
writeFileSync5(file, sql, "utf-8");
|
|
1581
|
+
return file;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/commands/schema-diff.ts
|
|
1585
|
+
function resolveMigrationsDir(override) {
|
|
1586
|
+
if (override) return resolve6(process.cwd(), override);
|
|
1587
|
+
return resolve6(process.cwd(), "apps", "api", "migrations");
|
|
1588
|
+
}
|
|
1589
|
+
async function schemaDiff(args) {
|
|
1590
|
+
const registry = args.registry ?? SEED_REGISTRY3;
|
|
1591
|
+
if (Object.keys(registry).length === 0) {
|
|
1592
|
+
console.log(pc13.yellow("\n \u2717 No seeds found \u2014 nothing to diff.\n"));
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const configPath = findWranglerConfig();
|
|
1596
|
+
const options = { db: args.db ?? resolveDbName(configPath), local: args.local, configPath };
|
|
1597
|
+
const seeds = sortSeedsByDependencies2(Object.values(registry));
|
|
1598
|
+
console.log(pc13.cyan(`
|
|
1599
|
+
Diffing schema vs ${args.local ? "local" : "remote"} D1 (${options.db})\u2026
|
|
1600
|
+
`));
|
|
1601
|
+
const diffs = [];
|
|
1602
|
+
let clean = true;
|
|
1603
|
+
for (const seed of seeds) {
|
|
1604
|
+
const d = await diffSeed(seed, options);
|
|
1605
|
+
diffs.push(d);
|
|
1606
|
+
renderSeedDiff(d);
|
|
1607
|
+
if (!isSeedClean(d)) clean = false;
|
|
1608
|
+
}
|
|
1609
|
+
if (clean) {
|
|
1610
|
+
console.log(pc13.green("\n Schema matches seeds. No migration needed.\n"));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const plan = buildMigrationSql(diffs, registry);
|
|
1614
|
+
if (!args.write) {
|
|
1615
|
+
console.log(pc13.dim("\n -- proposed additive migration (preview):\n"));
|
|
1616
|
+
console.log(plan.sql);
|
|
1617
|
+
if (plan.destructiveSlugs.length) {
|
|
1618
|
+
console.log(pc13.yellow(`
|
|
1619
|
+
\u26A0 Destructive drift in: ${plan.destructiveSlugs.join(", ")} \u2014 not auto-migrated.`));
|
|
1378
1620
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1621
|
+
console.log(pc13.cyan("\n \u2192 Re-run with --write to save the migration file.\n"));
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
if (plan.additiveCount === 0) {
|
|
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"));
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const dir = resolveMigrationsDir(args.migrationsDir);
|
|
1630
|
+
const index = nextMigrationIndex(dir);
|
|
1631
|
+
const file = writeMigrationFile(dir, index, args.name ?? "schema_sync", plan.sql);
|
|
1632
|
+
console.log(pc13.green(`
|
|
1633
|
+
\u2713 Wrote ${file} (${plan.additiveCount} statement(s)).`));
|
|
1634
|
+
console.log(pc13.dim(" Review, commit, then `wrangler d1 migrations apply --remote` in CI.\n"));
|
|
1635
|
+
if (plan.destructiveSlugs.length) {
|
|
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
|
|
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"];
|
|
1381
1859
|
} else {
|
|
1382
|
-
console.log(
|
|
1860
|
+
console.log(pc20.red(" \u2717 Coverage diff script not found (scripts/test-coverage-diff.mjs)."));
|
|
1861
|
+
process.exit(1);
|
|
1862
|
+
return;
|
|
1383
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);
|
|
1384
1905
|
}
|
|
1385
|
-
console.log(pc8.dim("\n Reset process finished.\n"));
|
|
1386
1906
|
}
|
|
1387
1907
|
export {
|
|
1908
|
+
dbMigrate,
|
|
1909
|
+
dbReset,
|
|
1388
1910
|
deploy,
|
|
1911
|
+
dev,
|
|
1912
|
+
devReset,
|
|
1913
|
+
devStop,
|
|
1914
|
+
devTunnel,
|
|
1915
|
+
doctor,
|
|
1916
|
+
generateTypes,
|
|
1389
1917
|
init,
|
|
1918
|
+
lint,
|
|
1919
|
+
logs,
|
|
1920
|
+
mailpitClear,
|
|
1390
1921
|
onboard,
|
|
1391
1922
|
reset,
|
|
1923
|
+
schemaDiff,
|
|
1392
1924
|
seedCreate,
|
|
1393
1925
|
seedLoad,
|
|
1926
|
+
test,
|
|
1394
1927
|
update,
|
|
1395
1928
|
validate,
|
|
1396
1929
|
validateSeeds
|