@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.
@@ -1,80 +1,80 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- import pc from 'picocolors'
5
- import type { Seed } from '@beechcms/core'
6
- import { SEED_REGISTRY, validateSeedDefinitions } from '@beechcms/core'
7
-
8
- export interface ValidateOptions {
9
- registry?: Record<string, Seed> | null
10
- }
11
-
12
- export interface SeedValidationError {
13
- slug: string
14
- messages: string[]
15
- /** true = abort seed:load; false = warning only */
16
- fatal: boolean
17
- }
18
-
19
- export function validateSeeds(registry: Record<string, Seed>): SeedValidationError[] {
20
- return validateSeedDefinitions(Object.values(registry))
21
- }
22
-
23
- export async function validate(args: ValidateOptions): Promise<void> {
24
- const registry = args.registry ?? SEED_REGISTRY
25
-
26
- if (Object.keys(registry).length === 0) {
27
- console.warn(pc.yellow('\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n'))
28
- return
29
- }
30
-
31
- console.log(pc.cyan('\n beech validate — checking seeds\n'))
32
-
33
- const errors = validateSeeds(registry)
34
- const fatalErrors = errors.filter(e => e.fatal)
35
- const warnings = errors.filter(e => !e.fatal)
36
-
37
- // Print fatal errors first
38
- for (const e of fatalErrors) {
39
- console.log(pc.red(` ✗ ${e.slug} (fatal)`))
40
- for (const msg of e.messages) {
41
- console.log(pc.red(` → ${msg}`))
42
- }
43
- }
44
-
45
- // Print per-seed warnings
46
- const warningMap = new Map(warnings.map(e => [e.slug, e.messages]))
47
- const allWarningSlugsSeen = new Set(warnings.map(e => e.slug))
48
-
49
- for (const seed of Object.values(registry)) {
50
- const msgs = warningMap.get(seed.slug)
51
- if (!msgs) {
52
- if (!allWarningSlugsSeen.has(seed.slug)) {
53
- // only print ✓ if no fatal error for this slug either
54
- const hasFatal = fatalErrors.some(e => e.slug === seed.slug)
55
- if (!hasFatal) console.log(pc.green(` ✓ ${seed.slug}`))
56
- }
57
- } else {
58
- console.log(pc.yellow(` ⚠ ${seed.slug}`))
59
- for (const msg of msgs) {
60
- console.log(pc.yellow(` → ${msg}`))
61
- }
62
- }
63
- }
64
-
65
- console.log('')
66
-
67
- const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0)
68
- const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0)
69
-
70
- if (totalFatal > 0) {
71
- const s = totalFatal !== 1 ? 's' : ''
72
- console.log(pc.red(` Found ${totalFatal} fatal error${s}. Fix before loading.\n`))
73
- process.exit(1)
74
- } else if (totalWarnings > 0) {
75
- const s = totalWarnings !== 1 ? 's' : ''
76
- console.log(pc.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.\n`))
77
- } else {
78
- console.log(pc.green(' All seeds valid.\n'))
79
- }
80
- }
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
4
+ import pc from 'picocolors'
5
+ import type { Seed } from '@beechcms/core'
6
+ import { SEED_REGISTRY, validateSeedDefinitions } from '@beechcms/core'
7
+
8
+ export interface ValidateOptions {
9
+ registry?: Record<string, Seed> | null
10
+ }
11
+
12
+ export interface SeedValidationError {
13
+ slug: string
14
+ messages: string[]
15
+ /** true = abort seed:load; false = warning only */
16
+ fatal: boolean
17
+ }
18
+
19
+ export function validateSeeds(registry: Record<string, Seed>): SeedValidationError[] {
20
+ return validateSeedDefinitions(Object.values(registry))
21
+ }
22
+
23
+ export async function validate(args: ValidateOptions): Promise<void> {
24
+ const registry = args.registry ?? SEED_REGISTRY
25
+
26
+ if (Object.keys(registry).length === 0) {
27
+ console.warn(pc.yellow('\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n'))
28
+ return
29
+ }
30
+
31
+ console.log(pc.cyan('\n beech validate — checking seeds\n'))
32
+
33
+ const errors = validateSeeds(registry)
34
+ const fatalErrors = errors.filter(e => e.fatal)
35
+ const warnings = errors.filter(e => !e.fatal)
36
+
37
+ // Print fatal errors first
38
+ for (const e of fatalErrors) {
39
+ console.log(pc.red(` ✗ ${e.slug} (fatal)`))
40
+ for (const msg of e.messages) {
41
+ console.log(pc.red(` → ${msg}`))
42
+ }
43
+ }
44
+
45
+ // Print per-seed warnings
46
+ const warningMap = new Map(warnings.map(e => [e.slug, e.messages]))
47
+ const allWarningSlugsSeen = new Set(warnings.map(e => e.slug))
48
+
49
+ for (const seed of Object.values(registry)) {
50
+ const msgs = warningMap.get(seed.slug)
51
+ if (!msgs) {
52
+ if (!allWarningSlugsSeen.has(seed.slug)) {
53
+ // only print ✓ if no fatal error for this slug either
54
+ const hasFatal = fatalErrors.some(e => e.slug === seed.slug)
55
+ if (!hasFatal) console.log(pc.green(` ✓ ${seed.slug}`))
56
+ }
57
+ } else {
58
+ console.log(pc.yellow(` ⚠ ${seed.slug}`))
59
+ for (const msg of msgs) {
60
+ console.log(pc.yellow(` → ${msg}`))
61
+ }
62
+ }
63
+ }
64
+
65
+ console.log('')
66
+
67
+ const totalFatal = fatalErrors.reduce((n, e) => n + e.messages.length, 0)
68
+ const totalWarnings = warnings.reduce((n, e) => n + e.messages.length, 0)
69
+
70
+ if (totalFatal > 0) {
71
+ const s = totalFatal !== 1 ? 's' : ''
72
+ console.log(pc.red(` Found ${totalFatal} fatal error${s}. Fix before loading.\n`))
73
+ process.exit(1)
74
+ } else if (totalWarnings > 0) {
75
+ const s = totalWarnings !== 1 ? 's' : ''
76
+ console.log(pc.yellow(` Found ${totalWarnings} warning${s}. Review seeds above.\n`))
77
+ } else {
78
+ console.log(pc.green(' All seeds valid.\n'))
79
+ }
80
+ }
package/src/index.ts CHANGED
@@ -1,17 +1,17 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- export { seedLoad } from './commands/seed-load.js'
5
- export type { SeedLoadOptions } from './commands/seed-load.js'
6
- export { init } from './commands/init.js'
7
- export type { InitOptions } from './commands/init.js'
8
- export { validate, validateSeeds } from './commands/validate.js'
9
- export type { ValidateOptions, SeedValidationError } from './commands/validate.js'
10
- export { seedCreate } from './commands/seed-create.js'
11
- export type { SeedCreateOptions } from './commands/seed-create.js'
12
- export { deploy } from './commands/deploy.js'
13
- export type { DeployOptions } from './commands/deploy.js'
14
- export { onboard } from './commands/onboard.js'
15
- export type { OnboardOptions } from './commands/onboard.js'
16
- export { update } from './commands/update.js'
17
- export type { UpdateOptions } from './commands/update.js'
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
4
+ export { seedLoad } from './commands/seed-load.js'
5
+ export type { SeedLoadOptions } from './commands/seed-load.js'
6
+ export { init } from './commands/init.js'
7
+ export type { InitOptions } from './commands/init.js'
8
+ export { validate, validateSeeds } from './commands/validate.js'
9
+ export type { ValidateOptions, SeedValidationError } from './commands/validate.js'
10
+ export { seedCreate } from './commands/seed-create.js'
11
+ export type { SeedCreateOptions } from './commands/seed-create.js'
12
+ export { deploy } from './commands/deploy.js'
13
+ export type { DeployOptions } from './commands/deploy.js'
14
+ export { onboard } from './commands/onboard.js'
15
+ export type { OnboardOptions } from './commands/onboard.js'
16
+ export { update } from './commands/update.js'
17
+ export type { UpdateOptions } from './commands/update.js'
@@ -1,150 +1,150 @@
1
- // SPDX-License-Identifier: MIT
2
- // Copyright (c) 2024–2026 Flavio De Musso
3
-
4
- import type { Seed } from '@beechcms/core'
5
- import { getExpectedColumns, type SchemaColumn } from '@beechcms/core'
6
- import type { WranglerOptions, D1Row } from './wrangler.js'
7
- import { queryD1 } from './wrangler.js'
8
-
9
- interface PragmaRow extends D1Row {
10
- name: string
11
- type: string
12
- notnull: number
13
- pk: number
14
- }
15
-
16
- interface FkRow extends D1Row {
17
- id: number
18
- seq: number
19
- table: string
20
- from: string
21
- to: string
22
- on_update: string
23
- on_delete: string
24
- match: string
25
- }
26
-
27
- interface IndexRow extends D1Row {
28
- seq: number
29
- name: string
30
- unique: number
31
- }
32
-
33
- export interface ColumnDiff {
34
- name: string
35
- status: 'ok' | 'missing' | 'extra' | 'type_mismatch' | 'fk_missing' | 'fk_mismatch' | 'index_missing'
36
- expectedType?: string
37
- actualType?: string
38
- /** For fk_missing/fk_mismatch: expected FK target table */
39
- expectedTarget?: string
40
- /** For fk_mismatch: what the DB actually has */
41
- expected?: string
42
- actual?: string
43
- }
44
-
45
- export interface SeedDiff {
46
- slug: string
47
- tableExists: boolean
48
- columns: ColumnDiff[]
49
- }
50
-
51
- export async function diffSeed(seed: Seed, options: WranglerOptions): Promise<SeedDiff> {
52
- const tableName = `content_${seed.slug}`
53
- const expected = getExpectedColumns(seed)
54
-
55
- let actual: PragmaRow[]
56
- try {
57
- actual = queryD1<PragmaRow>(`PRAGMA table_info(${tableName})`, options)
58
- } catch {
59
- return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
60
- }
61
-
62
- if (actual.length === 0) {
63
- return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
64
- }
65
-
66
- const actualMap = new Map<string, PragmaRow>(actual.map(r => [r.name, r]))
67
- const expectedSet = new Set<string>(expected.map(c => c.name))
68
-
69
- const columns: ColumnDiff[] = []
70
-
71
- // ── Column presence + type checks ────────────────────────────────────────
72
- for (const col of expected) {
73
- const actualRow = actualMap.get(col.name)
74
- if (!actualRow) {
75
- columns.push({ name: col.name, status: 'missing', expectedType: col.sqlType })
76
- } else if (actualRow.type.toUpperCase() !== col.sqlType) {
77
- columns.push({ name: col.name, status: 'type_mismatch', expectedType: col.sqlType, actualType: actualRow.type })
78
- } else {
79
- columns.push({ name: col.name, status: 'ok' })
80
- }
81
- }
82
-
83
- for (const row of actual) {
84
- if (!expectedSet.has(row.name)) {
85
- columns.push({ name: row.name, status: 'extra', actualType: row.type })
86
- }
87
- }
88
-
89
- // ── FK + index checks for relation branches ──────────────────────────────
90
- const relationBranches = seed.branches.filter(b => b.type === 'relation' && b.targetSeed)
91
-
92
- if (relationBranches.length > 0) {
93
- let fkList: FkRow[] = []
94
- let indexList: IndexRow[] = []
95
- try {
96
- fkList = queryD1<FkRow>(`PRAGMA foreign_key_list(${tableName})`, options)
97
- indexList = queryD1<IndexRow>(`PRAGMA index_list(${tableName})`, options)
98
- } catch {
99
- // If PRAGMA fails (table may not exist yet), skip FK checks
100
- }
101
-
102
- // Build maps for fast lookup
103
- // fkList has one row per FK column; `from` = local col, `table` = referenced table
104
- const fkByCol = new Map<string, FkRow>()
105
- for (const fk of fkList) {
106
- fkByCol.set(fk.from, fk)
107
- }
108
- const indexNames = new Set(indexList.map(i => i.name))
109
-
110
- for (const branch of relationBranches) {
111
- const expectedFkTable = `content_${branch.targetSeed}`
112
- const expectedOnDelete = (branch.onDelete ?? 'SET NULL').toUpperCase()
113
- const expectedIndexName = `idx_${seed.slug}_${branch.alias}`
114
-
115
- // Find column diff entry for this branch (already evaluated above)
116
- const colDiff = columns.find(c => c.name === branch.alias)
117
- if (!colDiff || colDiff.status === 'missing') continue // already flagged
118
-
119
- const fk = fkByCol.get(branch.alias)
120
-
121
- if (!fk) {
122
- // Column exists but no FK
123
- colDiff.status = 'fk_missing'
124
- colDiff.expectedTarget = branch.targetSeed
125
- } else {
126
- const actualTable = fk.table
127
- const actualOnDelete = fk.on_delete.toUpperCase()
128
- if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
129
- colDiff.status = 'fk_mismatch'
130
- colDiff.expected = `→ ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`
131
- colDiff.actual = `→ ${actualTable}(id) ON DELETE ${actualOnDelete}`
132
- colDiff.expectedTarget = branch.targetSeed
133
- }
134
- }
135
-
136
- // Check index separately (can coexist with fk status)
137
- if (!indexNames.has(expectedIndexName)) {
138
- // Only add index_missing if the column is otherwise OK (FK issue takes precedence)
139
- if (colDiff.status === 'ok') {
140
- colDiff.status = 'index_missing'
141
- } else {
142
- // Append index info to the existing diff row as a separate entry
143
- columns.push({ name: branch.alias, status: 'index_missing' })
144
- }
145
- }
146
- }
147
- }
148
-
149
- return { slug: seed.slug, tableExists: true, columns }
150
- }
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
4
+ import type { Seed } from '@beechcms/core'
5
+ import { getExpectedColumns, type SchemaColumn } from '@beechcms/core'
6
+ import type { WranglerOptions, D1Row } from './wrangler.js'
7
+ import { queryD1 } from './wrangler.js'
8
+
9
+ interface PragmaRow extends D1Row {
10
+ name: string
11
+ type: string
12
+ notnull: number
13
+ pk: number
14
+ }
15
+
16
+ interface FkRow extends D1Row {
17
+ id: number
18
+ seq: number
19
+ table: string
20
+ from: string
21
+ to: string
22
+ on_update: string
23
+ on_delete: string
24
+ match: string
25
+ }
26
+
27
+ interface IndexRow extends D1Row {
28
+ seq: number
29
+ name: string
30
+ unique: number
31
+ }
32
+
33
+ export interface ColumnDiff {
34
+ name: string
35
+ status: 'ok' | 'missing' | 'extra' | 'type_mismatch' | 'fk_missing' | 'fk_mismatch' | 'index_missing'
36
+ expectedType?: string
37
+ actualType?: string
38
+ /** For fk_missing/fk_mismatch: expected FK target table */
39
+ expectedTarget?: string
40
+ /** For fk_mismatch: what the DB actually has */
41
+ expected?: string
42
+ actual?: string
43
+ }
44
+
45
+ export interface SeedDiff {
46
+ slug: string
47
+ tableExists: boolean
48
+ columns: ColumnDiff[]
49
+ }
50
+
51
+ export async function diffSeed(seed: Seed, options: WranglerOptions): Promise<SeedDiff> {
52
+ const tableName = `content_${seed.slug}`
53
+ const expected = getExpectedColumns(seed)
54
+
55
+ let actual: PragmaRow[]
56
+ try {
57
+ actual = queryD1<PragmaRow>(`PRAGMA table_info(${tableName})`, options)
58
+ } catch {
59
+ return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
60
+ }
61
+
62
+ if (actual.length === 0) {
63
+ return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
64
+ }
65
+
66
+ const actualMap = new Map<string, PragmaRow>(actual.map(r => [r.name, r]))
67
+ const expectedSet = new Set<string>(expected.map(c => c.name))
68
+
69
+ const columns: ColumnDiff[] = []
70
+
71
+ // ── Column presence + type checks ────────────────────────────────────────
72
+ for (const col of expected) {
73
+ const actualRow = actualMap.get(col.name)
74
+ if (!actualRow) {
75
+ columns.push({ name: col.name, status: 'missing', expectedType: col.sqlType })
76
+ } else if (actualRow.type.toUpperCase() !== col.sqlType) {
77
+ columns.push({ name: col.name, status: 'type_mismatch', expectedType: col.sqlType, actualType: actualRow.type })
78
+ } else {
79
+ columns.push({ name: col.name, status: 'ok' })
80
+ }
81
+ }
82
+
83
+ for (const row of actual) {
84
+ if (!expectedSet.has(row.name)) {
85
+ columns.push({ name: row.name, status: 'extra', actualType: row.type })
86
+ }
87
+ }
88
+
89
+ // ── FK + index checks for relation branches ──────────────────────────────
90
+ const relationBranches = seed.branches.filter(b => b.type === 'relation' && b.targetSeed)
91
+
92
+ if (relationBranches.length > 0) {
93
+ let fkList: FkRow[] = []
94
+ let indexList: IndexRow[] = []
95
+ try {
96
+ fkList = queryD1<FkRow>(`PRAGMA foreign_key_list(${tableName})`, options)
97
+ indexList = queryD1<IndexRow>(`PRAGMA index_list(${tableName})`, options)
98
+ } catch {
99
+ // If PRAGMA fails (table may not exist yet), skip FK checks
100
+ }
101
+
102
+ // Build maps for fast lookup
103
+ // fkList has one row per FK column; `from` = local col, `table` = referenced table
104
+ const fkByCol = new Map<string, FkRow>()
105
+ for (const fk of fkList) {
106
+ fkByCol.set(fk.from, fk)
107
+ }
108
+ const indexNames = new Set(indexList.map(i => i.name))
109
+
110
+ for (const branch of relationBranches) {
111
+ const expectedFkTable = `content_${branch.targetSeed}`
112
+ const expectedOnDelete = (branch.onDelete ?? 'SET NULL').toUpperCase()
113
+ const expectedIndexName = `idx_${seed.slug}_${branch.alias}`
114
+
115
+ // Find column diff entry for this branch (already evaluated above)
116
+ const colDiff = columns.find(c => c.name === branch.alias)
117
+ if (!colDiff || colDiff.status === 'missing') continue // already flagged
118
+
119
+ const fk = fkByCol.get(branch.alias)
120
+
121
+ if (!fk) {
122
+ // Column exists but no FK
123
+ colDiff.status = 'fk_missing'
124
+ colDiff.expectedTarget = branch.targetSeed
125
+ } else {
126
+ const actualTable = fk.table
127
+ const actualOnDelete = fk.on_delete.toUpperCase()
128
+ if (actualTable !== expectedFkTable || actualOnDelete !== expectedOnDelete) {
129
+ colDiff.status = 'fk_mismatch'
130
+ colDiff.expected = `→ ${expectedFkTable}(id) ON DELETE ${expectedOnDelete}`
131
+ colDiff.actual = `→ ${actualTable}(id) ON DELETE ${actualOnDelete}`
132
+ colDiff.expectedTarget = branch.targetSeed
133
+ }
134
+ }
135
+
136
+ // Check index separately (can coexist with fk status)
137
+ if (!indexNames.has(expectedIndexName)) {
138
+ // Only add index_missing if the column is otherwise OK (FK issue takes precedence)
139
+ if (colDiff.status === 'ok') {
140
+ colDiff.status = 'index_missing'
141
+ } else {
142
+ // Append index info to the existing diff row as a separate entry
143
+ columns.push({ name: branch.alias, status: 'index_missing' })
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ return { slug: seed.slug, tableExists: true, columns }
150
+ }