@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,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
|
+
}
|