@beechcms/cli 0.6.0-preview.1 → 0.6.0-preview.2
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/coverage/index.html +21 -21
- package/coverage/lcov-report/index.html +21 -21
- package/coverage/lcov-report/validate.ts.html +99 -402
- package/coverage/lcov.info +76 -176
- package/coverage/validate.ts.html +99 -402
- package/dist/commands/seed-load.d.ts +8 -0
- package/dist/commands/seed-load.d.ts.map +1 -0
- package/dist/commands/seed-load.js +89 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/lib/schema-diff.d.ts +15 -0
- package/dist/lib/schema-diff.d.ts.map +1 -0
- package/dist/lib/schema-diff.js +37 -0
- package/dist/lib/wrangler.d.ts +17 -0
- package/dist/lib/wrangler.d.ts.map +1 -0
- package/dist/lib/wrangler.js +65 -0
- package/package.json +2 -2
- package/src/commands/deploy.ts +126 -126
- package/src/commands/init.ts +599 -599
- package/src/commands/onboard.ts +32 -32
- package/src/commands/seed-create.ts +192 -192
- package/src/commands/seed-load.ts +235 -235
- package/src/commands/update.ts +54 -54
- package/src/commands/validate.ts +80 -80
- package/src/index.ts +17 -17
- package/src/lib/schema-diff.ts +150 -150
- package/src/lib/wrangler.ts +129 -129
- package/tsconfig.json +16 -16
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +33 -32
|
@@ -1,235 +1,235 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
|
|
4
|
-
import pc from 'picocolors'
|
|
5
|
-
import {
|
|
6
|
-
SEED_REGISTRY,
|
|
7
|
-
generateCreateTable,
|
|
8
|
-
generateDraftTable,
|
|
9
|
-
generateIndexes,
|
|
10
|
-
generateFtsTable,
|
|
11
|
-
generateFtsTriggers,
|
|
12
|
-
generateJunctionTable,
|
|
13
|
-
generateJunctionIndexes,
|
|
14
|
-
generateJunctionDraftTable,
|
|
15
|
-
sortSeedsByDependencies,
|
|
16
|
-
type Seed,
|
|
17
|
-
} from '@beechcms/core'
|
|
18
|
-
import { executeD1File, findWranglerConfig, resolveDbName, queryD1, sqlQuote, type WranglerOptions } from '../lib/wrangler.js'
|
|
19
|
-
import { diffSeed } from '../lib/schema-diff.js'
|
|
20
|
-
import { validateSeeds } from './validate.js'
|
|
21
|
-
|
|
22
|
-
export interface SeedLoadOptions {
|
|
23
|
-
dryRun: boolean
|
|
24
|
-
diff: boolean
|
|
25
|
-
local: boolean
|
|
26
|
-
db?: string
|
|
27
|
-
registry?: Record<string, Seed> | null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function buildSeedRegistrationSql(seed: Seed): string {
|
|
31
|
-
const json = sqlQuote(JSON.stringify(seed))
|
|
32
|
-
return [
|
|
33
|
-
`INSERT INTO seeds (slug, definition, status, source, created_at, updated_at)`,
|
|
34
|
-
`VALUES (${sqlQuote(seed.slug)}, ${json}, 'active', 'code', unixepoch(), unixepoch())`,
|
|
35
|
-
`ON CONFLICT(slug) DO UPDATE SET definition = excluded.definition, status = 'active', updated_at = excluded.updated_at;`,
|
|
36
|
-
].join('\n')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const SEED_META_BUMP_SQL =
|
|
40
|
-
`UPDATE seed_meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE id = 'registry_version';`
|
|
41
|
-
|
|
42
|
-
function buildStatements(seed: Seed): string[] {
|
|
43
|
-
const stmts: string[] = [generateCreateTable(seed), ...generateIndexes(seed)]
|
|
44
|
-
|
|
45
|
-
const draft = generateDraftTable(seed)
|
|
46
|
-
if (draft) stmts.push(draft)
|
|
47
|
-
|
|
48
|
-
const fts = generateFtsTable(seed)
|
|
49
|
-
if (fts) {
|
|
50
|
-
stmts.push(fts, ...generateFtsTriggers(seed))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Many-to-many: junction table + indexes after parent table exists (topological order
|
|
54
|
-
// from sortSeedsByDependencies guarantees the target table also exists at this point).
|
|
55
|
-
for (const branch of seed.branches) {
|
|
56
|
-
if (branch.type !== 'relation' || branch.multiple !== true) continue
|
|
57
|
-
stmts.push(generateJunctionTable(seed, branch), ...generateJunctionIndexes(seed, branch))
|
|
58
|
-
const draftJunction = generateJunctionDraftTable(seed, branch)
|
|
59
|
-
if (draftJunction) stmts.push(draftJunction)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return stmts
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function runDiff(options: WranglerOptions, registry: Record<string, Seed>): Promise<void> {
|
|
66
|
-
const seeds = sortSeedsByDependencies(Object.values(registry))
|
|
67
|
-
console.log(pc.cyan('\n Diffing schema…\n'))
|
|
68
|
-
|
|
69
|
-
let allOk = true
|
|
70
|
-
for (const seed of seeds) {
|
|
71
|
-
const result = await diffSeed(seed, options)
|
|
72
|
-
const tableName = `content_${seed.slug}`
|
|
73
|
-
|
|
74
|
-
if (!result.tableExists) {
|
|
75
|
-
console.log(pc.red(` ✗ ${tableName} — table missing`))
|
|
76
|
-
allOk = false
|
|
77
|
-
continue
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const problems = result.columns.filter(c => c.status !== 'ok')
|
|
81
|
-
if (problems.length === 0) {
|
|
82
|
-
console.log(pc.green(` ✓ ${tableName}`))
|
|
83
|
-
continue
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
allOk = false
|
|
87
|
-
console.log(pc.yellow(` ⚠ ${tableName}`))
|
|
88
|
-
for (const col of problems) {
|
|
89
|
-
if (col.status === 'missing') {
|
|
90
|
-
console.log(pc.red(` + missing column: ${col.name} ${col.expectedType}`))
|
|
91
|
-
} else if (col.status === 'extra') {
|
|
92
|
-
console.log(pc.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) — exists in DB but not in seeds.ts`))
|
|
93
|
-
} else if (col.status === 'type_mismatch') {
|
|
94
|
-
console.log(pc.red(` ≠ type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`))
|
|
95
|
-
} else if (col.status === 'fk_missing') {
|
|
96
|
-
console.log(pc.red(` ⤬ missing FK: ${col.name} → content_${col.expectedTarget}(id)`))
|
|
97
|
-
} else if (col.status === 'fk_mismatch') {
|
|
98
|
-
console.log(pc.yellow(` ⤬ FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`))
|
|
99
|
-
} else if (col.status === 'index_missing') {
|
|
100
|
-
console.log(pc.yellow(` ⊘ missing index on ${col.name}`))
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
console.log('')
|
|
106
|
-
if (allOk) {
|
|
107
|
-
console.log(pc.green(' Schema matches seeds. No action needed.\n'))
|
|
108
|
-
} else {
|
|
109
|
-
console.log(pc.yellow(' Run `beech seed:load` to apply missing tables/columns.\n'))
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function runLoad(options: WranglerOptions, dryRun: boolean, registry: Record<string, Seed>): Promise<void> {
|
|
114
|
-
const seeds = sortSeedsByDependencies(Object.values(registry))
|
|
115
|
-
|
|
116
|
-
if (dryRun) {
|
|
117
|
-
console.log(pc.cyan('\n -- dry-run: SQL that would be executed\n'))
|
|
118
|
-
for (const seed of seeds) {
|
|
119
|
-
const stmts = buildStatements(seed)
|
|
120
|
-
console.log(pc.dim(` -- content_${seed.slug}`))
|
|
121
|
-
for (const stmt of stmts) {
|
|
122
|
-
console.log(stmt + '\n')
|
|
123
|
-
}
|
|
124
|
-
console.log(pc.dim(` -- register ${seed.slug} in seeds table`))
|
|
125
|
-
console.log(buildSeedRegistrationSql(seed) + '\n')
|
|
126
|
-
}
|
|
127
|
-
console.log(pc.dim(' -- bump registry_version'))
|
|
128
|
-
console.log(SEED_META_BUMP_SQL + '\n')
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
console.log(pc.cyan(`\n Loading seeds into ${options.local ? 'local' : 'remote'} D1 (${options.db})…\n`))
|
|
133
|
-
|
|
134
|
-
for (const seed of seeds) {
|
|
135
|
-
const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)]
|
|
136
|
-
const sql = stmts.join('\n\n') + '\n'
|
|
137
|
-
process.stdout.write(` ${pc.dim('→')} content_${seed.slug}… `)
|
|
138
|
-
const ok = executeD1File(sql, options)
|
|
139
|
-
if (!ok) {
|
|
140
|
-
console.log(pc.red('failed'))
|
|
141
|
-
console.log(pc.red(`\n ✗ Failed to apply schema for content_${seed.slug}\n`))
|
|
142
|
-
console.log(pc.dim(' wrangler reported an error above.'))
|
|
143
|
-
console.log(pc.dim(` Most likely causes:`))
|
|
144
|
-
console.log(pc.dim(` - Database "${options.db}" not found or wrong database_id`))
|
|
145
|
-
if (!options.local) {
|
|
146
|
-
console.log(pc.dim(' - Not logged in to Cloudflare'))
|
|
147
|
-
console.log(pc.cyan('\n → Run: npx wrangler login'))
|
|
148
|
-
console.log(pc.cyan(' → Then: npx beech seed:load\n'))
|
|
149
|
-
} else {
|
|
150
|
-
console.log(pc.cyan('\n → Run: npx beech init --db --local # re-initialise local DB'))
|
|
151
|
-
console.log(pc.cyan(' → Then: npx beech seed:load --local\n'))
|
|
152
|
-
}
|
|
153
|
-
process.exit(1)
|
|
154
|
-
}
|
|
155
|
-
console.log(pc.green('done'))
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Bump registry_version so live isolates re-hydrate
|
|
159
|
-
executeD1File(SEED_META_BUMP_SQL, options)
|
|
160
|
-
|
|
161
|
-
console.log(pc.green('\n All seeds loaded.\n'))
|
|
162
|
-
console.log(pc.dim(' Definitions registered in the database.'))
|
|
163
|
-
console.log(pc.dim(' seed.ts is no longer required at runtime — you may keep it for code-first edits or delete it.\n'))
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export async function seedLoad(args: SeedLoadOptions): Promise<void> {
|
|
167
|
-
const registry = args.registry ?? SEED_REGISTRY
|
|
168
|
-
|
|
169
|
-
if (Object.keys(registry).length === 0) {
|
|
170
|
-
console.log(pc.yellow('\n ✗ No seeds found\n'))
|
|
171
|
-
console.log(pc.dim(' Create a seeds.ts file in your project root with at least one content type.'))
|
|
172
|
-
console.log(pc.cyan('\n → Run: npx beech seed:create\n'))
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const validationErrors = validateSeeds(registry)
|
|
177
|
-
const fatalErrors = validationErrors.filter(e => e.fatal)
|
|
178
|
-
const warnings = validationErrors.filter(e => !e.fatal)
|
|
179
|
-
|
|
180
|
-
if (fatalErrors.length > 0) {
|
|
181
|
-
const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0)
|
|
182
|
-
const s = total !== 1 ? 's' : ''
|
|
183
|
-
console.log(pc.red(`\n ✗ Seed validation found ${total} fatal error${s}. Cannot load schema.\n`))
|
|
184
|
-
for (const e of fatalErrors) {
|
|
185
|
-
console.log(pc.red(` ✗ ${e.slug}`))
|
|
186
|
-
for (const msg of e.messages) {
|
|
187
|
-
console.log(pc.red(` → ${msg}`))
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
console.log('')
|
|
191
|
-
process.exit(1)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (warnings.length > 0) {
|
|
195
|
-
const total = warnings.reduce((n, e) => n + e.messages.length, 0)
|
|
196
|
-
const s = total !== 1 ? 's' : ''
|
|
197
|
-
console.log(pc.yellow(`\n ⚠ Seed validation found ${total} issue${s}. Schema changes will still be applied.\n`))
|
|
198
|
-
console.log(pc.dim(' Run "npx beech validate" for details.\n'))
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const configPath = findWranglerConfig()
|
|
202
|
-
const db = args.db ?? resolveDbName(configPath)
|
|
203
|
-
|
|
204
|
-
const options: WranglerOptions = {
|
|
205
|
-
db,
|
|
206
|
-
local: args.local,
|
|
207
|
-
configPath,
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!args.dryRun && !args.diff) {
|
|
211
|
-
try {
|
|
212
|
-
const rows = queryD1<{ name: string }>(
|
|
213
|
-
`SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
|
|
214
|
-
options
|
|
215
|
-
)
|
|
216
|
-
if (rows.length < 2) {
|
|
217
|
-
console.log(pc.red('\n ✗ System tables not found (seeds, seed_meta)\n'))
|
|
218
|
-
console.log(pc.dim(' Run `beech init --db` first to initialise the database.'))
|
|
219
|
-
const flag = args.local ? ' --local' : ''
|
|
220
|
-
console.log(pc.cyan(`\n → Run: npx beech init --db${flag}\n`))
|
|
221
|
-
process.exit(1)
|
|
222
|
-
}
|
|
223
|
-
} catch {
|
|
224
|
-
console.log(pc.red('\n ✗ Could not query the database\n'))
|
|
225
|
-
console.log(pc.dim(' Run `beech init --db` first to initialise the database.'))
|
|
226
|
-
process.exit(1)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (args.diff) {
|
|
231
|
-
await runDiff(options, registry)
|
|
232
|
-
} else {
|
|
233
|
-
await runLoad(options, args.dryRun, registry)
|
|
234
|
-
}
|
|
235
|
-
}
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
|
|
4
|
+
import pc from 'picocolors'
|
|
5
|
+
import {
|
|
6
|
+
SEED_REGISTRY,
|
|
7
|
+
generateCreateTable,
|
|
8
|
+
generateDraftTable,
|
|
9
|
+
generateIndexes,
|
|
10
|
+
generateFtsTable,
|
|
11
|
+
generateFtsTriggers,
|
|
12
|
+
generateJunctionTable,
|
|
13
|
+
generateJunctionIndexes,
|
|
14
|
+
generateJunctionDraftTable,
|
|
15
|
+
sortSeedsByDependencies,
|
|
16
|
+
type Seed,
|
|
17
|
+
} from '@beechcms/core'
|
|
18
|
+
import { executeD1File, findWranglerConfig, resolveDbName, queryD1, sqlQuote, type WranglerOptions } from '../lib/wrangler.js'
|
|
19
|
+
import { diffSeed } from '../lib/schema-diff.js'
|
|
20
|
+
import { validateSeeds } from './validate.js'
|
|
21
|
+
|
|
22
|
+
export interface SeedLoadOptions {
|
|
23
|
+
dryRun: boolean
|
|
24
|
+
diff: boolean
|
|
25
|
+
local: boolean
|
|
26
|
+
db?: string
|
|
27
|
+
registry?: Record<string, Seed> | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildSeedRegistrationSql(seed: Seed): string {
|
|
31
|
+
const json = sqlQuote(JSON.stringify(seed))
|
|
32
|
+
return [
|
|
33
|
+
`INSERT INTO seeds (slug, definition, status, source, created_at, updated_at)`,
|
|
34
|
+
`VALUES (${sqlQuote(seed.slug)}, ${json}, 'active', 'code', unixepoch(), unixepoch())`,
|
|
35
|
+
`ON CONFLICT(slug) DO UPDATE SET definition = excluded.definition, status = 'active', updated_at = excluded.updated_at;`,
|
|
36
|
+
].join('\n')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SEED_META_BUMP_SQL =
|
|
40
|
+
`UPDATE seed_meta SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT) WHERE id = 'registry_version';`
|
|
41
|
+
|
|
42
|
+
function buildStatements(seed: Seed): string[] {
|
|
43
|
+
const stmts: string[] = [generateCreateTable(seed), ...generateIndexes(seed)]
|
|
44
|
+
|
|
45
|
+
const draft = generateDraftTable(seed)
|
|
46
|
+
if (draft) stmts.push(draft)
|
|
47
|
+
|
|
48
|
+
const fts = generateFtsTable(seed)
|
|
49
|
+
if (fts) {
|
|
50
|
+
stmts.push(fts, ...generateFtsTriggers(seed))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Many-to-many: junction table + indexes after parent table exists (topological order
|
|
54
|
+
// from sortSeedsByDependencies guarantees the target table also exists at this point).
|
|
55
|
+
for (const branch of seed.branches) {
|
|
56
|
+
if (branch.type !== 'relation' || branch.multiple !== true) continue
|
|
57
|
+
stmts.push(generateJunctionTable(seed, branch), ...generateJunctionIndexes(seed, branch))
|
|
58
|
+
const draftJunction = generateJunctionDraftTable(seed, branch)
|
|
59
|
+
if (draftJunction) stmts.push(draftJunction)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return stmts
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function runDiff(options: WranglerOptions, registry: Record<string, Seed>): Promise<void> {
|
|
66
|
+
const seeds = sortSeedsByDependencies(Object.values(registry))
|
|
67
|
+
console.log(pc.cyan('\n Diffing schema…\n'))
|
|
68
|
+
|
|
69
|
+
let allOk = true
|
|
70
|
+
for (const seed of seeds) {
|
|
71
|
+
const result = await diffSeed(seed, options)
|
|
72
|
+
const tableName = `content_${seed.slug}`
|
|
73
|
+
|
|
74
|
+
if (!result.tableExists) {
|
|
75
|
+
console.log(pc.red(` ✗ ${tableName} — table missing`))
|
|
76
|
+
allOk = false
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const problems = result.columns.filter(c => c.status !== 'ok')
|
|
81
|
+
if (problems.length === 0) {
|
|
82
|
+
console.log(pc.green(` ✓ ${tableName}`))
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
allOk = false
|
|
87
|
+
console.log(pc.yellow(` ⚠ ${tableName}`))
|
|
88
|
+
for (const col of problems) {
|
|
89
|
+
if (col.status === 'missing') {
|
|
90
|
+
console.log(pc.red(` + missing column: ${col.name} ${col.expectedType}`))
|
|
91
|
+
} else if (col.status === 'extra') {
|
|
92
|
+
console.log(pc.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) — exists in DB but not in seeds.ts`))
|
|
93
|
+
} else if (col.status === 'type_mismatch') {
|
|
94
|
+
console.log(pc.red(` ≠ type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`))
|
|
95
|
+
} else if (col.status === 'fk_missing') {
|
|
96
|
+
console.log(pc.red(` ⤬ missing FK: ${col.name} → content_${col.expectedTarget}(id)`))
|
|
97
|
+
} else if (col.status === 'fk_mismatch') {
|
|
98
|
+
console.log(pc.yellow(` ⤬ FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`))
|
|
99
|
+
} else if (col.status === 'index_missing') {
|
|
100
|
+
console.log(pc.yellow(` ⊘ missing index on ${col.name}`))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('')
|
|
106
|
+
if (allOk) {
|
|
107
|
+
console.log(pc.green(' Schema matches seeds. No action needed.\n'))
|
|
108
|
+
} else {
|
|
109
|
+
console.log(pc.yellow(' Run `beech seed:load` to apply missing tables/columns.\n'))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runLoad(options: WranglerOptions, dryRun: boolean, registry: Record<string, Seed>): Promise<void> {
|
|
114
|
+
const seeds = sortSeedsByDependencies(Object.values(registry))
|
|
115
|
+
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
console.log(pc.cyan('\n -- dry-run: SQL that would be executed\n'))
|
|
118
|
+
for (const seed of seeds) {
|
|
119
|
+
const stmts = buildStatements(seed)
|
|
120
|
+
console.log(pc.dim(` -- content_${seed.slug}`))
|
|
121
|
+
for (const stmt of stmts) {
|
|
122
|
+
console.log(stmt + '\n')
|
|
123
|
+
}
|
|
124
|
+
console.log(pc.dim(` -- register ${seed.slug} in seeds table`))
|
|
125
|
+
console.log(buildSeedRegistrationSql(seed) + '\n')
|
|
126
|
+
}
|
|
127
|
+
console.log(pc.dim(' -- bump registry_version'))
|
|
128
|
+
console.log(SEED_META_BUMP_SQL + '\n')
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(pc.cyan(`\n Loading seeds into ${options.local ? 'local' : 'remote'} D1 (${options.db})…\n`))
|
|
133
|
+
|
|
134
|
+
for (const seed of seeds) {
|
|
135
|
+
const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)]
|
|
136
|
+
const sql = stmts.join('\n\n') + '\n'
|
|
137
|
+
process.stdout.write(` ${pc.dim('→')} content_${seed.slug}… `)
|
|
138
|
+
const ok = executeD1File(sql, options)
|
|
139
|
+
if (!ok) {
|
|
140
|
+
console.log(pc.red('failed'))
|
|
141
|
+
console.log(pc.red(`\n ✗ Failed to apply schema for content_${seed.slug}\n`))
|
|
142
|
+
console.log(pc.dim(' wrangler reported an error above.'))
|
|
143
|
+
console.log(pc.dim(` Most likely causes:`))
|
|
144
|
+
console.log(pc.dim(` - Database "${options.db}" not found or wrong database_id`))
|
|
145
|
+
if (!options.local) {
|
|
146
|
+
console.log(pc.dim(' - Not logged in to Cloudflare'))
|
|
147
|
+
console.log(pc.cyan('\n → Run: npx wrangler login'))
|
|
148
|
+
console.log(pc.cyan(' → Then: npx beech seed:load\n'))
|
|
149
|
+
} else {
|
|
150
|
+
console.log(pc.cyan('\n → Run: npx beech init --db --local # re-initialise local DB'))
|
|
151
|
+
console.log(pc.cyan(' → Then: npx beech seed:load --local\n'))
|
|
152
|
+
}
|
|
153
|
+
process.exit(1)
|
|
154
|
+
}
|
|
155
|
+
console.log(pc.green('done'))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Bump registry_version so live isolates re-hydrate
|
|
159
|
+
executeD1File(SEED_META_BUMP_SQL, options)
|
|
160
|
+
|
|
161
|
+
console.log(pc.green('\n All seeds loaded.\n'))
|
|
162
|
+
console.log(pc.dim(' Definitions registered in the database.'))
|
|
163
|
+
console.log(pc.dim(' seed.ts is no longer required at runtime — you may keep it for code-first edits or delete it.\n'))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function seedLoad(args: SeedLoadOptions): Promise<void> {
|
|
167
|
+
const registry = args.registry ?? SEED_REGISTRY
|
|
168
|
+
|
|
169
|
+
if (Object.keys(registry).length === 0) {
|
|
170
|
+
console.log(pc.yellow('\n ✗ No seeds found\n'))
|
|
171
|
+
console.log(pc.dim(' Create a seeds.ts file in your project root with at least one content type.'))
|
|
172
|
+
console.log(pc.cyan('\n → Run: npx beech seed:create\n'))
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const validationErrors = validateSeeds(registry)
|
|
177
|
+
const fatalErrors = validationErrors.filter(e => e.fatal)
|
|
178
|
+
const warnings = validationErrors.filter(e => !e.fatal)
|
|
179
|
+
|
|
180
|
+
if (fatalErrors.length > 0) {
|
|
181
|
+
const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0)
|
|
182
|
+
const s = total !== 1 ? 's' : ''
|
|
183
|
+
console.log(pc.red(`\n ✗ Seed validation found ${total} fatal error${s}. Cannot load schema.\n`))
|
|
184
|
+
for (const e of fatalErrors) {
|
|
185
|
+
console.log(pc.red(` ✗ ${e.slug}`))
|
|
186
|
+
for (const msg of e.messages) {
|
|
187
|
+
console.log(pc.red(` → ${msg}`))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
console.log('')
|
|
191
|
+
process.exit(1)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (warnings.length > 0) {
|
|
195
|
+
const total = warnings.reduce((n, e) => n + e.messages.length, 0)
|
|
196
|
+
const s = total !== 1 ? 's' : ''
|
|
197
|
+
console.log(pc.yellow(`\n ⚠ Seed validation found ${total} issue${s}. Schema changes will still be applied.\n`))
|
|
198
|
+
console.log(pc.dim(' Run "npx beech validate" for details.\n'))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const configPath = findWranglerConfig()
|
|
202
|
+
const db = args.db ?? resolveDbName(configPath)
|
|
203
|
+
|
|
204
|
+
const options: WranglerOptions = {
|
|
205
|
+
db,
|
|
206
|
+
local: args.local,
|
|
207
|
+
configPath,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!args.dryRun && !args.diff) {
|
|
211
|
+
try {
|
|
212
|
+
const rows = queryD1<{ name: string }>(
|
|
213
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
|
|
214
|
+
options
|
|
215
|
+
)
|
|
216
|
+
if (rows.length < 2) {
|
|
217
|
+
console.log(pc.red('\n ✗ System tables not found (seeds, seed_meta)\n'))
|
|
218
|
+
console.log(pc.dim(' Run `beech init --db` first to initialise the database.'))
|
|
219
|
+
const flag = args.local ? ' --local' : ''
|
|
220
|
+
console.log(pc.cyan(`\n → Run: npx beech init --db${flag}\n`))
|
|
221
|
+
process.exit(1)
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
console.log(pc.red('\n ✗ Could not query the database\n'))
|
|
225
|
+
console.log(pc.dim(' Run `beech init --db` first to initialise the database.'))
|
|
226
|
+
process.exit(1)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (args.diff) {
|
|
231
|
+
await runDiff(options, registry)
|
|
232
|
+
} else {
|
|
233
|
+
await runLoad(options, args.dryRun, registry)
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/commands/update.ts
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
|
|
4
|
-
import pc from 'picocolors'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
|
-
|
|
7
|
-
export interface UpdateOptions {}
|
|
8
|
-
|
|
9
|
-
export async function update(_args: UpdateOptions): Promise<void> {
|
|
10
|
-
console.log(pc.cyan('\n beech update\n'))
|
|
11
|
-
|
|
12
|
-
// Step 1 — install latest BeechCMS packages
|
|
13
|
-
console.log(pc.dim(' [1/2] Installing latest BeechCMS packages…\n'))
|
|
14
|
-
const installResult = spawnSync(
|
|
15
|
-
'npm',
|
|
16
|
-
['install', '@beechcms/api@latest', '@beechcms/core@latest'],
|
|
17
|
-
{ stdio: 'inherit', cwd: process.cwd(), shell: true }
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
if (installResult.status !== 0) {
|
|
21
|
-
console.log(pc.red('\n ✗ npm install failed\n'))
|
|
22
|
-
console.log(pc.dim(' Check the output above for details.'))
|
|
23
|
-
console.log(pc.dim(' You may need to resolve version conflicts manually.'))
|
|
24
|
-
console.log(pc.cyan('\n → Try: npm install --legacy-peer-deps\n'))
|
|
25
|
-
process.exit(1)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
console.log(pc.green('\n ✓ Packages updated'))
|
|
29
|
-
|
|
30
|
-
// Step 2 — apply any new system migrations to local DB
|
|
31
|
-
console.log(pc.dim('\n [2/2] Applying system migrations to local database…\n'))
|
|
32
|
-
const initResult = spawnSync(
|
|
33
|
-
'npx',
|
|
34
|
-
['beech', 'init', '--db', '--local'],
|
|
35
|
-
{ stdio: 'inherit', cwd: process.cwd(), shell: true }
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
if (initResult.status !== 0) {
|
|
39
|
-
console.log(pc.yellow('\n ⚠ Local DB update failed\n'))
|
|
40
|
-
console.log(pc.dim(' Apply system migrations manually:'))
|
|
41
|
-
console.log(pc.cyan(' → Run: npx beech init --db --local\n'))
|
|
42
|
-
} else {
|
|
43
|
-
console.log(pc.green('\n ✓ Local database updated'))
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
console.log(pc.dim('\n Local update complete.\n'))
|
|
47
|
-
console.log(pc.dim(' Next steps:'))
|
|
48
|
-
console.log(pc.cyan(' 1. npx beech seed:load --local'))
|
|
49
|
-
console.log(pc.dim(' → sync content schema to local DB'))
|
|
50
|
-
console.log(pc.cyan(' 2. npm run deploy'))
|
|
51
|
-
console.log(pc.dim(' → deploy updated API + dashboard'))
|
|
52
|
-
console.log(pc.cyan(' 3. npx beech seed:load'))
|
|
53
|
-
console.log(pc.dim(' → sync remote schema\n'))
|
|
54
|
-
}
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
|
|
4
|
+
import pc from 'picocolors'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
|
|
7
|
+
export interface UpdateOptions {}
|
|
8
|
+
|
|
9
|
+
export async function update(_args: UpdateOptions): Promise<void> {
|
|
10
|
+
console.log(pc.cyan('\n beech update\n'))
|
|
11
|
+
|
|
12
|
+
// Step 1 — install latest BeechCMS packages
|
|
13
|
+
console.log(pc.dim(' [1/2] Installing latest BeechCMS packages…\n'))
|
|
14
|
+
const installResult = spawnSync(
|
|
15
|
+
'npm',
|
|
16
|
+
['install', '@beechcms/api@latest', '@beechcms/core@latest'],
|
|
17
|
+
{ stdio: 'inherit', cwd: process.cwd(), shell: true }
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if (installResult.status !== 0) {
|
|
21
|
+
console.log(pc.red('\n ✗ npm install failed\n'))
|
|
22
|
+
console.log(pc.dim(' Check the output above for details.'))
|
|
23
|
+
console.log(pc.dim(' You may need to resolve version conflicts manually.'))
|
|
24
|
+
console.log(pc.cyan('\n → Try: npm install --legacy-peer-deps\n'))
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log(pc.green('\n ✓ Packages updated'))
|
|
29
|
+
|
|
30
|
+
// Step 2 — apply any new system migrations to local DB
|
|
31
|
+
console.log(pc.dim('\n [2/2] Applying system migrations to local database…\n'))
|
|
32
|
+
const initResult = spawnSync(
|
|
33
|
+
'npx',
|
|
34
|
+
['beech', 'init', '--db', '--local'],
|
|
35
|
+
{ stdio: 'inherit', cwd: process.cwd(), shell: true }
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (initResult.status !== 0) {
|
|
39
|
+
console.log(pc.yellow('\n ⚠ Local DB update failed\n'))
|
|
40
|
+
console.log(pc.dim(' Apply system migrations manually:'))
|
|
41
|
+
console.log(pc.cyan(' → Run: npx beech init --db --local\n'))
|
|
42
|
+
} else {
|
|
43
|
+
console.log(pc.green('\n ✓ Local database updated'))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(pc.dim('\n Local update complete.\n'))
|
|
47
|
+
console.log(pc.dim(' Next steps:'))
|
|
48
|
+
console.log(pc.cyan(' 1. npx beech seed:load --local'))
|
|
49
|
+
console.log(pc.dim(' → sync content schema to local DB'))
|
|
50
|
+
console.log(pc.cyan(' 2. npm run deploy'))
|
|
51
|
+
console.log(pc.dim(' → deploy updated API + dashboard'))
|
|
52
|
+
console.log(pc.cyan(' 3. npx beech seed:load'))
|
|
53
|
+
console.log(pc.dim(' → sync remote schema\n'))
|
|
54
|
+
}
|