@ranger1/dx 0.1.14 → 0.1.16
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/cli/dx-cli.js +1 -4
- package/lib/cli/help.js +0 -38
- package/package.json +1 -1
- package/lib/cli/commands/ai.js +0 -214
package/lib/cli/dx-cli.js
CHANGED
|
@@ -33,7 +33,6 @@ import { handlePackage } from './commands/package.js'
|
|
|
33
33
|
import { handleExport } from './commands/export.js'
|
|
34
34
|
import { handleContracts } from './commands/contracts.js'
|
|
35
35
|
import { handleRelease } from './commands/release.js'
|
|
36
|
-
import { handleAi } from './commands/ai.js'
|
|
37
36
|
|
|
38
37
|
class DxCli {
|
|
39
38
|
constructor(options = {}) {
|
|
@@ -68,7 +67,6 @@ class DxCli {
|
|
|
68
67
|
export: args => handleExport(this, args),
|
|
69
68
|
contracts: args => handleContracts(this, args),
|
|
70
69
|
release: args => handleRelease(this, args),
|
|
71
|
-
ai: args => handleAi(this, args),
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
this.flagDefinitions = FLAG_DEFINITIONS
|
|
@@ -299,8 +297,7 @@ class DxCli {
|
|
|
299
297
|
// - help: printing help should never require env vars.
|
|
300
298
|
// - status: should be available even when env is incomplete.
|
|
301
299
|
// - release: only edits package.json versions.
|
|
302
|
-
|
|
303
|
-
const skipStartupChecks = new Set(['help', 'status', 'release', 'ai'])
|
|
300
|
+
const skipStartupChecks = new Set(['help', 'status', 'release'])
|
|
304
301
|
if (skipStartupChecks.has(this.command)) {
|
|
305
302
|
await this.routeCommand()
|
|
306
303
|
return
|
package/lib/cli/help.js
CHANGED
|
@@ -68,9 +68,6 @@ export function showHelp() {
|
|
|
68
68
|
'',
|
|
69
69
|
' status 查看系统状态',
|
|
70
70
|
'',
|
|
71
|
-
' ai <name> 运行一个预配置的 opencode 任务(从 commands.json 读取)',
|
|
72
|
-
' 透传: -- 后的参数将原样传给 opencode run',
|
|
73
|
-
'',
|
|
74
71
|
'选项:',
|
|
75
72
|
' --dev, --development 使用开发环境',
|
|
76
73
|
' --prod, --production 使用生产环境',
|
|
@@ -104,9 +101,6 @@ export function showHelp() {
|
|
|
104
101
|
' dx contracts # 导出 OpenAPI 并生成 Zod 合约',
|
|
105
102
|
' dx release version 1.2.3 # 同步 backend/front/admin-front 版本号',
|
|
106
103
|
'',
|
|
107
|
-
' dx ai review',
|
|
108
|
-
' dx ai review -- --share --title "my run"',
|
|
109
|
-
'',
|
|
110
104
|
' # Stagewise 桥接(固定端口,自动清理占用)',
|
|
111
105
|
' dx start stagewise-front # 桥接 front: 3001 -> 3002(工作目录 apps/front)',
|
|
112
106
|
' dx start stagewise-admin # 桥接 admin-front: 3500 -> 3501(工作目录 apps/admin-front)',
|
|
@@ -257,38 +251,6 @@ release 命令用法:
|
|
|
257
251
|
`)
|
|
258
252
|
return
|
|
259
253
|
|
|
260
|
-
case 'ai':
|
|
261
|
-
console.log(
|
|
262
|
-
[
|
|
263
|
-
'',
|
|
264
|
-
'ai 命令用法:',
|
|
265
|
-
' dx ai <name> [-- <opencode flags>...]',
|
|
266
|
-
'',
|
|
267
|
-
'说明:',
|
|
268
|
-
' - 从 dx/config/commands.json 的 ai.<name> 读取一个固定配置并执行 opencode run',
|
|
269
|
-
' - 本次运行固定注入 OPENCODE_PERMISSION="allow"(全权限)',
|
|
270
|
-
'',
|
|
271
|
-
'commands.json 示例:',
|
|
272
|
-
' {',
|
|
273
|
-
' "ai": {',
|
|
274
|
-
' "review": {',
|
|
275
|
-
' "promptFile": "./prompts/review.md",',
|
|
276
|
-
' "model": "openai/gpt-4.1",',
|
|
277
|
-
' "agent": "general",',
|
|
278
|
-
' "format": "default",',
|
|
279
|
-
' "passthrough": ["--share"]',
|
|
280
|
-
' }',
|
|
281
|
-
' }',
|
|
282
|
-
' }',
|
|
283
|
-
'',
|
|
284
|
-
'示例:',
|
|
285
|
-
' dx ai review',
|
|
286
|
-
' dx ai review -- --title "my run"',
|
|
287
|
-
'',
|
|
288
|
-
].join('\n'),
|
|
289
|
-
)
|
|
290
|
-
return
|
|
291
|
-
|
|
292
254
|
default:
|
|
293
255
|
showHelp()
|
|
294
256
|
}
|
package/package.json
CHANGED
package/lib/cli/commands/ai.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { resolve, join, isAbsolute } from 'node:path'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
|
-
import { spawn, spawnSync } from 'node:child_process'
|
|
5
|
-
import { logger } from '../../logger.js'
|
|
6
|
-
import { confirmManager } from '../../confirm.js'
|
|
7
|
-
import { getPassthroughArgs } from '../args.js'
|
|
8
|
-
|
|
9
|
-
function ensureOpencodeAvailable() {
|
|
10
|
-
const result = spawnSync('opencode', ['--version'], { stdio: 'ignore' })
|
|
11
|
-
if (result?.error?.code === 'ENOENT') {
|
|
12
|
-
return { ok: false, reason: 'missing' }
|
|
13
|
-
}
|
|
14
|
-
return { ok: true }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function resolveAiConfig(cli, name) {
|
|
18
|
-
const raw = cli.commands?.ai?.[name]
|
|
19
|
-
if (!raw) return null
|
|
20
|
-
|
|
21
|
-
const environment = cli.determineEnvironment()
|
|
22
|
-
const envKey = cli.normalizeEnvKey(environment)
|
|
23
|
-
let config = raw
|
|
24
|
-
|
|
25
|
-
// 允许按环境分支(保持与 build/start/export 一致)
|
|
26
|
-
if (typeof config === 'object' && config && !config.promptFile && !config.command) {
|
|
27
|
-
if (config[envKey]) config = config[envKey]
|
|
28
|
-
else if (envKey === 'staging' && config.prod) config = config.prod
|
|
29
|
-
else config = config.dev || config
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return config
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function expandHomePath(input) {
|
|
36
|
-
const raw = String(input ?? '')
|
|
37
|
-
if (raw === '~') return homedir()
|
|
38
|
-
if (raw.startsWith('~/') || raw.startsWith('~\\')) {
|
|
39
|
-
return resolve(homedir(), raw.slice(2))
|
|
40
|
-
}
|
|
41
|
-
return raw
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isBareFilename(input) {
|
|
45
|
-
const raw = String(input ?? '')
|
|
46
|
-
if (!raw) return false
|
|
47
|
-
if (raw.startsWith('.')) return false
|
|
48
|
-
return !raw.includes('/') && !raw.includes('\\')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function resolvePromptPath(promptFile) {
|
|
52
|
-
const projectRoot = process.env.DX_PROJECT_ROOT || process.cwd()
|
|
53
|
-
const expanded = expandHomePath(promptFile)
|
|
54
|
-
|
|
55
|
-
if (isAbsolute(expanded)) return [expanded]
|
|
56
|
-
|
|
57
|
-
const dxDir = join(projectRoot, 'dx')
|
|
58
|
-
const candidates = []
|
|
59
|
-
|
|
60
|
-
// 仅文件名:优先在 dx/prompts 下找(不破坏历史:仍保留 project root fallback)
|
|
61
|
-
if (isBareFilename(expanded)) {
|
|
62
|
-
candidates.push(join(dxDir, 'prompts', expanded))
|
|
63
|
-
candidates.push(resolve(projectRoot, expanded))
|
|
64
|
-
candidates.push(resolve(dxDir, expanded))
|
|
65
|
-
return candidates
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// 相对路径:优先保持与历史一致(相对 project root),找不到再 fallback 到 dx/ 下
|
|
69
|
-
candidates.push(resolve(projectRoot, expanded))
|
|
70
|
-
candidates.push(resolve(dxDir, expanded))
|
|
71
|
-
return candidates
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function handleAi(cli, args = []) {
|
|
75
|
-
const name = args[0]
|
|
76
|
-
if (!name) {
|
|
77
|
-
const names = Object.keys(cli.commands?.ai || {})
|
|
78
|
-
logger.error('请指定 ai 任务名称')
|
|
79
|
-
logger.info(`用法: ${cli.invocation} ai <name> [-- <opencode flags>...]`)
|
|
80
|
-
if (names.length > 0) {
|
|
81
|
-
logger.info(`可用任务: ${names.join(', ')}`)
|
|
82
|
-
}
|
|
83
|
-
process.exitCode = 1
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const config = resolveAiConfig(cli, name)
|
|
88
|
-
if (!config) {
|
|
89
|
-
const names = Object.keys(cli.commands?.ai || {})
|
|
90
|
-
logger.error(`未找到 ai 任务配置: ${name}`)
|
|
91
|
-
if (names.length > 0) {
|
|
92
|
-
logger.info(`可用任务: ${names.join(', ')}`)
|
|
93
|
-
}
|
|
94
|
-
process.exitCode = 1
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const availability = ensureOpencodeAvailable()
|
|
99
|
-
if (!availability.ok) {
|
|
100
|
-
logger.error('未找到 opencode 可执行文件(PATH 中不存在 opencode)')
|
|
101
|
-
logger.info('请先安装并确保 `opencode --version` 可用')
|
|
102
|
-
process.exitCode = 1
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const promptFile = config?.promptFile
|
|
107
|
-
if (!promptFile) {
|
|
108
|
-
logger.error(`ai.${name} 缺少 promptFile 配置(指向一个 .md 文件)`)
|
|
109
|
-
process.exitCode = 1
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const promptCandidates = resolvePromptPath(promptFile)
|
|
114
|
-
const promptPath = promptCandidates.find(p => existsSync(p)) || promptCandidates[0]
|
|
115
|
-
if (!promptPath || !existsSync(promptPath)) {
|
|
116
|
-
logger.error(`未找到提示词文件: ${promptFile}`)
|
|
117
|
-
if (promptCandidates.length <= 1) {
|
|
118
|
-
logger.info(`解析后的路径: ${promptPath}`)
|
|
119
|
-
} else {
|
|
120
|
-
logger.info('解析后的候选路径:')
|
|
121
|
-
for (const p of promptCandidates) logger.info(`- ${p}`)
|
|
122
|
-
}
|
|
123
|
-
process.exitCode = 1
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let promptText = ''
|
|
128
|
-
try {
|
|
129
|
-
promptText = readFileSync(promptPath, 'utf8')
|
|
130
|
-
} catch (error) {
|
|
131
|
-
logger.error(`读取提示词文件失败: ${promptFile}`)
|
|
132
|
-
logger.error(error?.message || String(error))
|
|
133
|
-
process.exitCode = 1
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 固定全权限:这会让 opencode 在当前目录拥有 bash/edit 等工具的自动执行权
|
|
138
|
-
if (!cli.flags.Y) {
|
|
139
|
-
const confirmed = await confirmManager.confirmDangerous(
|
|
140
|
-
`ai.${name}(将以 OPENCODE_PERMISSION="allow" 运行,全权限)`,
|
|
141
|
-
'当前目录',
|
|
142
|
-
false,
|
|
143
|
-
)
|
|
144
|
-
if (!confirmed) {
|
|
145
|
-
logger.info('操作已取消')
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const model = config?.model ? String(config.model) : null
|
|
151
|
-
const agent = config?.agent ? String(config.agent) : null
|
|
152
|
-
const format = config?.format ? String(config.format) : null
|
|
153
|
-
const attach = config?.attach ? String(config.attach) : null
|
|
154
|
-
|
|
155
|
-
const passthrough = getPassthroughArgs(cli.args)
|
|
156
|
-
const configPassthrough = Array.isArray(config?.passthrough)
|
|
157
|
-
? config.passthrough.map(v => String(v))
|
|
158
|
-
: []
|
|
159
|
-
|
|
160
|
-
const opencodeArgs = ['run']
|
|
161
|
-
if (model) opencodeArgs.push('--model', model)
|
|
162
|
-
if (agent) opencodeArgs.push('--agent', agent)
|
|
163
|
-
if (format) opencodeArgs.push('--format', format)
|
|
164
|
-
if (attach) opencodeArgs.push('--attach', attach)
|
|
165
|
-
if (configPassthrough.length > 0) opencodeArgs.push(...configPassthrough)
|
|
166
|
-
if (Array.isArray(passthrough) && passthrough.length > 0) opencodeArgs.push(...passthrough)
|
|
167
|
-
opencodeArgs.push(promptText)
|
|
168
|
-
|
|
169
|
-
logger.step(`ai ${name}`)
|
|
170
|
-
logger.command(`opencode ${opencodeArgs.filter(a => a !== promptText).join(' ')} <prompt-from-file>`)
|
|
171
|
-
|
|
172
|
-
await new Promise(resolvePromise => {
|
|
173
|
-
const child = spawn('opencode', opencodeArgs, {
|
|
174
|
-
cwd: process.cwd(),
|
|
175
|
-
stdio: 'inherit',
|
|
176
|
-
env: {
|
|
177
|
-
...process.env,
|
|
178
|
-
// OpenCode expects OPENCODE_PERMISSION to be JSON (it JSON.parse's the value).
|
|
179
|
-
OPENCODE_PERMISSION: '"allow"',
|
|
180
|
-
},
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
const forwardSignal = signal => {
|
|
184
|
-
try {
|
|
185
|
-
child.kill(signal)
|
|
186
|
-
} catch {}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const onSigint = () => forwardSignal('SIGINT')
|
|
190
|
-
const onSigterm = () => forwardSignal('SIGTERM')
|
|
191
|
-
process.on('SIGINT', onSigint)
|
|
192
|
-
process.on('SIGTERM', onSigterm)
|
|
193
|
-
|
|
194
|
-
const cleanup = () => {
|
|
195
|
-
process.off('SIGINT', onSigint)
|
|
196
|
-
process.off('SIGTERM', onSigterm)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
child.on('error', error => {
|
|
200
|
-
cleanup()
|
|
201
|
-
logger.error(error?.message || String(error))
|
|
202
|
-
process.exitCode = 1
|
|
203
|
-
resolvePromise()
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
child.on('exit', code => {
|
|
207
|
-
cleanup()
|
|
208
|
-
if (typeof code === 'number' && code !== 0) {
|
|
209
|
-
process.exitCode = code
|
|
210
|
-
}
|
|
211
|
-
resolvePromise()
|
|
212
|
-
})
|
|
213
|
-
})
|
|
214
|
-
}
|