@beechcms/cli 0.5.0 → 0.6.0-preview.1

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,3 +1,6 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
1
4
  import pc from 'picocolors'
2
5
  import { createInterface } from 'node:readline/promises'
3
6
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
@@ -1,3 +1,6 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
1
4
  import pc from 'picocolors'
2
5
  import {
3
6
  SEED_REGISTRY,
@@ -6,9 +9,13 @@ import {
6
9
  generateIndexes,
7
10
  generateFtsTable,
8
11
  generateFtsTriggers,
12
+ generateJunctionTable,
13
+ generateJunctionIndexes,
14
+ generateJunctionDraftTable,
15
+ sortSeedsByDependencies,
9
16
  type Seed,
10
17
  } from '@beechcms/core'
11
- import { executeD1File, findWranglerConfig, resolveDbName, type WranglerOptions } from '../lib/wrangler.js'
18
+ import { executeD1File, findWranglerConfig, resolveDbName, queryD1, sqlQuote, type WranglerOptions } from '../lib/wrangler.js'
12
19
  import { diffSeed } from '../lib/schema-diff.js'
13
20
  import { validateSeeds } from './validate.js'
14
21
 
@@ -20,6 +27,18 @@ export interface SeedLoadOptions {
20
27
  registry?: Record<string, Seed> | null
21
28
  }
22
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
+
23
42
  function buildStatements(seed: Seed): string[] {
24
43
  const stmts: string[] = [generateCreateTable(seed), ...generateIndexes(seed)]
25
44
 
@@ -31,11 +50,20 @@ function buildStatements(seed: Seed): string[] {
31
50
  stmts.push(fts, ...generateFtsTriggers(seed))
32
51
  }
33
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
+
34
62
  return stmts
35
63
  }
36
64
 
37
65
  async function runDiff(options: WranglerOptions, registry: Record<string, Seed>): Promise<void> {
38
- const seeds = Object.values(registry)
66
+ const seeds = sortSeedsByDependencies(Object.values(registry))
39
67
  console.log(pc.cyan('\n Diffing schema…\n'))
40
68
 
41
69
  let allOk = true
@@ -64,6 +92,12 @@ async function runDiff(options: WranglerOptions, registry: Record<string, Seed>)
64
92
  console.log(pc.dim(` ~ orphaned column: "${col.name}" (${col.actualType}) — exists in DB but not in seeds.ts`))
65
93
  } else if (col.status === 'type_mismatch') {
66
94
  console.log(pc.red(` ≠ type mismatch: ${col.name} (expected ${col.expectedType}, got ${col.actualType})`))
95
+ } else if (col.status === 'fk_missing') {
96
+ console.log(pc.red(` ⤬ missing FK: ${col.name} → content_${col.expectedTarget}(id)`))
97
+ } else if (col.status === 'fk_mismatch') {
98
+ console.log(pc.yellow(` ⤬ FK mismatch: ${col.name} expected ${col.expected}, got ${col.actual}`))
99
+ } else if (col.status === 'index_missing') {
100
+ console.log(pc.yellow(` ⊘ missing index on ${col.name}`))
67
101
  }
68
102
  }
69
103
  }
@@ -77,7 +111,7 @@ async function runDiff(options: WranglerOptions, registry: Record<string, Seed>)
77
111
  }
78
112
 
79
113
  async function runLoad(options: WranglerOptions, dryRun: boolean, registry: Record<string, Seed>): Promise<void> {
80
- const seeds = Object.values(registry)
114
+ const seeds = sortSeedsByDependencies(Object.values(registry))
81
115
 
82
116
  if (dryRun) {
83
117
  console.log(pc.cyan('\n -- dry-run: SQL that would be executed\n'))
@@ -87,14 +121,18 @@ async function runLoad(options: WranglerOptions, dryRun: boolean, registry: Reco
87
121
  for (const stmt of stmts) {
88
122
  console.log(stmt + '\n')
89
123
  }
124
+ console.log(pc.dim(` -- register ${seed.slug} in seeds table`))
125
+ console.log(buildSeedRegistrationSql(seed) + '\n')
90
126
  }
127
+ console.log(pc.dim(' -- bump registry_version'))
128
+ console.log(SEED_META_BUMP_SQL + '\n')
91
129
  return
92
130
  }
93
131
 
94
132
  console.log(pc.cyan(`\n Loading seeds into ${options.local ? 'local' : 'remote'} D1 (${options.db})…\n`))
95
133
 
96
134
  for (const seed of seeds) {
97
- const stmts = buildStatements(seed)
135
+ const stmts = [...buildStatements(seed), buildSeedRegistrationSql(seed)]
98
136
  const sql = stmts.join('\n\n') + '\n'
99
137
  process.stdout.write(` ${pc.dim('→')} content_${seed.slug}… `)
100
138
  const ok = executeD1File(sql, options)
@@ -117,7 +155,12 @@ async function runLoad(options: WranglerOptions, dryRun: boolean, registry: Reco
117
155
  console.log(pc.green('done'))
118
156
  }
119
157
 
158
+ // Bump registry_version so live isolates re-hydrate
159
+ executeD1File(SEED_META_BUMP_SQL, options)
160
+
120
161
  console.log(pc.green('\n All seeds loaded.\n'))
162
+ console.log(pc.dim(' Definitions registered in the database.'))
163
+ console.log(pc.dim(' seed.ts is no longer required at runtime — you may keep it for code-first edits or delete it.\n'))
121
164
  }
122
165
 
123
166
  export async function seedLoad(args: SeedLoadOptions): Promise<void> {
@@ -131,8 +174,25 @@ export async function seedLoad(args: SeedLoadOptions): Promise<void> {
131
174
  }
132
175
 
133
176
  const validationErrors = validateSeeds(registry)
134
- if (validationErrors.length > 0) {
135
- const total = validationErrors.reduce((n, e) => n + e.messages.length, 0)
177
+ const fatalErrors = validationErrors.filter(e => e.fatal)
178
+ const warnings = validationErrors.filter(e => !e.fatal)
179
+
180
+ if (fatalErrors.length > 0) {
181
+ const total = fatalErrors.reduce((n, e) => n + e.messages.length, 0)
182
+ const s = total !== 1 ? 's' : ''
183
+ console.log(pc.red(`\n ✗ Seed validation found ${total} fatal error${s}. Cannot load schema.\n`))
184
+ for (const e of fatalErrors) {
185
+ console.log(pc.red(` ✗ ${e.slug}`))
186
+ for (const msg of e.messages) {
187
+ console.log(pc.red(` → ${msg}`))
188
+ }
189
+ }
190
+ console.log('')
191
+ process.exit(1)
192
+ }
193
+
194
+ if (warnings.length > 0) {
195
+ const total = warnings.reduce((n, e) => n + e.messages.length, 0)
136
196
  const s = total !== 1 ? 's' : ''
137
197
  console.log(pc.yellow(`\n ⚠ Seed validation found ${total} issue${s}. Schema changes will still be applied.\n`))
138
198
  console.log(pc.dim(' Run "npx beech validate" for details.\n'))
@@ -147,6 +207,26 @@ export async function seedLoad(args: SeedLoadOptions): Promise<void> {
147
207
  configPath,
148
208
  }
149
209
 
210
+ if (!args.dryRun && !args.diff) {
211
+ try {
212
+ const rows = queryD1<{ name: string }>(
213
+ `SELECT name FROM sqlite_master WHERE type='table' AND name IN ('seeds','seed_meta')`,
214
+ options
215
+ )
216
+ if (rows.length < 2) {
217
+ console.log(pc.red('\n ✗ System tables not found (seeds, seed_meta)\n'))
218
+ console.log(pc.dim(' Run `beech init --db` first to initialise the database.'))
219
+ const flag = args.local ? ' --local' : ''
220
+ console.log(pc.cyan(`\n → Run: npx beech init --db${flag}\n`))
221
+ process.exit(1)
222
+ }
223
+ } catch {
224
+ console.log(pc.red('\n ✗ Could not query the database\n'))
225
+ console.log(pc.dim(' Run `beech init --db` first to initialise the database.'))
226
+ process.exit(1)
227
+ }
228
+ }
229
+
150
230
  if (args.diff) {
151
231
  await runDiff(options, registry)
152
232
  } else {
@@ -1,3 +1,6 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
1
4
  import pc from 'picocolors'
2
5
  import { spawnSync } from 'node:child_process'
3
6
 
@@ -1,6 +1,9 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
1
4
  import pc from 'picocolors'
2
5
  import type { Seed } from '@beechcms/core'
3
- import { SEED_REGISTRY } from '@beechcms/core'
6
+ import { SEED_REGISTRY, validateSeedDefinitions } from '@beechcms/core'
4
7
 
5
8
  export interface ValidateOptions {
6
9
  registry?: Record<string, Seed> | null
@@ -9,39 +12,12 @@ export interface ValidateOptions {
9
12
  export interface SeedValidationError {
10
13
  slug: string
11
14
  messages: string[]
15
+ /** true = abort seed:load; false = warning only */
16
+ fatal: boolean
12
17
  }
13
18
 
14
19
  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
20
+ return validateSeedDefinitions(Object.values(registry))
45
21
  }
46
22
 
47
23
  export async function validate(args: ValidateOptions): Promise<void> {
@@ -55,28 +31,49 @@ export async function validate(args: ValidateOptions): Promise<void> {
55
31
  console.log(pc.cyan('\n beech validate — checking seeds\n'))
56
32
 
57
33
  const errors = validateSeeds(registry)
58
- const errorMap = new Map(errors.map(e => [e.slug, e.messages]))
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))
59
48
 
60
- let totalIssues = 0
61
49
  for (const seed of Object.values(registry)) {
62
- const msgs = errorMap.get(seed.slug)
50
+ const msgs = warningMap.get(seed.slug)
63
51
  if (!msgs) {
64
- console.log(pc.green(` ✓ ${seed.slug}`))
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
+ }
65
57
  } else {
66
- totalIssues += msgs.length
67
- console.log(pc.red(` ✗ ${seed.slug}`))
58
+ console.log(pc.yellow(` ⚠ ${seed.slug}`))
68
59
  for (const msg of msgs) {
69
- console.log(pc.red(` → ${msg}`))
60
+ console.log(pc.yellow(` → ${msg}`))
70
61
  }
71
62
  }
72
63
  }
73
64
 
74
65
  console.log('')
75
66
 
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`))
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`))
79
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`))
80
77
  } else {
81
78
  console.log(pc.green(' All seeds valid.\n'))
82
79
  }
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
1
4
  export { seedLoad } from './commands/seed-load.js'
2
5
  export type { SeedLoadOptions } from './commands/seed-load.js'
3
6
  export { init } from './commands/init.js'
@@ -8,5 +11,7 @@ export { seedCreate } from './commands/seed-create.js'
8
11
  export type { SeedCreateOptions } from './commands/seed-create.js'
9
12
  export { deploy } from './commands/deploy.js'
10
13
  export type { DeployOptions } from './commands/deploy.js'
14
+ export { onboard } from './commands/onboard.js'
15
+ export type { OnboardOptions } from './commands/onboard.js'
11
16
  export { update } from './commands/update.js'
12
17
  export type { UpdateOptions } from './commands/update.js'
@@ -1,3 +1,6 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
1
4
  import type { Seed } from '@beechcms/core'
2
5
  import { getExpectedColumns, type SchemaColumn } from '@beechcms/core'
3
6
  import type { WranglerOptions, D1Row } from './wrangler.js'
@@ -10,11 +13,33 @@ interface PragmaRow extends D1Row {
10
13
  pk: number
11
14
  }
12
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
+
13
33
  export interface ColumnDiff {
14
34
  name: string
15
- status: 'ok' | 'missing' | 'extra' | 'type_mismatch'
35
+ status: 'ok' | 'missing' | 'extra' | 'type_mismatch' | 'fk_missing' | 'fk_mismatch' | 'index_missing'
16
36
  expectedType?: string
17
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
18
43
  }
19
44
 
20
45
  export interface SeedDiff {
@@ -43,12 +68,13 @@ export async function diffSeed(seed: Seed, options: WranglerOptions): Promise<Se
43
68
 
44
69
  const columns: ColumnDiff[] = []
45
70
 
71
+ // ── Column presence + type checks ────────────────────────────────────────
46
72
  for (const col of expected) {
47
- const actual = actualMap.get(col.name)
48
- if (!actual) {
73
+ const actualRow = actualMap.get(col.name)
74
+ if (!actualRow) {
49
75
  columns.push({ name: col.name, status: 'missing', expectedType: col.sqlType })
50
- } else if (actual.type.toUpperCase() !== col.sqlType) {
51
- columns.push({ name: col.name, status: 'type_mismatch', expectedType: col.sqlType, actualType: actual.type })
76
+ } else if (actualRow.type.toUpperCase() !== col.sqlType) {
77
+ columns.push({ name: col.name, status: 'type_mismatch', expectedType: col.sqlType, actualType: actualRow.type })
52
78
  } else {
53
79
  columns.push({ name: col.name, status: 'ok' })
54
80
  }
@@ -60,5 +86,65 @@ export async function diffSeed(seed: Seed, options: WranglerOptions): Promise<Se
60
86
  }
61
87
  }
62
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
+
63
149
  return { slug: seed.slug, tableExists: true, columns }
64
150
  }
@@ -1,4 +1,7 @@
1
- import { spawnSync } from 'node:child_process'
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+
4
+ import { spawnSync, type SpawnSyncReturns } from 'node:child_process'
2
5
  import { writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs'
3
6
  import { tmpdir } from 'node:os'
4
7
  import { join, resolve } from 'node:path'
@@ -40,10 +43,25 @@ export function executeD1File(sql: string, options: WranglerOptions): boolean {
40
43
  }
41
44
  }
42
45
 
43
- /** Esegue una query SQL e ritorna i risultati come array di oggetti (--json). */
46
+ /**
47
+ * Esegue una query SQL e ritorna i risultati come array di oggetti (--json).
48
+ *
49
+ * Passa il SQL via file temporaneo (`--file`) anziché `--command`: su Windows,
50
+ * `spawnSync` con `shell: true` non preserva le virgolette/spazi/virgole interni
51
+ * a un argomento inline, e wrangler riceve il comando spezzato in token sciolti
52
+ * ("Unknown arguments: name, FROM, sqlite_master, …"). Il file evita del tutto
53
+ * il riquoting della shell — stesso approccio di `executeD1File`.
54
+ */
44
55
  export function queryD1<T extends D1Row = D1Row>(sql: string, options: WranglerOptions): T[] {
45
- const args = ['d1', 'execute', options.db, '--command', sql, '--json', ...buildArgs(options)]
46
- const result = spawnSync('npx', ['wrangler', ...args], { encoding: 'utf-8', cwd: process.cwd(), shell: true })
56
+ const tmpFile = join(tmpdir(), `beech-query-${Date.now()}.sql`)
57
+ let result: SpawnSyncReturns<string>
58
+ try {
59
+ writeFileSync(tmpFile, sql, 'utf-8')
60
+ const args = ['d1', 'execute', options.db, '--file', tmpFile, '--json', ...buildArgs(options)]
61
+ result = spawnSync('npx', ['wrangler', ...args], { encoding: 'utf-8', cwd: process.cwd(), shell: true })
62
+ } finally {
63
+ try { rmSync(tmpFile) } catch {}
64
+ }
47
65
 
48
66
  if (result.status !== 0) {
49
67
  throw new Error(`wrangler d1 execute failed:\n${result.stderr}`)
@@ -76,6 +94,11 @@ export function findWranglerConfig(): string | null {
76
94
  return null
77
95
  }
78
96
 
97
+ /** Wraps a string value in single quotes, escaping internal single quotes for SQL literals. */
98
+ export function sqlQuote(value: string): string {
99
+ return `'${value.replace(/'/g, "''")}'`
100
+ }
101
+
79
102
  /** Risolve il nome del database D1 da wrangler.jsonc (stripping JSONC comments). */
80
103
  export function resolveDbName(configPath: string | null): string {
81
104
  if (!configPath) return 'beech-db'