@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/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
+ })