@beechcms/cli 0.6.0-preview.3 → 0.6.0-preview.4
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/.turbo/turbo-build.log +2 -2
- package/.turbo/turbo-lint.log +1 -0
- package/coverage/coverage-summary.json +3 -0
- package/dist/index.js +445 -249
- package/package.json +3 -2
- package/src/commands/deploy.ts +126 -126
- package/src/commands/generate-types.ts +65 -0
- package/src/commands/init.ts +599 -599
- package/src/commands/onboard.ts +32 -32
- package/src/commands/schema-diff.ts +78 -0
- package/src/commands/seed-create.ts +192 -192
- package/src/commands/seed-load.ts +206 -235
- package/src/commands/update.ts +54 -54
- package/src/commands/validate.ts +80 -80
- package/src/index.ts +24 -20
- package/src/lib/migration-writer.ts +106 -0
- package/src/lib/schema-diff.ts +175 -150
- package/src/lib/wrangler.ts +129 -129
- package/src/test/generate-types.test.ts +58 -0
- package/src/test/schema-diff.test.ts +232 -0
- package/src/test/seed-load.test.ts +158 -158
- package/src/test/validate.test.ts +261 -261
- package/tsconfig.json +16 -16
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +33 -33
- 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/onboard.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
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 { init } from './init.js'
|
|
7
|
-
import { seedLoad } from './seed-load.js'
|
|
8
|
-
|
|
9
|
-
export interface OnboardOptions {
|
|
10
|
-
local: boolean
|
|
11
|
-
yes: boolean
|
|
12
|
-
db?: string
|
|
13
|
-
registry?: Record<string, Seed> | null
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function onboard(args: OnboardOptions): Promise<void> {
|
|
17
|
-
console.log(pc.cyan('\n beech onboard — full provisioning\n'))
|
|
18
|
-
|
|
19
|
-
// Step 1: file check + DB init
|
|
20
|
-
await init({ initDb: true, local: args.local, db: args.db, nonInteractive: args.yes })
|
|
21
|
-
|
|
22
|
-
// Step 2: create content tables + register definitions + bump registry_version
|
|
23
|
-
await seedLoad({ dryRun: false, diff: false, local: args.local, db: args.db, registry: args.registry ?? null })
|
|
24
|
-
|
|
25
|
-
// Step 3: next steps
|
|
26
|
-
console.log(pc.cyan('\n Provisioning complete.\n'))
|
|
27
|
-
console.log(pc.dim(' Next steps:'))
|
|
28
|
-
console.log(pc.cyan(' 1. npx wrangler dev'))
|
|
29
|
-
console.log(pc.dim(' → start API + dashboard'))
|
|
30
|
-
console.log(pc.dim(' 2. Open http://localhost:8789/admin'))
|
|
31
|
-
console.log(pc.dim(' → complete setup wizard to create admin user\n'))
|
|
32
|
-
}
|
|
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 { init } from './init.js'
|
|
7
|
+
import { seedLoad } from './seed-load.js'
|
|
8
|
+
|
|
9
|
+
export interface OnboardOptions {
|
|
10
|
+
local: boolean
|
|
11
|
+
yes: boolean
|
|
12
|
+
db?: string
|
|
13
|
+
registry?: Record<string, Seed> | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function onboard(args: OnboardOptions): Promise<void> {
|
|
17
|
+
console.log(pc.cyan('\n beech onboard — full provisioning\n'))
|
|
18
|
+
|
|
19
|
+
// Step 1: file check + DB init
|
|
20
|
+
await init({ initDb: true, local: args.local, db: args.db, nonInteractive: args.yes })
|
|
21
|
+
|
|
22
|
+
// Step 2: create content tables + register definitions + bump registry_version
|
|
23
|
+
await seedLoad({ dryRun: false, diff: false, local: args.local, db: args.db, registry: args.registry ?? null })
|
|
24
|
+
|
|
25
|
+
// Step 3: next steps
|
|
26
|
+
console.log(pc.cyan('\n Provisioning complete.\n'))
|
|
27
|
+
console.log(pc.dim(' Next steps:'))
|
|
28
|
+
console.log(pc.cyan(' 1. npx wrangler dev'))
|
|
29
|
+
console.log(pc.dim(' → start API + dashboard'))
|
|
30
|
+
console.log(pc.dim(' 2. Open http://localhost:8789/admin'))
|
|
31
|
+
console.log(pc.dim(' → complete setup wizard to create admin user\n'))
|
|
32
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 +1,192 @@
|
|
|
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
|
+
// 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
|
+
}
|