@ranger1/dx 0.1.0
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/LICENSE +21 -0
- package/README.md +103 -0
- package/bin/dx-with-version-env.js +8 -0
- package/bin/dx.js +86 -0
- package/lib/backend-package.js +664 -0
- package/lib/cli/args.js +19 -0
- package/lib/cli/commands/core.js +233 -0
- package/lib/cli/commands/db.js +239 -0
- package/lib/cli/commands/deploy.js +76 -0
- package/lib/cli/commands/export.js +34 -0
- package/lib/cli/commands/package.js +22 -0
- package/lib/cli/commands/stack.js +451 -0
- package/lib/cli/commands/start.js +83 -0
- package/lib/cli/commands/worktree.js +149 -0
- package/lib/cli/dx-cli.js +864 -0
- package/lib/cli/flags.js +96 -0
- package/lib/cli/help.js +209 -0
- package/lib/cli/index.js +4 -0
- package/lib/confirm.js +213 -0
- package/lib/env.js +296 -0
- package/lib/exec.js +643 -0
- package/lib/logger.js +188 -0
- package/lib/run-with-version-env.js +173 -0
- package/lib/sdk-build.js +424 -0
- package/lib/start-dev.js +401 -0
- package/lib/telegram-webhook.js +134 -0
- package/lib/validate-env.js +284 -0
- package/lib/vercel-deploy.js +237 -0
- package/lib/worktree.js +1032 -0
- package/package.json +34 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
import { confirmManager } from '../../confirm.js'
|
|
3
|
+
import { execManager } from '../../exec.js'
|
|
4
|
+
import { showHelp, showCommandHelp } from '../help.js'
|
|
5
|
+
|
|
6
|
+
export function handleHelp(cli, args = []) {
|
|
7
|
+
void cli
|
|
8
|
+
if (args[0]) showCommandHelp(args[0])
|
|
9
|
+
else showHelp()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function handleDev(cli, args = []) {
|
|
13
|
+
return cli.reportDevCommandRemoved(args)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function handleBuild(cli, args) {
|
|
17
|
+
const target = args[0] || 'all'
|
|
18
|
+
const environment = cli.determineEnvironment()
|
|
19
|
+
const envKey = cli.normalizeEnvKey(environment)
|
|
20
|
+
|
|
21
|
+
const buildConfig = cli.commands.build[target]
|
|
22
|
+
if (!buildConfig) {
|
|
23
|
+
// 兼容用户误输入或快捷命令,把 "all./scripts/dx" 这类粘连错误拆分
|
|
24
|
+
const fixed = String(target).split(/\s|\t|\r|\n|\u00A0|\.|\//)[0]
|
|
25
|
+
if (fixed && cli.commands.build[fixed]) {
|
|
26
|
+
logger.warn(`自动修正构建目标: ${target} -> ${fixed}`)
|
|
27
|
+
return await handleBuild(cli, [fixed])
|
|
28
|
+
}
|
|
29
|
+
logger.error(`未找到构建目标: ${target}`)
|
|
30
|
+
process.exitCode = 1
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
logger.step(`构建 ${target} (${environment})`)
|
|
35
|
+
|
|
36
|
+
// 处理嵌套配置
|
|
37
|
+
let config = buildConfig
|
|
38
|
+
if (typeof config === 'object' && !config.command) {
|
|
39
|
+
// 如果是嵌套配置,尝试获取环境特定的配置(兼容 dev/prod 与 development/production 命名)
|
|
40
|
+
if (config[envKey]) config = config[envKey]
|
|
41
|
+
else if (envKey === 'staging' && config.prod) config = config.prod
|
|
42
|
+
else config = config.dev || config
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (config.concurrent) {
|
|
46
|
+
await cli.handleConcurrentCommands(config.commands, 'build', envKey)
|
|
47
|
+
} else if (config.sequential) {
|
|
48
|
+
await cli.handleSequentialCommands(config.commands, envKey)
|
|
49
|
+
} else {
|
|
50
|
+
await cli.executeCommand(config)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function handleTest(cli, args) {
|
|
55
|
+
const type = args[0] || 'e2e'
|
|
56
|
+
const target = args[1] || 'all'
|
|
57
|
+
const testPath = args[2] // 可选的测试文件路径
|
|
58
|
+
|
|
59
|
+
// 解析 -t 参数用于指定特定测试用例(使用原始参数列表)
|
|
60
|
+
const allArgs = cli.args // 使用原始参数列表包含所有标志
|
|
61
|
+
const testNamePatternIndex = allArgs.indexOf('-t')
|
|
62
|
+
let testNamePattern = null
|
|
63
|
+
if (testNamePatternIndex !== -1 && testNamePatternIndex + 1 < allArgs.length) {
|
|
64
|
+
testNamePattern = allArgs[testNamePatternIndex + 1]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 根据测试类型自动设置环境标志
|
|
68
|
+
if (type === 'e2e' && !cli.flags.e2e) {
|
|
69
|
+
cli.flags.e2e = true
|
|
70
|
+
} else if (type === 'unit' && !cli.flags.test) {
|
|
71
|
+
cli.flags.test = true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let testConfig = cli.commands.test[type]?.[target] || cli.commands.test[type]
|
|
75
|
+
|
|
76
|
+
if (!testConfig) {
|
|
77
|
+
logger.error(`未找到测试配置: ${type}.${target}`)
|
|
78
|
+
process.exitCode = 1
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 如果提供了测试文件路径,使用专门的单文件测试配置
|
|
83
|
+
if (testPath && type === 'e2e' && target === 'backend') {
|
|
84
|
+
let command = `pnpm --filter ./apps/backend run test:e2e:file ${testPath}`
|
|
85
|
+
|
|
86
|
+
// 如果指定了测试用例名称,添加 -t 参数
|
|
87
|
+
if (testNamePattern) {
|
|
88
|
+
const escapedPattern = String(testNamePattern).replace(/(["`\\$])/g, '\\$1')
|
|
89
|
+
command += ` -t "${escapedPattern}"`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
testConfig = {
|
|
93
|
+
...testConfig,
|
|
94
|
+
command: command,
|
|
95
|
+
description: testNamePattern
|
|
96
|
+
? `运行单个E2E测试文件的特定用例: ${testPath} -> ${testNamePattern}`
|
|
97
|
+
: `运行单个E2E测试文件: ${testPath}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (testNamePattern) {
|
|
101
|
+
logger.step(`运行 ${type} 测试用例: ${testNamePattern} (文件: ${testPath})`)
|
|
102
|
+
} else {
|
|
103
|
+
logger.step(`运行单个 ${type} 测试: ${testPath}`)
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
logger.step(`运行 ${type} 测试`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await cli.executeCommand(testConfig)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function handleLint(cli, args) {
|
|
113
|
+
void args
|
|
114
|
+
const baseConfig = cli.commands.lint
|
|
115
|
+
if (!baseConfig || !baseConfig.command) {
|
|
116
|
+
logger.error('未找到 lint 命令配置')
|
|
117
|
+
process.exitCode = 1
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const config = { ...baseConfig }
|
|
122
|
+
|
|
123
|
+
if (cli.flags.fix) {
|
|
124
|
+
logger.step('运行代码检查(自动修复模式: --fix)')
|
|
125
|
+
const cmd = String(config.command)
|
|
126
|
+
// 若已包含 ` -- ` 分隔符,直接在末尾追加 --fix;否则通过 `--` 传递给 Nx 下游
|
|
127
|
+
config.command = cmd.includes(' -- ')
|
|
128
|
+
? `${cmd} --fix`
|
|
129
|
+
: `${cmd} -- --fix`
|
|
130
|
+
} else {
|
|
131
|
+
logger.step('运行代码检查')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await cli.executeCommand(config)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function handleClean(cli, args) {
|
|
138
|
+
const target = args[0] || 'all'
|
|
139
|
+
const cleanConfig = cli.commands.clean[target]
|
|
140
|
+
|
|
141
|
+
if (!cleanConfig) {
|
|
142
|
+
logger.error(`未找到清理目标: ${target}`)
|
|
143
|
+
process.exitCode = 1
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 危险操作确认
|
|
148
|
+
if (cleanConfig.dangerous) {
|
|
149
|
+
const confirmed = await confirmManager.confirmDangerous(
|
|
150
|
+
`清理操作: ${target}`,
|
|
151
|
+
'当前环境',
|
|
152
|
+
cli.flags.Y
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if (!confirmed) {
|
|
156
|
+
logger.info('操作已取消')
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
logger.step(`清理 ${target}`)
|
|
162
|
+
await cli.executeCommand(cleanConfig)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function handleCache(cli, args) {
|
|
166
|
+
const action = args[0] || 'clear'
|
|
167
|
+
const cacheConfig = cli.commands.cache?.[action]
|
|
168
|
+
|
|
169
|
+
if (!cacheConfig) {
|
|
170
|
+
logger.error(`未找到缓存操作: ${action}`)
|
|
171
|
+
logger.info('用法: dx cache clear')
|
|
172
|
+
process.exitCode = 1
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 危险操作确认
|
|
177
|
+
if (cacheConfig.dangerous) {
|
|
178
|
+
const confirmed = await confirmManager.confirmDangerous(
|
|
179
|
+
`缓存清理: ${action}`,
|
|
180
|
+
'当前环境',
|
|
181
|
+
cli.flags.Y
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if (!confirmed) {
|
|
185
|
+
logger.info('操作已取消')
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 二次确认(更醒目):强调将清理全局 pnpm store 与 ~/.pnpm-store
|
|
190
|
+
if (!cli.flags.Y && action === 'clear') {
|
|
191
|
+
const second = await confirmManager.confirm(
|
|
192
|
+
'二次确认:将清理全局 pnpm store 与 ~/.pnpm-store,可能影响其他项目,是否继续?',
|
|
193
|
+
false,
|
|
194
|
+
false
|
|
195
|
+
)
|
|
196
|
+
if (!second) {
|
|
197
|
+
logger.info('操作已取消')
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
logger.step(`执行缓存操作: ${action}`)
|
|
204
|
+
await cli.executeCommand(cacheConfig)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function handleInstall(cli, args) {
|
|
208
|
+
void args
|
|
209
|
+
const installConfig = cli.commands.install
|
|
210
|
+
if (!installConfig) {
|
|
211
|
+
logger.error('未找到 install 命令配置')
|
|
212
|
+
process.exitCode = 1
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
logger.step('安装依赖')
|
|
217
|
+
await cli.executeCommand(installConfig)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function handleStatus(cli, args) {
|
|
221
|
+
void args
|
|
222
|
+
logger.step('系统状态')
|
|
223
|
+
|
|
224
|
+
const status = execManager.getStatus()
|
|
225
|
+
console.log(`运行中的进程: ${status.runningProcesses}`)
|
|
226
|
+
|
|
227
|
+
if (status.processes.length > 0) {
|
|
228
|
+
logger.table(
|
|
229
|
+
status.processes.map(p => [p.id, p.command, `${Math.round(p.duration/1000)}s`]),
|
|
230
|
+
['进程ID', '命令', '运行时长']
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
import { confirmManager } from '../../confirm.js'
|
|
3
|
+
import { envManager } from '../../env.js'
|
|
4
|
+
import { getPassthroughArgs } from '../args.js'
|
|
5
|
+
|
|
6
|
+
export async function handleDatabase(cli, args) {
|
|
7
|
+
const action = args[0]
|
|
8
|
+
if (!action) {
|
|
9
|
+
logger.error('请指定数据库操作: generate, migrate, deploy, reset, seed, format, script')
|
|
10
|
+
process.exitCode = 1
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const dbConfig = cli.commands.db[action]
|
|
15
|
+
if (!dbConfig) {
|
|
16
|
+
logger.error(`未找到数据库操作: ${action}`)
|
|
17
|
+
process.exitCode = 1
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const environment = cli.determineEnvironment()
|
|
22
|
+
const envKey = cli.normalizeEnvKey(environment)
|
|
23
|
+
|
|
24
|
+
// 兼容旧用法:非 dev 环境使用 migrate 时给出明确提示,推荐改用 deploy
|
|
25
|
+
if (action === 'migrate' && envKey && envKey !== 'dev') {
|
|
26
|
+
const envFlag = cli.getEnvironmentFlagExample(envKey) || `--${envKey}`
|
|
27
|
+
logger.warn(`检测到在非开发环境执行 migrate: ${environment || envKey}`)
|
|
28
|
+
logger.info('建议仅在开发环境使用 `dx db migrate --dev --name <migration-name>` 创建迁移。')
|
|
29
|
+
logger.info(`如需在当前环境应用已有迁移,请使用: ${cli.invocation} db deploy ${envFlag}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 处理 script 子命令
|
|
33
|
+
if (action === 'script') {
|
|
34
|
+
const scriptName = args[1]
|
|
35
|
+
if (!scriptName) {
|
|
36
|
+
logger.error('请指定要运行的脚本名称')
|
|
37
|
+
logger.info('用法: dx db script <script-name> [环境标志]')
|
|
38
|
+
logger.info('示例: dx db script fix-email-verified-status --dev')
|
|
39
|
+
process.exitCode = 1
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
return await handleDatabaseScript(cli, scriptName, envKey, dbConfig)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.step(`执行数据库操作: ${action} (${environment})`)
|
|
46
|
+
|
|
47
|
+
// 处理嵌套配置
|
|
48
|
+
let config = dbConfig
|
|
49
|
+
if (typeof config === 'object' && !config.command) {
|
|
50
|
+
// 如果是嵌套配置,尝试获取环境特定的配置(兼容 dev/prod 与 development/production 命名)
|
|
51
|
+
if (config[envKey]) config = config[envKey]
|
|
52
|
+
else if (envKey === 'staging' && config.prod) config = config.prod
|
|
53
|
+
else config = config.dev || config
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 危险操作确认
|
|
57
|
+
if (config.dangerous) {
|
|
58
|
+
const confirmed = await confirmManager.confirmDatabaseOperation(
|
|
59
|
+
action,
|
|
60
|
+
envManager.getEnvironmentDescription(environment),
|
|
61
|
+
cli.flags.Y,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (!confirmed) {
|
|
65
|
+
logger.info('操作已取消')
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 支持为 migrate 传入迁移名:--name/-n
|
|
71
|
+
let command = config.command
|
|
72
|
+
if (action === 'migrate' && envKey === 'dev') {
|
|
73
|
+
const allArgs = cli.args
|
|
74
|
+
let migrationName = null
|
|
75
|
+
// 优先解析显式标志 --name/-n
|
|
76
|
+
const nameIdx = allArgs.indexOf('--name')
|
|
77
|
+
const shortIdx = allArgs.indexOf('-n')
|
|
78
|
+
if (nameIdx !== -1 && nameIdx + 1 < allArgs.length) {
|
|
79
|
+
migrationName = allArgs[nameIdx + 1]
|
|
80
|
+
} else if (shortIdx !== -1 && shortIdx + 1 < allArgs.length) {
|
|
81
|
+
migrationName = allArgs[shortIdx + 1]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!migrationName) {
|
|
85
|
+
logger.error('开发环境执行 migrate 时必须通过 --name 或 -n 指定迁移名称(禁止位置参数)')
|
|
86
|
+
logger.info('原因: 缺少迁移名会进入 Prisma 的交互式输入流程,脚本将被阻塞。')
|
|
87
|
+
logger.info(`正确示例: ${cli.invocation} db migrate --dev --name init-user-table`)
|
|
88
|
+
logger.info(`如仅需应用已有迁移,请使用 ${cli.invocation} db deploy --dev。`)
|
|
89
|
+
logger.info(`提示: 执行 ${cli.invocation} help db 查看完整用法与更多示例。`)
|
|
90
|
+
process.exitCode = 1
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const escaped = String(migrationName).replace(/(["`\\$])/g, '\\$1')
|
|
95
|
+
// 将参数传递给 Nx 下游 prisma:需要通过 -- 分割
|
|
96
|
+
command = `${command} -- --name "${escaped}"`
|
|
97
|
+
logger.info(`使用迁移名: ${migrationName}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 对以下数据库操作固定禁用 Nx 缓存,避免命中缓存导致未实际执行:generate/migrate/deploy/reset/seed
|
|
101
|
+
const disableCacheActions = new Set(['generate', 'migrate', 'deploy', 'reset', 'seed'])
|
|
102
|
+
const extraEnv = disableCacheActions.has(action) ? { NX_CACHE: 'false' } : {}
|
|
103
|
+
if (disableCacheActions.has(action)) {
|
|
104
|
+
logger.info('为数据库操作禁用 Nx 缓存: NX_CACHE=false')
|
|
105
|
+
// 为每个子命令注入 --skip-nx-cache(支持 && || ; 拼接的多命令)
|
|
106
|
+
command = command
|
|
107
|
+
.split(/(\s*&&\s*|\s*\|\|\s*|\s*;\s*)/)
|
|
108
|
+
.map(part => {
|
|
109
|
+
// 保留分隔符原样
|
|
110
|
+
if (/^(\s*&&\s*|\s*\|\|\s*|\s*;\s*)$/.test(part)) return part
|
|
111
|
+
// 跳过空字符串
|
|
112
|
+
if (!part.trim()) return part
|
|
113
|
+
// 只对 npx nx 命令注入
|
|
114
|
+
if (!part.includes('npx nx')) return part
|
|
115
|
+
// 在 -- 之前插入,或追加到末尾
|
|
116
|
+
if (part.includes(' -- ')) {
|
|
117
|
+
return part.replace(' -- ', ' --skip-nx-cache -- ')
|
|
118
|
+
}
|
|
119
|
+
return `${part} --skip-nx-cache`
|
|
120
|
+
})
|
|
121
|
+
.join('')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (action === 'reset' && envKey !== 'prod') {
|
|
125
|
+
extraEnv.PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION = 'yes'
|
|
126
|
+
logger.info('非生产环境重置数据库,已自动确认危险操作: PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION=yes')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const execFlags = { ...cli.flags }
|
|
130
|
+
;['dev', 'development', 'prod', 'production', 'test', 'e2e', 'staging', 'stage'].forEach(
|
|
131
|
+
key => delete execFlags[key],
|
|
132
|
+
)
|
|
133
|
+
if (envKey === 'prod') execFlags.prod = true
|
|
134
|
+
else if (envKey === 'dev') execFlags.dev = true
|
|
135
|
+
else if (envKey === 'test') execFlags.test = true
|
|
136
|
+
else if (envKey === 'e2e') execFlags.e2e = true
|
|
137
|
+
else if (envKey === 'staging') execFlags.staging = true
|
|
138
|
+
|
|
139
|
+
await cli.executeCommand(
|
|
140
|
+
{ ...config, command, env: { ...(config.env || {}), ...extraEnv } },
|
|
141
|
+
execFlags,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function handleDatabaseScript(cli, scriptName, envKey, dbConfig) {
|
|
146
|
+
// 自动去除 .ts 扩展名(如果用户提供了)
|
|
147
|
+
const cleanScriptName = scriptName.endsWith('.ts') ? scriptName.slice(0, -3) : scriptName
|
|
148
|
+
|
|
149
|
+
// 基础路径校验,避免路径遍历
|
|
150
|
+
if (
|
|
151
|
+
cleanScriptName.includes('/') ||
|
|
152
|
+
cleanScriptName.includes('\\') ||
|
|
153
|
+
cleanScriptName.includes('..')
|
|
154
|
+
) {
|
|
155
|
+
logger.error(`脚本名称不能包含路径分隔符或父目录引用: ${cleanScriptName}`)
|
|
156
|
+
process.exitCode = 1
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { existsSync } = await import('fs')
|
|
161
|
+
const { join, resolve, relative } = await import('path')
|
|
162
|
+
const scriptsRoot = join(process.cwd(), 'apps/backend/prisma/scripts')
|
|
163
|
+
const scriptPath = resolve(scriptsRoot, `${cleanScriptName}.ts`)
|
|
164
|
+
|
|
165
|
+
if (relative(scriptsRoot, scriptPath).startsWith('..')) {
|
|
166
|
+
logger.error(`脚本路径解析结果已超出允许目录: ${scriptPath}`)
|
|
167
|
+
process.exitCode = 1
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!existsSync(scriptPath)) {
|
|
172
|
+
logger.error(`脚本文件不存在: ${scriptPath}`)
|
|
173
|
+
logger.info('可用的脚本文件位于: apps/backend/prisma/scripts/')
|
|
174
|
+
process.exitCode = 1
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const environment = envKey === 'dev' ? 'development' : envKey === 'prod' ? 'production' : envKey
|
|
179
|
+
logger.step(`执行数据库脚本: ${cleanScriptName} (${environment})`)
|
|
180
|
+
logger.info(`脚本路径: ${scriptPath}`)
|
|
181
|
+
|
|
182
|
+
// 处理嵌套配置
|
|
183
|
+
let config = dbConfig
|
|
184
|
+
if (typeof config === 'object' && !config.command) {
|
|
185
|
+
if (config[envKey]) config = config[envKey]
|
|
186
|
+
else if (envKey === 'staging' && config.prod) config = config.prod
|
|
187
|
+
else config = config.dev || config
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 危险操作确认
|
|
191
|
+
if (config.dangerous && envKey === 'prod') {
|
|
192
|
+
const confirmed = await confirmManager.confirmDatabaseOperation(
|
|
193
|
+
`script: ${cleanScriptName}`,
|
|
194
|
+
envManager.getEnvironmentDescription(environment),
|
|
195
|
+
cli.flags.Y,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if (!confirmed) {
|
|
199
|
+
logger.info('操作已取消')
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 替换命令中的脚本名称占位符
|
|
205
|
+
let command = config.command.replace('{SCRIPT_NAME}', cleanScriptName)
|
|
206
|
+
|
|
207
|
+
// 获取 -- 后面的 passthrough 参数并追加到命令
|
|
208
|
+
const passthroughArgs = getPassthroughArgs(cli.args)
|
|
209
|
+
if (passthroughArgs.length > 0) {
|
|
210
|
+
const escapedArgs = passthroughArgs.map(arg => {
|
|
211
|
+
// 对包含空格或特殊字符的参数加引号
|
|
212
|
+
if (/[\s"'`$\\]/.test(arg)) {
|
|
213
|
+
return `"${arg.replace(/(["`\\$])/g, '\\$1')}"`
|
|
214
|
+
}
|
|
215
|
+
return arg
|
|
216
|
+
})
|
|
217
|
+
command = `${command} ${escapedArgs.join(' ')}`
|
|
218
|
+
logger.info(`传递给脚本的参数: ${passthroughArgs.join(' ')}`)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 为脚本执行禁用 Nx 缓存
|
|
222
|
+
const extraEnv = { NX_CACHE: 'false' }
|
|
223
|
+
logger.info('为数据库脚本禁用 Nx 缓存: NX_CACHE=false')
|
|
224
|
+
|
|
225
|
+
const execFlags = { ...cli.flags }
|
|
226
|
+
;['dev', 'development', 'prod', 'production', 'test', 'e2e', 'staging', 'stage'].forEach(
|
|
227
|
+
key => delete execFlags[key],
|
|
228
|
+
)
|
|
229
|
+
if (envKey === 'prod') execFlags.prod = true
|
|
230
|
+
else if (envKey === 'dev') execFlags.dev = true
|
|
231
|
+
else if (envKey === 'test') execFlags.test = true
|
|
232
|
+
else if (envKey === 'e2e') execFlags.e2e = true
|
|
233
|
+
else if (envKey === 'staging') execFlags.staging = true
|
|
234
|
+
|
|
235
|
+
await cli.executeCommand(
|
|
236
|
+
{ ...config, command, env: { ...(config.env || {}), ...extraEnv } },
|
|
237
|
+
execFlags,
|
|
238
|
+
)
|
|
239
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
import { envManager } from '../../env.js'
|
|
3
|
+
import { validateEnvironment } from '../../validate-env.js'
|
|
4
|
+
|
|
5
|
+
export async function handleDeploy(cli, args) {
|
|
6
|
+
const target = args[0]
|
|
7
|
+
if (!target) {
|
|
8
|
+
logger.error('请指定部署目标: front, admin, all')
|
|
9
|
+
logger.info(`用法: ${cli.invocation} deploy <target> [环境标志]`)
|
|
10
|
+
logger.info(`示例: ${cli.invocation} deploy front --staging`)
|
|
11
|
+
process.exitCode = 1
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const normalizedTarget = String(target).toLowerCase()
|
|
16
|
+
const deployTargets = Object.keys(cli.commands.deploy || {})
|
|
17
|
+
if (!cli.commands.deploy?.[normalizedTarget]) {
|
|
18
|
+
logger.error(`未找到部署目标: ${target}`)
|
|
19
|
+
if (deployTargets.length > 0) {
|
|
20
|
+
logger.info(`可用目标: ${deployTargets.join(', ')}`)
|
|
21
|
+
}
|
|
22
|
+
logger.info(`用法: ${cli.invocation} deploy <target> [环境标志]`)
|
|
23
|
+
process.exitCode = 1
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (cli.flags.test || cli.flags.e2e) {
|
|
28
|
+
logger.error('deploy 命令仅支持 --dev/--staging/--prod 环境标志')
|
|
29
|
+
process.exitCode = 1
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const selectedEnvs = []
|
|
34
|
+
if (cli.flags.dev) selectedEnvs.push('development')
|
|
35
|
+
if (cli.flags.staging) selectedEnvs.push('staging')
|
|
36
|
+
if (cli.flags.prod) selectedEnvs.push('production')
|
|
37
|
+
|
|
38
|
+
if (selectedEnvs.length > 1) {
|
|
39
|
+
logger.error('deploy 命令不支持同时传入多个环境标志')
|
|
40
|
+
logger.info('请使用 --dev、--staging 或 --prod 中的一个')
|
|
41
|
+
process.exitCode = 1
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const environment = selectedEnvs[0] || 'staging'
|
|
46
|
+
|
|
47
|
+
cli.ensureRepoRoot()
|
|
48
|
+
|
|
49
|
+
// 只执行基础环境校验(检查 .env 文件结构),跳过后端环境变量校验
|
|
50
|
+
// Vercel 部署所需的环境变量由 vercel-deploy.js 自行校验
|
|
51
|
+
try {
|
|
52
|
+
validateEnvironment()
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.error(error.message)
|
|
55
|
+
process.exitCode = 1
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 加载环境变量层,但不校验后端必需变量
|
|
60
|
+
const layeredEnv = envManager.collectEnvFromLayers(null, environment)
|
|
61
|
+
if (envManager.latestEnvWarnings && envManager.latestEnvWarnings.length > 0) {
|
|
62
|
+
envManager.latestEnvWarnings.forEach(message => logger.warn(message))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 仅在目标变量不存在或是占位符时才使用 .env 文件的值
|
|
66
|
+
for (const [key, value] of Object.entries(layeredEnv)) {
|
|
67
|
+
const currentValue = process.env[key]
|
|
68
|
+
if (!currentValue || envManager.isPlaceholderEnvValue(currentValue)) {
|
|
69
|
+
process.env[key] = value
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
envManager.syncEnvironments(environment)
|
|
73
|
+
|
|
74
|
+
const { deployToVercel } = await import('../../vercel-deploy.js')
|
|
75
|
+
await deployToVercel(normalizedTarget, { environment })
|
|
76
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
|
|
3
|
+
export async function handleExport(cli, args) {
|
|
4
|
+
const target = args[0]
|
|
5
|
+
if (!target) {
|
|
6
|
+
logger.error('请指定导出目标')
|
|
7
|
+
logger.info(`用法: ${cli.invocation} export <target> [环境标志]`)
|
|
8
|
+
process.exitCode = 1
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const exportConfig = cli.commands.export?.[target]
|
|
13
|
+
if (!exportConfig) {
|
|
14
|
+
const targets = Object.keys(cli.commands.export || {})
|
|
15
|
+
logger.error(`未找到导出目标: ${target}`)
|
|
16
|
+
if (targets.length > 0) {
|
|
17
|
+
logger.info(`可用目标: ${targets.join(', ')}`)
|
|
18
|
+
}
|
|
19
|
+
process.exitCode = 1
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const environment = cli.determineEnvironment()
|
|
24
|
+
const envKey = cli.normalizeEnvKey(environment)
|
|
25
|
+
let config = exportConfig
|
|
26
|
+
if (typeof config === 'object' && !config.command) {
|
|
27
|
+
if (config[envKey]) config = config[envKey]
|
|
28
|
+
else if (envKey === 'staging' && config.prod) config = config.prod
|
|
29
|
+
else config = config.dev || config
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logger.step(`导出 ${target} (${environment})`)
|
|
33
|
+
await cli.executeCommand(config)
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
|
|
3
|
+
export async function handlePackage(cli, args) {
|
|
4
|
+
const target = args[0] || 'backend'
|
|
5
|
+
if (target !== 'backend') {
|
|
6
|
+
logger.error(`暂不支持打包目标: ${target}`)
|
|
7
|
+
logger.info(`当前仅支持 ${cli.invocation} package backend`)
|
|
8
|
+
process.exitCode = 1
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
cli.ensureRepoRoot()
|
|
13
|
+
|
|
14
|
+
const environment = cli.determineEnvironment()
|
|
15
|
+
const passthroughFlags = cli.args.filter(token =>
|
|
16
|
+
['--skip-build', '--keep-workdir'].includes(token),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger.step(`打包 ${target} (${environment})`)
|
|
20
|
+
const { runBackendPackage } = await import('../../backend-package.js')
|
|
21
|
+
await runBackendPackage([`--env=${environment}`, ...passthroughFlags])
|
|
22
|
+
}
|