@weotro/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 +755 -0
- package/bin/dx-with-version-env.js +8 -0
- package/bin/dx.js +187 -0
- package/lib/artifact-deploy/artifact-builder.js +144 -0
- package/lib/artifact-deploy/config.js +180 -0
- package/lib/artifact-deploy/remote-script.js +301 -0
- package/lib/artifact-deploy/remote-transport.js +86 -0
- package/lib/artifact-deploy.js +70 -0
- package/lib/backend-artifact-deploy/artifact-builder.js +267 -0
- package/lib/backend-artifact-deploy/config.js +218 -0
- package/lib/backend-artifact-deploy/path-utils.js +18 -0
- package/lib/backend-artifact-deploy/remote-phases.js +14 -0
- package/lib/backend-artifact-deploy/remote-result.js +44 -0
- package/lib/backend-artifact-deploy/remote-script.js +507 -0
- package/lib/backend-artifact-deploy/remote-transport.js +123 -0
- package/lib/backend-artifact-deploy/rollback.js +5 -0
- package/lib/backend-artifact-deploy/runtime-package.js +46 -0
- package/lib/backend-artifact-deploy.js +91 -0
- package/lib/backend-package.js +674 -0
- package/lib/cli/args.js +38 -0
- package/lib/cli/command-result.js +1 -0
- package/lib/cli/commands/contracts.js +60 -0
- package/lib/cli/commands/core.js +533 -0
- package/lib/cli/commands/db.js +231 -0
- package/lib/cli/commands/deploy.js +175 -0
- package/lib/cli/commands/env.js +120 -0
- package/lib/cli/commands/export.js +39 -0
- package/lib/cli/commands/package.js +22 -0
- package/lib/cli/commands/release.js +55 -0
- package/lib/cli/commands/stack.js +427 -0
- package/lib/cli/commands/start.js +58 -0
- package/lib/cli/commands/worktree.js +145 -0
- package/lib/cli/dx-cli.js +1072 -0
- package/lib/cli/flags.js +123 -0
- package/lib/cli/help-model.js +222 -0
- package/lib/cli/help-renderer.js +137 -0
- package/lib/cli/help-schema.js +552 -0
- package/lib/cli/help.js +141 -0
- package/lib/cli/index.js +4 -0
- package/lib/cli/nx-command.js +13 -0
- package/lib/codex-initial.js +271 -0
- package/lib/confirm.js +213 -0
- package/lib/env-policy.js +134 -0
- package/lib/env-profile.js +435 -0
- package/lib/env.js +261 -0
- package/lib/exec.js +692 -0
- package/lib/logger.js +239 -0
- package/lib/nx-ignore.js +45 -0
- package/lib/run-with-version-env.js +163 -0
- package/lib/sdk-build.js +424 -0
- package/lib/start-dev.js +401 -0
- package/lib/telegram-webhook.js +431 -0
- package/lib/validate-env.js +317 -0
- package/lib/vercel-deploy.js +549 -0
- package/lib/version.js +14 -0
- package/lib/worktree.js +1052 -0
- package/package.json +45 -0
- package/skills/create-issue/SKILL.md +90 -0
- package/skills/delivering-design-handoff/SKILL.md +290 -0
- package/skills/doctor/SKILL.md +76 -0
- package/skills/gh-dependabot-cleanup/SKILL.md +54 -0
- package/skills/gh-dependabot-cleanup/agents/openai.yaml +7 -0
- package/skills/git-release/SKILL.md +194 -0
- package/skills/git-release/agents/openai.yaml +7 -0
- package/skills/online-debug-guard/SKILL.md +111 -0
- package/skills/ship-issue-pr/SKILL.md +676 -0
- package/skills/stagewise-ui-debugging/SKILL.md +48 -0
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
// 创建迁移只允许在开发环境进行,避免 AI/用户在非开发环境误执行
|
|
25
|
+
if (action === 'migrate' && envKey && envKey !== 'development') {
|
|
26
|
+
const envFlag = cli.getEnvironmentFlagExample(envKey) || `--${envKey}`
|
|
27
|
+
logger.error('dx db migrate 仅允许在 --dev 环境下创建迁移')
|
|
28
|
+
logger.info('请使用 `dx db migrate --dev --name <migration-name>` 创建新迁移。')
|
|
29
|
+
logger.info(`非开发环境请改用: ${cli.invocation} db deploy ${envFlag}`)
|
|
30
|
+
process.exitCode = 1
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 处理 script 子命令
|
|
35
|
+
if (action === 'script') {
|
|
36
|
+
const scriptName = args[1]
|
|
37
|
+
if (!scriptName) {
|
|
38
|
+
logger.error('请指定要运行的脚本名称')
|
|
39
|
+
logger.info('用法: dx db script <script-name> [环境标志]')
|
|
40
|
+
logger.info('示例: dx db script fix-email-verified-status --dev')
|
|
41
|
+
process.exitCode = 1
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
return await handleDatabaseScript(cli, scriptName, envKey, dbConfig)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
logger.step(`执行数据库操作: ${action} (${environment})`)
|
|
48
|
+
|
|
49
|
+
// 处理嵌套配置
|
|
50
|
+
let config = dbConfig
|
|
51
|
+
if (typeof config === 'object' && !config.command) {
|
|
52
|
+
// 严格按显式环境选择配置,不做环境回退
|
|
53
|
+
if (config[envKey]) config = config[envKey]
|
|
54
|
+
else config = null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!config) {
|
|
58
|
+
logger.error(`数据库操作 ${action} 未提供 ${cli.getEnvironmentFlagExample(envKey) || envKey} 环境配置`)
|
|
59
|
+
process.exitCode = 1
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 危险操作确认
|
|
64
|
+
if (config.dangerous) {
|
|
65
|
+
const confirmed = await confirmManager.confirmDatabaseOperation(
|
|
66
|
+
action,
|
|
67
|
+
envManager.getEnvironmentDescription(environment),
|
|
68
|
+
cli.flags.Y,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (!confirmed) {
|
|
72
|
+
logger.info('操作已取消')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 支持为 migrate 传入迁移名:--name/-n
|
|
78
|
+
let command = config.command
|
|
79
|
+
if (action === 'migrate' && envKey === 'development') {
|
|
80
|
+
const allArgs = cli.args
|
|
81
|
+
let migrationName = null
|
|
82
|
+
// 优先解析显式标志 --name/-n
|
|
83
|
+
const nameIdx = allArgs.indexOf('--name')
|
|
84
|
+
const shortIdx = allArgs.indexOf('-n')
|
|
85
|
+
if (nameIdx !== -1 && nameIdx + 1 < allArgs.length) {
|
|
86
|
+
migrationName = allArgs[nameIdx + 1]
|
|
87
|
+
} else if (shortIdx !== -1 && shortIdx + 1 < allArgs.length) {
|
|
88
|
+
migrationName = allArgs[shortIdx + 1]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!migrationName) {
|
|
92
|
+
logger.error('开发环境执行 migrate 时必须通过 --name 或 -n 指定迁移名称(禁止位置参数)')
|
|
93
|
+
logger.info('原因: 缺少迁移名会进入 Prisma 的交互式输入流程,脚本将被阻塞。')
|
|
94
|
+
logger.info(`正确示例: ${cli.invocation} db migrate --dev --name init-user-table`)
|
|
95
|
+
logger.info(`如仅需应用已有迁移,请使用 ${cli.invocation} db deploy --dev。`)
|
|
96
|
+
logger.info(`提示: 执行 ${cli.invocation} help db 查看完整用法与更多示例。`)
|
|
97
|
+
process.exitCode = 1
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const escaped = String(migrationName).replace(/(["`\\$])/g, '\\$1')
|
|
102
|
+
// 将参数传递给 Nx 下游 prisma:需要通过 -- 分割
|
|
103
|
+
command = `${command} -- --name "${escaped}"`
|
|
104
|
+
logger.info(`使用迁移名: ${migrationName}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 对以下数据库操作固定禁用 Nx 缓存,避免命中缓存导致未实际执行:generate/migrate/deploy/reset/seed
|
|
108
|
+
const disableCacheActions = new Set(['generate', 'migrate', 'deploy', 'reset', 'seed'])
|
|
109
|
+
const extraEnv = disableCacheActions.has(action) ? { NX_CACHE: 'false' } : {}
|
|
110
|
+
if (disableCacheActions.has(action)) {
|
|
111
|
+
logger.info('为数据库操作禁用 Nx 缓存: NX_CACHE=false')
|
|
112
|
+
// 为每个子命令注入 --skip-nx-cache(支持 && || ; 拼接的多命令)
|
|
113
|
+
command = command
|
|
114
|
+
.split(/(\s*&&\s*|\s*\|\|\s*|\s*;\s*)/)
|
|
115
|
+
.map(part => {
|
|
116
|
+
// 保留分隔符原样
|
|
117
|
+
if (/^(\s*&&\s*|\s*\|\|\s*|\s*;\s*)$/.test(part)) return part
|
|
118
|
+
// 跳过空字符串
|
|
119
|
+
if (!part.trim()) return part
|
|
120
|
+
// 只对 npx nx 命令注入
|
|
121
|
+
if (!part.includes('npx nx')) return part
|
|
122
|
+
// 在 -- 之前插入,或追加到末尾
|
|
123
|
+
if (part.includes(' -- ')) {
|
|
124
|
+
return part.replace(' -- ', ' --skip-nx-cache -- ')
|
|
125
|
+
}
|
|
126
|
+
return `${part} --skip-nx-cache`
|
|
127
|
+
})
|
|
128
|
+
.join('')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (action === 'reset' && envKey !== 'production') {
|
|
132
|
+
extraEnv.PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION = 'yes'
|
|
133
|
+
logger.info('非生产环境重置数据库,已自动确认危险操作: PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION=yes')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await cli.executeCommand(
|
|
137
|
+
{ ...config, command, env: { ...(config.env || {}), ...extraEnv } },
|
|
138
|
+
cli.createExecutionFlags(environment),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function handleDatabaseScript(cli, scriptName, envKey, dbConfig) {
|
|
143
|
+
// 自动去除 .ts 扩展名(如果用户提供了)
|
|
144
|
+
const cleanScriptName = scriptName.endsWith('.ts') ? scriptName.slice(0, -3) : scriptName
|
|
145
|
+
|
|
146
|
+
// 基础路径校验,避免路径遍历
|
|
147
|
+
if (
|
|
148
|
+
cleanScriptName.includes('/') ||
|
|
149
|
+
cleanScriptName.includes('\\') ||
|
|
150
|
+
cleanScriptName.includes('..')
|
|
151
|
+
) {
|
|
152
|
+
logger.error(`脚本名称不能包含路径分隔符或父目录引用: ${cleanScriptName}`)
|
|
153
|
+
process.exitCode = 1
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { existsSync } = await import('fs')
|
|
158
|
+
const { join, resolve, relative } = await import('path')
|
|
159
|
+
const scriptsRoot = join(process.cwd(), 'apps/backend/prisma/scripts')
|
|
160
|
+
const scriptPath = resolve(scriptsRoot, `${cleanScriptName}.ts`)
|
|
161
|
+
|
|
162
|
+
if (relative(scriptsRoot, scriptPath).startsWith('..')) {
|
|
163
|
+
logger.error(`脚本路径解析结果已超出允许目录: ${scriptPath}`)
|
|
164
|
+
process.exitCode = 1
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!existsSync(scriptPath)) {
|
|
169
|
+
logger.error(`脚本文件不存在: ${scriptPath}`)
|
|
170
|
+
logger.info('可用的脚本文件位于: apps/backend/prisma/scripts/')
|
|
171
|
+
process.exitCode = 1
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const environment = envKey
|
|
176
|
+
logger.step(`执行数据库脚本: ${cleanScriptName} (${environment})`)
|
|
177
|
+
logger.info(`脚本路径: ${scriptPath}`)
|
|
178
|
+
|
|
179
|
+
// 处理嵌套配置
|
|
180
|
+
let config = dbConfig
|
|
181
|
+
if (typeof config === 'object' && !config.command) {
|
|
182
|
+
if (config[envKey]) config = config[envKey]
|
|
183
|
+
else config = null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!config) {
|
|
187
|
+
logger.error(`数据库脚本 ${scriptName} 未提供 ${cli.getEnvironmentFlagExample(envKey) || envKey} 环境配置`)
|
|
188
|
+
process.exitCode = 1
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 危险操作确认
|
|
193
|
+
if (config.dangerous && envKey === 'production') {
|
|
194
|
+
const confirmed = await confirmManager.confirmDatabaseOperation(
|
|
195
|
+
`script: ${cleanScriptName}`,
|
|
196
|
+
envManager.getEnvironmentDescription(environment),
|
|
197
|
+
cli.flags.Y,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if (!confirmed) {
|
|
201
|
+
logger.info('操作已取消')
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 替换命令中的脚本名称占位符
|
|
207
|
+
let command = config.command.replace('{SCRIPT_NAME}', cleanScriptName)
|
|
208
|
+
|
|
209
|
+
// 获取 -- 后面的 passthrough 参数并追加到命令
|
|
210
|
+
const passthroughArgs = getPassthroughArgs(cli.args)
|
|
211
|
+
if (passthroughArgs.length > 0) {
|
|
212
|
+
const escapedArgs = passthroughArgs.map(arg => {
|
|
213
|
+
// 对包含空格或特殊字符的参数加引号
|
|
214
|
+
if (/[\s"'`$\\]/.test(arg)) {
|
|
215
|
+
return `"${arg.replace(/(["`\\$])/g, '\\$1')}"`
|
|
216
|
+
}
|
|
217
|
+
return arg
|
|
218
|
+
})
|
|
219
|
+
command = `${command} ${escapedArgs.join(' ')}`
|
|
220
|
+
logger.info(`传递给脚本的参数: ${passthroughArgs.join(' ')}`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 为脚本执行禁用 Nx 缓存
|
|
224
|
+
const extraEnv = { NX_CACHE: 'false' }
|
|
225
|
+
logger.info('为数据库脚本禁用 Nx 缓存: NX_CACHE=false')
|
|
226
|
+
|
|
227
|
+
await cli.executeCommand(
|
|
228
|
+
{ ...config, command, env: { ...(config.env || {}), ...extraEnv } },
|
|
229
|
+
cli.createExecutionFlags(environment),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
import { envManager } from '../../env.js'
|
|
3
|
+
import { validateEnvironment } from '../../validate-env.js'
|
|
4
|
+
|
|
5
|
+
export function mergeLayeredDeployEnv(layeredEnv, manager, runtimeEnv = process.env) {
|
|
6
|
+
const vercelCriticalKeys = new Set([
|
|
7
|
+
'VERCEL_TOKEN',
|
|
8
|
+
'VERCEL_ORG_ID',
|
|
9
|
+
'VERCEL_PROJECT_ID_FRONT',
|
|
10
|
+
'VERCEL_PROJECT_ID_ADMIN',
|
|
11
|
+
'VERCEL_PROJECT_ID_MERCHANT',
|
|
12
|
+
'VERCEL_PROJECT_ID_TELEGRAM_BOT',
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
for (const [key, value] of Object.entries(layeredEnv)) {
|
|
16
|
+
if (vercelCriticalKeys.has(key)) {
|
|
17
|
+
const currentValue = runtimeEnv[key]
|
|
18
|
+
const currentUsable =
|
|
19
|
+
currentValue !== undefined && currentValue !== null && !manager.isPlaceholderEnvValue(currentValue)
|
|
20
|
+
const incomingIsPlaceholder = manager.isPlaceholderEnvValue(value)
|
|
21
|
+
|
|
22
|
+
// 保留外部显式注入的真实凭据,避免被 .env.<env> 的占位符回退污染。
|
|
23
|
+
if (currentUsable && incomingIsPlaceholder) continue
|
|
24
|
+
|
|
25
|
+
runtimeEnv[key] = value
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const currentValue = runtimeEnv[key]
|
|
30
|
+
if (!currentValue || manager.isPlaceholderEnvValue(currentValue)) {
|
|
31
|
+
runtimeEnv[key] = value
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseTelegramWebhookFlags(argv = []) {
|
|
37
|
+
const args = Array.isArray(argv) ? argv : []
|
|
38
|
+
|
|
39
|
+
const idx = args.indexOf('--webhook-path')
|
|
40
|
+
const webhookPath = idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined
|
|
41
|
+
|
|
42
|
+
const dryRun = args.includes('--webhook-dry-run') ? true : undefined
|
|
43
|
+
|
|
44
|
+
// 默认值由下游决定(当前为默认严格),这里仅处理显式覆盖
|
|
45
|
+
const strict = args.includes('--strict-webhook')
|
|
46
|
+
? true
|
|
47
|
+
: args.includes('--no-strict-webhook')
|
|
48
|
+
? false
|
|
49
|
+
: undefined
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
webhookPath,
|
|
53
|
+
dryRun,
|
|
54
|
+
strict,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function handleDeploy(cli, args) {
|
|
59
|
+
const target = args[0]
|
|
60
|
+
const deployTargets = Object.keys(cli.commands.deploy || {})
|
|
61
|
+
if (!target) {
|
|
62
|
+
logger.error(
|
|
63
|
+
deployTargets.length > 0
|
|
64
|
+
? `请指定部署目标: ${deployTargets.join(', ')}`
|
|
65
|
+
: '请指定部署目标',
|
|
66
|
+
)
|
|
67
|
+
logger.info(`用法: ${cli.invocation} deploy <target> [环境标志]`)
|
|
68
|
+
logger.info(`示例: ${cli.invocation} deploy front --staging`)
|
|
69
|
+
process.exitCode = 1
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const normalizedTarget = String(target).toLowerCase()
|
|
74
|
+
if (!cli.commands.deploy?.[normalizedTarget]) {
|
|
75
|
+
logger.error(`未找到部署目标: ${target}`)
|
|
76
|
+
if (deployTargets.length > 0) {
|
|
77
|
+
logger.info(`可用目标: ${deployTargets.join(', ')}`)
|
|
78
|
+
}
|
|
79
|
+
logger.info(`用法: ${cli.invocation} deploy <target> [环境标志]`)
|
|
80
|
+
process.exitCode = 1
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (cli.flags.test || cli.flags.e2e) {
|
|
85
|
+
logger.error('deploy 命令仅支持 --dev/--staging/--prod 环境标志')
|
|
86
|
+
process.exitCode = 1
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const selectedEnvs = []
|
|
91
|
+
if (cli.flags.dev) selectedEnvs.push('development')
|
|
92
|
+
if (cli.flags.staging) selectedEnvs.push('staging')
|
|
93
|
+
if (cli.flags.prod) selectedEnvs.push('production')
|
|
94
|
+
|
|
95
|
+
if (selectedEnvs.length > 1) {
|
|
96
|
+
logger.error('deploy 命令不支持同时传入多个环境标志')
|
|
97
|
+
logger.info('请使用 --dev、--staging 或 --prod 中的一个')
|
|
98
|
+
process.exitCode = 1
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const targetConfig = cli.commands.deploy?.[normalizedTarget]
|
|
103
|
+
const artifactInternal = targetConfig?.internal
|
|
104
|
+
const isBackendArtifactDeploy = artifactInternal === 'backend-artifact-deploy'
|
|
105
|
+
const isGenericArtifactDeploy = artifactInternal === 'artifact-deploy'
|
|
106
|
+
const isArtifactDeploy = isBackendArtifactDeploy || isGenericArtifactDeploy
|
|
107
|
+
const environment = selectedEnvs[0] || (isArtifactDeploy ? 'development' : 'staging')
|
|
108
|
+
|
|
109
|
+
if (isBackendArtifactDeploy) {
|
|
110
|
+
const { runBackendArtifactDeploy } = await import('../../backend-artifact-deploy.js')
|
|
111
|
+
await runBackendArtifactDeploy({
|
|
112
|
+
cli,
|
|
113
|
+
target: normalizedTarget,
|
|
114
|
+
args,
|
|
115
|
+
environment,
|
|
116
|
+
})
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (isGenericArtifactDeploy) {
|
|
121
|
+
const { runArtifactDeploy } = await import('../../artifact-deploy.js')
|
|
122
|
+
await runArtifactDeploy({
|
|
123
|
+
cli,
|
|
124
|
+
target: normalizedTarget,
|
|
125
|
+
args,
|
|
126
|
+
environment,
|
|
127
|
+
})
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
cli.ensureRepoRoot()
|
|
132
|
+
|
|
133
|
+
// 只执行基础环境校验(检查 .env 文件结构),跳过后端环境变量校验
|
|
134
|
+
// Vercel 部署所需的环境变量由 vercel-deploy.js 自行校验
|
|
135
|
+
try {
|
|
136
|
+
validateEnvironment()
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.error(error.message)
|
|
139
|
+
process.exitCode = 1
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 加载环境变量层,但不校验后端必需变量
|
|
144
|
+
const layeredEnv = envManager.collectEnvFromLayers(null, environment)
|
|
145
|
+
if (envManager.latestEnvWarnings && envManager.latestEnvWarnings.length > 0) {
|
|
146
|
+
envManager.latestEnvWarnings.forEach(message => {
|
|
147
|
+
logger.warn(message)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 默认仅在缺失/占位时覆盖;Vercel 关键变量额外避免“真实值被占位符覆盖”的回退污染。
|
|
152
|
+
mergeLayeredDeployEnv(layeredEnv, envManager, process.env)
|
|
153
|
+
envManager.syncEnvironments(environment)
|
|
154
|
+
|
|
155
|
+
const { deployToVercel } = await import('../../vercel-deploy.js')
|
|
156
|
+
|
|
157
|
+
if (normalizedTarget === 'front') {
|
|
158
|
+
process.env.FRONT_I18N_PROFILE = cli.flags.BR ? 'br' : 'default'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const telegramWebhook = normalizedTarget === 'telegram-bot'
|
|
162
|
+
? parseTelegramWebhookFlags(cli.args)
|
|
163
|
+
: null
|
|
164
|
+
|
|
165
|
+
const strictContext = process.env.DX_VERCEL_STRICT_CONTEXT != null
|
|
166
|
+
? !['0', 'false', 'no'].includes(String(process.env.DX_VERCEL_STRICT_CONTEXT).toLowerCase())
|
|
167
|
+
: true
|
|
168
|
+
|
|
169
|
+
await deployToVercel(normalizedTarget, {
|
|
170
|
+
environment,
|
|
171
|
+
telegramWebhook,
|
|
172
|
+
strictContext,
|
|
173
|
+
targetConfig: targetConfig?.vercel,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { logger } from '../../logger.js'
|
|
2
|
+
import {
|
|
3
|
+
describeEnvProfiles,
|
|
4
|
+
executeWithEnvProfile,
|
|
5
|
+
validateEnvProfile,
|
|
6
|
+
} from '../../env-profile.js'
|
|
7
|
+
import { getPassthroughArgs } from '../args.js'
|
|
8
|
+
import { COMMAND_NOT_HANDLED } from '../command-result.js'
|
|
9
|
+
|
|
10
|
+
function hasExplicitEnvironment(flags = {}) {
|
|
11
|
+
return Boolean(flags.dev || flags.staging || flags.prod || flags.test || flags.e2e)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeDxEnvironmentFlag(flag) {
|
|
15
|
+
switch (flag) {
|
|
16
|
+
case '--dev':
|
|
17
|
+
return 'development'
|
|
18
|
+
case '--staging':
|
|
19
|
+
return 'staging'
|
|
20
|
+
case '--prod':
|
|
21
|
+
return 'production'
|
|
22
|
+
case '--test':
|
|
23
|
+
return 'test'
|
|
24
|
+
case '--e2e':
|
|
25
|
+
return 'e2e'
|
|
26
|
+
default:
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function prepareChildCommand(cli, command, args, environment) {
|
|
32
|
+
if (command !== 'dx') return { command, args }
|
|
33
|
+
|
|
34
|
+
const explicit = args
|
|
35
|
+
.map(normalizeDxEnvironmentFlag)
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
const mismatched = explicit.find(value => value !== environment)
|
|
38
|
+
if (mismatched) {
|
|
39
|
+
throw new Error(`env profile 环境为 ${environment},但内部 dx 命令指定了 ${mismatched}`)
|
|
40
|
+
}
|
|
41
|
+
if (explicit.length > 0) return { command, args }
|
|
42
|
+
|
|
43
|
+
const separator = args.indexOf('--')
|
|
44
|
+
const flag = cli.getEnvironmentFlag(environment)
|
|
45
|
+
if (separator === -1) return { command, args: [...args, flag] }
|
|
46
|
+
return {
|
|
47
|
+
command,
|
|
48
|
+
args: [...args.slice(0, separator), flag, ...args.slice(separator)],
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requireExplicitEnvironment(cli, action) {
|
|
53
|
+
if (!hasExplicitEnvironment(cli.flags)) {
|
|
54
|
+
throw new Error(`dx env ${action} 必须显式指定 --staging 或 --prod(也可使用 --dev)`)
|
|
55
|
+
}
|
|
56
|
+
const environment = cli.determineEnvironment()
|
|
57
|
+
if (environment === 'test' || environment === 'e2e') {
|
|
58
|
+
throw new Error(`dx env ${action} 不支持 ${environment} profile`)
|
|
59
|
+
}
|
|
60
|
+
return environment
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function handleEnv(cli, args = []) {
|
|
64
|
+
const [action, profile, ...extras] = args
|
|
65
|
+
|
|
66
|
+
if (action === 'status') {
|
|
67
|
+
if (profile || extras.length > 0) throw new Error('用法: dx env status')
|
|
68
|
+
const rows = describeEnvProfiles({ projectRoot: cli.projectRoot, configDir: cli.configDir })
|
|
69
|
+
logger.table(
|
|
70
|
+
rows.map(row => [
|
|
71
|
+
row.profile,
|
|
72
|
+
row.label,
|
|
73
|
+
row.environment,
|
|
74
|
+
row.exists ? '存在' : '缺失',
|
|
75
|
+
row.mode,
|
|
76
|
+
row.active ? '当前已装配' : '-',
|
|
77
|
+
]),
|
|
78
|
+
['PROFILE', '名称', '环境', '私有配置', '权限', '状态'],
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (action !== 'validate' && action !== 'exec') {
|
|
84
|
+
return COMMAND_NOT_HANDLED
|
|
85
|
+
}
|
|
86
|
+
if (!profile || extras.length > 0) throw new Error(`dx env ${action} 需要且仅需要一个 profile 名称`)
|
|
87
|
+
const environment = requireExplicitEnvironment(cli, action)
|
|
88
|
+
|
|
89
|
+
if (action === 'validate') {
|
|
90
|
+
const result = validateEnvProfile({
|
|
91
|
+
projectRoot: cli.projectRoot,
|
|
92
|
+
configDir: cli.configDir,
|
|
93
|
+
profile,
|
|
94
|
+
environment,
|
|
95
|
+
})
|
|
96
|
+
logger.success(
|
|
97
|
+
`env profile 校验通过: ${result.profile}/${result.environment}(${result.keyCount} 个本地键)`,
|
|
98
|
+
)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const passthrough = getPassthroughArgs(cli.args)
|
|
103
|
+
if (passthrough.length === 0) throw new Error('dx env exec 必须在 -- 后提供要执行的命令')
|
|
104
|
+
const prepared = prepareChildCommand(cli, passthrough[0], passthrough.slice(1), environment)
|
|
105
|
+
logger.command([prepared.command, ...prepared.args].join(' '))
|
|
106
|
+
const code = await executeWithEnvProfile({
|
|
107
|
+
projectRoot: cli.projectRoot,
|
|
108
|
+
configDir: cli.configDir,
|
|
109
|
+
profile,
|
|
110
|
+
environment,
|
|
111
|
+
command: prepared.command,
|
|
112
|
+
args: prepared.args,
|
|
113
|
+
})
|
|
114
|
+
if (code !== 0) {
|
|
115
|
+
process.exitCode = code
|
|
116
|
+
logger.error(`env profile 命令执行失败,退出码 ${code};临时 env 已清理`)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
logger.success('env profile 命令执行成功,临时 env 已清理')
|
|
120
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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 config = null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!config) {
|
|
32
|
+
logger.error(`导出目标 ${target} 未提供 ${cli.getEnvironmentFlagExample(envKey) || envKey} 环境配置`)
|
|
33
|
+
process.exitCode = 1
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger.step(`导出 ${target} (${environment})`)
|
|
38
|
+
await cli.executeCommand(config)
|
|
39
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { logger } from '../../logger.js'
|
|
4
|
+
import { COMMAND_NOT_HANDLED } from '../command-result.js'
|
|
5
|
+
|
|
6
|
+
export async function handleRelease(cli, args = []) {
|
|
7
|
+
const action = args[0]
|
|
8
|
+
const version = args[1]
|
|
9
|
+
|
|
10
|
+
if (action !== 'version') {
|
|
11
|
+
return COMMAND_NOT_HANDLED
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!version) {
|
|
15
|
+
logger.error(`请提供要发布的版本号,例如: ${cli.invocation} release version 1.2.3`)
|
|
16
|
+
process.exitCode = 1
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
cli.ensureRepoRoot()
|
|
21
|
+
|
|
22
|
+
const semverPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/
|
|
23
|
+
if (!semverPattern.test(version)) {
|
|
24
|
+
logger.warn(`版本号 ${version} 不符合常见语义化版本格式,仍将继续更新`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const packageFiles = [
|
|
28
|
+
'apps/backend/package.json',
|
|
29
|
+
'apps/front/package.json',
|
|
30
|
+
'apps/admin-front/package.json',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
logger.step(`统一更新版本号 -> ${version}`)
|
|
34
|
+
|
|
35
|
+
for (const relativePath of packageFiles) {
|
|
36
|
+
const fullPath = join(process.cwd(), relativePath)
|
|
37
|
+
if (!existsSync(fullPath)) {
|
|
38
|
+
logger.warn(`跳过,未找到文件: ${relativePath}`)
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const raw = readFileSync(fullPath, 'utf8')
|
|
44
|
+
const pkg = JSON.parse(raw)
|
|
45
|
+
const previous = pkg.version || '0.0.0'
|
|
46
|
+
pkg.version = version
|
|
47
|
+
writeFileSync(fullPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
|
|
48
|
+
logger.info(`更新 ${relativePath}: ${previous} -> ${version}`)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error(`更新 ${relativePath} 失败: ${error?.message || String(error)}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
logger.success(`版本号已同步为 ${version}`)
|
|
55
|
+
}
|