@beechcms/cli 0.4.0-preview.11 → 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
package/src/lib/schema-diff.ts
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import type { Seed } from '@beechcms/core'
|
|
2
|
-
import { getExpectedColumns, type SchemaColumn } from '@beechcms/core'
|
|
3
|
-
import type { WranglerOptions, D1Row } from './wrangler.js'
|
|
4
|
-
import { queryD1 } from './wrangler.js'
|
|
5
|
-
|
|
6
|
-
interface PragmaRow extends D1Row {
|
|
7
|
-
name: string
|
|
8
|
-
type: string
|
|
9
|
-
notnull: number
|
|
10
|
-
pk: number
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ColumnDiff {
|
|
14
|
-
name: string
|
|
15
|
-
status: 'ok' | 'missing' | 'extra' | 'type_mismatch'
|
|
16
|
-
expectedType?: string
|
|
17
|
-
actualType?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface SeedDiff {
|
|
21
|
-
slug: string
|
|
22
|
-
tableExists: boolean
|
|
23
|
-
columns: ColumnDiff[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function diffSeed(seed: Seed, options: WranglerOptions): Promise<SeedDiff> {
|
|
27
|
-
const tableName = `content_${seed.slug}`
|
|
28
|
-
const expected = getExpectedColumns(seed)
|
|
29
|
-
|
|
30
|
-
let actual: PragmaRow[]
|
|
31
|
-
try {
|
|
32
|
-
actual = queryD1<PragmaRow>(`PRAGMA table_info(${tableName})`, options)
|
|
33
|
-
} catch {
|
|
34
|
-
return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (actual.length === 0) {
|
|
38
|
-
return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const actualMap = new Map<string, PragmaRow>(actual.map(r => [r.name, r]))
|
|
42
|
-
const expectedSet = new Set<string>(expected.map(c => c.name))
|
|
43
|
-
|
|
44
|
-
const columns: ColumnDiff[] = []
|
|
45
|
-
|
|
46
|
-
for (const col of expected) {
|
|
47
|
-
const actual = actualMap.get(col.name)
|
|
48
|
-
if (!actual) {
|
|
49
|
-
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 })
|
|
52
|
-
} else {
|
|
53
|
-
columns.push({ name: col.name, status: 'ok' })
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
for (const row of actual) {
|
|
58
|
-
if (!expectedSet.has(row.name)) {
|
|
59
|
-
columns.push({ name: row.name, status: 'extra', actualType: row.type })
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { slug: seed.slug, tableExists: true, columns }
|
|
64
|
-
}
|
|
1
|
+
import type { Seed } from '@beechcms/core'
|
|
2
|
+
import { getExpectedColumns, type SchemaColumn } from '@beechcms/core'
|
|
3
|
+
import type { WranglerOptions, D1Row } from './wrangler.js'
|
|
4
|
+
import { queryD1 } from './wrangler.js'
|
|
5
|
+
|
|
6
|
+
interface PragmaRow extends D1Row {
|
|
7
|
+
name: string
|
|
8
|
+
type: string
|
|
9
|
+
notnull: number
|
|
10
|
+
pk: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ColumnDiff {
|
|
14
|
+
name: string
|
|
15
|
+
status: 'ok' | 'missing' | 'extra' | 'type_mismatch'
|
|
16
|
+
expectedType?: string
|
|
17
|
+
actualType?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SeedDiff {
|
|
21
|
+
slug: string
|
|
22
|
+
tableExists: boolean
|
|
23
|
+
columns: ColumnDiff[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function diffSeed(seed: Seed, options: WranglerOptions): Promise<SeedDiff> {
|
|
27
|
+
const tableName = `content_${seed.slug}`
|
|
28
|
+
const expected = getExpectedColumns(seed)
|
|
29
|
+
|
|
30
|
+
let actual: PragmaRow[]
|
|
31
|
+
try {
|
|
32
|
+
actual = queryD1<PragmaRow>(`PRAGMA table_info(${tableName})`, options)
|
|
33
|
+
} catch {
|
|
34
|
+
return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (actual.length === 0) {
|
|
38
|
+
return { slug: seed.slug, tableExists: false, columns: expected.map(c => ({ name: c.name, status: 'missing', expectedType: c.sqlType })) }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const actualMap = new Map<string, PragmaRow>(actual.map(r => [r.name, r]))
|
|
42
|
+
const expectedSet = new Set<string>(expected.map(c => c.name))
|
|
43
|
+
|
|
44
|
+
const columns: ColumnDiff[] = []
|
|
45
|
+
|
|
46
|
+
for (const col of expected) {
|
|
47
|
+
const actual = actualMap.get(col.name)
|
|
48
|
+
if (!actual) {
|
|
49
|
+
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 })
|
|
52
|
+
} else {
|
|
53
|
+
columns.push({ name: col.name, status: 'ok' })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const row of actual) {
|
|
58
|
+
if (!expectedSet.has(row.name)) {
|
|
59
|
+
columns.push({ name: row.name, status: 'extra', actualType: row.type })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { slug: seed.slug, tableExists: true, columns }
|
|
64
|
+
}
|
package/src/lib/wrangler.ts
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join, resolve } from 'node:path'
|
|
5
|
-
|
|
6
|
-
export interface WranglerOptions {
|
|
7
|
-
db: string
|
|
8
|
-
local: boolean
|
|
9
|
-
configPath: string | null
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface D1Row {
|
|
13
|
-
[key: string]: unknown
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface WranglerResult {
|
|
17
|
-
results: D1Row[]
|
|
18
|
-
success: boolean
|
|
19
|
-
error?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function buildArgs(options: WranglerOptions): string[] {
|
|
23
|
-
const args: string[] = []
|
|
24
|
-
if (options.configPath) args.push('--config', options.configPath)
|
|
25
|
-
if (options.local) args.push('--local')
|
|
26
|
-
else args.push('--remote')
|
|
27
|
-
return args
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Esegue SQL da file temporaneo via `wrangler d1 execute --file`. */
|
|
31
|
-
export function executeD1File(sql: string, options: WranglerOptions): void {
|
|
32
|
-
const tmpFile = join(tmpdir(), `beech-seed-${Date.now()}.sql`)
|
|
33
|
-
try {
|
|
34
|
-
writeFileSync(tmpFile, sql, 'utf-8')
|
|
35
|
-
const args = ['d1', 'execute', options.db, '--file', tmpFile, ...buildArgs(options)]
|
|
36
|
-
const result = spawnSync('npx', ['wrangler', ...args], { stdio: 'inherit', cwd: process.cwd(), shell: true })
|
|
37
|
-
if (result.status !== 0) {
|
|
38
|
-
process.exit(result.status ?? 1)
|
|
39
|
-
}
|
|
40
|
-
} finally {
|
|
41
|
-
try { rmSync(tmpFile) } catch {}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Esegue una query SQL e ritorna i risultati come array di oggetti (--json). */
|
|
46
|
-
export function queryD1<T extends D1Row = D1Row>(sql: string, options: WranglerOptions): T[] {
|
|
47
|
-
const args = ['d1', 'execute', options.db, '--command', sql, '--json', ...buildArgs(options)]
|
|
48
|
-
const result = spawnSync('npx', ['wrangler', ...args], { encoding: 'utf-8', cwd: process.cwd(), shell: true })
|
|
49
|
-
|
|
50
|
-
if (result.status !== 0) {
|
|
51
|
-
throw new Error(`wrangler d1 execute failed:\n${result.stderr}`)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const parsed: WranglerResult[] = JSON.parse(result.stdout)
|
|
56
|
-
return (parsed[0]?.results ?? []) as T[]
|
|
57
|
-
} catch {
|
|
58
|
-
throw new Error(`Failed to parse wrangler JSON output:\n${result.stdout}`)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Trova il path di wrangler.jsonc risalendo l'albero da CWD fino alla root del filesystem. */
|
|
63
|
-
export function findWranglerConfig(): string | null {
|
|
64
|
-
let dir = process.cwd()
|
|
65
|
-
while (true) {
|
|
66
|
-
for (const name of ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']) {
|
|
67
|
-
const p = resolve(dir, name)
|
|
68
|
-
if (existsSync(p)) return p
|
|
69
|
-
}
|
|
70
|
-
for (const name of ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']) {
|
|
71
|
-
const p = resolve(dir, 'apps', 'api', name)
|
|
72
|
-
if (existsSync(p)) return p
|
|
73
|
-
}
|
|
74
|
-
const parent = resolve(dir, '..')
|
|
75
|
-
if (parent === dir) break
|
|
76
|
-
dir = parent
|
|
77
|
-
}
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Risolve il nome del database D1 da wrangler.jsonc (stripping JSONC comments). */
|
|
82
|
-
export function resolveDbName(configPath: string | null): string {
|
|
83
|
-
if (!configPath) return 'beech-db'
|
|
84
|
-
try {
|
|
85
|
-
const raw = readFileSync(configPath, 'utf-8')
|
|
86
|
-
|
|
87
|
-
if (configPath.endsWith('.toml')) {
|
|
88
|
-
// Basic regex-based TOML parsing for d1_databases
|
|
89
|
-
// Matches both [d1_databases] and [[d1_databases]]
|
|
90
|
-
const d1SectionMatch = raw.match(/\[\[?d1_databases\]\]?[\s\S]*?(?=\n\[|$)/)
|
|
91
|
-
if (d1SectionMatch) {
|
|
92
|
-
const section = d1SectionMatch[0]
|
|
93
|
-
const dbNameMatch = section.match(/database_name\s*=\s*["'](.+?)["']/)
|
|
94
|
-
if (dbNameMatch) return dbNameMatch[1]
|
|
95
|
-
}
|
|
96
|
-
return 'beech-db'
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const stripped = raw
|
|
100
|
-
.replace(/\/\/[^\n]*/g, '')
|
|
101
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
102
|
-
const parsed = JSON.parse(stripped)
|
|
103
|
-
const bindings: { database_name?: string }[] = parsed?.d1_databases ?? []
|
|
104
|
-
return bindings[0]?.database_name ?? 'beech-db'
|
|
105
|
-
} catch {
|
|
106
|
-
return 'beech-db'
|
|
107
|
-
}
|
|
108
|
-
}
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
export interface WranglerOptions {
|
|
7
|
+
db: string
|
|
8
|
+
local: boolean
|
|
9
|
+
configPath: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface D1Row {
|
|
13
|
+
[key: string]: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface WranglerResult {
|
|
17
|
+
results: D1Row[]
|
|
18
|
+
success: boolean
|
|
19
|
+
error?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildArgs(options: WranglerOptions): string[] {
|
|
23
|
+
const args: string[] = []
|
|
24
|
+
if (options.configPath) args.push('--config', options.configPath)
|
|
25
|
+
if (options.local) args.push('--local')
|
|
26
|
+
else args.push('--remote')
|
|
27
|
+
return args
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Esegue SQL da file temporaneo via `wrangler d1 execute --file`. */
|
|
31
|
+
export function executeD1File(sql: string, options: WranglerOptions): void {
|
|
32
|
+
const tmpFile = join(tmpdir(), `beech-seed-${Date.now()}.sql`)
|
|
33
|
+
try {
|
|
34
|
+
writeFileSync(tmpFile, sql, 'utf-8')
|
|
35
|
+
const args = ['d1', 'execute', options.db, '--file', tmpFile, ...buildArgs(options)]
|
|
36
|
+
const result = spawnSync('npx', ['wrangler', ...args], { stdio: 'inherit', cwd: process.cwd(), shell: true })
|
|
37
|
+
if (result.status !== 0) {
|
|
38
|
+
process.exit(result.status ?? 1)
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
try { rmSync(tmpFile) } catch {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Esegue una query SQL e ritorna i risultati come array di oggetti (--json). */
|
|
46
|
+
export function queryD1<T extends D1Row = D1Row>(sql: string, options: WranglerOptions): T[] {
|
|
47
|
+
const args = ['d1', 'execute', options.db, '--command', sql, '--json', ...buildArgs(options)]
|
|
48
|
+
const result = spawnSync('npx', ['wrangler', ...args], { encoding: 'utf-8', cwd: process.cwd(), shell: true })
|
|
49
|
+
|
|
50
|
+
if (result.status !== 0) {
|
|
51
|
+
throw new Error(`wrangler d1 execute failed:\n${result.stderr}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const parsed: WranglerResult[] = JSON.parse(result.stdout)
|
|
56
|
+
return (parsed[0]?.results ?? []) as T[]
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(`Failed to parse wrangler JSON output:\n${result.stdout}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Trova il path di wrangler.jsonc risalendo l'albero da CWD fino alla root del filesystem. */
|
|
63
|
+
export function findWranglerConfig(): string | null {
|
|
64
|
+
let dir = process.cwd()
|
|
65
|
+
while (true) {
|
|
66
|
+
for (const name of ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']) {
|
|
67
|
+
const p = resolve(dir, name)
|
|
68
|
+
if (existsSync(p)) return p
|
|
69
|
+
}
|
|
70
|
+
for (const name of ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml']) {
|
|
71
|
+
const p = resolve(dir, 'apps', 'api', name)
|
|
72
|
+
if (existsSync(p)) return p
|
|
73
|
+
}
|
|
74
|
+
const parent = resolve(dir, '..')
|
|
75
|
+
if (parent === dir) break
|
|
76
|
+
dir = parent
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Risolve il nome del database D1 da wrangler.jsonc (stripping JSONC comments). */
|
|
82
|
+
export function resolveDbName(configPath: string | null): string {
|
|
83
|
+
if (!configPath) return 'beech-db'
|
|
84
|
+
try {
|
|
85
|
+
const raw = readFileSync(configPath, 'utf-8')
|
|
86
|
+
|
|
87
|
+
if (configPath.endsWith('.toml')) {
|
|
88
|
+
// Basic regex-based TOML parsing for d1_databases
|
|
89
|
+
// Matches both [d1_databases] and [[d1_databases]]
|
|
90
|
+
const d1SectionMatch = raw.match(/\[\[?d1_databases\]\]?[\s\S]*?(?=\n\[|$)/)
|
|
91
|
+
if (d1SectionMatch) {
|
|
92
|
+
const section = d1SectionMatch[0]
|
|
93
|
+
const dbNameMatch = section.match(/database_name\s*=\s*["'](.+?)["']/)
|
|
94
|
+
if (dbNameMatch) return dbNameMatch[1]
|
|
95
|
+
}
|
|
96
|
+
return 'beech-db'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const stripped = raw
|
|
100
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
101
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
102
|
+
const parsed = JSON.parse(stripped)
|
|
103
|
+
const bindings: { database_name?: string }[] = parsed?.d1_databases ?? []
|
|
104
|
+
return bindings[0]?.database_name ?? 'beech-db'
|
|
105
|
+
} catch {
|
|
106
|
+
return 'beech-db'
|
|
107
|
+
}
|
|
108
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"composite": true,
|
|
5
|
-
"declaration": true,
|
|
6
|
-
"declarationMap": true,
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"rootDir": "src",
|
|
9
|
-
"module": "ESNext",
|
|
10
|
-
"moduleResolution": "Bundler",
|
|
11
|
-
"target": "ES2022"
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*"],
|
|
14
|
-
"exclude": ["node_modules", "dist"],
|
|
15
|
-
"references": [{ "path": "../core" }]
|
|
16
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"target": "ES2022"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"],
|
|
15
|
+
"references": [{ "path": "../core" }]
|
|
16
|
+
}
|