@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
package/lib/logger.js ADDED
@@ -0,0 +1,239 @@
1
+ import { writeFileSync, mkdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ function resolveProjectRoot() {
5
+ return process.env.DX_PROJECT_ROOT || process.cwd()
6
+ }
7
+
8
+ export function sanitizeForLog(input) {
9
+ let text = input == null ? '' : String(input)
10
+
11
+ // CLI token args (vercel)
12
+ text = text.replace(/--token=("[^"]*"|'[^']*'|[^\s]+)/gi, '--token=***')
13
+ text = text.replace(/--token\s+("[^"]*"|'[^']*'|[^\s]+)/gi, '--token ***')
14
+
15
+ // Env style secrets
16
+ text = text.replace(/\bVERCEL_TOKEN=([^\s]+)/g, 'VERCEL_TOKEN=***')
17
+ text = text.replace(/\bTELEGRAM_BOT_TOKEN=([^\s]+)/g, 'TELEGRAM_BOT_TOKEN=***')
18
+ text = text.replace(
19
+ /\bTELEGRAM_BOT_WEBHOOK_SECRET=([^\s]+)/g,
20
+ 'TELEGRAM_BOT_WEBHOOK_SECRET=***',
21
+ )
22
+
23
+ // Authorization bearer
24
+ text = text.replace(
25
+ /Authorization:\s*Bearer\s+([^\s]+)/gi,
26
+ 'Authorization: Bearer ***',
27
+ )
28
+
29
+ // JSON-ish token fields
30
+ text = text.replace(/"token"\s*:\s*"[^"]*"/gi, '"token":"***"')
31
+ text = text.replace(
32
+ /("secret_token"\s*:\s*")([^"]*)(")/gi,
33
+ '$1***$3',
34
+ )
35
+ text = text.replace(/\bsecret_token=([^\s&]+)/gi, 'secret_token=***')
36
+
37
+ // Telegram bot token in URLs
38
+ text = text.replace(
39
+ /api\.telegram\.org\/bot([^/\s]+)(\/|$)/gi,
40
+ 'api.telegram.org/bot***$2',
41
+ )
42
+
43
+ return text
44
+ }
45
+
46
+ // 处理输出管道被关闭导致的 EPIPE 错误,避免进程在清理阶段崩溃
47
+ try {
48
+ const handleBrokenPipe = err => {
49
+ if (err && (err.code === 'EPIPE' || err.code === 'ERR_STREAM_WRITE_AFTER_END')) {
50
+ // 静默忽略,允许进程优雅退出
51
+ try {
52
+ /* noop */
53
+ } catch {}
54
+ }
55
+ }
56
+ if (process?.stdout?.on) process.stdout.on('error', handleBrokenPipe)
57
+ if (process?.stderr?.on) process.stderr.on('error', handleBrokenPipe)
58
+ } catch {
59
+ /* 安全保护,忽略环境不支持的情况 */
60
+ }
61
+
62
+ export class Logger {
63
+ constructor(options = {}) {
64
+ this.logLevel = options.level || 'info'
65
+ this.enableFile = options.enableFile || false
66
+ this.logDir = options.logDir || join(resolveProjectRoot(), 'dx', 'logs')
67
+
68
+ if (this.enableFile) {
69
+ this.ensureLogDir()
70
+ }
71
+ }
72
+
73
+ // 确保日志目录存在
74
+ ensureLogDir() {
75
+ try {
76
+ mkdirSync(this.logDir, { recursive: true })
77
+ } catch (error) {
78
+ // 目录可能已存在,忽略错误
79
+ }
80
+ }
81
+
82
+ // 格式化时间戳
83
+ formatTimestamp() {
84
+ return new Date().toLocaleString('zh-CN')
85
+ }
86
+
87
+ // 基础日志方法
88
+ info(message, prefix = '🚀') {
89
+ const safeMessage = sanitizeForLog(message)
90
+ const output = `${prefix} ${safeMessage}`
91
+ console.log(output)
92
+ this.writeLog('info', safeMessage)
93
+ }
94
+
95
+ success(message) {
96
+ const safeMessage = sanitizeForLog(message)
97
+ const output = `✅ ${safeMessage}`
98
+ console.log(output)
99
+ this.writeLog('success', safeMessage)
100
+ }
101
+
102
+ warn(message) {
103
+ const safeMessage = sanitizeForLog(message)
104
+ const output = `⚠️ ${safeMessage}`
105
+ console.log(output)
106
+ this.writeLog('warn', safeMessage)
107
+ }
108
+
109
+ error(message) {
110
+ const safeMessage = sanitizeForLog(message)
111
+ const output = `❌ ${safeMessage}`
112
+ console.log(output)
113
+ this.writeLog('error', safeMessage)
114
+ }
115
+
116
+ debug(message) {
117
+ if (this.logLevel === 'debug') {
118
+ const safeMessage = sanitizeForLog(message)
119
+ const output = `🐛 ${safeMessage}`
120
+ console.log(output)
121
+ this.writeLog('debug', safeMessage)
122
+ }
123
+ }
124
+
125
+ // 步骤显示
126
+ step(message, stepNumber = null) {
127
+ const prefix = stepNumber ? `步骤 ${stepNumber}:` : '执行:'
128
+ const separator = '=================================='
129
+
130
+ const safeMessage = sanitizeForLog(message)
131
+
132
+ console.log(`\n${separator}`)
133
+ console.log(`🚀 ${prefix} ${safeMessage}`)
134
+ console.log(separator)
135
+
136
+ this.writeLog('step', `${prefix} ${safeMessage}`)
137
+ }
138
+
139
+ // 进度显示
140
+ progress(message) {
141
+ const safeMessage = sanitizeForLog(message)
142
+ process.stdout.write(`⌛ ${safeMessage}...`)
143
+ this.writeLog('progress', `开始: ${safeMessage}`)
144
+ }
145
+
146
+ progressDone() {
147
+ console.log(' 完成')
148
+ this.writeLog('progress', '完成')
149
+ }
150
+
151
+ // 命令执行日志
152
+ command(command) {
153
+ const safeCommand = sanitizeForLog(command)
154
+ console.log(`💻 执行: ${safeCommand}`)
155
+ this.writeLog('command', safeCommand)
156
+ }
157
+
158
+ // 分隔符
159
+ separator() {
160
+ console.log(`\n${'='.repeat(50)}`)
161
+ }
162
+
163
+ // 表格显示
164
+ table(data, headers = []) {
165
+ if (data.length === 0) return
166
+
167
+ if (headers.length > 0) {
168
+ const safeHeaders = headers.map(h => sanitizeForLog(h))
169
+ console.log(`\n${safeHeaders.join('\t')}`)
170
+ console.log('-'.repeat(safeHeaders.join('\t').length))
171
+ }
172
+
173
+ data.forEach(row => {
174
+ if (Array.isArray(row)) {
175
+ console.log(row.map(cell => sanitizeForLog(cell)).join('\t'))
176
+ } else {
177
+ console.log(sanitizeForLog(row))
178
+ }
179
+ })
180
+ console.log()
181
+ }
182
+
183
+ // 端口信息显示
184
+ ports(portInfo) {
185
+ console.log('\n📡 服务端口信息:')
186
+ portInfo.forEach(({ service, port, url }) => {
187
+ const safeService = sanitizeForLog(service)
188
+ const safeUrl = url ? sanitizeForLog(url) : ''
189
+ console.log(` ${safeService}: http://localhost:${port} ${safeUrl ? `(${safeUrl})` : ''}`)
190
+ })
191
+ console.log()
192
+ }
193
+
194
+ // 写入日志文件
195
+ writeLog(level, message) {
196
+ if (!this.enableFile) return
197
+
198
+ try {
199
+ const safeMessage = sanitizeForLog(message)
200
+ const timestamp = this.formatTimestamp()
201
+ const logLine = `[${timestamp}] [${level.toUpperCase()}] ${safeMessage}\n`
202
+
203
+ const logFile = join(this.logDir, `ai-cli-${new Date().toISOString().split('T')[0]}.log`)
204
+ writeFileSync(logFile, logLine, { flag: 'a', encoding: 'utf8' })
205
+ } catch (error) {
206
+ // 写入日志失败时静默处理,避免影响主流程
207
+ }
208
+ }
209
+
210
+ // 创建子日志器
211
+ createChild(prefix) {
212
+ const childLogger = new Logger({
213
+ level: this.logLevel,
214
+ enableFile: this.enableFile,
215
+ logDir: this.logDir,
216
+ })
217
+
218
+ // 重写方法以添加前缀
219
+ const originalMethods = ['info', 'success', 'warn', 'error', 'debug']
220
+ originalMethods.forEach(method => {
221
+ const originalMethod = childLogger[method].bind(childLogger)
222
+ childLogger[method] = (message, customPrefix) => {
223
+ const finalPrefix = customPrefix || prefix
224
+ originalMethod(message, finalPrefix)
225
+ }
226
+ })
227
+
228
+ return childLogger
229
+ }
230
+ }
231
+
232
+ // 导出默认实例
233
+ export const logger = new Logger()
234
+
235
+ // 导出带文件日志的实例
236
+ export const fileLogger = new Logger({ enableFile: true })
237
+
238
+ // 导出调试日志实例
239
+ export const debugLogger = new Logger({ level: 'debug', enableFile: true })
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export const NX_IGNORE_TOOL_DIR_PATTERNS = [
5
+ '.cache/',
6
+ '.claude/',
7
+ '.codex/',
8
+ '.idea/',
9
+ '.omc/',
10
+ '.omx/',
11
+ '.opencode/',
12
+ '.pytest_cache/',
13
+ ]
14
+
15
+ const MANAGED_BLOCK_START = '# dx managed tool metadata ignores'
16
+
17
+ export function isNxCommand(command) {
18
+ return /\bnx(?:\.js)?\b/.test(String(command || ''))
19
+ }
20
+
21
+ export function ensureNxIgnoreToolDirs(projectRoot = process.cwd()) {
22
+ const root = String(projectRoot || process.cwd())
23
+ const nxIgnorePath = join(root, '.nxignore')
24
+ const existing = existsSync(nxIgnorePath) ? readFileSync(nxIgnorePath, 'utf8') : ''
25
+ const existingLines = new Set(
26
+ existing
27
+ .split(/\r?\n/)
28
+ .map(line => line.trim())
29
+ .filter(Boolean),
30
+ )
31
+ const missing = NX_IGNORE_TOOL_DIR_PATTERNS.filter(pattern => !existingLines.has(pattern))
32
+
33
+ if (missing.length === 0) {
34
+ return { changed: false, path: nxIgnorePath, added: [] }
35
+ }
36
+
37
+ const prefix = existing.trimEnd()
38
+ const blockLines = existingLines.has(MANAGED_BLOCK_START)
39
+ ? missing
40
+ : [MANAGED_BLOCK_START, ...missing]
41
+ const next = `${prefix ? `${prefix}\n\n` : ''}${blockLines.join('\n')}\n`
42
+
43
+ writeFileSync(nxIgnorePath, next)
44
+ return { changed: true, path: nxIgnorePath, added: missing }
45
+ }
@@ -0,0 +1,163 @@
1
+ import { spawn, execSync } from 'node:child_process'
2
+ import { sanitizeChildEnv } from './exec.js'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { resolve, join, dirname } from 'node:path'
5
+ import { existsSync } from 'node:fs'
6
+
7
+ const allowedApps = ['backend', 'front', 'admin']
8
+
9
+ function findWorkspaceRoot(startDir) {
10
+ let current = resolve(startDir)
11
+ while (true) {
12
+ if (existsSync(join(current, 'dx', 'config', 'commands.json'))) return current
13
+ if (existsSync(join(current, 'pnpm-workspace.yaml'))) return current
14
+ const parent = dirname(current)
15
+ if (parent === current) return startDir
16
+ current = parent
17
+ }
18
+ }
19
+
20
+ function parseArgs(argv) {
21
+ const args = [...argv]
22
+ let app = null
23
+ const extraEnv = {}
24
+ while (args.length > 0) {
25
+ const current = args.shift()
26
+ if (current === '--') {
27
+ return { app, command: args, extraEnv }
28
+ }
29
+ if ((current === '--app' || current === '-a') && args.length > 0) {
30
+ app = args.shift()
31
+ continue
32
+ }
33
+ if (current.startsWith('--set=')) {
34
+ const pair = current.slice('--set='.length)
35
+ const [key, ...rest] = pair.split('=')
36
+ extraEnv[key] = rest.join('=') ?? ''
37
+ continue
38
+ }
39
+ throw new Error(`无法识别的参数 ${current}`)
40
+ }
41
+ throw new Error('缺少命令分隔符 --')
42
+ }
43
+
44
+ async function readPackageVersion(app) {
45
+ const root = process.env.DX_PROJECT_ROOT || findWorkspaceRoot(process.cwd())
46
+ const appDirMap = {
47
+ backend: 'apps/backend/package.json',
48
+ front: 'apps/front/package.json',
49
+ admin: 'apps/admin-front/package.json'
50
+ }
51
+ const pkgPath = appDirMap[app]
52
+ if (!pkgPath) return '0.0.0'
53
+ const absPath = resolve(root, pkgPath)
54
+ try {
55
+ const raw = await readFile(absPath, 'utf8')
56
+ const pkg = JSON.parse(raw)
57
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0'
58
+ } catch (error) {
59
+ console.warn(`[with-version-env] 读取 ${pkgPath} 失败,使用默认版本 0.0.0`, error)
60
+ return '0.0.0'
61
+ }
62
+ }
63
+
64
+ function resolveGitSha() {
65
+ try {
66
+ const root = process.env.DX_PROJECT_ROOT || findWorkspaceRoot(process.cwd())
67
+ const sha = execSync('git rev-parse HEAD', { encoding: 'utf8', cwd: root }).trim()
68
+ return sha
69
+ } catch (error) {
70
+ console.warn('[with-version-env] 获取 Git SHA 失败,使用 unknown', error)
71
+ return 'unknown'
72
+ }
73
+ }
74
+
75
+ function buildEnv(app, baseEnv, extraEnv) {
76
+ const now = new Date().toISOString()
77
+ const gitSha = resolveGitSha()
78
+ const gitShort = gitSha && gitSha !== 'unknown' ? gitSha.slice(0, 7) : 'unknown'
79
+ const buildNumber =
80
+ process.env.BUILD_NUMBER || process.env.GITHUB_RUN_NUMBER || process.env.CI_PIPELINE_IID || ''
81
+
82
+ const result = { ...baseEnv, ...extraEnv }
83
+
84
+ const assign = (entries = []) => {
85
+ entries.forEach(([key, value]) => {
86
+ if (value !== undefined && value !== null) {
87
+ result[key] = String(value)
88
+ }
89
+ })
90
+ }
91
+
92
+ switch (app) {
93
+ case 'backend': {
94
+ const version = baseEnv.__APP_VERSION__
95
+ assign([
96
+ ['APP_VERSION', version],
97
+ ['APP_BUILD_VERSION', version],
98
+ ['GIT_SHA', gitSha],
99
+ ['GIT_SHORT_SHA', gitShort],
100
+ ['BUILD_GIT_SHA', gitSha],
101
+ ['BUILD_TIME', now],
102
+ ['BUILD_TIMESTAMP', now],
103
+ ['BUILD_NUMBER', buildNumber]
104
+ ])
105
+ break
106
+ }
107
+ case 'front': {
108
+ const version = baseEnv.__APP_VERSION__
109
+ assign([
110
+ ['NEXT_PUBLIC_APP_VERSION', version],
111
+ ['NEXT_PUBLIC_GIT_SHA', gitSha],
112
+ ['NEXT_PUBLIC_GIT_SHORT_SHA', gitShort],
113
+ ['NEXT_PUBLIC_BUILD_TIME', now],
114
+ ['NEXT_PUBLIC_BUILD_NUMBER', buildNumber]
115
+ ])
116
+ break
117
+ }
118
+ case 'admin': {
119
+ const version = baseEnv.__APP_VERSION__
120
+ assign([
121
+ ['VITE_APP_VERSION', version],
122
+ ['VITE_GIT_SHA', gitSha],
123
+ ['VITE_GIT_SHORT_SHA', gitShort],
124
+ ['VITE_BUILD_TIME', now],
125
+ ['VITE_BUILD_NUMBER', buildNumber]
126
+ ])
127
+ break
128
+ }
129
+ default:
130
+ break
131
+ }
132
+
133
+ delete result.__APP_VERSION__
134
+ return result
135
+ }
136
+
137
+ export async function runWithVersionEnv(argv = []) {
138
+ const { app, command, extraEnv } = parseArgs(Array.isArray(argv) ? argv : [])
139
+ if (!app || !allowedApps.includes(app)) {
140
+ throw new Error(`必须使用 --app 指定 ${allowedApps.join(', ')} 之一`)
141
+ }
142
+ if (!command || command.length === 0) {
143
+ throw new Error('缺少待执行命令')
144
+ }
145
+
146
+ const version = await readPackageVersion(app)
147
+ const env = buildEnv(app, { ...process.env, __APP_VERSION__: version }, extraEnv)
148
+
149
+ const child = spawn(command[0], command.slice(1), {
150
+ stdio: 'inherit',
151
+ env: sanitizeChildEnv(env),
152
+ shell: false
153
+ })
154
+
155
+ child.on('exit', code => {
156
+ process.exit(code ?? 0)
157
+ })
158
+
159
+ child.on('error', error => {
160
+ console.error('[with-version-env] 启动命令失败', error)
161
+ process.exit(1)
162
+ })
163
+ }