Skip to content

Commit 168d210

Browse files
committed
feat: refactor dev into command group with dynamic custom subcommands
Restructure dev/dev:web/dev:celery into a `dev` command group (dev web, dev celery), and support user-defined dev commands from project .fba.json `devs` config. Custom commands are dynamically registered as subcommands and visible via `fba dev -h`. Made-with: Cursor
1 parent ff44139 commit 168d210

5 files changed

Lines changed: 78 additions & 16 deletions

File tree

src/commands/dev.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
// dev.ts — dev / dev:web / dev:celery 命令
1+
// dev.ts — dev 命令组:server / web / celery / 自定义
22
import * as clack from '@clack/prompts'
33
import chalk from 'chalk'
44
import { existsSync } from 'fs'
5-
import { join } from 'path'
5+
import { join, resolve } from 'path'
66
import { readProjectConfig, getInfraDir } from '../lib/config.js'
77
import { requireProjectDir, requireBackendDir, requireFrontendDir, warn } from '../lib/errors.js'
88
import { isComposeRunning, composeUp } from '../lib/docker.js'
99
import { runInherited } from '../lib/process.js'
1010
import { t } from '../lib/i18n.js'
11+
import type { DevEntry } from '../types/config.js'
1112

1213
/**
1314
* fba dev — 启动后端
@@ -91,3 +92,21 @@ export async function devCeleryAction(subcommand: string, options: { project?: s
9192
const exitCode = await runInherited('uv', ['run', 'fba', 'celery', subcommand], backendDir)
9293
process.exit(exitCode)
9394
}
95+
96+
/**
97+
* fba dev <name> — 运行自定义开发命令
98+
*/
99+
export async function devCustomAction(
100+
name: string,
101+
entry: DevEntry,
102+
options: { project?: string },
103+
) {
104+
const projectDir = requireProjectDir(options.project)
105+
const cwd = entry.pwd ? resolve(projectDir, entry.pwd) : projectDir
106+
107+
console.log(chalk.cyan(`\n ${t('devCustomStarting')} ${name}...\n`))
108+
109+
const [cmd, ...args] = entry.cmd.trim().split(/\s+/) as [string, ...string[]]
110+
const exitCode = await runInherited(cmd, args, cwd, { env: entry.envs })
111+
process.exit(exitCode)
112+
}

src/index.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
#!/usr/bin/env node
22
// index.ts — FBA CLI 入口:Commander 命令路由
3-
import { readFileSync } from 'fs'
3+
import { readFileSync, existsSync } from 'fs'
44
import { dirname, resolve } from 'path'
55
import { fileURLToPath } from 'url'
66
import { Command } from 'commander'
77
import chalk from 'chalk'
8-
import { readGlobalConfig } from './lib/config.js'
8+
import { readGlobalConfig, readProjectConfig, resolveProjectDir } from './lib/config.js'
99
import { initI18nFromConfig, t } from './lib/i18n.js'
1010

1111
const currentDir = dirname(fileURLToPath(import.meta.url))
@@ -55,10 +55,10 @@ program
5555
await addAction()
5656
})
5757

58-
// ─── dev ───
59-
program
58+
// ─── dev (command group) ───
59+
const devCmd = program
6060
.command('dev')
61-
.description(t('cmdDev'))
61+
.description(t('cmdDevGroup'))
6262
.option('--host <host>', t('optHost'), '127.0.0.1')
6363
.option('--port <port>', t('optPort'))
6464
.option('--no-reload', t('optNoReload'))
@@ -68,9 +68,8 @@ program
6868
await devAction({ ...options, project: program.opts().project })
6969
})
7070

71-
// ─── dev:web ───
72-
program
73-
.command('dev:web')
71+
devCmd
72+
.command('web')
7473
.description(t('cmdDevWeb'))
7574
.option('--host <host>', t('optHost'))
7675
.option('--port <port>', t('optPort'))
@@ -79,15 +78,43 @@ program
7978
await devWebAction({ ...options, project: program.opts().project })
8079
})
8180

82-
// ─── dev:celery ───
83-
program
84-
.command('dev:celery <subcommand>')
81+
devCmd
82+
.command('celery <subcommand>')
8583
.description(t('cmdDevCelery'))
8684
.action(async (subcommand) => {
8785
const { devCeleryAction } = await import('./commands/dev.js')
8886
await devCeleryAction(subcommand, { project: program.opts().project })
8987
})
9088

89+
// Dynamic dev subcommands from project .fba.json
90+
{
91+
const preProject = (() => {
92+
const args = process.argv.slice(2)
93+
for (let i = 0; i < args.length; i++) {
94+
if ((args[i] === '-p' || args[i] === '--project') && args[i + 1]) return args[i + 1]
95+
if (args[i]?.startsWith('--project=')) return args[i]!.slice('--project='.length)
96+
}
97+
return undefined
98+
})()
99+
const projectDir = resolveProjectDir(preProject)
100+
if (projectDir && existsSync(projectDir)) {
101+
const projectConfig = readProjectConfig(projectDir)
102+
if (projectConfig.devs) {
103+
const builtins = new Set(['web', 'celery', 'help'])
104+
for (const [name, entry] of Object.entries(projectConfig.devs)) {
105+
if (builtins.has(name)) continue
106+
devCmd
107+
.command(name)
108+
.description(entry.desc ?? entry.cmd)
109+
.action(async () => {
110+
const { devCustomAction } = await import('./commands/dev.js')
111+
await devCustomAction(name, entry, { project: program.opts().project })
112+
})
113+
}
114+
}
115+
}
116+
}
117+
91118
// ─── plugin ───
92119
const pluginCmd = program
93120
.command('plugin')

src/lib/i18n.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ const messages = {
205205
devStartingBackend: "正在启动后端服务器,端口:",
206206
devStartingFrontend: "正在启动前端开发服务器...",
207207
devStartingCelery: "正在启动 Celery",
208+
devCustomStarting: "正在启动",
208209
invalidSubcommand: "无效子命令",
209210
validOptions: "有效选项",
210211

@@ -253,6 +254,7 @@ const messages = {
253254
cliDescription:
254255
"创建、管理和运行 fastapi-best-architecture 项目的 Super CLI 工具",
255256
cmdCreate: "创建新的 FBA 项目(引导流程)",
257+
cmdDevGroup: "开发服务器命令",
256258
cmdDev: "启动后端开发服务器",
257259
cmdDevWeb: "启动前端开发服务器",
258260
cmdDevCelery: "启动 Celery 服务 (worker | beat | flower)",
@@ -478,6 +480,7 @@ const messages = {
478480
devStartingBackend: "Starting backend server on port",
479481
devStartingFrontend: "Starting frontend dev server...",
480482
devStartingCelery: "Starting Celery",
483+
devCustomStarting: "Starting",
481484
invalidSubcommand: "Invalid subcommand",
482485
validOptions: "Valid options",
483486

@@ -523,6 +526,7 @@ const messages = {
523526
cliDescription:
524527
"Super CLI tool for creating, managing, and running fastapi-best-architecture projects",
525528
cmdCreate: "Create a new FBA project (guided flow)",
529+
cmdDevGroup: "Development server commands",
526530
cmdDev: "Start backend development server",
527531
cmdDevWeb: "Start frontend development server",
528532
cmdDevCelery: "Start Celery service (worker | beat | flower)",

src/lib/process.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,20 +112,24 @@ export async function runInherited(
112112
cmd: string,
113113
args: string[] = [],
114114
cwd?: string,
115+
options?: { env?: Record<string, string> },
115116
): Promise<number> {
116-
// 保存 stdin 的 raw mode 状态,并恢复为正常模式
117117
const wasRaw = process.stdin.isRaw
118118
if (wasRaw && process.stdin.isTTY) {
119119
process.stdin.setRawMode(false)
120120
}
121121

122122
try {
123-
const result = await execa(cmd, args, { cwd, stdio: 'inherit', reject: false })
123+
const result = await execa(cmd, args, {
124+
cwd,
125+
stdio: 'inherit',
126+
reject: false,
127+
env: options?.env,
128+
})
124129
return result.exitCode ?? 1
125130
} catch {
126131
return 1
127132
} finally {
128-
// 恢复 stdin raw mode 状态
129133
if (wasRaw && process.stdin.isTTY) {
130134
process.stdin.setRawMode(true)
131135
}

src/types/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export interface ProjectEntry {
1616
createdAt: string
1717
}
1818

19+
export interface DevEntry {
20+
cmd: string
21+
pwd?: string
22+
envs?: Record<string, string>
23+
desc?: string
24+
}
25+
1926
export interface ProjectConfig {
2027
name: string
2128
backend_name: string
@@ -25,6 +32,7 @@ export interface ProjectConfig {
2532
infra: boolean
2633
infra_services: string[]
2734
db_type?: DatabaseType
35+
devs?: Record<string, DevEntry>
2836
}
2937

3038
export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {

0 commit comments

Comments
 (0)