@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/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beechcms/cli",
|
|
3
|
-
"version": "0.6.0-preview.
|
|
3
|
+
"version": "0.6.0-preview.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@beechcms/core": "^0.6.0-preview.
|
|
10
|
+
"@beechcms/core": "^0.6.0-preview.4",
|
|
11
11
|
"picocolors": "^1.1.1"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc --noEmit && esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --outfile=dist/index.js",
|
|
21
21
|
"dev": "esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --outfile=dist/index.js --watch",
|
|
22
|
+
"lint": "eslint .",
|
|
22
23
|
"test": "vitest run",
|
|
23
24
|
"test:coverage": "vitest run --coverage"
|
|
24
25
|
}
|
package/src/commands/deploy.ts
CHANGED
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
|
|
4
|
-
import pc from 'picocolors'
|
|
5
|
-
import { spawnSync } from 'node:child_process'
|
|
6
|
-
import { readFileSync } from 'node:fs'
|
|
7
|
-
import { findWranglerConfig } from '../lib/wrangler.js'
|
|
8
|
-
|
|
9
|
-
export interface DeployOptions {
|
|
10
|
-
skipSeed?: boolean
|
|
11
|
-
skipCheck?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function readWorkerName(configPath: string | null): string | null {
|
|
15
|
-
if (!configPath) return null
|
|
16
|
-
try {
|
|
17
|
-
const raw = readFileSync(configPath, 'utf-8')
|
|
18
|
-
const stripped = raw
|
|
19
|
-
.replace(/\/\/[^\n]*/g, '')
|
|
20
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
21
|
-
const parsed = JSON.parse(stripped)
|
|
22
|
-
return (parsed?.name as string) ?? null
|
|
23
|
-
} catch {
|
|
24
|
-
return null
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Extracts the first workers.dev URL from wrangler deploy stdout.
|
|
29
|
-
// wrangler writes progress to stderr (shown live) and the summary to stdout (captured).
|
|
30
|
-
function extractWorkerUrl(output: string): string | null {
|
|
31
|
-
const match = output.match(/https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.workers\.dev\b/)
|
|
32
|
-
return match?.[0] ?? null
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function checkAdmin(url: string): Promise<{ ok: boolean; status: number | null }> {
|
|
36
|
-
try {
|
|
37
|
-
const res = await fetch(`${url}/admin`, {
|
|
38
|
-
method: 'HEAD',
|
|
39
|
-
redirect: 'follow',
|
|
40
|
-
signal: AbortSignal.timeout(12_000),
|
|
41
|
-
})
|
|
42
|
-
return { ok: res.status < 500, status: res.status }
|
|
43
|
-
} catch {
|
|
44
|
-
return { ok: false, status: null }
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function deploy(args: DeployOptions): Promise<void> {
|
|
49
|
-
console.log(pc.cyan('\n beech deploy\n'))
|
|
50
|
-
|
|
51
|
-
// Step 1: wrangler deploy via npm run deploy.
|
|
52
|
-
// stdout captured to extract the deployed URL; stderr stays on the terminal for live progress.
|
|
53
|
-
console.log(pc.dim(' [1/3] Deploying Worker…\n'))
|
|
54
|
-
const deployResult = spawnSync('npm', ['run', 'deploy'], {
|
|
55
|
-
stdio: ['inherit', 'pipe', 'inherit'],
|
|
56
|
-
encoding: 'utf-8',
|
|
57
|
-
cwd: process.cwd(),
|
|
58
|
-
shell: true,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const deployStdout = deployResult.stdout ?? ''
|
|
62
|
-
if (deployStdout) process.stdout.write(deployStdout)
|
|
63
|
-
|
|
64
|
-
if (deployResult.status !== 0) {
|
|
65
|
-
console.log(pc.red('\n ✗ Worker deploy failed\n'))
|
|
66
|
-
console.log(pc.dim(' Check the wrangler output above for details.'))
|
|
67
|
-
console.log(pc.cyan('\n → Run: npx wrangler login # if not authenticated'))
|
|
68
|
-
console.log(pc.cyan(' → Or: Update wrangler.jsonc # if database_id is wrong\n'))
|
|
69
|
-
process.exit(1)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const deployedUrl = extractWorkerUrl(deployStdout)
|
|
73
|
-
console.log(pc.green('\n ✓ Worker deployed'))
|
|
74
|
-
|
|
75
|
-
// Step 2: seed:load --remote as a subprocess so that wrangler failures
|
|
76
|
-
// (which call process.exit internally) don't abort our own process.
|
|
77
|
-
if (args.skipSeed) {
|
|
78
|
-
console.log(pc.dim('\n [2/3] Skipping seed:load (--skip-seed)'))
|
|
79
|
-
} else {
|
|
80
|
-
console.log(pc.dim('\n [2/3] Syncing content schema to remote D1…\n'))
|
|
81
|
-
const seedResult = spawnSync('npx', ['beech', 'seed:load'], {
|
|
82
|
-
stdio: 'inherit',
|
|
83
|
-
cwd: process.cwd(),
|
|
84
|
-
shell: true,
|
|
85
|
-
})
|
|
86
|
-
if (seedResult.status !== 0) {
|
|
87
|
-
console.log(pc.yellow('\n ⚠ seed:load failed\n'))
|
|
88
|
-
console.log(pc.dim(' Sync the remote content schema manually:'))
|
|
89
|
-
console.log(pc.cyan(' → Run: npx beech seed:load\n'))
|
|
90
|
-
} else {
|
|
91
|
-
console.log(pc.green('\n ✓ Content schema synced'))
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Step 3: check /admin reachability.
|
|
96
|
-
// Use URL extracted from deploy output; fall back to worker name from wrangler.jsonc.
|
|
97
|
-
if (args.skipCheck) {
|
|
98
|
-
console.log(pc.dim('\n [3/3] Skipping admin check (--skip-check)\n'))
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const adminBase = deployedUrl ?? (() => {
|
|
103
|
-
const workerName = readWorkerName(findWranglerConfig())
|
|
104
|
-
// We can't reliably construct the full workers.dev subdomain without knowing the account,
|
|
105
|
-
// so only use the name-based URL as a fallback when nothing better is available.
|
|
106
|
-
return workerName ? `https://${workerName}.workers.dev` : null
|
|
107
|
-
})()
|
|
108
|
-
|
|
109
|
-
if (!adminBase) {
|
|
110
|
-
console.log(pc.dim('\n [3/3] Could not determine worker URL — skipping admin check\n'))
|
|
111
|
-
console.log(pc.dim(' The deployed URL is printed by wrangler above. Open <url>/admin to verify.\n'))
|
|
112
|
-
return
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
console.log(pc.dim(`\n [3/3] Checking ${adminBase}/admin…\n`))
|
|
116
|
-
const { ok, status } = await checkAdmin(adminBase)
|
|
117
|
-
|
|
118
|
-
if (ok) {
|
|
119
|
-
console.log(pc.green(` ✓ Admin reachable at: ${adminBase}/admin\n`))
|
|
120
|
-
} else {
|
|
121
|
-
const statusStr = status != null ? ` (HTTP ${status})` : ''
|
|
122
|
-
console.log(pc.yellow(` ⚠ Admin returned an error${statusStr} at: ${adminBase}/admin\n`))
|
|
123
|
-
console.log(pc.dim(' The database may not be fully initialized.'))
|
|
124
|
-
console.log(pc.cyan(' → Run: npx beech init --db --remote\n'))
|
|
125
|
-
}
|
|
126
|
-
}
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
|
|
4
|
+
import pc from 'picocolors'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { findWranglerConfig } from '../lib/wrangler.js'
|
|
8
|
+
|
|
9
|
+
export interface DeployOptions {
|
|
10
|
+
skipSeed?: boolean
|
|
11
|
+
skipCheck?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readWorkerName(configPath: string | null): string | null {
|
|
15
|
+
if (!configPath) return null
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(configPath, 'utf-8')
|
|
18
|
+
const stripped = raw
|
|
19
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
20
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
21
|
+
const parsed = JSON.parse(stripped)
|
|
22
|
+
return (parsed?.name as string) ?? null
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Extracts the first workers.dev URL from wrangler deploy stdout.
|
|
29
|
+
// wrangler writes progress to stderr (shown live) and the summary to stdout (captured).
|
|
30
|
+
function extractWorkerUrl(output: string): string | null {
|
|
31
|
+
const match = output.match(/https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.workers\.dev\b/)
|
|
32
|
+
return match?.[0] ?? null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function checkAdmin(url: string): Promise<{ ok: boolean; status: number | null }> {
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${url}/admin`, {
|
|
38
|
+
method: 'HEAD',
|
|
39
|
+
redirect: 'follow',
|
|
40
|
+
signal: AbortSignal.timeout(12_000),
|
|
41
|
+
})
|
|
42
|
+
return { ok: res.status < 500, status: res.status }
|
|
43
|
+
} catch {
|
|
44
|
+
return { ok: false, status: null }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function deploy(args: DeployOptions): Promise<void> {
|
|
49
|
+
console.log(pc.cyan('\n beech deploy\n'))
|
|
50
|
+
|
|
51
|
+
// Step 1: wrangler deploy via npm run deploy.
|
|
52
|
+
// stdout captured to extract the deployed URL; stderr stays on the terminal for live progress.
|
|
53
|
+
console.log(pc.dim(' [1/3] Deploying Worker…\n'))
|
|
54
|
+
const deployResult = spawnSync('npm', ['run', 'deploy'], {
|
|
55
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
cwd: process.cwd(),
|
|
58
|
+
shell: true,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const deployStdout = deployResult.stdout ?? ''
|
|
62
|
+
if (deployStdout) process.stdout.write(deployStdout)
|
|
63
|
+
|
|
64
|
+
if (deployResult.status !== 0) {
|
|
65
|
+
console.log(pc.red('\n ✗ Worker deploy failed\n'))
|
|
66
|
+
console.log(pc.dim(' Check the wrangler output above for details.'))
|
|
67
|
+
console.log(pc.cyan('\n → Run: npx wrangler login # if not authenticated'))
|
|
68
|
+
console.log(pc.cyan(' → Or: Update wrangler.jsonc # if database_id is wrong\n'))
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const deployedUrl = extractWorkerUrl(deployStdout)
|
|
73
|
+
console.log(pc.green('\n ✓ Worker deployed'))
|
|
74
|
+
|
|
75
|
+
// Step 2: seed:load --remote as a subprocess so that wrangler failures
|
|
76
|
+
// (which call process.exit internally) don't abort our own process.
|
|
77
|
+
if (args.skipSeed) {
|
|
78
|
+
console.log(pc.dim('\n [2/3] Skipping seed:load (--skip-seed)'))
|
|
79
|
+
} else {
|
|
80
|
+
console.log(pc.dim('\n [2/3] Syncing content schema to remote D1…\n'))
|
|
81
|
+
const seedResult = spawnSync('npx', ['beech', 'seed:load'], {
|
|
82
|
+
stdio: 'inherit',
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
shell: true,
|
|
85
|
+
})
|
|
86
|
+
if (seedResult.status !== 0) {
|
|
87
|
+
console.log(pc.yellow('\n ⚠ seed:load failed\n'))
|
|
88
|
+
console.log(pc.dim(' Sync the remote content schema manually:'))
|
|
89
|
+
console.log(pc.cyan(' → Run: npx beech seed:load\n'))
|
|
90
|
+
} else {
|
|
91
|
+
console.log(pc.green('\n ✓ Content schema synced'))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Step 3: check /admin reachability.
|
|
96
|
+
// Use URL extracted from deploy output; fall back to worker name from wrangler.jsonc.
|
|
97
|
+
if (args.skipCheck) {
|
|
98
|
+
console.log(pc.dim('\n [3/3] Skipping admin check (--skip-check)\n'))
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const adminBase = deployedUrl ?? (() => {
|
|
103
|
+
const workerName = readWorkerName(findWranglerConfig())
|
|
104
|
+
// We can't reliably construct the full workers.dev subdomain without knowing the account,
|
|
105
|
+
// so only use the name-based URL as a fallback when nothing better is available.
|
|
106
|
+
return workerName ? `https://${workerName}.workers.dev` : null
|
|
107
|
+
})()
|
|
108
|
+
|
|
109
|
+
if (!adminBase) {
|
|
110
|
+
console.log(pc.dim('\n [3/3] Could not determine worker URL — skipping admin check\n'))
|
|
111
|
+
console.log(pc.dim(' The deployed URL is printed by wrangler above. Open <url>/admin to verify.\n'))
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(pc.dim(`\n [3/3] Checking ${adminBase}/admin…\n`))
|
|
116
|
+
const { ok, status } = await checkAdmin(adminBase)
|
|
117
|
+
|
|
118
|
+
if (ok) {
|
|
119
|
+
console.log(pc.green(` ✓ Admin reachable at: ${adminBase}/admin\n`))
|
|
120
|
+
} else {
|
|
121
|
+
const statusStr = status != null ? ` (HTTP ${status})` : ''
|
|
122
|
+
console.log(pc.yellow(` ⚠ Admin returned an error${statusStr} at: ${adminBase}/admin\n`))
|
|
123
|
+
console.log(pc.dim(' The database may not be fully initialized.'))
|
|
124
|
+
console.log(pc.cyan(' → Run: npx beech init --db --remote\n'))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
|
|
4
|
+
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
5
|
+
import { dirname, resolve } from 'node:path'
|
|
6
|
+
import pc from 'picocolors'
|
|
7
|
+
import type { Seed } from '@beechcms/core'
|
|
8
|
+
import { generateSeedTypes } from '@beechcms/core'
|
|
9
|
+
import {
|
|
10
|
+
queryD1,
|
|
11
|
+
findWranglerConfig,
|
|
12
|
+
resolveDbName,
|
|
13
|
+
type WranglerOptions,
|
|
14
|
+
} from '../lib/wrangler.js'
|
|
15
|
+
|
|
16
|
+
export interface GenerateTypesOptions {
|
|
17
|
+
/** Output path for the generated .ts file. */
|
|
18
|
+
out: string
|
|
19
|
+
/** Read from in-code SEED_REGISTRY (true) instead of introspecting D1 (false). */
|
|
20
|
+
local: boolean
|
|
21
|
+
/** Pre-resolved registry (injected by bin/ for --local, and by tests). */
|
|
22
|
+
registry?: Record<string, Seed> | null
|
|
23
|
+
/** Override D1 database name (remote path only). */
|
|
24
|
+
db?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SeedRow { slug: string; definition: string; [key: string]: unknown }
|
|
28
|
+
|
|
29
|
+
/** Remote path: read canonical Seed JSON from the `seeds` system table. */
|
|
30
|
+
function loadSeedsFromD1(db: string): Seed[] {
|
|
31
|
+
const configPath = findWranglerConfig()
|
|
32
|
+
const options: WranglerOptions = { db, local: false, configPath }
|
|
33
|
+
const rows = queryD1<SeedRow>(
|
|
34
|
+
`SELECT slug, definition FROM seeds WHERE status = 'active';`,
|
|
35
|
+
options,
|
|
36
|
+
)
|
|
37
|
+
return rows.map(r => JSON.parse(r.definition) as Seed)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function generateTypes(args: GenerateTypesOptions): Promise<void> {
|
|
41
|
+
let seeds: Seed[]
|
|
42
|
+
|
|
43
|
+
if (args.local) {
|
|
44
|
+
const registry = args.registry ?? {}
|
|
45
|
+
if (Object.keys(registry).length === 0) {
|
|
46
|
+
console.log(pc.red('\n ✗ No seeds found (seeds.ts empty or missing).\n'))
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
seeds = Object.values(registry)
|
|
50
|
+
} else {
|
|
51
|
+
const db = args.db ?? resolveDbName(findWranglerConfig())
|
|
52
|
+
seeds = loadSeedsFromD1(db)
|
|
53
|
+
if (seeds.length === 0) {
|
|
54
|
+
console.log(pc.red(`\n ✗ No active seeds in D1 (${db}). Run \`beech seed:load\` first.\n`))
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const code = generateSeedTypes(seeds)
|
|
60
|
+
const outPath = resolve(process.cwd(), args.out)
|
|
61
|
+
mkdirSync(dirname(outPath), { recursive: true })
|
|
62
|
+
writeFileSync(outPath, code, 'utf-8')
|
|
63
|
+
|
|
64
|
+
console.log(pc.green(`\n ✓ Generated ${seeds.length} interface(s) → ${args.out}\n`))
|
|
65
|
+
}
|