@beechcms/cli 0.5.0 → 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.
Files changed (45) hide show
  1. package/coverage/base.css +224 -0
  2. package/coverage/block-navigation.js +87 -0
  3. package/coverage/favicon.png +0 -0
  4. package/coverage/index.html +116 -0
  5. package/coverage/lcov-report/base.css +224 -0
  6. package/coverage/lcov-report/block-navigation.js +87 -0
  7. package/coverage/lcov-report/favicon.png +0 -0
  8. package/coverage/lcov-report/index.html +116 -0
  9. package/coverage/lcov-report/prettify.css +1 -0
  10. package/coverage/lcov-report/prettify.js +2 -0
  11. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  12. package/coverage/lcov-report/sorter.js +210 -0
  13. package/coverage/lcov-report/validate.ts.html +325 -0
  14. package/coverage/lcov.info +80 -0
  15. package/coverage/prettify.css +1 -0
  16. package/coverage/prettify.js +2 -0
  17. package/coverage/sort-arrow-sprite.png +0 -0
  18. package/coverage/sorter.js +210 -0
  19. package/coverage/validate.ts.html +325 -0
  20. package/dist/commands/seed-load.d.ts +8 -0
  21. package/dist/commands/seed-load.d.ts.map +1 -0
  22. package/dist/commands/seed-load.js +89 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +241 -72
  26. package/dist/lib/schema-diff.d.ts +15 -0
  27. package/dist/lib/schema-diff.d.ts.map +1 -0
  28. package/dist/lib/schema-diff.js +37 -0
  29. package/dist/lib/wrangler.d.ts +17 -0
  30. package/dist/lib/wrangler.d.ts.map +1 -0
  31. package/dist/lib/wrangler.js +65 -0
  32. package/package.json +9 -5
  33. package/src/commands/deploy.ts +126 -123
  34. package/src/commands/init.ts +599 -557
  35. package/src/commands/onboard.ts +32 -0
  36. package/src/commands/seed-create.ts +192 -189
  37. package/src/commands/seed-load.ts +235 -155
  38. package/src/commands/update.ts +54 -51
  39. package/src/commands/validate.ts +80 -83
  40. package/src/index.ts +17 -12
  41. package/src/lib/schema-diff.ts +150 -64
  42. package/src/lib/wrangler.ts +129 -106
  43. package/tsconfig.json +16 -16
  44. package/tsconfig.tsbuildinfo +1 -1
  45. package/vitest.config.ts +33 -0
@@ -0,0 +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,189 +1,192 @@
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
+ // 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
+ }