@beechcms/cli 0.6.0-preview.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,157 +0,0 @@
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
- import { existsSync, readFileSync, rmSync } from 'node:fs'
7
- import { resolve } from 'node:path'
8
- import { createInterface } from 'node:readline/promises'
9
-
10
- export interface ResetOptions {
11
- db?: boolean
12
- docker?: boolean
13
- all?: boolean
14
- }
15
-
16
- function isDockerInstalled(): boolean {
17
- try {
18
- const result = spawnSync('docker', ['--version'], { stdio: 'ignore', shell: true })
19
- return result.status === 0
20
- } catch {
21
- return false
22
- }
23
- }
24
-
25
- function isDockerRunning(): boolean {
26
- try {
27
- const result = spawnSync('docker', ['info'], { stdio: 'ignore', shell: true })
28
- return result.status === 0
29
- } catch {
30
- return false
31
- }
32
- }
33
-
34
- export async function reset(args: ResetOptions): Promise<void> {
35
- console.log(pc.cyan('\n beech reset — cleanup environments\n'))
36
-
37
- let resetDb = args.db || args.all
38
- let resetDocker = args.docker || args.all
39
-
40
- if (!args.db && !args.docker && !args.all) {
41
- if (process.stdin.isTTY) {
42
- const rl = createInterface({ input: process.stdin, output: process.stdout })
43
- try {
44
- const answer = (await rl.question(
45
- pc.cyan(' → No options provided. Would you like to reset everything (DB & Docker)? (y/N): ')
46
- )).trim().toLowerCase()
47
- if (answer === 'y' || answer === 'yes') {
48
- resetDb = true
49
- resetDocker = true
50
- } else {
51
- console.log(pc.dim('\n Reset cancelled. Use --db, --docker, or --all.\n'))
52
- return
53
- }
54
- } finally {
55
- rl.close()
56
- }
57
- } else {
58
- console.log(pc.red('\n ✗ Error: Please specify what to reset using --db, --docker, or --all.\n'))
59
- process.exit(1)
60
- }
61
- }
62
-
63
- const cwd = process.cwd()
64
-
65
- // Reset Docker
66
- if (resetDocker) {
67
- if (!isDockerInstalled()) {
68
- console.log(pc.red(' ✗ Docker is not installed or not found in your PATH.'))
69
- console.log(pc.dim(' Please install Docker to reset Docker containers and volumes.\n'))
70
- if (!args.all) {
71
- process.exit(1)
72
- }
73
- } else if (!isDockerRunning()) {
74
- console.log(pc.yellow(' ⚠ Docker is installed, but the Docker daemon is NOT running.'))
75
- console.log(pc.dim(' Please start Docker Desktop or your Docker daemon to reset containers.\n'))
76
- if (!args.all) {
77
- process.exit(1)
78
- }
79
- } else {
80
- console.log(pc.dim(' Resetting Docker containers and volumes…\n'))
81
- const result = spawnSync('docker', ['compose', 'down', '-v'], {
82
- stdio: 'inherit',
83
- cwd,
84
- shell: true,
85
- })
86
-
87
- if (result.status === 0) {
88
- console.log(pc.green('\n ✓ Docker containers stopped and volumes removed.'))
89
- } else {
90
- console.log(pc.red('\n ✗ Docker reset failed.'))
91
- }
92
- }
93
- }
94
-
95
- // Reset DB
96
- if (resetDb) {
97
- console.log(pc.dim('\n Resetting local database…\n'))
98
- let dbResetSuccess = false
99
- const apiDir = resolve(cwd, 'apps', 'api')
100
-
101
- if (existsSync(resolve(apiDir, 'package.json'))) {
102
- const result = spawnSync('npm', ['run', 'db:reset:local'], {
103
- stdio: 'inherit',
104
- cwd: apiDir,
105
- shell: true,
106
- })
107
- dbResetSuccess = result.status === 0
108
- } else if (existsSync(resolve(cwd, 'package.json'))) {
109
- const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf-8'))
110
- if (pkg.scripts?.['db:reset:local']) {
111
- const result = spawnSync('npm', ['run', 'db:reset:local'], {
112
- stdio: 'inherit',
113
- cwd,
114
- shell: true,
115
- })
116
- dbResetSuccess = result.status === 0
117
- } else {
118
- const wranglerStateDir = resolve(cwd, '.wrangler/state')
119
- if (existsSync(wranglerStateDir)) {
120
- console.log(pc.dim(' Removing .wrangler/state…'))
121
- rmSync(wranglerStateDir, { recursive: true, force: true })
122
- }
123
- if (existsSync(resolve(cwd, 'scripts', 'bootstrap-d1.mjs'))) {
124
- const result = spawnSync('node', ['scripts/bootstrap-d1.mjs'], {
125
- stdio: 'inherit',
126
- cwd,
127
- shell: true,
128
- })
129
- dbResetSuccess = result.status === 0
130
- } else {
131
- console.log(pc.yellow(' ⚠ Could not find database reset script.'))
132
- const initResult = spawnSync('npx', ['beech', 'init', '--db', '--local'], {
133
- stdio: 'inherit',
134
- cwd,
135
- shell: true,
136
- })
137
- dbResetSuccess = initResult.status === 0
138
- }
139
- }
140
- } else {
141
- const wranglerStateDir = resolve(cwd, '.wrangler/state')
142
- if (existsSync(wranglerStateDir)) {
143
- console.log(pc.dim(' Removing .wrangler/state…'))
144
- rmSync(wranglerStateDir, { recursive: true, force: true })
145
- }
146
- dbResetSuccess = true
147
- }
148
-
149
- if (dbResetSuccess) {
150
- console.log(pc.green('\n ✓ Local database reset completed.'))
151
- } else {
152
- console.log(pc.red('\n ✗ Database reset failed.'))
153
- }
154
- }
155
-
156
- console.log(pc.dim('\n Reset process finished.\n'))
157
- }
@@ -1,78 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- import pc from 'picocolors'
5
- import { resolve } from 'node:path'
6
- import { SEED_REGISTRY, sortSeedsByDependencies, type Seed } from '@beechcms/core'
7
- import { findWranglerConfig, resolveDbName, type WranglerOptions } from '../lib/wrangler.js'
8
- import { diffSeed, renderSeedDiff, isSeedClean } from '../lib/schema-diff.js'
9
- import { nextMigrationIndex, buildMigrationSql, writeMigrationFile } from '../lib/migration-writer.js'
10
-
11
- export interface SchemaDiffOptions {
12
- /** Compare against remote D1 (default: local). */
13
- local: boolean
14
- /** When set, write an additive migration file instead of just printing. */
15
- write: boolean
16
- /** Optional migration name (used in the filename). */
17
- name?: string
18
- /** Override migrations dir (default: <cwd>/apps/api/migrations). */
19
- migrationsDir?: string
20
- /** Override D1 database name. */
21
- db?: string
22
- registry?: Record<string, Seed> | null
23
- }
24
-
25
- function resolveMigrationsDir(override?: string): string {
26
- if (override) return resolve(process.cwd(), override)
27
- return resolve(process.cwd(), 'apps', 'api', 'migrations')
28
- }
29
-
30
- export async function schemaDiff(args: SchemaDiffOptions): Promise<void> {
31
- const registry = args.registry ?? SEED_REGISTRY
32
- if (Object.keys(registry).length === 0) {
33
- console.log(pc.yellow('\n ✗ No seeds found — nothing to diff.\n'))
34
- return
35
- }
36
-
37
- const configPath = findWranglerConfig()
38
- const options: WranglerOptions = { db: args.db ?? resolveDbName(configPath), local: args.local, configPath }
39
- const seeds = sortSeedsByDependencies(Object.values(registry))
40
-
41
- console.log(pc.cyan(`\n Diffing schema vs ${args.local ? 'local' : 'remote'} D1 (${options.db})…\n`))
42
- const diffs = []
43
- let clean = true
44
- for (const seed of seeds) {
45
- const d = await diffSeed(seed, options)
46
- diffs.push(d)
47
- renderSeedDiff(d)
48
- if (!isSeedClean(d)) clean = false
49
- }
50
-
51
- if (clean) { console.log(pc.green('\n Schema matches seeds. No migration needed.\n')); return }
52
-
53
- const plan = buildMigrationSql(diffs, registry)
54
- if (!args.write) {
55
- console.log(pc.dim('\n -- proposed additive migration (preview):\n'))
56
- console.log(plan.sql)
57
- if (plan.destructiveSlugs.length) {
58
- console.log(pc.yellow(`\n ⚠ Destructive drift in: ${plan.destructiveSlugs.join(', ')} — not auto-migrated.`))
59
- }
60
- console.log(pc.cyan('\n → Re-run with --write to save the migration file.\n'))
61
- return
62
- }
63
-
64
- if (plan.additiveCount === 0) {
65
- console.log(pc.yellow('\n ⚠ Only destructive drift detected — no additive migration written.'))
66
- console.log(pc.dim(' Author a reviewed migration by hand for renames/drops/type changes.\n'))
67
- return
68
- }
69
-
70
- const dir = resolveMigrationsDir(args.migrationsDir)
71
- const index = nextMigrationIndex(dir)
72
- const file = writeMigrationFile(dir, index, args.name ?? 'schema_sync', plan.sql)
73
- console.log(pc.green(`\n ✓ Wrote ${file} (${plan.additiveCount} statement(s)).`))
74
- console.log(pc.dim(' Review, commit, then `wrangler d1 migrations apply --remote` in CI.\n'))
75
- if (plan.destructiveSlugs.length) {
76
- console.log(pc.yellow(` ⚠ Destructive drift in ${plan.destructiveSlugs.join(', ')} was NOT included.\n`))
77
- }
78
- }
@@ -1,192 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- import pc from 'picocolors'
5
- import { createInterface } from 'node:readline/promises'
6
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
7
- import { resolve } from 'node:path'
8
- import type { BranchType } from '@beechcms/core'
9
-
10
- export type SeedCreateOptions = Record<string, never>
11
-
12
- const BRANCH_TYPES: BranchType[] = ['text', 'number', 'boolean', 'date', 'richtext', 'file', 'tags']
13
-
14
- function slugify(str: string): string {
15
- return str
16
- .toLowerCase()
17
- .trim()
18
- .replace(/\s+/g, '-')
19
- .replace(/[^a-z0-9-]/g, '')
20
- }
21
-
22
- function toConstName(slug: string): string {
23
- return slug.replace(/-/g, '_').toUpperCase() + '_SEED'
24
- }
25
-
26
- function toLabel(alias: string): string {
27
- return alias
28
- .replace(/([A-Z])/g, ' $1')
29
- .replace(/^./, s => s.toUpperCase())
30
- .trim()
31
- }
32
-
33
- interface BranchDef {
34
- alias: string
35
- label: string
36
- type: BranchType
37
- required: boolean
38
- }
39
-
40
- function generateSeedBlock(slug: string, label: string, labelPlural: string, branches: BranchDef[]): string {
41
- const displayAlias =
42
- branches.find(b => b.type === 'text')?.alias ??
43
- branches[0]?.alias ??
44
- 'name'
45
-
46
- const cName = toConstName(slug)
47
- const branchLines = branches.map(b => {
48
- const parts: string[] = [
49
- `alias: '${b.alias}'`,
50
- `label: '${b.label}'`,
51
- `type: '${b.type}'`,
52
- ]
53
- if (b.required) parts.push('requiredOnCreate: true')
54
- return ` { ${parts.join(', ')} },`
55
- })
56
-
57
- return [
58
- '',
59
- `export const ${cName} = defineSeed({`,
60
- ` slug: '${slug}',`,
61
- ` label: '${label}',`,
62
- ` labelPlural: '${labelPlural}',`,
63
- ` displayNameAlias: '${displayAlias}',`,
64
- ' branches: [',
65
- ...branchLines,
66
- ' ],',
67
- ' dashboard: {',
68
- " icon: 'Folder',",
69
- " group: 'Content',",
70
- ' },',
71
- '})',
72
- '',
73
- ].join('\n')
74
- }
75
-
76
- function ensureDefineSeedImport(content: string): string {
77
- if (content.includes('defineSeed')) return content
78
- return `import { defineSeed } from '@beechcms/core'\n` + content
79
- }
80
-
81
- function tryInsertRegistryEntry(content: string, slug: string, cName: string): string | null {
82
- const match = content.match(/(SEED_REGISTRY[^{]*\{)([\s\S]*?)(\n\})/m)
83
- if (!match) return null
84
- const [full, open, inner, close] = match
85
- const newEntry = `\n ${slug}: ${cName},`
86
- return content.replace(full, open + inner + newEntry + close)
87
- }
88
-
89
- function findSeedsFile(): string | null {
90
- const cwd = process.cwd()
91
- const searchDirs = [cwd, resolve(cwd, 'apps', 'api')]
92
- for (const dir of searchDirs) {
93
- for (const name of ['seeds.ts', 'seed.ts']) {
94
- const p = resolve(dir, name)
95
- if (existsSync(p)) return p
96
- }
97
- }
98
- return null
99
- }
100
-
101
- export async function seedCreate(_args: SeedCreateOptions): Promise<void> {
102
- const rl = createInterface({ input: process.stdin, output: process.stdout })
103
-
104
- const ask = async (q: string, fallback = ''): Promise<string> => {
105
- const hint = fallback ? pc.dim(` [${fallback}]`) : ''
106
- const answer = await rl.question(` ${q}${hint}: `)
107
- return answer.trim() || fallback
108
- }
109
-
110
- const askYN = async (q: string, defaultYes = true): Promise<boolean> => {
111
- const hint = defaultYes ? pc.dim(' (Y/n)') : pc.dim(' (y/N)')
112
- const answer = (await rl.question(` ${q}${hint}: `)).trim().toLowerCase()
113
- if (!answer) return defaultYes
114
- return answer === 'y' || answer === 'yes'
115
- }
116
-
117
- console.log(pc.cyan('\n beech seed:create — new content type wizard\n'))
118
-
119
- try {
120
- const label = await ask('Content type name (singular, e.g. "Article")')
121
- if (!label) {
122
- rl.close()
123
- console.log(pc.red('\n ✗ Name required.\n'))
124
- process.exit(1)
125
- }
126
-
127
- const defaultSlug = slugify(label) + 's'
128
- const slug = slugify(await ask('Slug (plural, used in URL + table name)', defaultSlug)) || defaultSlug
129
- const labelPlural = (await ask('Plural label', label + 's')) || label + 's'
130
-
131
- const branches: BranchDef[] = []
132
- console.log(pc.dim('\n Now define the fields. Press Enter to accept defaults.\n'))
133
-
134
- let addMore = true
135
- while (addMore) {
136
- console.log(pc.dim(` ─── Field ${branches.length + 1} ───`))
137
- const alias = await ask(' Alias (camelCase, e.g. "title", "publishedAt")')
138
- if (!alias) {
139
- console.log(pc.yellow(' Alias required — skipping.'))
140
- addMore = await askYN('\n Add a field?')
141
- continue
142
- }
143
-
144
- const fieldLabel = (await ask(' Label', toLabel(alias))) || toLabel(alias)
145
- const typeList = BRANCH_TYPES.join(' | ')
146
- const rawType = (await ask(` Type (${typeList})`, 'text')).toLowerCase()
147
- const type: BranchType = (BRANCH_TYPES.includes(rawType as BranchType) ? rawType : 'text') as BranchType
148
- const required = await askYN(' Required on create?', false)
149
-
150
- branches.push({ alias, label: fieldLabel, type, required })
151
- addMore = await askYN('\n Add another field?')
152
- }
153
-
154
- rl.close()
155
-
156
- if (branches.length === 0) {
157
- console.log(pc.yellow('\n No fields defined — seed not created.\n'))
158
- process.exit(0)
159
- }
160
-
161
- const seedBlock = generateSeedBlock(slug, label, labelPlural, branches)
162
- const cName = toConstName(slug)
163
- const seedsPath = findSeedsFile()
164
-
165
- if (!seedsPath) {
166
- console.log(pc.yellow('\n Could not find seeds.ts. Add this to your seeds file manually:\n'))
167
- console.log(seedBlock)
168
- process.exit(0)
169
- }
170
-
171
- let content = readFileSync(seedsPath, 'utf-8')
172
- content = ensureDefineSeedImport(content)
173
-
174
- const withSeed = content + seedBlock
175
- const withRegistry = tryInsertRegistryEntry(withSeed, slug, cName)
176
- writeFileSync(seedsPath, withRegistry ?? withSeed, 'utf-8')
177
-
178
- console.log(pc.green(`\n ✓ Seed "${slug}" appended to ${seedsPath}\n`))
179
-
180
- if (!withRegistry) {
181
- console.log(pc.yellow(` ⚠ Could not auto-update SEED_REGISTRY — add this entry manually:\n`))
182
- console.log(pc.cyan(` ${slug}: ${cName},\n`))
183
- }
184
-
185
- console.log(pc.dim(' Next steps:'))
186
- console.log(pc.cyan(' npx beech seed:load --local'))
187
- console.log(pc.dim(' → create the new content table in your local D1 database\n'))
188
- } catch (err) {
189
- rl.close()
190
- throw err
191
- }
192
- }
@@ -1,206 +0,0 @@
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, renderSeedDiff, isSeedClean } 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
- renderSeedDiff(result)
73
- if (!isSeedClean(result)) allOk = false
74
- }
75
-
76
- console.log('')
77
- if (allOk) {
78
- console.log(pc.green(' Schema matches seeds. No action needed.\n'))
79
- } else {
80
- console.log(pc.yellow(' Run `beech seed:load` to apply missing tables/columns.\n'))
81
- }
82
- }
83
-
84
- async function runLoad(options: WranglerOptions, dryRun: boolean, registry: Record<string, Seed>): Promise<void> {
85
- const seeds = sortSeedsByDependencies(Object.values(registry))
86
-
87
- if (dryRun) {
88
- console.log(pc.cyan('\n -- dry-run: SQL that would be executed\n'))
89
- for (const seed of seeds) {
90
- const stmts = buildStatements(seed)
91
- console.log(pc.dim(` -- content_${seed.slug}`))
92
- for (const stmt of stmts) {
93
- console.log(stmt + '\n')
94
- }
95
- console.log(pc.dim(` -- register ${seed.slug} in seeds table`))
96
- console.log(buildSeedRegistrationSql(seed) + '\n')
97
- }
98
- console.log(pc.dim(' -- bump registry_version'))
99
- console.log(SEED_META_BUMP_SQL + '\n')
100
- return
101
- }
102
-
103
- console.log(pc.cyan(`\n Loading seeds into ${options.local ? 'local' : 'remote'} D1 (${options.db})…\n`))
104
-
105
- for (const seed of seeds) {
106
- const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)]
107
- const sql = stmts.join('\n\n') + '\n'
108
- process.stdout.write(` ${pc.dim('→')} content_${seed.slug}… `)
109
- const ok = executeD1File(sql, options)
110
- if (!ok) {
111
- console.log(pc.red('failed'))
112
- console.log(pc.red(`\n ✗ Failed to apply schema for content_${seed.slug}\n`))
113
- console.log(pc.dim(' wrangler reported an error above.'))
114
- console.log(pc.dim(` Most likely causes:`))
115
- console.log(pc.dim(` - Database "${options.db}" not found or wrong database_id`))
116
- if (!options.local) {
117
- console.log(pc.dim(' - Not logged in to Cloudflare'))
118
- console.log(pc.cyan('\n → Run: npx wrangler login'))
119
- console.log(pc.cyan(' → Then: npx beech seed:load\n'))
120
- } else {
121
- console.log(pc.cyan('\n → Run: npx beech init --db --local # re-initialise local DB'))
122
- console.log(pc.cyan(' → Then: npx beech seed:load --local\n'))
123
- }
124
- process.exit(1)
125
- }
126
- console.log(pc.green('done'))
127
- }
128
-
129
- // Bump registry_version so live isolates re-hydrate
130
- executeD1File(SEED_META_BUMP_SQL, options)
131
-
132
- console.log(pc.green('\n All seeds loaded.\n'))
133
- console.log(pc.dim(' Definitions registered in the database.'))
134
- console.log(pc.dim(' seed.ts is no longer required at runtime — you may keep it for code-first edits or delete it.\n'))
135
- }
136
-
137
- export async function seedLoad(args: SeedLoadOptions): Promise<void> {
138
- const registry = args.registry ?? SEED_REGISTRY
139
-
140
- if (Object.keys(registry).length === 0) {
141
- console.log(pc.yellow('\n ✗ No seeds found\n'))
142
- console.log(pc.dim(' Create a seeds.ts file in your project root with at least one content type.'))
143
- console.log(pc.cyan('\n → Run: npx beech seed:create\n'))
144
- return
145
- }
146
-
147
- const validationErrors = validateSeeds(registry)
148
- const fatalErrors = validationErrors.filter(e => e.fatal)
149
- const warnings = validationErrors.filter(e => !e.fatal)
150
-
151
- if (fatalErrors.length > 0) {
152
- const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0)
153
- const s = total !== 1 ? 's' : ''
154
- console.log(pc.red(`\n ✗ Seed validation found ${total} fatal error${s}. Cannot load schema.\n`))
155
- for (const e of fatalErrors) {
156
- console.log(pc.red(` ✗ ${e.slug}`))
157
- for (const msg of e.messages) {
158
- console.log(pc.red(` → ${msg}`))
159
- }
160
- }
161
- console.log('')
162
- process.exit(1)
163
- }
164
-
165
- if (warnings.length > 0) {
166
- const total = warnings.reduce((n, e) => n + e.messages.length, 0)
167
- const s = total !== 1 ? 's' : ''
168
- console.log(pc.yellow(`\n ⚠ Seed validation found ${total} issue${s}. Schema changes will still be applied.\n`))
169
- console.log(pc.dim(' Run "npx beech validate" for details.\n'))
170
- }
171
-
172
- const configPath = findWranglerConfig()
173
- const db = args.db ?? resolveDbName(configPath)
174
-
175
- const options: WranglerOptions = {
176
- db,
177
- local: args.local,
178
- configPath,
179
- }
180
-
181
- if (!args.dryRun && !args.diff) {
182
- try {
183
- const rows = queryD1<{ name: string }>(
184
- `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
185
- options
186
- )
187
- if (rows.length < 2) {
188
- console.log(pc.red('\n ✗ System tables not found (seeds, seed_meta)\n'))
189
- console.log(pc.dim(' Run `beech init --db` first to initialise the database.'))
190
- const flag = args.local ? ' --local' : ''
191
- console.log(pc.cyan(`\n → Run: npx beech init --db${flag}\n`))
192
- process.exit(1)
193
- }
194
- } catch {
195
- console.log(pc.red('\n ✗ Could not query the database\n'))
196
- console.log(pc.dim(' Run `beech init --db` first to initialise the database.'))
197
- process.exit(1)
198
- }
199
- }
200
-
201
- if (args.diff) {
202
- await runDiff(options, registry)
203
- } else {
204
- await runLoad(options, args.dryRun, registry)
205
- }
206
- }
@@ -1,54 +0,0 @@
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
- }