@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,549 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync, readFileSync, rmSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { envManager } from './env.js'
5
+ import { logger } from './logger.js'
6
+
7
+ const ALLOWED_ENVIRONMENTS = ['development', 'staging', 'production']
8
+ const TARGET_CONFIGS = {
9
+ front: {
10
+ configFile: 'vercel.front.json',
11
+ projectIdEnvVar: 'VERCEL_PROJECT_ID_FRONT',
12
+ deployCwd: '.',
13
+ deployMode: 'prebuilt',
14
+ prebuiltCwd: '.'
15
+ },
16
+ admin: {
17
+ configFile: 'vercel.admin.json',
18
+ projectIdEnvVar: 'VERCEL_PROJECT_ID_ADMIN',
19
+ deployCwd: '.',
20
+ deployMode: 'prebuilt',
21
+ prebuiltCwd: '.'
22
+ },
23
+ merchant: {
24
+ configFile: 'vercel.merchant.json',
25
+ projectIdEnvVar: 'VERCEL_PROJECT_ID_MERCHANT',
26
+ deployCwd: '.',
27
+ deployMode: 'prebuilt',
28
+ prebuiltCwd: '.'
29
+ },
30
+ 'telegram-bot': {
31
+ configFile: 'vercel.telegram-bot.json',
32
+ projectIdEnvVar: 'VERCEL_PROJECT_ID_TELEGRAM_BOT',
33
+ deployMode: 'prebuilt'
34
+ }
35
+ }
36
+
37
+ const ALLOWED_DEPLOY_MODES = ['prebuilt']
38
+
39
+ const DX_ENV_FLAG_MAP = {
40
+ development: '--dev',
41
+ staging: '--staging',
42
+ production: '--prod'
43
+ }
44
+
45
+ const VERCEL_PROJECT_LINK_PATH = '.vercel/project.json'
46
+ const VERCEL_DOWNLOADED_ENV_PATH = '.vercel/.env.production.local'
47
+
48
+ function getTargetConfig(target, configuredTarget) {
49
+ if (configuredTarget && typeof configuredTarget === 'object') {
50
+ return {
51
+ configFile: configuredTarget.configFile,
52
+ projectIdEnvVar: configuredTarget.projectIdEnvVar,
53
+ deployCwd: configuredTarget.deployCwd ?? '.',
54
+ deployMode: configuredTarget.deployMode ?? 'prebuilt',
55
+ prebuiltCwd: configuredTarget.prebuiltCwd ?? configuredTarget.deployCwd ?? '.',
56
+ }
57
+ }
58
+ return TARGET_CONFIGS[target]
59
+ }
60
+
61
+ function collectErrorText(err) {
62
+ const parts = []
63
+ if (err?.message) parts.push(String(err.message))
64
+ if (err?.stderr) parts.push(String(err.stderr))
65
+ if (err?.stdout) parts.push(String(err.stdout))
66
+ return parts.join('\n')
67
+ }
68
+
69
+ function isMissingFilesError(err) {
70
+ const text = collectErrorText(err)
71
+ return (
72
+ text.includes('missing_files') ||
73
+ text.includes('Missing files') ||
74
+ text.includes('code":"missing_files"')
75
+ )
76
+ }
77
+
78
+ function isNextPrebuiltMissingPathError(err) {
79
+ const text = collectErrorText(err)
80
+ return (
81
+ text.includes('ENOENT') &&
82
+ (text.includes('next-server.js') || text.includes('/node_modules/.pnpm/'))
83
+ )
84
+ }
85
+
86
+ function listMissingVarKeys(targetConfigs, token, orgId) {
87
+ const missing = []
88
+
89
+ if (!token || envManager.isPlaceholderEnvValue(token)) {
90
+ missing.push('VERCEL_TOKEN')
91
+ }
92
+
93
+ if (!orgId || envManager.isPlaceholderEnvValue(orgId)) {
94
+ missing.push('VERCEL_ORG_ID')
95
+ }
96
+
97
+ for (const config of targetConfigs) {
98
+ const projectId = process.env[config.projectIdEnvVar]
99
+ if (!projectId || envManager.isPlaceholderEnvValue(projectId)) {
100
+ missing.push(config.projectIdEnvVar)
101
+ }
102
+ }
103
+
104
+ return [...new Set(missing)]
105
+ }
106
+
107
+ function listMissingConfigs(targetConfigs, projectRoot) {
108
+ const missing = []
109
+
110
+ for (const config of targetConfigs) {
111
+ const configPath = join(projectRoot, config.configFile)
112
+ if (!existsSync(configPath)) {
113
+ missing.push(config.configFile)
114
+ }
115
+ }
116
+
117
+ return missing
118
+ }
119
+
120
+ function appendTargetArgs(baseArgs, { orgId, token }) {
121
+ const args = [...baseArgs]
122
+
123
+ if (orgId) {
124
+ args.push('--scope', orgId)
125
+ }
126
+
127
+ if (token) {
128
+ args.push('--token', token)
129
+ }
130
+
131
+ return args
132
+ }
133
+
134
+ export function resolveTargetRunCwd(projectRoot, targetConfig) {
135
+ if (!targetConfig?.deployCwd) return projectRoot
136
+ return join(projectRoot, targetConfig.deployCwd)
137
+ }
138
+
139
+ export function resolveTargetDeployMode(targetConfig) {
140
+ if (!targetConfig?.deployMode) return 'prebuilt'
141
+ return targetConfig.deployMode
142
+ }
143
+
144
+ export function isSupportedDeployMode(mode) {
145
+ return ALLOWED_DEPLOY_MODES.includes(mode)
146
+ }
147
+
148
+ export function resolveTargetPrebuiltCwd(projectRoot, targetConfig, runCwd) {
149
+ if (!targetConfig?.prebuiltCwd) return runCwd
150
+ return join(projectRoot, targetConfig.prebuiltCwd)
151
+ }
152
+
153
+ function maskIdentifier(value) {
154
+ const raw = String(value || '').trim()
155
+ if (raw.length <= 10) return raw || '-'
156
+ return `${raw.slice(0, 6)}...${raw.slice(-4)}`
157
+ }
158
+
159
+ function readLinkedProjectContext(contextRoot) {
160
+ const path = join(contextRoot, VERCEL_PROJECT_LINK_PATH)
161
+ if (!existsSync(path)) {
162
+ return { exists: false, path, orgId: null, projectId: null, parseError: null }
163
+ }
164
+
165
+ try {
166
+ const content = readFileSync(path, 'utf8')
167
+ const parsed = JSON.parse(content)
168
+ return {
169
+ exists: true,
170
+ path,
171
+ orgId: parsed?.orgId || null,
172
+ projectId: parsed?.projectId || null,
173
+ parseError: null
174
+ }
175
+ } catch (error) {
176
+ return {
177
+ exists: true,
178
+ path,
179
+ orgId: null,
180
+ projectId: null,
181
+ parseError: error
182
+ }
183
+ }
184
+ }
185
+
186
+ function clearLinkedProjectContext(contextRoot) {
187
+ const linkDir = join(contextRoot, '.vercel')
188
+ rmSync(linkDir, { force: true, recursive: true })
189
+ }
190
+
191
+ function clearVercelBuildOutput(contextRoot) {
192
+ const outputDir = join(contextRoot, '.vercel', 'output')
193
+ rmSync(outputDir, { force: true, recursive: true })
194
+ }
195
+
196
+ function clearDownloadedVercelEnv(contextRoot) {
197
+ rmSync(join(contextRoot, VERCEL_DOWNLOADED_ENV_PATH), { force: true })
198
+ }
199
+
200
+ async function runVercel(args, options = {}) {
201
+ const { env, cwd } = options
202
+ const MAX_CAPTURE = 20000
203
+
204
+ return new Promise((resolve, reject) => {
205
+ const child = spawn('vercel', args, {
206
+ cwd: cwd || process.cwd(),
207
+ env: env || process.env,
208
+ stdio: ['inherit', 'pipe', 'pipe']
209
+ })
210
+
211
+ let stdout = ''
212
+ let stderr = ''
213
+
214
+ const append = (current, chunk) => {
215
+ const next = current + chunk
216
+ return next.length > MAX_CAPTURE ? next.slice(-MAX_CAPTURE) : next
217
+ }
218
+
219
+ child.stdout.on('data', data => {
220
+ process.stdout.write(data)
221
+ stdout = append(stdout, data.toString('utf8'))
222
+ })
223
+
224
+ child.stderr.on('data', data => {
225
+ process.stderr.write(data)
226
+ stderr = append(stderr, data.toString('utf8'))
227
+ })
228
+
229
+ child.on('error', error => {
230
+ error.stdout = stdout
231
+ error.stderr = stderr
232
+ reject(error)
233
+ })
234
+
235
+ child.on('close', code => {
236
+ if (code === 0) return resolve({ code, stdout, stderr })
237
+ const error = new Error(`vercel ${args[0] || ''} 失败 (exit ${code})`)
238
+ error.code = code
239
+ error.stdout = stdout
240
+ error.stderr = stderr
241
+ reject(error)
242
+ })
243
+ })
244
+ }
245
+
246
+ export async function deployPrebuiltWithFallback(options) {
247
+ const {
248
+ baseArgs,
249
+ env,
250
+ cwd,
251
+ run = runVercel,
252
+ cleanupArchiveParts = () => {},
253
+ onMissingFiles = () => {},
254
+ } = options || {}
255
+
256
+ try {
257
+ const result = await run(baseArgs, { env, cwd })
258
+ return { usedArchive: false, result }
259
+ } catch (error) {
260
+ if (!isMissingFilesError(error)) {
261
+ throw error
262
+ }
263
+
264
+ onMissingFiles(error)
265
+ cleanupArchiveParts()
266
+
267
+ const archiveArgs = []
268
+ let inserted = false
269
+ for (const arg of baseArgs || []) {
270
+ archiveArgs.push(arg)
271
+ if (!inserted && arg === '--prebuilt') {
272
+ archiveArgs.push('--archive=tgz')
273
+ inserted = true
274
+ }
275
+ }
276
+ if (!inserted) {
277
+ archiveArgs.push('--archive=tgz')
278
+ }
279
+
280
+ const result = await run(archiveArgs, { env, cwd })
281
+ return { usedArchive: true, result }
282
+ }
283
+ }
284
+
285
+ export async function deployToVercel(target, options = {}) {
286
+ const {
287
+ environment = 'staging',
288
+ telegramWebhook = null,
289
+ telegramWebhookHandler = null,
290
+ strictContext = true,
291
+ run = runVercel,
292
+ targetConfig: configuredTarget = null,
293
+ } = options
294
+
295
+ process.exitCode = undefined
296
+
297
+ // 校验环境参数
298
+ if (!ALLOWED_ENVIRONMENTS.includes(environment)) {
299
+ logger.error(`不支持的部署环境: ${environment}`)
300
+ logger.info(`可用环境: ${ALLOWED_ENVIRONMENTS.join(', ')}`)
301
+ process.exitCode = 1
302
+ return
303
+ }
304
+
305
+ const normalizedTarget = String(target || '').toLowerCase()
306
+ const targets = normalizedTarget === 'all' ? ['front', 'admin', 'merchant'] : [normalizedTarget]
307
+ const projectRoot = process.cwd()
308
+
309
+ // 校验目标有效性
310
+ for (const t of targets) {
311
+ const resolvedTargetConfig = getTargetConfig(
312
+ t,
313
+ targets.length === 1 ? configuredTarget : null,
314
+ )
315
+ if (!resolvedTargetConfig) {
316
+ logger.error(`不支持的部署目标: ${t}`)
317
+ logger.info('请在 commands.json 的 deploy.<target>.vercel 中声明 Vercel target')
318
+ process.exitCode = 1
319
+ return
320
+ }
321
+ if (!resolvedTargetConfig.configFile || !resolvedTargetConfig.projectIdEnvVar) {
322
+ logger.error(`Vercel target 配置不完整: ${t}`)
323
+ logger.info('必须声明 configFile 和 projectIdEnvVar')
324
+ process.exitCode = 1
325
+ return
326
+ }
327
+ }
328
+
329
+ const targetConfigs = targets.map(t =>
330
+ getTargetConfig(t, targets.length === 1 ? configuredTarget : null),
331
+ )
332
+ const token = process.env.VERCEL_TOKEN
333
+ const orgId = process.env.VERCEL_ORG_ID
334
+
335
+ const missingVars = listMissingVarKeys(targetConfigs, token, orgId)
336
+ const missingConfigFiles = listMissingConfigs(targetConfigs, projectRoot)
337
+
338
+ if (missingVars.length > 0 || missingConfigFiles.length > 0) {
339
+ if (missingVars.length > 0) {
340
+ logger.error('缺少以下 Vercel 环境变量:')
341
+ missingVars.forEach(v => {
342
+ logger.error(` - ${v}`)
343
+ })
344
+ logger.info('')
345
+ logger.info('请在 .env.<env>.local 中配置这些变量,例如:')
346
+ logger.info(' VERCEL_TOKEN=<your-vercel-token>')
347
+ logger.info(' VERCEL_ORG_ID=team_xxx')
348
+ logger.info(' VERCEL_PROJECT_ID_FRONT=prj_xxx')
349
+ logger.info(' VERCEL_PROJECT_ID_ADMIN=prj_xxx')
350
+ logger.info(' VERCEL_PROJECT_ID_MERCHANT=prj_xxx')
351
+ logger.info(' VERCEL_PROJECT_ID_TELEGRAM_BOT=prj_xxx')
352
+ logger.info('')
353
+ logger.info('获取方式:')
354
+ logger.info(
355
+ ' 1. VERCEL_TOKEN: vercel login 后查看 ~/Library/Application Support/com.vercel.cli/auth.json'
356
+ )
357
+ logger.info(' 2. PROJECT_ID: vercel project ls --scope <org> 或通过 Vercel Dashboard 获取')
358
+ logger.info('')
359
+ logger.info('提示:部署命令会显式校验 --scope 与环境变量上下文,避免环境漂移。')
360
+ }
361
+
362
+ if (missingConfigFiles.length > 0) {
363
+ logger.error('缺少以下 Vercel 配置文件:')
364
+ missingConfigFiles.forEach(name => {
365
+ logger.error(` - ${name}`)
366
+ })
367
+ logger.info('')
368
+ logger.info('请确认同级目录存在对应的 vercel.*.json 文件。')
369
+ }
370
+
371
+ process.exitCode = 1
372
+ return
373
+ }
374
+
375
+ // deploy 不再硬编码任何 Nx 构建步骤。
376
+ // - 前置构建/生成(shared/contracts/backend 等)应由项目自己的 Nx 依赖图或 Vercel buildCommand 负责。
377
+ // - 这样 dx deploy 能兼容不同 monorepo 布局(不强依赖 apps/sdk 等目录)。
378
+
379
+ const dxEnvFlag = DX_ENV_FLAG_MAP[environment]
380
+
381
+ for (const t of targets) {
382
+ const targetConfig = getTargetConfig(t, targets.length === 1 ? configuredTarget : null)
383
+ const projectId = process.env[targetConfig.projectIdEnvVar]
384
+ const configFile = targetConfig.configFile
385
+ const configPath = join(projectRoot, configFile)
386
+ const runCwd = resolveTargetRunCwd(projectRoot, targetConfig)
387
+ const deployMode = resolveTargetDeployMode(targetConfig)
388
+ const prebuiltCwd = resolveTargetPrebuiltCwd(projectRoot, targetConfig, runCwd)
389
+
390
+ if (!existsSync(runCwd)) {
391
+ logger.error(
392
+ `部署目录不存在: target=${t} deployCwd=${targetConfig.deployCwd || '.'} resolved=${runCwd}`
393
+ )
394
+ process.exitCode = 1
395
+ return
396
+ }
397
+
398
+ if (!isSupportedDeployMode(deployMode)) {
399
+ logger.error(`不支持的部署模式: target=${t} mode=${deployMode}`)
400
+ logger.info(`可用部署模式: ${ALLOWED_DEPLOY_MODES.join(', ')}`)
401
+ process.exitCode = 1
402
+ return
403
+ }
404
+
405
+ if (!existsSync(prebuiltCwd)) {
406
+ logger.error(
407
+ `预构建部署目录不存在: target=${t} prebuiltCwd=${targetConfig.prebuiltCwd || targetConfig.deployCwd || '.'} resolved=${prebuiltCwd}`
408
+ )
409
+ process.exitCode = 1
410
+ return
411
+ }
412
+
413
+ const linkedContext = readLinkedProjectContext(prebuiltCwd)
414
+
415
+ const linkedMismatch =
416
+ linkedContext.exists &&
417
+ linkedContext.parseError == null &&
418
+ ((linkedContext.orgId && linkedContext.orgId !== orgId) ||
419
+ (linkedContext.projectId && linkedContext.projectId !== projectId))
420
+
421
+ if (linkedContext.exists && linkedContext.parseError) {
422
+ logger.warn(
423
+ `检测到 ${VERCEL_PROJECT_LINK_PATH} 但解析失败: ${linkedContext.parseError.message}`
424
+ )
425
+ if (strictContext) {
426
+ logger.warn('strictContext 已开启:自动清理 .vercel 后继续部署')
427
+ clearLinkedProjectContext(prebuiltCwd)
428
+ }
429
+ }
430
+
431
+ if (linkedMismatch) {
432
+ logger.error('检测到 .vercel 链接冲突')
433
+ logger.error(` 当前目标: org=${maskIdentifier(orgId)} project=${maskIdentifier(projectId)}`)
434
+ logger.error(
435
+ ` 本地链接: org=${maskIdentifier(linkedContext.orgId)} project=${maskIdentifier(linkedContext.projectId)}`
436
+ )
437
+ if (strictContext) {
438
+ logger.warn('strictContext 已开启:检测到冲突,自动清理 .vercel 后继续部署')
439
+ clearLinkedProjectContext(prebuiltCwd)
440
+ } else {
441
+ logger.warn('strictContext 已关闭,继续执行(可能存在误部署风险)')
442
+ }
443
+ }
444
+
445
+ logger.info(
446
+ `[deploy-context] env=${environment} target=${t} mode=${deployMode} runCwd=${runCwd} prebuiltCwd=${prebuiltCwd} strict=${strictContext ? 1 : 0} org=${maskIdentifier(orgId)} project=${maskIdentifier(projectId)} linked=${linkedContext.exists ? 'yes' : 'no'} token=env`
447
+ )
448
+
449
+ const envVars = {
450
+ ...process.env,
451
+ VERCEL_PROJECT_ID: projectId,
452
+ APP_ENV: environment,
453
+ NEXT_PUBLIC_APP_ENV: environment,
454
+ VITE_APP_ENV: environment,
455
+ DX_ENV_FLAG: dxEnvFlag,
456
+ NODE_ENV: envManager.mapAppEnvToNodeEnv(environment),
457
+ VERCEL_ORG_ID: orgId
458
+ }
459
+
460
+ // 不通过 CLI args 传递 token,避免出现在错误信息/日志中
461
+ envVars.VERCEL_TOKEN = token
462
+
463
+ try {
464
+ if (strictContext && process.env.DX_VERCEL_KEEP_LINK !== '1') {
465
+ clearLinkedProjectContext(prebuiltCwd)
466
+ }
467
+ // 避免 Vercel 本地预构建产物残留导致 EEXIST(如 _global-error.func 符号链接冲突)
468
+ clearVercelBuildOutput(prebuiltCwd)
469
+
470
+ // 第一步:本地构建
471
+ logger.step(`本地构建 ${t} (${environment})`)
472
+ const buildArgs = appendTargetArgs(['build', '--local-config', configPath, '--yes'], {
473
+ orgId,
474
+ token
475
+ })
476
+
477
+ // staging 和 production 环境需要 --prod 标志,确保构建产物与部署环境匹配
478
+ if (environment === 'staging' || environment === 'production') {
479
+ buildArgs.push('--prod')
480
+ }
481
+
482
+ await run(buildArgs, { env: envVars, cwd: runCwd })
483
+ logger.success(`${t} 本地构建成功`)
484
+
485
+ logger.step(`部署 ${t} 到 Vercel (${environment})`)
486
+
487
+ const baseDeployArgs = appendTargetArgs(
488
+ ['deploy', '--prebuilt', '--local-config', configPath, '--yes'],
489
+ {
490
+ orgId,
491
+ token
492
+ }
493
+ )
494
+
495
+ // staging 和 production 环境都添加 --prod 标志以绑定固定域名
496
+ if (environment === 'staging' || environment === 'production') {
497
+ baseDeployArgs.push('--prod')
498
+ }
499
+
500
+ const deployResult = await deployPrebuiltWithFallback({
501
+ baseArgs: baseDeployArgs,
502
+ env: envVars,
503
+ cwd: prebuiltCwd,
504
+ run
505
+ })
506
+
507
+ const deployOutput = [deployResult?.result?.stdout, deployResult?.result?.stderr]
508
+ .filter(Boolean)
509
+ .join('\n')
510
+
511
+ // Telegram Bot 部署成功后自动设置 Webhook(并做严格校验)
512
+ if (t === 'telegram-bot') {
513
+ const resolvedWebhookHandler = telegramWebhookHandler
514
+ || (await import('./telegram-webhook.js')).handleTelegramBotDeploy
515
+ const webhookResult = await resolvedWebhookHandler(environment, projectId, orgId, token, {
516
+ deployOutput,
517
+ projectNameHint: 'telegram-bot',
518
+ ...(telegramWebhook || {})
519
+ })
520
+
521
+ if (webhookResult?.status === 'success') {
522
+ logger.success(`${t} 部署成功(Webhook 已校验)`)
523
+ } else if (webhookResult?.status === 'failed') {
524
+ const reason = webhookResult?.message || webhookResult?.reason || '未知原因'
525
+ logger.error(`${t} 部署成功,但 Webhook 配置失败: ${reason}`)
526
+ process.exitCode = 1
527
+ return
528
+ } else {
529
+ const reason = webhookResult?.message || webhookResult?.reason || '未严格校验'
530
+ logger.warn(`${t} 部署成功,但 Webhook 未完成校验: ${reason}`)
531
+ logger.success(`${t} 部署成功`)
532
+ }
533
+ } else {
534
+ logger.success(`${t} 部署成功`)
535
+ }
536
+ } catch (error) {
537
+ if (deployMode === 'prebuilt' && isNextPrebuiltMissingPathError(error)) {
538
+ logger.error(
539
+ '高优先级提示:检测到 Next.js 预构建产物缺失(next-server.js/node_modules/.pnpm)。请检查 front prebuiltCwd 与构建产物路径是否一致。'
540
+ )
541
+ }
542
+ const message = error?.message || String(error)
543
+ logger.error(`${t} 构建或部署失败: ${message}`)
544
+ process.exitCode = 1
545
+ } finally {
546
+ clearDownloadedVercelEnv(prebuiltCwd)
547
+ }
548
+ }
549
+ }
package/lib/version.js ADDED
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join, dirname } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ export function getPackageVersion() {
6
+ try {
7
+ const here = dirname(fileURLToPath(import.meta.url))
8
+ const pkgPath = join(here, '..', 'package.json')
9
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
10
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0'
11
+ } catch {
12
+ return '0.0.0'
13
+ }
14
+ }