@ranger1/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 +103 -0
- package/bin/dx-with-version-env.js +8 -0
- package/bin/dx.js +86 -0
- package/lib/backend-package.js +664 -0
- package/lib/cli/args.js +19 -0
- package/lib/cli/commands/core.js +233 -0
- package/lib/cli/commands/db.js +239 -0
- package/lib/cli/commands/deploy.js +76 -0
- package/lib/cli/commands/export.js +34 -0
- package/lib/cli/commands/package.js +22 -0
- package/lib/cli/commands/stack.js +451 -0
- package/lib/cli/commands/start.js +83 -0
- package/lib/cli/commands/worktree.js +149 -0
- package/lib/cli/dx-cli.js +864 -0
- package/lib/cli/flags.js +96 -0
- package/lib/cli/help.js +209 -0
- package/lib/cli/index.js +4 -0
- package/lib/confirm.js +213 -0
- package/lib/env.js +296 -0
- package/lib/exec.js +643 -0
- package/lib/logger.js +188 -0
- package/lib/run-with-version-env.js +173 -0
- package/lib/sdk-build.js +424 -0
- package/lib/start-dev.js +401 -0
- package/lib/telegram-webhook.js +134 -0
- package/lib/validate-env.js +284 -0
- package/lib/vercel-deploy.js +237 -0
- package/lib/worktree.js +1032 -0
- package/package.json +34 -0
package/lib/logger.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
// 处理输出管道被关闭导致的 EPIPE 错误,避免进程在清理阶段崩溃
|
|
9
|
+
try {
|
|
10
|
+
const handleBrokenPipe = err => {
|
|
11
|
+
if (err && (err.code === 'EPIPE' || err.code === 'ERR_STREAM_WRITE_AFTER_END')) {
|
|
12
|
+
// 静默忽略,允许进程优雅退出
|
|
13
|
+
try {
|
|
14
|
+
/* noop */
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (process?.stdout?.on) process.stdout.on('error', handleBrokenPipe)
|
|
19
|
+
if (process?.stderr?.on) process.stderr.on('error', handleBrokenPipe)
|
|
20
|
+
} catch {
|
|
21
|
+
/* 安全保护,忽略环境不支持的情况 */
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class Logger {
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.logLevel = options.level || 'info'
|
|
27
|
+
this.enableFile = options.enableFile || false
|
|
28
|
+
this.logDir = options.logDir || join(resolveProjectRoot(), 'dx', 'logs')
|
|
29
|
+
|
|
30
|
+
if (this.enableFile) {
|
|
31
|
+
this.ensureLogDir()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 确保日志目录存在
|
|
36
|
+
ensureLogDir() {
|
|
37
|
+
try {
|
|
38
|
+
mkdirSync(this.logDir, { recursive: true })
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// 目录可能已存在,忽略错误
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 格式化时间戳
|
|
45
|
+
formatTimestamp() {
|
|
46
|
+
return new Date().toLocaleString('zh-CN')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 基础日志方法
|
|
50
|
+
info(message, prefix = '🚀') {
|
|
51
|
+
const output = `${prefix} ${message}`
|
|
52
|
+
console.log(output)
|
|
53
|
+
this.writeLog('info', message)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
success(message) {
|
|
57
|
+
const output = `✅ ${message}`
|
|
58
|
+
console.log(output)
|
|
59
|
+
this.writeLog('success', message)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
warn(message) {
|
|
63
|
+
const output = `⚠️ ${message}`
|
|
64
|
+
console.log(output)
|
|
65
|
+
this.writeLog('warn', message)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
error(message) {
|
|
69
|
+
const output = `❌ ${message}`
|
|
70
|
+
console.log(output)
|
|
71
|
+
this.writeLog('error', message)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
debug(message) {
|
|
75
|
+
if (this.logLevel === 'debug') {
|
|
76
|
+
const output = `🐛 ${message}`
|
|
77
|
+
console.log(output)
|
|
78
|
+
this.writeLog('debug', message)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 步骤显示
|
|
83
|
+
step(message, stepNumber = null) {
|
|
84
|
+
const prefix = stepNumber ? `步骤 ${stepNumber}:` : '执行:'
|
|
85
|
+
const separator = '=================================='
|
|
86
|
+
|
|
87
|
+
console.log(`\n${separator}`)
|
|
88
|
+
console.log(`🚀 ${prefix} ${message}`)
|
|
89
|
+
console.log(separator)
|
|
90
|
+
|
|
91
|
+
this.writeLog('step', `${prefix} ${message}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 进度显示
|
|
95
|
+
progress(message) {
|
|
96
|
+
process.stdout.write(`⌛ ${message}...`)
|
|
97
|
+
this.writeLog('progress', `开始: ${message}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
progressDone() {
|
|
101
|
+
console.log(' 完成')
|
|
102
|
+
this.writeLog('progress', '完成')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 命令执行日志
|
|
106
|
+
command(command) {
|
|
107
|
+
console.log(`💻 执行: ${command}`)
|
|
108
|
+
this.writeLog('command', command)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 分隔符
|
|
112
|
+
separator() {
|
|
113
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 表格显示
|
|
117
|
+
table(data, headers = []) {
|
|
118
|
+
if (data.length === 0) return
|
|
119
|
+
|
|
120
|
+
if (headers.length > 0) {
|
|
121
|
+
console.log(`\n${headers.join('\t')}`)
|
|
122
|
+
console.log('-'.repeat(headers.join('\t').length))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
data.forEach(row => {
|
|
126
|
+
if (Array.isArray(row)) {
|
|
127
|
+
console.log(row.join('\t'))
|
|
128
|
+
} else {
|
|
129
|
+
console.log(row)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
console.log()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 端口信息显示
|
|
136
|
+
ports(portInfo) {
|
|
137
|
+
console.log('\n📡 服务端口信息:')
|
|
138
|
+
portInfo.forEach(({ service, port, url }) => {
|
|
139
|
+
console.log(` ${service}: http://localhost:${port} ${url ? `(${url})` : ''}`)
|
|
140
|
+
})
|
|
141
|
+
console.log()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 写入日志文件
|
|
145
|
+
writeLog(level, message) {
|
|
146
|
+
if (!this.enableFile) return
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const timestamp = this.formatTimestamp()
|
|
150
|
+
const logLine = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`
|
|
151
|
+
|
|
152
|
+
const logFile = join(this.logDir, `ai-cli-${new Date().toISOString().split('T')[0]}.log`)
|
|
153
|
+
writeFileSync(logFile, logLine, { flag: 'a', encoding: 'utf8' })
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// 写入日志失败时静默处理,避免影响主流程
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 创建子日志器
|
|
160
|
+
createChild(prefix) {
|
|
161
|
+
const childLogger = new Logger({
|
|
162
|
+
level: this.logLevel,
|
|
163
|
+
enableFile: this.enableFile,
|
|
164
|
+
logDir: this.logDir,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// 重写方法以添加前缀
|
|
168
|
+
const originalMethods = ['info', 'success', 'warn', 'error', 'debug']
|
|
169
|
+
originalMethods.forEach(method => {
|
|
170
|
+
const originalMethod = childLogger[method].bind(childLogger)
|
|
171
|
+
childLogger[method] = (message, customPrefix) => {
|
|
172
|
+
const finalPrefix = customPrefix || prefix
|
|
173
|
+
originalMethod(message, finalPrefix)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
return childLogger
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 导出默认实例
|
|
182
|
+
export const logger = new Logger()
|
|
183
|
+
|
|
184
|
+
// 导出带文件日志的实例
|
|
185
|
+
export const fileLogger = new Logger({ enableFile: true })
|
|
186
|
+
|
|
187
|
+
// 导出调试日志实例
|
|
188
|
+
export const debugLogger = new Logger({ level: 'debug', enableFile: true })
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, execSync } from 'node:child_process'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { resolve, join, dirname } from 'node:path'
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
const allowedApps = ['backend', 'front', 'admin']
|
|
9
|
+
|
|
10
|
+
function findWorkspaceRoot(startDir) {
|
|
11
|
+
let current = resolve(startDir)
|
|
12
|
+
while (true) {
|
|
13
|
+
if (existsSync(join(current, 'dx', 'config', 'commands.json'))) return current
|
|
14
|
+
if (existsSync(join(current, 'pnpm-workspace.yaml'))) return current
|
|
15
|
+
const parent = dirname(current)
|
|
16
|
+
if (parent === current) return startDir
|
|
17
|
+
current = parent
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const args = [...argv]
|
|
23
|
+
let app = null
|
|
24
|
+
const extraEnv = {}
|
|
25
|
+
while (args.length > 0) {
|
|
26
|
+
const current = args.shift()
|
|
27
|
+
if (current === '--') {
|
|
28
|
+
return { app, command: args, extraEnv }
|
|
29
|
+
}
|
|
30
|
+
if ((current === '--app' || current === '-a') && args.length > 0) {
|
|
31
|
+
app = args.shift()
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
if (current.startsWith('--set=')) {
|
|
35
|
+
const pair = current.slice('--set='.length)
|
|
36
|
+
const [key, ...rest] = pair.split('=')
|
|
37
|
+
extraEnv[key] = rest.join('=') ?? ''
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`无法识别的参数 ${current}`)
|
|
41
|
+
}
|
|
42
|
+
throw new Error('缺少命令分隔符 --')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readPackageVersion(app) {
|
|
46
|
+
const root = process.env.DX_PROJECT_ROOT || findWorkspaceRoot(process.cwd())
|
|
47
|
+
const appDirMap = {
|
|
48
|
+
backend: 'apps/backend/package.json',
|
|
49
|
+
front: 'apps/front/package.json',
|
|
50
|
+
admin: 'apps/admin-front/package.json',
|
|
51
|
+
}
|
|
52
|
+
const pkgPath = appDirMap[app]
|
|
53
|
+
if (!pkgPath) return '0.0.0'
|
|
54
|
+
const absPath = resolve(root, pkgPath)
|
|
55
|
+
try {
|
|
56
|
+
const raw = await readFile(absPath, 'utf8')
|
|
57
|
+
const pkg = JSON.parse(raw)
|
|
58
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0'
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.warn(`[with-version-env] 读取 ${pkgPath} 失败,使用默认版本 0.0.0`, error)
|
|
61
|
+
return '0.0.0'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveGitSha() {
|
|
66
|
+
try {
|
|
67
|
+
const root = process.env.DX_PROJECT_ROOT || findWorkspaceRoot(process.cwd())
|
|
68
|
+
const sha = execSync('git rev-parse HEAD', { encoding: 'utf8', cwd: root }).trim()
|
|
69
|
+
return sha
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.warn('[with-version-env] 获取 Git SHA 失败,使用 unknown', error)
|
|
72
|
+
return 'unknown'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildEnv(app, baseEnv, extraEnv) {
|
|
77
|
+
const now = new Date().toISOString()
|
|
78
|
+
const gitSha = resolveGitSha()
|
|
79
|
+
const gitShort = gitSha && gitSha !== 'unknown' ? gitSha.slice(0, 7) : 'unknown'
|
|
80
|
+
const buildNumber =
|
|
81
|
+
process.env.BUILD_NUMBER || process.env.GITHUB_RUN_NUMBER || process.env.CI_PIPELINE_IID || ''
|
|
82
|
+
|
|
83
|
+
const result = { ...baseEnv, ...extraEnv }
|
|
84
|
+
|
|
85
|
+
const assign = (entries = []) => {
|
|
86
|
+
entries.forEach(([key, value]) => {
|
|
87
|
+
if (value !== undefined && value !== null) {
|
|
88
|
+
result[key] = String(value)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
switch (app) {
|
|
94
|
+
case 'backend': {
|
|
95
|
+
const version = baseEnv.__APP_VERSION__
|
|
96
|
+
assign([
|
|
97
|
+
['APP_VERSION', version],
|
|
98
|
+
['APP_BUILD_VERSION', version],
|
|
99
|
+
['GIT_SHA', gitSha],
|
|
100
|
+
['GIT_SHORT_SHA', gitShort],
|
|
101
|
+
['BUILD_GIT_SHA', gitSha],
|
|
102
|
+
['BUILD_TIME', now],
|
|
103
|
+
['BUILD_TIMESTAMP', now],
|
|
104
|
+
['BUILD_NUMBER', buildNumber],
|
|
105
|
+
])
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
case 'front': {
|
|
109
|
+
const version = baseEnv.__APP_VERSION__
|
|
110
|
+
assign([
|
|
111
|
+
['NEXT_PUBLIC_APP_VERSION', version],
|
|
112
|
+
['NEXT_PUBLIC_GIT_SHA', gitSha],
|
|
113
|
+
['NEXT_PUBLIC_GIT_SHORT_SHA', gitShort],
|
|
114
|
+
['NEXT_PUBLIC_BUILD_TIME', now],
|
|
115
|
+
['NEXT_PUBLIC_BUILD_NUMBER', buildNumber],
|
|
116
|
+
])
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
case 'admin': {
|
|
120
|
+
const version = baseEnv.__APP_VERSION__
|
|
121
|
+
assign([
|
|
122
|
+
['VITE_APP_VERSION', version],
|
|
123
|
+
['VITE_GIT_SHA', gitSha],
|
|
124
|
+
['VITE_GIT_SHORT_SHA', gitShort],
|
|
125
|
+
['VITE_BUILD_TIME', now],
|
|
126
|
+
['VITE_BUILD_NUMBER', buildNumber],
|
|
127
|
+
])
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
delete result.__APP_VERSION__
|
|
135
|
+
return result
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function runWithVersionEnv(argv = []) {
|
|
139
|
+
const { app, command, extraEnv } = parseArgs(Array.isArray(argv) ? argv : [])
|
|
140
|
+
if (!app || !allowedApps.includes(app)) {
|
|
141
|
+
throw new Error(`必须使用 --app 指定 ${allowedApps.join(', ')} 之一`)
|
|
142
|
+
}
|
|
143
|
+
if (!command || command.length === 0) {
|
|
144
|
+
throw new Error('缺少待执行命令')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const version = await readPackageVersion(app)
|
|
148
|
+
const env = buildEnv(app, { ...process.env, __APP_VERSION__: version }, extraEnv)
|
|
149
|
+
|
|
150
|
+
const child = spawn(command[0], command.slice(1), {
|
|
151
|
+
stdio: 'inherit',
|
|
152
|
+
env,
|
|
153
|
+
shell: false,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
child.on('exit', code => {
|
|
157
|
+
process.exit(code ?? 0)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
child.on('error', error => {
|
|
161
|
+
console.error('[with-version-env] 启动命令失败', error)
|
|
162
|
+
process.exit(1)
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function main() {
|
|
167
|
+
await runWithVersionEnv(process.argv.slice(2))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main().catch(error => {
|
|
171
|
+
console.error('[with-version-env] 执行失败:', error)
|
|
172
|
+
process.exit(1)
|
|
173
|
+
})
|