@icode-js/icode 3.0.4 → 3.2.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 +142 -5
- package/bin/icode.js +4 -4
- package/package.json +1 -1
- package/src/cli.js +60 -23
- package/src/commands/ai.js +4 -4
- package/src/commands/checkout.js +2 -2
- package/src/commands/clean.js +5 -2
- package/src/commands/codereview.js +5 -2
- package/src/commands/completion.js +93 -0
- package/src/commands/config.js +123 -10
- package/src/commands/doctor.js +80 -0
- package/src/commands/explain.js +6 -3
- package/src/commands/help.js +17 -0
- package/src/commands/info.js +5 -2
- package/src/commands/migrate.js +40 -3
- package/src/commands/push.js +72 -24
- package/src/commands/ready.js +110 -0
- package/src/commands/save.js +98 -0
- package/src/commands/sync.js +2 -2
- package/src/commands/tag.js +2 -2
- package/src/commands/undo.js +73 -10
- package/src/core/{ai-client.js → ai/client.js} +3 -3
- package/src/core/{ai-config.js → ai/config.js} +2 -2
- package/src/core/ai/diff-prompt.js +204 -0
- package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
- package/src/core/{args.js → cli/args.js} +23 -2
- package/src/core/cli/completion.js +720 -0
- package/src/core/cli/update-notifier.js +138 -0
- package/src/core/{shell.js → commands/shell.js} +13 -1
- package/src/core/{config-store.js → config/store.js} +53 -3
- package/src/core/{git-context.js → git/context.js} +24 -10
- package/src/core/git/preconditions.js +88 -0
- package/src/core/{git-service.js → git/service.js} +195 -5
- package/src/core/remote-merge/client.js +42 -0
- package/src/core/remote-merge/providers/gitlab.js +596 -0
- package/src/core/remote-merge/shared.js +49 -0
- package/src/core/tools/interactive.js +368 -0
- package/src/core/{loading.js → tools/loading.js} +5 -1
- package/src/core/{logger.js → tools/logger.js} +5 -0
- package/src/workflows/ai-codereview-workflow.js +18 -6
- package/src/workflows/ai-commit-workflow.js +20 -12
- package/src/workflows/ai-conflict-workflow.js +4 -4
- package/src/workflows/ai-explain-workflow.js +4 -4
- package/src/workflows/ai-risk-review-workflow.js +1 -1
- package/src/workflows/checkout-workflow.js +17 -5
- package/src/workflows/clean-workflow.js +92 -75
- package/src/workflows/doctor-workflow.js +131 -0
- package/src/workflows/info-workflow.js +4 -4
- package/src/workflows/migrate-workflow.js +269 -62
- package/src/workflows/push-workflow.js +475 -58
- package/src/workflows/ready-workflow.js +278 -0
- package/src/workflows/rollback-workflow.js +24 -9
- package/src/workflows/save-workflow.js +152 -0
- package/src/workflows/sync-workflow.js +22 -6
- package/src/workflows/tag-workflow.js +4 -4
- package/src/workflows/undo-workflow.js +37 -12
- package/src/core/prompts.js +0 -152
- /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
- /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
- /package/src/core/{hook-diagnostics.js → git/hook-diagnostics.js} +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises'
|
|
2
|
+
import { stdin, stdout } from 'node:process'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 判断当前 stdin/stdout 是否连接到交互式终端。
|
|
6
|
+
* 输入为空,输出为布尔值。
|
|
7
|
+
*/
|
|
8
|
+
function isInteractive() {
|
|
9
|
+
return Boolean(stdin.isTTY && stdout.isTTY)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 对外暴露交互终端判断,供工作流决定是否进入交互模式。
|
|
14
|
+
* 输入为空,输出为布尔值。
|
|
15
|
+
*/
|
|
16
|
+
export function isInteractiveTerminal() {
|
|
17
|
+
return isInteractive()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 归一化 confirm 的默认值配置,兼容布尔值与 options.defaultValue 两种调用方式。
|
|
22
|
+
* 输入为 confirm 第二参数,输出为布尔默认值。
|
|
23
|
+
*/
|
|
24
|
+
function resolveConfirmDefaultValue(optionsOrDefaultValue) {
|
|
25
|
+
if (typeof optionsOrDefaultValue === 'boolean') {
|
|
26
|
+
return optionsOrDefaultValue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (optionsOrDefaultValue && typeof optionsOrDefaultValue === 'object') {
|
|
30
|
+
return optionsOrDefaultValue.defaultValue === true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 读取确认输入并返回布尔结果;非交互环境直接返回默认值。
|
|
38
|
+
* 输入为提示文案与默认值配置,输出为用户是否确认。
|
|
39
|
+
*/
|
|
40
|
+
export async function confirm(message, optionsOrDefaultValue = false) {
|
|
41
|
+
const defaultValue = resolveConfirmDefaultValue(optionsOrDefaultValue)
|
|
42
|
+
if (!isInteractive()) {
|
|
43
|
+
return defaultValue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rl = createInterface({ input: stdin, output: stdout })
|
|
47
|
+
const suffix = defaultValue ? '[Y/n]' : '[y/N]'
|
|
48
|
+
const answer = (await rl.question(`${message} ${suffix} `)).trim().toLowerCase()
|
|
49
|
+
rl.close()
|
|
50
|
+
|
|
51
|
+
if (!answer) {
|
|
52
|
+
return defaultValue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (['y', 'yes', '1', 'true', 'ok', 'sure', '是'].includes(answer)) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (['n', 'no', '0', 'false', '否'].includes(answer)) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return defaultValue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 读取一行文本输入;非交互环境直接返回默认值。
|
|
68
|
+
* 输入为提示文案与默认值,输出为归一化后的文本。
|
|
69
|
+
*/
|
|
70
|
+
export async function input(message, defaultValue = '') {
|
|
71
|
+
if (!isInteractive()) {
|
|
72
|
+
return defaultValue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rl = createInterface({ input: stdin, output: stdout })
|
|
76
|
+
const answer = await rl.question(`${message}${defaultValue ? ` (${defaultValue})` : ''}: `)
|
|
77
|
+
rl.close()
|
|
78
|
+
const normalized = answer.trim()
|
|
79
|
+
return normalized || defaultValue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 在给定选项中返回单个值;非交互环境返回默认索引对应值。
|
|
84
|
+
* 输入为提示文案、选项列表与默认索引,输出为选中的 value。
|
|
85
|
+
*/
|
|
86
|
+
export async function chooseOne(message, choices, defaultIndex = 0) {
|
|
87
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
88
|
+
throw new Error('chooseOne 需要至少一个可选项')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const safeDefaultIndex = Math.min(Math.max(defaultIndex, 0), choices.length - 1)
|
|
92
|
+
if (!isInteractive()) {
|
|
93
|
+
return choices[safeDefaultIndex].value
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
stdout.write(`${message}\n`)
|
|
97
|
+
choices.forEach((choice, index) => {
|
|
98
|
+
stdout.write(` ${index + 1}. ${choice.label}\n`)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const rl = createInterface({ input: stdin, output: stdout })
|
|
102
|
+
const answer = (await rl.question(`请选择 [${safeDefaultIndex + 1}]: `)).trim()
|
|
103
|
+
rl.close()
|
|
104
|
+
|
|
105
|
+
if (!answer) {
|
|
106
|
+
return choices[safeDefaultIndex].value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const numeric = Number(answer)
|
|
110
|
+
if (!Number.isInteger(numeric) || numeric < 1 || numeric > choices.length) {
|
|
111
|
+
return choices[safeDefaultIndex].value
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return choices[numeric - 1].value
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 按选项顺序输出已选 value。
|
|
119
|
+
* 输入为选项与已选集合,输出为稳定顺序的 value 数组。
|
|
120
|
+
*/
|
|
121
|
+
function collectSelectedValues(choices, selected, maxSelections) {
|
|
122
|
+
return choices
|
|
123
|
+
.map((choice) => choice.value)
|
|
124
|
+
.filter((value) => selected.has(value))
|
|
125
|
+
.slice(0, maxSelections)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 使用旧版数字菜单进行多选,供不支持 raw mode 的终端回退。
|
|
130
|
+
* 输入为多选上下文,输出为已选 value 数组或 null。
|
|
131
|
+
*/
|
|
132
|
+
async function chooseManyByNumberInput(message, choices, context) {
|
|
133
|
+
const selected = new Set(context.selectedValues)
|
|
134
|
+
const doneValue = '__prompt_done__'
|
|
135
|
+
const cancelValue = '__prompt_cancel__'
|
|
136
|
+
|
|
137
|
+
while (true) {
|
|
138
|
+
const menuChoices = choices.map((choice) => ({
|
|
139
|
+
value: choice.value,
|
|
140
|
+
label: `${selected.has(choice.value) ? '[x]' : '[ ]'} ${choice.label}`
|
|
141
|
+
}))
|
|
142
|
+
menuChoices.push({
|
|
143
|
+
value: doneValue,
|
|
144
|
+
label: `${context.doneLabel}(已选 ${selected.size} 项)`
|
|
145
|
+
})
|
|
146
|
+
menuChoices.push({
|
|
147
|
+
value: cancelValue,
|
|
148
|
+
label: context.cancelLabel
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const defaultChoiceValue = selected.size >= context.minSelections ? doneValue : choices[0].value
|
|
152
|
+
const resolvedDefaultIndex = Math.max(0, menuChoices.findIndex((item) => item.value === defaultChoiceValue))
|
|
153
|
+
const selectedAction = await chooseOne(message, menuChoices, resolvedDefaultIndex)
|
|
154
|
+
|
|
155
|
+
if (selectedAction === cancelValue) {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (selectedAction === doneValue) {
|
|
160
|
+
if (selected.size < context.minSelections) {
|
|
161
|
+
stdout.write(`至少需要选择 ${context.minSelections} 项。\n`)
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
return collectSelectedValues(choices, selected, context.maxSelections)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (selected.has(selectedAction)) {
|
|
168
|
+
selected.delete(selectedAction)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (selected.size >= context.maxSelections) {
|
|
173
|
+
stdout.write(`最多只能选择 ${context.maxSelections} 项。\n`)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
selected.add(selectedAction)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 渲染 checkbox 多选列表。
|
|
183
|
+
* 输入为当前光标和选择状态,输出为本次渲染占用的行数。
|
|
184
|
+
*/
|
|
185
|
+
function renderCheckboxChoices(message, choices, context, cursorIndex, selected, statusMessage, renderedLines) {
|
|
186
|
+
if (renderedLines > 0) {
|
|
187
|
+
stdout.write(`\x1b[${renderedLines}F`)
|
|
188
|
+
stdout.write('\x1b[J')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const lines = [
|
|
192
|
+
message,
|
|
193
|
+
`↑/↓ 移动,Space 选中/取消,Enter ${context.doneLabel},q ${context.cancelLabel}(已选 ${selected.size} 项)`
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
if (statusMessage) {
|
|
197
|
+
lines.push(statusMessage)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
choices.forEach((choice, index) => {
|
|
201
|
+
const cursor = index === cursorIndex ? '>' : ' '
|
|
202
|
+
const checkbox = selected.has(choice.value) ? '[x]' : '[ ]'
|
|
203
|
+
lines.push(`${cursor} ${checkbox} ${choice.label}`)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
stdout.write(`${lines.join('\n')}\n`)
|
|
207
|
+
return lines.length
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 使用键盘 checkbox 进行多选。
|
|
212
|
+
* 输入为多选上下文,输出为已选 value 数组或 null。
|
|
213
|
+
*/
|
|
214
|
+
function chooseManyByCheckbox(message, choices, context) {
|
|
215
|
+
const selected = new Set(context.selectedValues)
|
|
216
|
+
let cursorIndex = 0
|
|
217
|
+
let renderedLines = 0
|
|
218
|
+
let statusMessage = ''
|
|
219
|
+
let closed = false
|
|
220
|
+
const wasRaw = stdin.isRaw === true
|
|
221
|
+
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
const finish = (value) => {
|
|
224
|
+
if (closed) {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
closed = true
|
|
229
|
+
stdin.off('data', onData)
|
|
230
|
+
stdin.setRawMode(wasRaw)
|
|
231
|
+
stdin.pause()
|
|
232
|
+
resolve(value)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const render = () => {
|
|
236
|
+
renderedLines = renderCheckboxChoices(message, choices, context, cursorIndex, selected, statusMessage, renderedLines)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const moveCursor = (offset) => {
|
|
240
|
+
statusMessage = ''
|
|
241
|
+
cursorIndex = (cursorIndex + offset + choices.length) % choices.length
|
|
242
|
+
render()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const toggleCurrent = () => {
|
|
246
|
+
statusMessage = ''
|
|
247
|
+
const currentValue = choices[cursorIndex].value
|
|
248
|
+
if (selected.has(currentValue)) {
|
|
249
|
+
selected.delete(currentValue)
|
|
250
|
+
render()
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (selected.size >= context.maxSelections) {
|
|
255
|
+
statusMessage = `最多只能选择 ${context.maxSelections} 项。`
|
|
256
|
+
render()
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
selected.add(currentValue)
|
|
261
|
+
render()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const complete = () => {
|
|
265
|
+
if (selected.size < context.minSelections) {
|
|
266
|
+
statusMessage = `至少需要选择 ${context.minSelections} 项。`
|
|
267
|
+
render()
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
finish(collectSelectedValues(choices, selected, context.maxSelections))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function onData(chunk) {
|
|
274
|
+
const input = chunk.toString('utf8')
|
|
275
|
+
|
|
276
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
277
|
+
if (closed) {
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const sequence = input.slice(index, index + 3)
|
|
282
|
+
if (sequence === '\u001b[A') {
|
|
283
|
+
moveCursor(-1)
|
|
284
|
+
index += 2
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (sequence === '\u001b[B') {
|
|
289
|
+
moveCursor(1)
|
|
290
|
+
index += 2
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const key = input[index]
|
|
295
|
+
if (key === '\u0003' || key === '\u001b' || key.toLowerCase() === 'q') {
|
|
296
|
+
finish(null)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (key === '\r' || key === '\n') {
|
|
301
|
+
complete()
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (key === ' ') {
|
|
306
|
+
toggleCurrent()
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (key.toLowerCase() === 'k') {
|
|
311
|
+
moveCursor(-1)
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (key.toLowerCase() === 'j') {
|
|
316
|
+
moveCursor(1)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
stdin.setRawMode(true)
|
|
322
|
+
stdin.resume()
|
|
323
|
+
stdin.on('data', onData)
|
|
324
|
+
render()
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 在给定选项中进行多选;非交互环境返回默认值集合。
|
|
330
|
+
* 输入为提示文案、选项列表与多选配置,输出为选中的 value 数组或 null。
|
|
331
|
+
*/
|
|
332
|
+
export async function chooseMany(message, choices, options = {}) {
|
|
333
|
+
if (!Array.isArray(choices) || choices.length === 0) {
|
|
334
|
+
throw new Error('chooseMany 需要至少一个可选项')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const parsedMin = Number(options.minSelections ?? 0)
|
|
338
|
+
const parsedMax = Number(options.maxSelections ?? choices.length)
|
|
339
|
+
const minSelections = Number.isFinite(parsedMin) ? Math.max(0, Math.floor(parsedMin)) : 0
|
|
340
|
+
const maxCap = Number.isFinite(parsedMax) ? Math.max(0, Math.floor(parsedMax)) : choices.length
|
|
341
|
+
const maxSelections = Math.max(minSelections, Math.min(choices.length, maxCap))
|
|
342
|
+
const doneLabel = options.doneLabel || '完成选择'
|
|
343
|
+
const cancelLabel = options.cancelLabel || '取消'
|
|
344
|
+
const defaultValues = Array.isArray(options.defaultValues) ? options.defaultValues : []
|
|
345
|
+
const defaultSet = new Set(defaultValues)
|
|
346
|
+
const selectedValues = choices
|
|
347
|
+
.map((choice) => choice.value)
|
|
348
|
+
.filter((value) => defaultSet.has(value))
|
|
349
|
+
.slice(0, maxSelections)
|
|
350
|
+
|
|
351
|
+
if (!isInteractive()) {
|
|
352
|
+
return selectedValues
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const context = {
|
|
356
|
+
minSelections,
|
|
357
|
+
maxSelections,
|
|
358
|
+
doneLabel,
|
|
359
|
+
cancelLabel,
|
|
360
|
+
selectedValues
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (typeof stdin.setRawMode !== 'function') {
|
|
364
|
+
return chooseManyByNumberInput(message, choices, context)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return chooseManyByCheckbox(message, choices, context)
|
|
368
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const SPINNER_FRAMES = ['-', '\\', '|', '/']
|
|
2
2
|
|
|
3
|
+
// 判断当前进程是否适合渲染动态等待效果,输入为空,输出布尔值。
|
|
3
4
|
function canRenderSpinner() {
|
|
4
5
|
return Boolean(process.stderr.isTTY) && process.env.ICODE_NO_SPINNER !== '1'
|
|
5
6
|
}
|
|
6
7
|
|
|
8
|
+
// 清理当前终端行,输入为空,输出为空,仅在 TTY 中生效。
|
|
7
9
|
function clearLine() {
|
|
8
10
|
if (!canRenderSpinner()) {
|
|
9
11
|
return
|
|
@@ -12,6 +14,7 @@ function clearLine() {
|
|
|
12
14
|
process.stderr.write('\r\x1b[2K')
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
// 执行异步任务时展示等待动画,输入展示文案和任务函数,输出任务结果。
|
|
15
18
|
export async function withSpinner(text, task) {
|
|
16
19
|
if (!canRenderSpinner()) {
|
|
17
20
|
return task()
|
|
@@ -21,7 +24,8 @@ export async function withSpinner(text, task) {
|
|
|
21
24
|
const render = () => {
|
|
22
25
|
const frame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]
|
|
23
26
|
frameIndex += 1
|
|
24
|
-
|
|
27
|
+
clearLine()
|
|
28
|
+
process.stderr.write(`[icode] ${frame} ${text}`)
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
render()
|
|
@@ -25,6 +25,11 @@ function print(stream, level, message) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export const logger = {
|
|
28
|
+
// 按指定颜色渲染短文本,输入颜色名和文本,输出可直接拼进日志的 ANSI 字符串。
|
|
29
|
+
color(color, message) {
|
|
30
|
+
return colorize(color, String(message ?? ''))
|
|
31
|
+
},
|
|
32
|
+
|
|
28
33
|
setVerbose(enabled) {
|
|
29
34
|
verboseEnabled = Boolean(enabled)
|
|
30
35
|
},
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { askAi } from '../core/ai
|
|
2
|
-
import {
|
|
1
|
+
import { askAi } from '../core/ai/client.js'
|
|
2
|
+
import { buildLimitedDiffPrompt, formatDiffCoverage, summarizeDiffCoverage } from '../core/ai/diff-prompt.js'
|
|
3
|
+
import { resolveAiDiffRange } from '../core/ai/diff-range.js'
|
|
3
4
|
import { IcodeError } from '../core/errors.js'
|
|
4
|
-
import { resolveGitContext } from '../core/git
|
|
5
|
-
import { GitService } from '../core/git
|
|
5
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
6
|
+
import { GitService } from '../core/git/service.js'
|
|
6
7
|
|
|
7
8
|
function truncate(value, limit) {
|
|
8
9
|
const text = value || ''
|
|
@@ -19,6 +20,14 @@ function joinSections(sections) {
|
|
|
19
20
|
.join('\n\n')
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// 构建 AI code review prompt 中的 diff 片段,输入完整 diff,输出受限 diff 与覆盖范围。
|
|
24
|
+
function buildReviewDiffPrompt(diff) {
|
|
25
|
+
return buildLimitedDiffPrompt(diff, {
|
|
26
|
+
limit: 18000
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 执行 AI 代码审查工作流,输入 diff 范围和 AI 选项,输出审查文本与 diff 覆盖范围。
|
|
22
31
|
export async function runAiCodeReviewWorkflow(options) {
|
|
23
32
|
const context = await resolveGitContext({
|
|
24
33
|
cwd: options.cwd,
|
|
@@ -101,10 +110,12 @@ export async function runAiCodeReviewWorkflow(options) {
|
|
|
101
110
|
])
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
const limitedDiff = buildReviewDiffPrompt(diff)
|
|
114
|
+
|
|
104
115
|
const review = await askAi(
|
|
105
116
|
{
|
|
106
117
|
systemPrompt: '你是严格的软件代码审查工程师,请优先关注 bug、安全风险、行为回归、缺失测试。输出中文 Markdown。',
|
|
107
|
-
userPrompt: `请按如下结构输出:\n1. Findings(按严重度从高到低)\n2. Open Questions\n3. Summary\n\nFocus: ${options.focus || 'general'}\nRange: ${rangeSpec}\nDiff Source: ${diffSource}\n\nDiff Stat:\n${truncate(stat, 3000)}\n\nName Status:\n${truncate(nameStatus, 3000)}\n\nUnified Diff:\n${
|
|
118
|
+
userPrompt: `请按如下结构输出:\n1. Findings(按严重度从高到低)\n2. Open Questions\n3. Summary\n\nFocus: ${options.focus || 'general'}\nRange: ${rangeSpec}\nDiff Source: ${diffSource}\n\n${formatDiffCoverage(limitedDiff.coverage)}\n\nDiff Stat:\n${truncate(stat, 3000)}\n\nName Status:\n${truncate(nameStatus, 3000)}\n\nUnified Diff:\n${limitedDiff.diff}`
|
|
108
119
|
},
|
|
109
120
|
{
|
|
110
121
|
profile: options.profile,
|
|
@@ -121,6 +132,7 @@ export async function runAiCodeReviewWorkflow(options) {
|
|
|
121
132
|
|
|
122
133
|
return {
|
|
123
134
|
rangeSpec,
|
|
124
|
-
review
|
|
135
|
+
review,
|
|
136
|
+
diffCoverage: summarizeDiffCoverage(limitedDiff.coverage)
|
|
125
137
|
}
|
|
126
138
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { confirm } from '../core/
|
|
2
|
+
import { askAiJson } from '../core/ai/client.js'
|
|
3
|
+
import { buildLimitedDiffPrompt, formatDiffCoverage, summarizeDiffCoverage } from '../core/ai/diff-prompt.js'
|
|
4
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
5
|
+
import { scanCommitConventions } from '../core/git/commit-conventions.js'
|
|
6
|
+
import { GitService } from '../core/git/service.js'
|
|
7
|
+
import { confirm } from '../core/tools/interactive.js'
|
|
8
|
+
import { logger } from '../core/tools/logger.js'
|
|
8
9
|
|
|
9
10
|
function normalizeCommitType(value) {
|
|
10
11
|
const allowed = new Set(['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'build', 'ci', 'revert'])
|
|
@@ -74,7 +75,9 @@ export async function runAiCommitWorkflow(options) {
|
|
|
74
75
|
})
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
const limitedDiff = diff
|
|
78
|
+
const limitedDiff = buildLimitedDiffPrompt(diff, {
|
|
79
|
+
limit: 12000
|
|
80
|
+
})
|
|
78
81
|
const conventionContext = scanCommitConventions(context)
|
|
79
82
|
const conventionPrompt = conventionContext.hasConventions
|
|
80
83
|
? `Local commit conventions were detected from repository hooks/config files. Follow these local rules first when generating the commit message.\n\n${conventionContext.summary}\n\n`
|
|
@@ -89,7 +92,7 @@ export async function runAiCommitWorkflow(options) {
|
|
|
89
92
|
const { parsed, text } = await askAiJson(
|
|
90
93
|
{
|
|
91
94
|
systemPrompt: `You are a senior software engineer. Generate a concise Conventional Commit message. Output JSON only. Language: ${language}.`,
|
|
92
|
-
userPrompt: `${conventionPrompt}Based on the following git diff, return JSON with fields:\n{\"type\":\"feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert\",\"scope\":\"optional\",\"subject\":\"required one-line summary\",\"body\":\"optional details\"}\n\nDiff Source: ${diffSource}\n\nDiff:\n${limitedDiff}`
|
|
95
|
+
userPrompt: `${conventionPrompt}Based on the following git diff, return JSON with fields:\n{\"type\":\"feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert\",\"scope\":\"optional\",\"subject\":\"required one-line summary\",\"body\":\"optional details\"}\n\nDiff Source: ${diffSource}\n\n${formatDiffCoverage(limitedDiff.coverage)}\n\nDiff:\n${limitedDiff.diff}`
|
|
93
96
|
},
|
|
94
97
|
{
|
|
95
98
|
profile: options.profile
|
|
@@ -98,13 +101,16 @@ export async function runAiCommitWorkflow(options) {
|
|
|
98
101
|
|
|
99
102
|
const commitMessage = buildCommitMessage(parsed)
|
|
100
103
|
|
|
101
|
-
|
|
104
|
+
if (!options.silentResultLog) {
|
|
105
|
+
logger.success(`AI 建议提交信息:\n${commitMessage}`)
|
|
106
|
+
}
|
|
102
107
|
|
|
103
108
|
if (!options.apply) {
|
|
104
109
|
return {
|
|
105
110
|
applied: false,
|
|
106
111
|
commitMessage,
|
|
107
|
-
raw: text
|
|
112
|
+
raw: text,
|
|
113
|
+
diffCoverage: summarizeDiffCoverage(limitedDiff.coverage)
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -115,6 +121,7 @@ export async function runAiCommitWorkflow(options) {
|
|
|
115
121
|
applied: false,
|
|
116
122
|
commitMessage,
|
|
117
123
|
raw: text,
|
|
124
|
+
diffCoverage: summarizeDiffCoverage(limitedDiff.coverage),
|
|
118
125
|
canceled: true
|
|
119
126
|
}
|
|
120
127
|
}
|
|
@@ -130,7 +137,7 @@ export async function runAiCommitWorkflow(options) {
|
|
|
130
137
|
})
|
|
131
138
|
|
|
132
139
|
const commitId = await git.revParseShort('HEAD')
|
|
133
|
-
if (commitId) {
|
|
140
|
+
if (commitId && !options.silentResultLog) {
|
|
134
141
|
logger.success(`AI commit 已创建: ${commitId}`)
|
|
135
142
|
}
|
|
136
143
|
|
|
@@ -138,6 +145,7 @@ export async function runAiCommitWorkflow(options) {
|
|
|
138
145
|
applied: true,
|
|
139
146
|
commitId,
|
|
140
147
|
commitMessage,
|
|
141
|
-
raw: text
|
|
148
|
+
raw: text,
|
|
149
|
+
diffCoverage: summarizeDiffCoverage(limitedDiff.coverage)
|
|
142
150
|
}
|
|
143
151
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { askAi } from '../core/ai
|
|
3
|
+
import { askAi } from '../core/ai/client.js'
|
|
4
4
|
import { IcodeError } from '../core/errors.js'
|
|
5
|
-
import { resolveGitContext } from '../core/git
|
|
6
|
-
import { GitService } from '../core/git
|
|
7
|
-
import { logger } from '../core/logger.js'
|
|
5
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
6
|
+
import { GitService } from '../core/git/service.js'
|
|
7
|
+
import { logger } from '../core/tools/logger.js'
|
|
8
8
|
|
|
9
9
|
function extractConflictBlocks(content, maxBlocks = 3, maxLines = 80) {
|
|
10
10
|
const lines = content.split('\n')
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { askAi } from '../core/ai
|
|
2
|
-
import { resolveAiDiffRange } from '../core/ai
|
|
1
|
+
import { askAi } from '../core/ai/client.js'
|
|
2
|
+
import { resolveAiDiffRange } from '../core/ai/diff-range.js'
|
|
3
3
|
import { IcodeError } from '../core/errors.js'
|
|
4
|
-
import { resolveGitContext } from '../core/git
|
|
5
|
-
import { GitService } from '../core/git
|
|
4
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
5
|
+
import { GitService } from '../core/git/service.js'
|
|
6
6
|
|
|
7
7
|
function truncate(value, limit) {
|
|
8
8
|
const text = value || ''
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import {
|
|
4
|
+
assertCleanWorktree,
|
|
5
|
+
assertNoConflictedFiles,
|
|
6
|
+
assertNoPendingOperation,
|
|
7
|
+
readGitSafetyState
|
|
8
|
+
} from '../core/git/preconditions.js'
|
|
9
|
+
import { GitService } from '../core/git/service.js'
|
|
10
|
+
import { logger } from '../core/tools/logger.js'
|
|
5
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 执行 checkout 工作流。
|
|
14
|
+
* 输入为目标分支、基线分支和执行选项,输出分支切换结果;实际切换前要求工作区干净且没有未完成 Git 操作。
|
|
15
|
+
*/
|
|
6
16
|
export async function runCheckoutWorkflow(input) {
|
|
7
17
|
if (!input.branchName) {
|
|
8
18
|
throw new IcodeError('缺少分支名: icode checkout <branch> [base]', {
|
|
@@ -19,6 +29,10 @@ export async function runCheckoutWorkflow(input) {
|
|
|
19
29
|
const git = new GitService(context)
|
|
20
30
|
const branchName = input.branchName.trim()
|
|
21
31
|
const baseBranchName = input.baseBranchName?.trim() || context.defaultBranch
|
|
32
|
+
const safetyState = await readGitSafetyState(git)
|
|
33
|
+
assertNoPendingOperation(safetyState.operation, 'checkout', 'CHECKOUT_GIT_OPERATION_IN_PROGRESS')
|
|
34
|
+
assertNoConflictedFiles(safetyState.changes, 'checkout', 'CHECKOUT_CONFLICTED_FILES')
|
|
35
|
+
assertCleanWorktree(safetyState.changes, 'checkout', 'CHECKOUT_DIRTY_WORKTREE')
|
|
22
36
|
|
|
23
37
|
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
24
38
|
if (context.inheritedFromParent) {
|
|
@@ -55,7 +69,6 @@ export async function runCheckoutWorkflow(input) {
|
|
|
55
69
|
if (input.pullMain && context.defaultBranch !== branchName) {
|
|
56
70
|
logger.info(`同步主分支到当前分支: ${context.defaultBranch}`)
|
|
57
71
|
await git.pull(context.defaultBranch, {
|
|
58
|
-
allowUnrelatedHistories: true,
|
|
59
72
|
noRebase: true
|
|
60
73
|
})
|
|
61
74
|
}
|
|
@@ -63,7 +76,6 @@ export async function runCheckoutWorkflow(input) {
|
|
|
63
76
|
if (remoteExists) {
|
|
64
77
|
logger.info(`拉取远程分支: ${branchName}`)
|
|
65
78
|
await git.pull(branchName, {
|
|
66
|
-
allowUnrelatedHistories: true,
|
|
67
79
|
noRebase: true
|
|
68
80
|
})
|
|
69
81
|
}
|