@beechcms/cli 0.4.0-preview.12 → 0.4.0-preview.13
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 +681 -20
- package/package.json +2 -2
- package/src/commands/deploy.ts +122 -0
- package/src/commands/init.ts +400 -0
- package/src/commands/seed-create.ts +189 -0
- package/src/commands/seed-load.ts +137 -128
- package/src/commands/validate.ts +83 -0
- package/src/index.ts +10 -2
- package/src/lib/schema-diff.ts +64 -64
- package/src/lib/wrangler.ts +108 -108
- package/tsconfig.json +16 -16
- package/tsconfig.tsbuildinfo +1 -1
- 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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import pc from 'picocolors'
|
|
2
|
+
import { createInterface } from 'node:readline/promises'
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { resolve } from 'node:path'
|
|
5
|
+
import type { BranchType } from '@beechcms/core'
|
|
6
|
+
|
|
7
|
+
export type SeedCreateOptions = Record<string, never>
|
|
8
|
+
|
|
9
|
+
const BRANCH_TYPES: BranchType[] = ['text', 'number', 'boolean', 'date', 'richtext', 'file', 'tags']
|
|
10
|
+
|
|
11
|
+
function slugify(str: string): string {
|
|
12
|
+
return str
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.trim()
|
|
15
|
+
.replace(/\s+/g, '-')
|
|
16
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toConstName(slug: string): string {
|
|
20
|
+
return slug.replace(/-/g, '_').toUpperCase() + '_SEED'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toLabel(alias: string): string {
|
|
24
|
+
return alias
|
|
25
|
+
.replace(/([A-Z])/g, ' $1')
|
|
26
|
+
.replace(/^./, s => s.toUpperCase())
|
|
27
|
+
.trim()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BranchDef {
|
|
31
|
+
alias: string
|
|
32
|
+
label: string
|
|
33
|
+
type: BranchType
|
|
34
|
+
required: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateSeedBlock(slug: string, label: string, labelPlural: string, branches: BranchDef[]): string {
|
|
38
|
+
const displayAlias =
|
|
39
|
+
branches.find(b => b.type === 'text')?.alias ??
|
|
40
|
+
branches[0]?.alias ??
|
|
41
|
+
'name'
|
|
42
|
+
|
|
43
|
+
const cName = toConstName(slug)
|
|
44
|
+
const branchLines = branches.map(b => {
|
|
45
|
+
const parts: string[] = [
|
|
46
|
+
`alias: '${b.alias}'`,
|
|
47
|
+
`label: '${b.label}'`,
|
|
48
|
+
`type: '${b.type}'`,
|
|
49
|
+
]
|
|
50
|
+
if (b.required) parts.push('requiredOnCreate: true')
|
|
51
|
+
return ` { ${parts.join(', ')} },`
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
'',
|
|
56
|
+
`export const ${cName} = defineSeed({`,
|
|
57
|
+
` slug: '${slug}',`,
|
|
58
|
+
` label: '${label}',`,
|
|
59
|
+
` labelPlural: '${labelPlural}',`,
|
|
60
|
+
` displayNameAlias: '${displayAlias}',`,
|
|
61
|
+
' branches: [',
|
|
62
|
+
...branchLines,
|
|
63
|
+
' ],',
|
|
64
|
+
' dashboard: {',
|
|
65
|
+
" icon: 'Folder',",
|
|
66
|
+
" group: 'Content',",
|
|
67
|
+
' },',
|
|
68
|
+
'})',
|
|
69
|
+
'',
|
|
70
|
+
].join('\n')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureDefineSeedImport(content: string): string {
|
|
74
|
+
if (content.includes('defineSeed')) return content
|
|
75
|
+
return `import { defineSeed } from '@beechcms/core'\n` + content
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tryInsertRegistryEntry(content: string, slug: string, cName: string): string | null {
|
|
79
|
+
const match = content.match(/(SEED_REGISTRY[^{]*\{)([\s\S]*?)(\n\})/m)
|
|
80
|
+
if (!match) return null
|
|
81
|
+
const [full, open, inner, close] = match
|
|
82
|
+
const newEntry = `\n ${slug}: ${cName},`
|
|
83
|
+
return content.replace(full, open + inner + newEntry + close)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findSeedsFile(): string | null {
|
|
87
|
+
const cwd = process.cwd()
|
|
88
|
+
const searchDirs = [cwd, resolve(cwd, 'apps', 'api')]
|
|
89
|
+
for (const dir of searchDirs) {
|
|
90
|
+
for (const name of ['seeds.ts', 'seed.ts']) {
|
|
91
|
+
const p = resolve(dir, name)
|
|
92
|
+
if (existsSync(p)) return p
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function seedCreate(_args: SeedCreateOptions): Promise<void> {
|
|
99
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
100
|
+
|
|
101
|
+
const ask = async (q: string, fallback = ''): Promise<string> => {
|
|
102
|
+
const hint = fallback ? pc.dim(` [${fallback}]`) : ''
|
|
103
|
+
const answer = await rl.question(` ${q}${hint}: `)
|
|
104
|
+
return answer.trim() || fallback
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const askYN = async (q: string, defaultYes = true): Promise<boolean> => {
|
|
108
|
+
const hint = defaultYes ? pc.dim(' (Y/n)') : pc.dim(' (y/N)')
|
|
109
|
+
const answer = (await rl.question(` ${q}${hint}: `)).trim().toLowerCase()
|
|
110
|
+
if (!answer) return defaultYes
|
|
111
|
+
return answer === 'y' || answer === 'yes'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(pc.cyan('\n beech seed:create — new content type wizard\n'))
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const label = await ask('Content type name (singular, e.g. "Article")')
|
|
118
|
+
if (!label) {
|
|
119
|
+
rl.close()
|
|
120
|
+
console.log(pc.red('\n ✗ Name required.\n'))
|
|
121
|
+
process.exit(1)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const defaultSlug = slugify(label) + 's'
|
|
125
|
+
const slug = slugify(await ask('Slug (plural, used in URL + table name)', defaultSlug)) || defaultSlug
|
|
126
|
+
const labelPlural = (await ask('Plural label', label + 's')) || label + 's'
|
|
127
|
+
|
|
128
|
+
const branches: BranchDef[] = []
|
|
129
|
+
console.log(pc.dim('\n Now define the fields. Press Enter to accept defaults.\n'))
|
|
130
|
+
|
|
131
|
+
let addMore = true
|
|
132
|
+
while (addMore) {
|
|
133
|
+
console.log(pc.dim(` ─── Field ${branches.length + 1} ───`))
|
|
134
|
+
const alias = await ask(' Alias (camelCase, e.g. "title", "publishedAt")')
|
|
135
|
+
if (!alias) {
|
|
136
|
+
console.log(pc.yellow(' Alias required — skipping.'))
|
|
137
|
+
addMore = await askYN('\n Add a field?')
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const fieldLabel = (await ask(' Label', toLabel(alias))) || toLabel(alias)
|
|
142
|
+
const typeList = BRANCH_TYPES.join(' | ')
|
|
143
|
+
const rawType = (await ask(` Type (${typeList})`, 'text')).toLowerCase()
|
|
144
|
+
const type: BranchType = (BRANCH_TYPES.includes(rawType as BranchType) ? rawType : 'text') as BranchType
|
|
145
|
+
const required = await askYN(' Required on create?', false)
|
|
146
|
+
|
|
147
|
+
branches.push({ alias, label: fieldLabel, type, required })
|
|
148
|
+
addMore = await askYN('\n Add another field?')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
rl.close()
|
|
152
|
+
|
|
153
|
+
if (branches.length === 0) {
|
|
154
|
+
console.log(pc.yellow('\n No fields defined — seed not created.\n'))
|
|
155
|
+
process.exit(0)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const seedBlock = generateSeedBlock(slug, label, labelPlural, branches)
|
|
159
|
+
const cName = toConstName(slug)
|
|
160
|
+
const seedsPath = findSeedsFile()
|
|
161
|
+
|
|
162
|
+
if (!seedsPath) {
|
|
163
|
+
console.log(pc.yellow('\n Could not find seeds.ts. Add this to your seeds file manually:\n'))
|
|
164
|
+
console.log(seedBlock)
|
|
165
|
+
process.exit(0)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let content = readFileSync(seedsPath, 'utf-8')
|
|
169
|
+
content = ensureDefineSeedImport(content)
|
|
170
|
+
|
|
171
|
+
const withSeed = content + seedBlock
|
|
172
|
+
const withRegistry = tryInsertRegistryEntry(withSeed, slug, cName)
|
|
173
|
+
writeFileSync(seedsPath, withRegistry ?? withSeed, 'utf-8')
|
|
174
|
+
|
|
175
|
+
console.log(pc.green(`\n ✓ Seed "${slug}" appended to ${seedsPath}\n`))
|
|
176
|
+
|
|
177
|
+
if (!withRegistry) {
|
|
178
|
+
console.log(pc.yellow(` ⚠ Could not auto-update SEED_REGISTRY — add this entry manually:\n`))
|
|
179
|
+
console.log(pc.cyan(` ${slug}: ${cName},\n`))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(pc.dim(' Next steps:'))
|
|
183
|
+
console.log(pc.cyan(' npx beech seed:load --local'))
|
|
184
|
+
console.log(pc.dim(' → create the new content table in your local D1 database\n'))
|
|
185
|
+
} catch (err) {
|
|
186
|
+
rl.close()
|
|
187
|
+
throw err
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -1,128 +1,137 @@
|
|
|
1
|
-
import pc from 'picocolors'
|
|
2
|
-
import {
|
|
3
|
-
SEED_REGISTRY,
|
|
4
|
-
generateCreateTable,
|
|
5
|
-
generateDraftTable,
|
|
6
|
-
generateIndexes,
|
|
7
|
-
generateFtsTable,
|
|
8
|
-
generateFtsTriggers,
|
|
9
|
-
type Seed,
|
|
10
|
-
} from '@beechcms/core'
|
|
11
|
-
import { executeD1File, findWranglerConfig, resolveDbName, type WranglerOptions } from '../lib/wrangler.js'
|
|
12
|
-
import { diffSeed } from '../lib/schema-diff.js'
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
1
|
+
import pc from 'picocolors'
|
|
2
|
+
import {
|
|
3
|
+
SEED_REGISTRY,
|
|
4
|
+
generateCreateTable,
|
|
5
|
+
generateDraftTable,
|
|
6
|
+
generateIndexes,
|
|
7
|
+
generateFtsTable,
|
|
8
|
+
generateFtsTriggers,
|
|
9
|
+
type Seed,
|
|
10
|
+
} from '@beechcms/core'
|
|
11
|
+
import { executeD1File, findWranglerConfig, resolveDbName, type WranglerOptions } from '../lib/wrangler.js'
|
|
12
|
+
import { diffSeed } from '../lib/schema-diff.js'
|
|
13
|
+
import { validateSeeds } from './validate.js'
|
|
14
|
+
|
|
15
|
+
export interface SeedLoadOptions {
|
|
16
|
+
dryRun: boolean
|
|
17
|
+
diff: boolean
|
|
18
|
+
local: boolean
|
|
19
|
+
db?: string
|
|
20
|
+
registry?: Record<string, Seed> | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildStatements(seed: Seed): string[] {
|
|
24
|
+
const stmts: string[] = [generateCreateTable(seed), ...generateIndexes(seed)]
|
|
25
|
+
|
|
26
|
+
const draft = generateDraftTable(seed)
|
|
27
|
+
if (draft) stmts.push(draft)
|
|
28
|
+
|
|
29
|
+
const fts = generateFtsTable(seed)
|
|
30
|
+
if (fts) {
|
|
31
|
+
stmts.push(fts, ...generateFtsTriggers(seed))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return stmts
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function runDiff(options: WranglerOptions, registry: Record<string, Seed>): Promise<void> {
|
|
38
|
+
const seeds = Object.values(registry)
|
|
39
|
+
console.log(pc.cyan('\n Diffing schema…\n'))
|
|
40
|
+
|
|
41
|
+
let allOk = true
|
|
42
|
+
for (const seed of seeds) {
|
|
43
|
+
const result = await diffSeed(seed, options)
|
|
44
|
+
const tableName = `content_${seed.slug}`
|
|
45
|
+
|
|
46
|
+
if (!result.tableExists) {
|
|
47
|
+
console.log(pc.red(` ✗ ${tableName} — table missing`))
|
|
48
|
+
allOk = false
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const problems = result.columns.filter(c => c.status !== 'ok')
|
|
53
|
+
if (problems.length === 0) {
|
|
54
|
+
console.log(pc.green(` ✓ ${tableName}`))
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
allOk = false
|
|
59
|
+
console.log(pc.yellow(` ⚠ ${tableName}`))
|
|
60
|
+
for (const col of problems) {
|
|
61
|
+
if (col.status === 'missing') {
|
|
62
|
+
console.log(pc.red(` + missing column: ${col.name} ${col.expectedType}`))
|
|
63
|
+
} else if (col.status === 'extra') {
|
|
64
|
+
console.log(pc.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) — exists in DB but not in seeds.ts`))
|
|
65
|
+
} else if (col.status === 'type_mismatch') {
|
|
66
|
+
console.log(pc.red(` ≠ type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log('')
|
|
72
|
+
if (allOk) {
|
|
73
|
+
console.log(pc.green(' Schema matches seeds. No action needed.\n'))
|
|
74
|
+
} else {
|
|
75
|
+
console.log(pc.yellow(' Run `beech seed:load` to apply missing tables/columns.\n'))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runLoad(options: WranglerOptions, dryRun: boolean, registry: Record<string, Seed>): Promise<void> {
|
|
80
|
+
const seeds = Object.values(registry)
|
|
81
|
+
|
|
82
|
+
if (dryRun) {
|
|
83
|
+
console.log(pc.cyan('\n -- dry-run: SQL that would be executed\n'))
|
|
84
|
+
for (const seed of seeds) {
|
|
85
|
+
const stmts = buildStatements(seed)
|
|
86
|
+
console.log(pc.dim(` -- content_${seed.slug}`))
|
|
87
|
+
for (const stmt of stmts) {
|
|
88
|
+
console.log(stmt + '\n')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(pc.cyan(`\n Loading seeds into ${options.local ? 'local' : 'remote'} D1 (${options.db})…\n`))
|
|
95
|
+
|
|
96
|
+
for (const seed of seeds) {
|
|
97
|
+
const stmts = buildStatements(seed)
|
|
98
|
+
const sql = stmts.join('\n\n') + '\n'
|
|
99
|
+
process.stdout.write(` ${pc.dim('→')} content_${seed.slug}… `)
|
|
100
|
+
executeD1File(sql, options)
|
|
101
|
+
console.log(pc.green('done'))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(pc.green('\n All seeds loaded.\n'))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function seedLoad(args: SeedLoadOptions): Promise<void> {
|
|
108
|
+
const registry = args.registry ?? SEED_REGISTRY
|
|
109
|
+
|
|
110
|
+
if (Object.keys(registry).length === 0) {
|
|
111
|
+
console.warn(pc.yellow('\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n'))
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const validationErrors = validateSeeds(registry)
|
|
116
|
+
if (validationErrors.length > 0) {
|
|
117
|
+
const total = validationErrors.reduce((n, e) => n + e.messages.length, 0)
|
|
118
|
+
const s = total !== 1 ? 's' : ''
|
|
119
|
+
console.log(pc.yellow(`\n ⚠ Seed validation found ${total} issue${s}. Schema changes will still be applied.\n`))
|
|
120
|
+
console.log(pc.dim(' Run "npx beech validate" for details.\n'))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const configPath = findWranglerConfig()
|
|
124
|
+
const db = args.db ?? resolveDbName(configPath)
|
|
125
|
+
|
|
126
|
+
const options: WranglerOptions = {
|
|
127
|
+
db,
|
|
128
|
+
local: args.local,
|
|
129
|
+
configPath,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (args.diff) {
|
|
133
|
+
await runDiff(options, registry)
|
|
134
|
+
} else {
|
|
135
|
+
await runLoad(options, args.dryRun, registry)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import pc from 'picocolors'
|
|
2
|
+
import type { Seed } from '@beechcms/core'
|
|
3
|
+
import { SEED_REGISTRY } from '@beechcms/core'
|
|
4
|
+
|
|
5
|
+
export interface ValidateOptions {
|
|
6
|
+
registry?: Record<string, Seed> | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SeedValidationError {
|
|
10
|
+
slug: string
|
|
11
|
+
messages: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateSeeds(registry: Record<string, Seed>): SeedValidationError[] {
|
|
15
|
+
const result: SeedValidationError[] = []
|
|
16
|
+
const slugsSeen = new Set<string>()
|
|
17
|
+
|
|
18
|
+
for (const seed of Object.values(registry)) {
|
|
19
|
+
const messages: string[] = []
|
|
20
|
+
|
|
21
|
+
if (slugsSeen.has(seed.slug)) {
|
|
22
|
+
messages.push(`duplicate slug "${seed.slug}" — each seed must have a unique slug`)
|
|
23
|
+
}
|
|
24
|
+
slugsSeen.add(seed.slug)
|
|
25
|
+
|
|
26
|
+
const aliasesSeen = new Set<string>()
|
|
27
|
+
for (const branch of seed.branches) {
|
|
28
|
+
if (aliasesSeen.has(branch.alias)) {
|
|
29
|
+
messages.push(`duplicate branch alias "${branch.alias}"`)
|
|
30
|
+
}
|
|
31
|
+
aliasesSeen.add(branch.alias)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const allAliases = new Set(seed.branches.map(b => b.alias))
|
|
35
|
+
if (!allAliases.has(seed.displayNameAlias)) {
|
|
36
|
+
messages.push(`displayNameAlias "${seed.displayNameAlias}" not found in branches`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (messages.length > 0) {
|
|
40
|
+
result.push({ slug: seed.slug, messages })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function validate(args: ValidateOptions): Promise<void> {
|
|
48
|
+
const registry = args.registry ?? SEED_REGISTRY
|
|
49
|
+
|
|
50
|
+
if (Object.keys(registry).length === 0) {
|
|
51
|
+
console.warn(pc.yellow('\n Warning: SEED_REGISTRY is empty. Create a seeds.ts in your project root.\n'))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(pc.cyan('\n beech validate — checking seeds\n'))
|
|
56
|
+
|
|
57
|
+
const errors = validateSeeds(registry)
|
|
58
|
+
const errorMap = new Map(errors.map(e => [e.slug, e.messages]))
|
|
59
|
+
|
|
60
|
+
let totalIssues = 0
|
|
61
|
+
for (const seed of Object.values(registry)) {
|
|
62
|
+
const msgs = errorMap.get(seed.slug)
|
|
63
|
+
if (!msgs) {
|
|
64
|
+
console.log(pc.green(` ✓ ${seed.slug}`))
|
|
65
|
+
} else {
|
|
66
|
+
totalIssues += msgs.length
|
|
67
|
+
console.log(pc.red(` ✗ ${seed.slug}`))
|
|
68
|
+
for (const msg of msgs) {
|
|
69
|
+
console.log(pc.red(` → ${msg}`))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log('')
|
|
75
|
+
|
|
76
|
+
if (totalIssues > 0) {
|
|
77
|
+
const s = totalIssues !== 1 ? 's' : ''
|
|
78
|
+
console.log(pc.red(` Found ${totalIssues} issue${s}. Fix the seeds above before loading.\n`))
|
|
79
|
+
process.exit(1)
|
|
80
|
+
} else {
|
|
81
|
+
console.log(pc.green(' All seeds valid.\n'))
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
export { seedLoad } from './commands/seed-load.js'
|
|
2
|
-
export type { SeedLoadOptions } from './commands/seed-load.js'
|
|
1
|
+
export { seedLoad } from './commands/seed-load.js'
|
|
2
|
+
export type { SeedLoadOptions } from './commands/seed-load.js'
|
|
3
|
+
export { init } from './commands/init.js'
|
|
4
|
+
export type { InitOptions } from './commands/init.js'
|
|
5
|
+
export { validate, validateSeeds } from './commands/validate.js'
|
|
6
|
+
export type { ValidateOptions, SeedValidationError } from './commands/validate.js'
|
|
7
|
+
export { seedCreate } from './commands/seed-create.js'
|
|
8
|
+
export type { SeedCreateOptions } from './commands/seed-create.js'
|
|
9
|
+
export { deploy } from './commands/deploy.js'
|
|
10
|
+
export type { DeployOptions } from './commands/deploy.js'
|