@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +755 -0
  3. package/bin/dx-with-version-env.js +8 -0
  4. package/bin/dx.js +187 -0
  5. package/lib/artifact-deploy/artifact-builder.js +144 -0
  6. package/lib/artifact-deploy/config.js +180 -0
  7. package/lib/artifact-deploy/remote-script.js +301 -0
  8. package/lib/artifact-deploy/remote-transport.js +86 -0
  9. package/lib/artifact-deploy.js +70 -0
  10. package/lib/backend-artifact-deploy/artifact-builder.js +267 -0
  11. package/lib/backend-artifact-deploy/config.js +218 -0
  12. package/lib/backend-artifact-deploy/path-utils.js +18 -0
  13. package/lib/backend-artifact-deploy/remote-phases.js +14 -0
  14. package/lib/backend-artifact-deploy/remote-result.js +44 -0
  15. package/lib/backend-artifact-deploy/remote-script.js +507 -0
  16. package/lib/backend-artifact-deploy/remote-transport.js +123 -0
  17. package/lib/backend-artifact-deploy/rollback.js +5 -0
  18. package/lib/backend-artifact-deploy/runtime-package.js +46 -0
  19. package/lib/backend-artifact-deploy.js +91 -0
  20. package/lib/backend-package.js +674 -0
  21. package/lib/cli/args.js +38 -0
  22. package/lib/cli/command-result.js +1 -0
  23. package/lib/cli/commands/contracts.js +60 -0
  24. package/lib/cli/commands/core.js +533 -0
  25. package/lib/cli/commands/db.js +231 -0
  26. package/lib/cli/commands/deploy.js +175 -0
  27. package/lib/cli/commands/env.js +120 -0
  28. package/lib/cli/commands/export.js +39 -0
  29. package/lib/cli/commands/package.js +22 -0
  30. package/lib/cli/commands/release.js +55 -0
  31. package/lib/cli/commands/stack.js +427 -0
  32. package/lib/cli/commands/start.js +58 -0
  33. package/lib/cli/commands/worktree.js +145 -0
  34. package/lib/cli/dx-cli.js +1072 -0
  35. package/lib/cli/flags.js +123 -0
  36. package/lib/cli/help-model.js +222 -0
  37. package/lib/cli/help-renderer.js +137 -0
  38. package/lib/cli/help-schema.js +552 -0
  39. package/lib/cli/help.js +141 -0
  40. package/lib/cli/index.js +4 -0
  41. package/lib/cli/nx-command.js +13 -0
  42. package/lib/codex-initial.js +271 -0
  43. package/lib/confirm.js +213 -0
  44. package/lib/env-policy.js +134 -0
  45. package/lib/env-profile.js +435 -0
  46. package/lib/env.js +261 -0
  47. package/lib/exec.js +692 -0
  48. package/lib/logger.js +239 -0
  49. package/lib/nx-ignore.js +45 -0
  50. package/lib/run-with-version-env.js +163 -0
  51. package/lib/sdk-build.js +424 -0
  52. package/lib/start-dev.js +401 -0
  53. package/lib/telegram-webhook.js +431 -0
  54. package/lib/validate-env.js +317 -0
  55. package/lib/vercel-deploy.js +549 -0
  56. package/lib/version.js +14 -0
  57. package/lib/worktree.js +1052 -0
  58. package/package.json +45 -0
  59. package/skills/create-issue/SKILL.md +90 -0
  60. package/skills/delivering-design-handoff/SKILL.md +290 -0
  61. package/skills/doctor/SKILL.md +76 -0
  62. package/skills/gh-dependabot-cleanup/SKILL.md +54 -0
  63. package/skills/gh-dependabot-cleanup/agents/openai.yaml +7 -0
  64. package/skills/git-release/SKILL.md +194 -0
  65. package/skills/git-release/agents/openai.yaml +7 -0
  66. package/skills/online-debug-guard/SKILL.md +111 -0
  67. package/skills/ship-issue-pr/SKILL.md +676 -0
  68. 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
+ }