@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,431 @@
1
+ import { execSync } from 'node:child_process'
2
+ import { logger } from './logger.js'
3
+ import { envManager } from './env.js'
4
+
5
+ function normalizeWebhookPath(raw) {
6
+ const s = String(raw || '').trim()
7
+ if (!s) return '/api/webhook'
8
+ if (s.startsWith('/')) return s
9
+ return `/${s}`
10
+ }
11
+
12
+ function normalizeDeployUrl(raw) {
13
+ const s = String(raw || '')
14
+ const m = s.match(/(https?:\/\/)?([a-z0-9-]+\.vercel\.app)\b/i)
15
+ if (!m) return null
16
+ return `https://${m[2]}`
17
+ }
18
+
19
+ export function parseDeployUrlFromDeployOutput(output) {
20
+ const lines = String(output || '')
21
+ .split(/\r?\n/)
22
+ .map(line => String(line || '').trim())
23
+ .filter(Boolean)
24
+
25
+ const pickUrl = line => {
26
+ const m = line.match(/(https?:\/\/)?([a-z0-9-]+\.vercel\.app)\b/i)
27
+ return m ? `https://${m[2]}` : null
28
+ }
29
+
30
+ for (const line of lines) {
31
+ if (!/^production\s*:/i.test(line)) continue
32
+ const url = pickUrl(line)
33
+ if (url) return url
34
+ }
35
+
36
+ for (const line of lines) {
37
+ if (!/^preview\s*:/i.test(line)) continue
38
+ const url = pickUrl(line)
39
+ if (url) return url
40
+ }
41
+
42
+ for (const line of lines) {
43
+ if (/to deploy to production/i.test(line)) continue
44
+ const url = pickUrl(line)
45
+ if (url) return url
46
+ }
47
+
48
+ return null
49
+ }
50
+
51
+ export function parseDeployUrlFromVercelListOutput(output, projectNameHint) {
52
+ const lines = String(output || '')
53
+ .split(/\r?\n/)
54
+ .map(l => l.trim())
55
+ .filter(Boolean)
56
+
57
+ const isReady = line => /\b(Ready|READY)\b/.test(line)
58
+ const hasUrl = line => /\b[a-z0-9-]+\.vercel\.app\b/i.test(line)
59
+ const pickUrl = line => {
60
+ const m = line.match(/(https?:\/\/)?([a-z0-9-]+\.vercel\.app)\b/i)
61
+ return m ? `https://${m[2]}` : null
62
+ }
63
+
64
+ const hint = projectNameHint ? String(projectNameHint) : ''
65
+ if (hint) {
66
+ for (const line of lines) {
67
+ if (!line.includes(hint)) continue
68
+ if (!isReady(line)) continue
69
+ if (!hasUrl(line)) continue
70
+ const url = pickUrl(line)
71
+ if (url) return url
72
+ }
73
+ }
74
+
75
+ for (const line of lines) {
76
+ if (!isReady(line)) continue
77
+ if (!hasUrl(line)) continue
78
+ const url = pickUrl(line)
79
+ if (url) return url
80
+ }
81
+
82
+ return null
83
+ }
84
+
85
+ /**
86
+ * 处理 Telegram Bot 部署后的 Webhook 配置
87
+ */
88
+ export async function handleTelegramBotDeploy(environment, projectId, orgId, token, options = {}) {
89
+ logger.step('配置 Telegram Webhook...')
90
+
91
+ const {
92
+ deployOutput,
93
+ projectNameHint,
94
+ webhookPath: webhookPathOverride,
95
+ dryRun: dryRunOverride,
96
+ strict: strictOverride,
97
+ } = options || {}
98
+
99
+ const strictDefault = true
100
+
101
+ const strictEnv = process.env.DX_TELEGRAM_WEBHOOK_STRICT != null
102
+ ? !['0', 'false', 'no'].includes(String(process.env.DX_TELEGRAM_WEBHOOK_STRICT).toLowerCase())
103
+ : undefined
104
+
105
+ const strict = strictOverride ?? strictEnv ?? strictDefault
106
+
107
+ const dryRunEnv = ['1', 'true', 'yes'].includes(String(process.env.DX_TELEGRAM_WEBHOOK_DRY_RUN || '').toLowerCase())
108
+ const dryRun = dryRunOverride ?? (dryRunEnv ? true : false)
109
+
110
+ const webhookPath = normalizeWebhookPath(webhookPathOverride ?? process.env.DX_TELEGRAM_WEBHOOK_PATH ?? '/api/webhook')
111
+
112
+ // 1. 验证必需环境变量
113
+ const botToken = process.env.TELEGRAM_BOT_TOKEN
114
+ const webhookSecret = process.env.TELEGRAM_BOT_WEBHOOK_SECRET
115
+
116
+ const missingVars = []
117
+ if (!botToken || envManager.isPlaceholderEnvValue(botToken)) {
118
+ missingVars.push('TELEGRAM_BOT_TOKEN')
119
+ }
120
+ if (!webhookSecret || envManager.isPlaceholderEnvValue(webhookSecret)) {
121
+ missingVars.push('TELEGRAM_BOT_WEBHOOK_SECRET')
122
+ }
123
+
124
+ if (missingVars.length > 0) {
125
+ logger.error('缺少以下 Telegram Bot 环境变量:')
126
+ missingVars.forEach(v => {
127
+ logger.error(` - ${v}`)
128
+ })
129
+
130
+ const message = 'Telegram Webhook 配置失败:缺少必需环境变量'
131
+ if (strict) {
132
+ return {
133
+ status: 'failed',
134
+ reason: 'missing_env_vars',
135
+ strict,
136
+ message,
137
+ missingVars,
138
+ }
139
+ }
140
+
141
+ logger.warn('跳过 Webhook 配置,请手动设置')
142
+ return {
143
+ status: 'warning',
144
+ reason: 'missing_env_vars',
145
+ strict,
146
+ message,
147
+ missingVars,
148
+ }
149
+ }
150
+
151
+ try {
152
+ // 2. 获取 Vercel 部署 URL
153
+ const deploymentUrl = await getLatestDeploymentUrl({
154
+ projectId,
155
+ orgId,
156
+ token,
157
+ environment,
158
+ deployOutput,
159
+ projectNameHint,
160
+ })
161
+ if (!deploymentUrl) {
162
+ const message = '无法获取 Vercel 部署 URL'
163
+ if (strict) {
164
+ return {
165
+ status: 'failed',
166
+ reason: 'missing_deploy_url',
167
+ strict,
168
+ message,
169
+ }
170
+ }
171
+ logger.error('无法获取 Vercel 部署 URL,跳过 Webhook 配置')
172
+ return {
173
+ status: 'warning',
174
+ reason: 'missing_deploy_url',
175
+ strict,
176
+ message,
177
+ }
178
+ }
179
+
180
+ const webhookUrl = `${deploymentUrl}${webhookPath}`
181
+ logger.info(`Webhook URL: ${webhookUrl}`)
182
+
183
+ if (dryRun) {
184
+ logger.warn('DX_TELEGRAM_WEBHOOK_DRY_RUN=1,已跳过 setWebhook/getWebhookInfo 调用')
185
+ return {
186
+ status: 'warning',
187
+ reason: 'dry_run',
188
+ strict,
189
+ message: 'DX_TELEGRAM_WEBHOOK_DRY_RUN=1',
190
+ webhookUrl,
191
+ }
192
+ }
193
+
194
+ // 3. 调用 Telegram API 设置 Webhook
195
+ const telegramApiUrl = `https://api.telegram.org/bot${botToken}/setWebhook`
196
+ const payload = JSON.stringify({
197
+ url: webhookUrl,
198
+ secret_token: webhookSecret,
199
+ drop_pending_updates: false,
200
+ })
201
+
202
+ const curlCmd = [
203
+ 'curl',
204
+ '-X POST',
205
+ `"${telegramApiUrl}"`,
206
+ '-H "Content-Type: application/json"',
207
+ `-d '${payload}'`,
208
+ '--silent',
209
+ ].join(' ')
210
+
211
+ const response = execSync(curlCmd, { encoding: 'utf8' })
212
+ const result = JSON.parse(response)
213
+
214
+ if (result.ok) {
215
+ logger.success('Telegram Webhook 设置成功')
216
+ logger.info(`Webhook URL: ${webhookUrl}`)
217
+
218
+ // 4. 验证 Webhook 状态
219
+ const verifyResult = await verifyWebhook(botToken, webhookUrl, { strict })
220
+ if (verifyResult.status === 'success') {
221
+ return {
222
+ status: 'success',
223
+ reason: 'verified',
224
+ strict,
225
+ webhookUrl,
226
+ }
227
+ }
228
+
229
+ return {
230
+ status: strict ? 'failed' : 'warning',
231
+ reason: 'verify_failed',
232
+ strict,
233
+ message: verifyResult.message,
234
+ webhookUrl,
235
+ }
236
+ }
237
+ else {
238
+ const desc = result.description || '未知错误'
239
+ const message = `Telegram Webhook 设置失败: ${desc}`
240
+ if (strict) {
241
+ return {
242
+ status: 'failed',
243
+ reason: 'set_webhook_failed',
244
+ strict,
245
+ message,
246
+ webhookUrl,
247
+ }
248
+ }
249
+
250
+ logger.error(message)
251
+ logger.info('请手动执行以下命令(不要把明文 token/secret 写进日志):')
252
+ const manualPayload = JSON.stringify({
253
+ url: webhookUrl,
254
+ secret_token: '<YOUR_WEBHOOK_SECRET>',
255
+ drop_pending_updates: false,
256
+ })
257
+ logger.info(
258
+ `curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" -H "Content-Type: application/json" -d '${manualPayload}' --silent`,
259
+ )
260
+ return {
261
+ status: 'warning',
262
+ reason: 'set_webhook_failed',
263
+ strict,
264
+ message,
265
+ webhookUrl,
266
+ }
267
+ }
268
+ }
269
+ catch (error) {
270
+ const message = error?.message || String(error)
271
+ logger.error(`Webhook 配置失败: ${message}`)
272
+
273
+ if (!strict) {
274
+ logger.warn('请手动设置 Webhook(参考 apps/telegram-bot/README.md)')
275
+ }
276
+ return {
277
+ status: strict ? 'failed' : 'warning',
278
+ reason: 'runtime_error',
279
+ strict,
280
+ message,
281
+ }
282
+ }
283
+ }
284
+
285
+ /**
286
+ * 获取最新部署的 URL
287
+ */
288
+ async function getLatestDeploymentUrl({
289
+ projectId,
290
+ orgId,
291
+ token,
292
+ environment,
293
+ deployOutput,
294
+ projectNameHint,
295
+ }) {
296
+ const fromDeploy = parseDeployUrlFromDeployOutput(deployOutput)
297
+ if (fromDeploy) return fromDeploy
298
+
299
+ const fromApi = await getDeploymentUrlFromVercelApi({ projectId, orgId, token, environment })
300
+ if (fromApi) return fromApi
301
+
302
+ const fromList = await getDeploymentUrlFromVercelList({ orgId, token, projectNameHint })
303
+ if (fromList) return fromList
304
+
305
+ return null
306
+ }
307
+
308
+ export function pickDeploymentUrlFromVercelApiResponse(json) {
309
+ const deployments = json?.deployments
310
+ if (!Array.isArray(deployments)) return null
311
+
312
+ for (const d of deployments) {
313
+ const url = d?.url
314
+ if (!url) continue
315
+ const state = d?.state || d?.readyState
316
+ if (state && String(state).toUpperCase() !== 'READY') continue
317
+ const normalized = normalizeDeployUrl(url)
318
+ if (normalized) return normalized
319
+ }
320
+
321
+ return null
322
+ }
323
+
324
+ async function getDeploymentUrlFromVercelApi({ projectId, orgId, token, environment }) {
325
+ try {
326
+ const qs = new URLSearchParams({
327
+ projectId: String(projectId),
328
+ state: 'READY',
329
+ limit: '10',
330
+ })
331
+
332
+ // dx 的 deploy 实现里:staging/production 都会传 --prod,因此对应 Vercel 的 production target。
333
+ // development 环境若需要兜底查询,则不强制 target(避免与 Vercel CLI/REST 字段差异耦合)。
334
+ if (environment !== 'development') {
335
+ qs.set('target', 'production')
336
+ }
337
+
338
+ if (orgId) {
339
+ const scope = String(orgId)
340
+ if (scope.startsWith('team_')) qs.set('teamId', scope)
341
+ else qs.set('slug', scope)
342
+ }
343
+
344
+ const url = `https://api.vercel.com/v6/deployments?${qs.toString()}`
345
+ const res = await fetch(url, {
346
+ method: 'GET',
347
+ headers: {
348
+ Authorization: `Bearer ${token}`,
349
+ },
350
+ })
351
+
352
+ if (!res.ok) {
353
+ logger.warn(`Vercel API 获取部署列表失败: HTTP ${res.status}`)
354
+ return null
355
+ }
356
+
357
+ const json = await res.json()
358
+ return pickDeploymentUrlFromVercelApiResponse(json)
359
+ } catch (error) {
360
+ logger.warn(`Vercel API 获取部署列表失败: ${error?.message || String(error)}`)
361
+ return null
362
+ }
363
+ }
364
+
365
+ async function getDeploymentUrlFromVercelList({ orgId, token, projectNameHint }) {
366
+ try {
367
+ const cmd = ['vercel', 'list', orgId ? `--scope=${orgId}` : '']
368
+ .filter(Boolean)
369
+ .join(' ')
370
+
371
+ const output = execSync(cmd, {
372
+ encoding: 'utf8',
373
+ env: {
374
+ ...process.env,
375
+ VERCEL_TOKEN: token,
376
+ },
377
+ })
378
+
379
+ return parseDeployUrlFromVercelListOutput(output, projectNameHint)
380
+ } catch (error) {
381
+ logger.warn(`vercel list 获取部署 URL 失败: ${error?.message || String(error)}`)
382
+ return null
383
+ }
384
+ }
385
+
386
+ /**
387
+ * 验证 Webhook 配置
388
+ */
389
+ async function verifyWebhook(botToken, expectedWebhookUrl, options = {}) {
390
+ const { strict = false } = options || {}
391
+ try {
392
+ const cmd = `curl -s "https://api.telegram.org/bot${botToken}/getWebhookInfo"`
393
+ const response = execSync(cmd, { encoding: 'utf8' })
394
+ const result = JSON.parse(response)
395
+
396
+ if (result.ok && result.result) {
397
+ const info = result.result
398
+ logger.info('Webhook 状态:')
399
+ logger.info(` URL: ${info.url}`)
400
+ logger.info(` Pending Updates: ${info.pending_update_count}`)
401
+ if (info.last_error_message) {
402
+ logger.warn(` 最后错误: ${info.last_error_message}`)
403
+ }
404
+
405
+ if (expectedWebhookUrl && info.url !== expectedWebhookUrl) {
406
+ const message = `Webhook 未生效:期望 ${expectedWebhookUrl},实际 ${info.url || '(empty)'}`
407
+ if (strict) {
408
+ return { status: 'failed', message }
409
+ }
410
+ logger.warn(message)
411
+ return { status: 'warning', message }
412
+ }
413
+ return { status: 'success' }
414
+ }
415
+ else {
416
+ const desc = result?.description || '未知错误'
417
+ const message = `getWebhookInfo 失败: ${desc}`
418
+ if (strict) {
419
+ return { status: 'failed', message }
420
+ }
421
+ logger.warn(message)
422
+ return { status: 'warning', message }
423
+ }
424
+ }
425
+ catch (error) {
426
+ const message = error?.message || String(error)
427
+ if (strict) return { status: 'failed', message }
428
+ logger.warn('无法验证 Webhook 状态')
429
+ return { status: 'warning', message: '无法验证 Webhook 状态' }
430
+ }
431
+ }