@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,720 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { listAiProfiles } from '../ai/config.js'
|
|
3
|
+
|
|
4
|
+
const REPO_MODE_VALUES = ['auto', 'strict']
|
|
5
|
+
const AI_FORMAT_VALUES = ['openai', 'anthropic', 'ollama']
|
|
6
|
+
const AI_OPTION_SCOPE_VALUES = ['commit', 'conflict', 'explain', 'push']
|
|
7
|
+
const AI_COMMIT_LANG_VALUES = ['zh', 'en']
|
|
8
|
+
const UNDO_MODE_VALUES = ['revert', 'soft', 'mixed', 'hard']
|
|
9
|
+
const UNDO_RECOVER_VALUES = ['continue', 'abort', 'keep']
|
|
10
|
+
const SHELL_VALUES = ['bash', 'zsh']
|
|
11
|
+
const CONFIG_PATH_VALUES = [
|
|
12
|
+
'defaults.repoMode',
|
|
13
|
+
'defaults.defaultMainBranches',
|
|
14
|
+
'ai.activeProfile',
|
|
15
|
+
'ai.profiles',
|
|
16
|
+
'ai.options',
|
|
17
|
+
'repositories'
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const CODE_REVIEW_NODE = {
|
|
21
|
+
options: [
|
|
22
|
+
{ long: '--base', valueSource: 'ref' },
|
|
23
|
+
{ long: '--head', valueSource: 'ref' },
|
|
24
|
+
{ long: '--focus', valueSource: 'text' },
|
|
25
|
+
{ long: '--profile', valueSource: 'ai-profile' },
|
|
26
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
27
|
+
{ long: '--dump-response' },
|
|
28
|
+
{ long: '--help', short: '-h' }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const EXPLAIN_NODE = {
|
|
33
|
+
options: [
|
|
34
|
+
{ long: '--base', valueSource: 'ref' },
|
|
35
|
+
{ long: '--head', valueSource: 'ref' },
|
|
36
|
+
{ long: '--profile', valueSource: 'ai-profile' },
|
|
37
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
38
|
+
{ long: '--dump-response' },
|
|
39
|
+
{ long: '--help', short: '-h' }
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ROOT_COMPLETION_SPEC = {
|
|
44
|
+
options: [
|
|
45
|
+
{ long: '--debug', short: '-d' },
|
|
46
|
+
{ long: '--help', short: '-h' }
|
|
47
|
+
],
|
|
48
|
+
subcommands: {
|
|
49
|
+
ai: {
|
|
50
|
+
options: [{ long: '--help', short: '-h' }],
|
|
51
|
+
subcommands: {
|
|
52
|
+
commit: {
|
|
53
|
+
options: [
|
|
54
|
+
{ long: '--apply' },
|
|
55
|
+
{ long: '--lang', valueSource: 'ai-commit-lang' },
|
|
56
|
+
{ long: '--profile', valueSource: 'ai-profile' },
|
|
57
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
58
|
+
{ long: '--no-verify' },
|
|
59
|
+
{ long: '--yes', short: '-y' },
|
|
60
|
+
{ long: '--help', short: '-h' }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
conflict: {
|
|
64
|
+
options: [
|
|
65
|
+
{ long: '--profile', valueSource: 'ai-profile' },
|
|
66
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
67
|
+
{ long: '--help', short: '-h' }
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
codereview: CODE_REVIEW_NODE,
|
|
71
|
+
review: CODE_REVIEW_NODE,
|
|
72
|
+
explain: EXPLAIN_NODE
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
codereview: CODE_REVIEW_NODE,
|
|
76
|
+
checkout: {
|
|
77
|
+
positionals: [
|
|
78
|
+
{ valueSource: 'branch' },
|
|
79
|
+
{ valueSource: 'branch' }
|
|
80
|
+
],
|
|
81
|
+
options: [
|
|
82
|
+
{ long: '--yes', short: '-y' },
|
|
83
|
+
{ long: '--push-origin' },
|
|
84
|
+
{ long: '--pull-main' },
|
|
85
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
86
|
+
{ long: '--no-verify' },
|
|
87
|
+
{ long: '--help', short: '-h' }
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
ready: {
|
|
91
|
+
options: [
|
|
92
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
93
|
+
{ long: '--json' },
|
|
94
|
+
{ long: '--help', short: '-h' }
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
save: {
|
|
98
|
+
options: [
|
|
99
|
+
{ long: '--message', short: '-m', valueSource: 'text' },
|
|
100
|
+
{ long: '--ai-commit' },
|
|
101
|
+
{ long: '--profile', valueSource: 'ai-profile' },
|
|
102
|
+
{ long: '--lang', valueSource: 'ai-commit-lang' },
|
|
103
|
+
{ long: '--dry-run' },
|
|
104
|
+
{ long: '--json' },
|
|
105
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
106
|
+
{ long: '--no-verify' },
|
|
107
|
+
{ long: '--yes', short: '-y' },
|
|
108
|
+
{ long: '--help', short: '-h' }
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
push: {
|
|
112
|
+
positionals: [{ valueSource: 'branch', repeat: true }],
|
|
113
|
+
options: [
|
|
114
|
+
{ long: '--message', short: '-m', valueSource: 'text' },
|
|
115
|
+
{ long: '--yes', short: '-y' },
|
|
116
|
+
{ long: '--local-merge' },
|
|
117
|
+
{ long: '--remote-merge', short: '-r' },
|
|
118
|
+
{ long: '--dry-run' },
|
|
119
|
+
{ long: '--ai-commit' },
|
|
120
|
+
{ long: '--ai-profile', valueSource: 'ai-profile' },
|
|
121
|
+
{ long: '--help', short: '-h' },
|
|
122
|
+
{ long: '--pull-main' },
|
|
123
|
+
{ long: '--not-push-current' },
|
|
124
|
+
{ long: '--force-protected' },
|
|
125
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
126
|
+
{ long: '--no-verify' }
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
sync: {
|
|
130
|
+
positionals: [{ valueSource: 'branch', repeat: true }],
|
|
131
|
+
options: [
|
|
132
|
+
{ long: '--all-local' },
|
|
133
|
+
{ long: '--merge-main' },
|
|
134
|
+
{ long: '--rebase' },
|
|
135
|
+
{ long: '--push' },
|
|
136
|
+
{ long: '--yes', short: '-y' },
|
|
137
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
138
|
+
{ long: '--no-verify' },
|
|
139
|
+
{ long: '--help', short: '-h' }
|
|
140
|
+
]
|
|
141
|
+
},
|
|
142
|
+
clean: {
|
|
143
|
+
options: [
|
|
144
|
+
{ long: '--merged-target', valueSource: 'branch' },
|
|
145
|
+
{ long: '--keep', valueSource: 'branch' },
|
|
146
|
+
{ long: '--remote' },
|
|
147
|
+
{ long: '--force' },
|
|
148
|
+
{ long: '--yes', short: '-y' },
|
|
149
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
150
|
+
{ long: '--help', short: '-h' }
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
undo: {
|
|
154
|
+
positionals: [{ valueSource: 'ref' }],
|
|
155
|
+
options: [
|
|
156
|
+
{ long: '--mode', valueSource: 'undo-mode' },
|
|
157
|
+
{ long: '--ref', valueSource: 'ref' },
|
|
158
|
+
{ long: '--hash', valueSource: 'ref' },
|
|
159
|
+
{ long: '--recover', valueSource: 'undo-recover' },
|
|
160
|
+
{ long: '--json' },
|
|
161
|
+
{ long: '--yes', short: '-y' },
|
|
162
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
163
|
+
{ long: '--help', short: '-h' }
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
migrate: {
|
|
167
|
+
positionals: [
|
|
168
|
+
{ valueSource: 'branch' },
|
|
169
|
+
{ valueSource: 'branch' }
|
|
170
|
+
],
|
|
171
|
+
options: [
|
|
172
|
+
{ long: '--interactive', short: '-i' },
|
|
173
|
+
{ long: '--range', valueSource: 'text' },
|
|
174
|
+
{ long: '--dry-run' },
|
|
175
|
+
{ long: '--json' },
|
|
176
|
+
{ long: '--push' },
|
|
177
|
+
{ long: '--yes', short: '-y' },
|
|
178
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
179
|
+
{ long: '--no-verify' },
|
|
180
|
+
{ long: '--help', short: '-h' }
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
tag: {
|
|
184
|
+
options: [
|
|
185
|
+
{ long: '--name', short: '-n', valueSource: 'text' },
|
|
186
|
+
{ long: '--message', short: '-m', valueSource: 'text' },
|
|
187
|
+
{ long: '--from', valueSource: 'ref' },
|
|
188
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
189
|
+
{ long: '--no-verify' },
|
|
190
|
+
{ long: '--help', short: '-h' }
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
config: {
|
|
194
|
+
options: [
|
|
195
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
196
|
+
{ long: '--help', short: '-h' }
|
|
197
|
+
],
|
|
198
|
+
subcommands: {
|
|
199
|
+
list: {},
|
|
200
|
+
get: {
|
|
201
|
+
positionals: [{ valueSource: 'config-path' }]
|
|
202
|
+
},
|
|
203
|
+
set: {
|
|
204
|
+
positionals: [
|
|
205
|
+
{ valueSource: 'config-path' },
|
|
206
|
+
{ valueSource: 'text' }
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
delete: {
|
|
210
|
+
positionals: [{ valueSource: 'config-path' }]
|
|
211
|
+
},
|
|
212
|
+
ai: {
|
|
213
|
+
subcommands: {
|
|
214
|
+
list: {},
|
|
215
|
+
show: {
|
|
216
|
+
positionals: [{ valueSource: 'ai-profile' }]
|
|
217
|
+
},
|
|
218
|
+
set: {
|
|
219
|
+
positionals: [{ valueSource: 'ai-profile' }],
|
|
220
|
+
options: [
|
|
221
|
+
{ long: '--provider', valueSource: 'text' },
|
|
222
|
+
{ long: '--format', valueSource: 'ai-format' },
|
|
223
|
+
{ long: '--base-url', valueSource: 'text' },
|
|
224
|
+
{ long: '--api-key', valueSource: 'text' },
|
|
225
|
+
{ long: '--model', valueSource: 'text' },
|
|
226
|
+
{ long: '--temperature', valueSource: 'text' },
|
|
227
|
+
{ long: '--max-tokens', valueSource: 'text' },
|
|
228
|
+
{ long: '--headers', valueSource: 'text' },
|
|
229
|
+
{ long: '--request-body', valueSource: 'text' },
|
|
230
|
+
{ long: '--activate' }
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
options: {
|
|
234
|
+
subcommands: {
|
|
235
|
+
list: {},
|
|
236
|
+
show: {
|
|
237
|
+
positionals: [{ valueSource: 'ai-option-scope' }]
|
|
238
|
+
},
|
|
239
|
+
set: {
|
|
240
|
+
positionals: [{ valueSource: 'ai-option-scope' }],
|
|
241
|
+
options: [
|
|
242
|
+
{ long: '--json', valueSource: 'text' },
|
|
243
|
+
{ long: '--replace' }
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
remove: {
|
|
247
|
+
positionals: [{ valueSource: 'ai-option-scope' }]
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
options: [{ long: '--help', short: '-h' }]
|
|
251
|
+
},
|
|
252
|
+
use: {
|
|
253
|
+
positionals: [{ valueSource: 'ai-profile' }]
|
|
254
|
+
},
|
|
255
|
+
remove: {
|
|
256
|
+
positionals: [{ valueSource: 'ai-profile' }]
|
|
257
|
+
},
|
|
258
|
+
test: {
|
|
259
|
+
positionals: [{ valueSource: 'ai-profile' }]
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
options: [{ long: '--help', short: '-h' }]
|
|
263
|
+
},
|
|
264
|
+
protect: {
|
|
265
|
+
subcommands: {
|
|
266
|
+
list: {},
|
|
267
|
+
add: {
|
|
268
|
+
positionals: [{ valueSource: 'branch', repeat: true }]
|
|
269
|
+
},
|
|
270
|
+
remove: {
|
|
271
|
+
positionals: [{ valueSource: 'branch', repeat: true }]
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
doctor: {
|
|
278
|
+
options: [
|
|
279
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
280
|
+
{ long: '--json' },
|
|
281
|
+
{ long: '--help', short: '-h' }
|
|
282
|
+
]
|
|
283
|
+
},
|
|
284
|
+
explain: EXPLAIN_NODE,
|
|
285
|
+
info: {
|
|
286
|
+
options: [
|
|
287
|
+
{ long: '--repo-mode', valueSource: 'repo-mode' },
|
|
288
|
+
{ long: '--help', short: '-h' }
|
|
289
|
+
]
|
|
290
|
+
},
|
|
291
|
+
help: {},
|
|
292
|
+
completion: {
|
|
293
|
+
positionals: [{ valueSource: 'shell' }],
|
|
294
|
+
options: [{ long: '--help', short: '-h' }]
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 生成当前输入上下文的补全候选项。
|
|
301
|
+
* 输入为已确认的前置参数、当前正在补全的片段和可注入候选提供器,输出为去重后的字符串列表。
|
|
302
|
+
*/
|
|
303
|
+
export function getCompletionCandidates(previousWords = [], currentWord = '', dependencies = {}) {
|
|
304
|
+
const providers = resolveCompletionProviders(dependencies)
|
|
305
|
+
const context = walkCompletionContext(ROOT_COMPLETION_SPEC, previousWords)
|
|
306
|
+
const inlineValueContext = resolveInlineValueContext(context.node, currentWord)
|
|
307
|
+
|
|
308
|
+
if (inlineValueContext) {
|
|
309
|
+
const candidates = resolveValueCandidates(inlineValueContext.option.valueSource, providers)
|
|
310
|
+
return filterCandidates(candidates, inlineValueContext.query).map(
|
|
311
|
+
(candidate) => `${inlineValueContext.prefix}${candidate}`
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (context.pendingOption) {
|
|
316
|
+
return filterCandidates(resolveValueCandidates(context.pendingOption.valueSource, providers), currentWord)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const candidates = []
|
|
320
|
+
if (shouldSuggestSubcommands(context.node, context.positionalCount)) {
|
|
321
|
+
candidates.push(...listSubcommandNames(context.node))
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (shouldSuggestOptions(currentWord)) {
|
|
325
|
+
candidates.push(...listOptionFlags(context.node))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!currentWord.startsWith('-')) {
|
|
329
|
+
candidates.push(...resolvePositionalCandidates(context.node, context.positionalCount, providers))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return filterCandidates(candidates, currentWord)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 生成指定 shell 的补全脚本文本。
|
|
337
|
+
* 输入为 shell 名称,输出为可直接 source 的补全脚本内容。
|
|
338
|
+
*/
|
|
339
|
+
export function renderCompletionScript(shellName) {
|
|
340
|
+
const normalizedShell = String(shellName || '').trim().toLowerCase()
|
|
341
|
+
|
|
342
|
+
if (normalizedShell === 'bash') {
|
|
343
|
+
return renderBashCompletionScript()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (normalizedShell === 'zsh') {
|
|
347
|
+
return renderZshCompletionScript()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
throw new Error(`不支持的 shell: ${shellName}`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 统一解析补全依赖,优先使用测试注入,默认走本地真实数据源。
|
|
355
|
+
* 输入为可选依赖对象,输出为完整的候选提供器集合。
|
|
356
|
+
*/
|
|
357
|
+
function resolveCompletionProviders(dependencies = {}) {
|
|
358
|
+
return {
|
|
359
|
+
listBranchCandidates: dependencies.listBranchCandidates || listBranchCandidates,
|
|
360
|
+
listRefCandidates: dependencies.listRefCandidates || listRefCandidates,
|
|
361
|
+
listAiProfileCandidates: dependencies.listAiProfileCandidates || listAiProfileCandidates
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 沿着命令树消费已确认参数,定位当前节点、待补全 option 和 positional 下标。
|
|
367
|
+
* 输入为根节点与已确认参数数组,输出为补全上下文对象。
|
|
368
|
+
*/
|
|
369
|
+
function walkCompletionContext(rootNode, previousWords) {
|
|
370
|
+
let node = rootNode
|
|
371
|
+
let pendingOption = null
|
|
372
|
+
let positionalCount = 0
|
|
373
|
+
|
|
374
|
+
for (const word of previousWords) {
|
|
375
|
+
if (pendingOption) {
|
|
376
|
+
pendingOption = null
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const inlineOption = resolveInlineOption(node, word)
|
|
381
|
+
if (inlineOption) {
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const option = findOption(node, word)
|
|
386
|
+
if (option) {
|
|
387
|
+
pendingOption = option.valueSource ? option : null
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const subcommand = node.subcommands?.[word]
|
|
392
|
+
if (subcommand) {
|
|
393
|
+
node = subcommand
|
|
394
|
+
positionalCount = 0
|
|
395
|
+
pendingOption = null
|
|
396
|
+
continue
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
positionalCount += 1
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
node,
|
|
404
|
+
pendingOption,
|
|
405
|
+
positionalCount
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 识别 `--option=value` 形式的当前输入,并返回对应 option 与过滤前缀。
|
|
411
|
+
* 输入为当前命令节点与当前词,输出为内联 value 上下文或 null。
|
|
412
|
+
*/
|
|
413
|
+
function resolveInlineValueContext(node, currentWord) {
|
|
414
|
+
const separatorIndex = currentWord.indexOf('=')
|
|
415
|
+
if (separatorIndex === -1) {
|
|
416
|
+
return null
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const optionToken = currentWord.slice(0, separatorIndex)
|
|
420
|
+
const option = findOption(node, optionToken)
|
|
421
|
+
if (!option || !option.valueSource) {
|
|
422
|
+
return null
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
option,
|
|
427
|
+
prefix: `${optionToken}=`,
|
|
428
|
+
query: currentWord.slice(separatorIndex + 1)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 识别已确认参数里的 `--option=value`,用于避免错误地把它当作 positional。
|
|
434
|
+
* 输入为当前命令节点与单个参数,输出为命中的 option 或 null。
|
|
435
|
+
*/
|
|
436
|
+
function resolveInlineOption(node, word) {
|
|
437
|
+
const separatorIndex = word.indexOf('=')
|
|
438
|
+
if (separatorIndex === -1) {
|
|
439
|
+
return null
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return findOption(node, word.slice(0, separatorIndex))
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 按当前节点查找长参数或短参数定义。
|
|
447
|
+
* 输入为命令节点与待匹配 token,输出为 option 定义或 null。
|
|
448
|
+
*/
|
|
449
|
+
function findOption(node, token) {
|
|
450
|
+
if (!token.startsWith('-')) {
|
|
451
|
+
return null
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const option of node.options || []) {
|
|
455
|
+
if (option.long === token || option.short === token) {
|
|
456
|
+
return option
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return null
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 判断当前上下文是否仍应优先展示子命令。
|
|
465
|
+
* 输入为当前节点与已消费 positional 数量,输出为布尔值。
|
|
466
|
+
*/
|
|
467
|
+
function shouldSuggestSubcommands(node, positionalCount) {
|
|
468
|
+
return Boolean(node.subcommands && positionalCount === 0)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* 判断当前词是否适合展示参数开关候选。
|
|
473
|
+
* 输入为当前词,输出为布尔值。
|
|
474
|
+
*/
|
|
475
|
+
function shouldSuggestOptions(currentWord) {
|
|
476
|
+
return currentWord === '' || currentWord.startsWith('-')
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* 列出当前节点下可见子命令名称。
|
|
481
|
+
* 输入为命令节点,输出为子命令名数组。
|
|
482
|
+
*/
|
|
483
|
+
function listSubcommandNames(node) {
|
|
484
|
+
return Object.keys(node.subcommands || {})
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* 列出当前节点下的长短参数名。
|
|
489
|
+
* 输入为命令节点,输出为 flag 字符串数组。
|
|
490
|
+
*/
|
|
491
|
+
function listOptionFlags(node) {
|
|
492
|
+
const flags = []
|
|
493
|
+
|
|
494
|
+
for (const option of node.options || []) {
|
|
495
|
+
if (option.long) {
|
|
496
|
+
flags.push(option.long)
|
|
497
|
+
}
|
|
498
|
+
if (option.short) {
|
|
499
|
+
flags.push(option.short)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return flags
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 根据当前 positional 下标解析候选提供器。
|
|
508
|
+
* 输入为命令节点、已消费 positional 数量和提供器集合,输出为候选数组。
|
|
509
|
+
*/
|
|
510
|
+
function resolvePositionalCandidates(node, positionalCount, providers) {
|
|
511
|
+
const positionals = node.positionals || []
|
|
512
|
+
if (!positionals.length) {
|
|
513
|
+
return []
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const spec = positionals[positionalCount] || positionals[positionals.length - 1]
|
|
517
|
+
if (!spec || (!spec.repeat && positionalCount >= positionals.length)) {
|
|
518
|
+
return []
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return resolveValueCandidates(spec.valueSource, providers)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 按 valueSource 解析动态或静态候选项。
|
|
526
|
+
* 输入为 valueSource 名称与提供器集合,输出为字符串数组。
|
|
527
|
+
*/
|
|
528
|
+
function resolveValueCandidates(valueSource, providers) {
|
|
529
|
+
if (!valueSource || valueSource === 'text') {
|
|
530
|
+
return []
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (valueSource === 'repo-mode') {
|
|
534
|
+
return REPO_MODE_VALUES
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (valueSource === 'ai-format') {
|
|
538
|
+
return AI_FORMAT_VALUES
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (valueSource === 'ai-option-scope') {
|
|
542
|
+
return AI_OPTION_SCOPE_VALUES
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (valueSource === 'ai-commit-lang') {
|
|
546
|
+
return AI_COMMIT_LANG_VALUES
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (valueSource === 'undo-mode') {
|
|
550
|
+
return UNDO_MODE_VALUES
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (valueSource === 'undo-recover') {
|
|
554
|
+
return UNDO_RECOVER_VALUES
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (valueSource === 'shell') {
|
|
558
|
+
return SHELL_VALUES
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (valueSource === 'config-path') {
|
|
562
|
+
return CONFIG_PATH_VALUES
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (valueSource === 'branch') {
|
|
566
|
+
return providers.listBranchCandidates()
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (valueSource === 'ref') {
|
|
570
|
+
return providers.listRefCandidates()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (valueSource === 'ai-profile') {
|
|
574
|
+
return providers.listAiProfileCandidates()
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return []
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* 过滤、去重并保留与当前前缀匹配的候选项。
|
|
582
|
+
* 输入为候选数组与当前前缀,输出为去重后的候选数组。
|
|
583
|
+
*/
|
|
584
|
+
function filterCandidates(candidates, currentWord) {
|
|
585
|
+
const prefix = String(currentWord || '')
|
|
586
|
+
const unique = new Set()
|
|
587
|
+
|
|
588
|
+
for (const candidate of candidates) {
|
|
589
|
+
const normalized = String(candidate || '').trim()
|
|
590
|
+
if (!normalized) {
|
|
591
|
+
continue
|
|
592
|
+
}
|
|
593
|
+
if (prefix && !normalized.startsWith(prefix)) {
|
|
594
|
+
continue
|
|
595
|
+
}
|
|
596
|
+
unique.add(normalized)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return [...unique]
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* 读取当前仓库的分支候选项,失败时静默返回空数组。
|
|
604
|
+
* 输入为空,输出为本地与远程分支名数组。
|
|
605
|
+
*/
|
|
606
|
+
function listBranchCandidates() {
|
|
607
|
+
return listGitCandidates(['refs/heads', 'refs/remotes'])
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* 读取当前仓库的 ref 候选项,失败时静默返回空数组。
|
|
612
|
+
* 输入为空,输出为 HEAD、分支与 tag 名数组。
|
|
613
|
+
*/
|
|
614
|
+
function listRefCandidates() {
|
|
615
|
+
return ['HEAD', ...listGitCandidates(['refs/heads', 'refs/remotes', 'refs/tags'])]
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* 读取已配置的 AI profile 名称,失败时静默返回空数组。
|
|
620
|
+
* 输入为空,输出为 profile 名称数组。
|
|
621
|
+
*/
|
|
622
|
+
function listAiProfileCandidates() {
|
|
623
|
+
try {
|
|
624
|
+
return listAiProfiles().map((item) => item.name)
|
|
625
|
+
} catch {
|
|
626
|
+
return []
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 执行 `git for-each-ref` 并格式化为去重后的短 ref 名。
|
|
632
|
+
* 输入为 ref 前缀列表,输出为 ref 名数组。
|
|
633
|
+
*/
|
|
634
|
+
function listGitCandidates(refPrefixes) {
|
|
635
|
+
try {
|
|
636
|
+
const result = spawnSync('git', ['for-each-ref', '--format=%(refname:short)', ...refPrefixes], {
|
|
637
|
+
cwd: process.cwd(),
|
|
638
|
+
encoding: 'utf8'
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
if (result.status !== 0) {
|
|
642
|
+
return []
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const unique = new Set()
|
|
646
|
+
for (const line of String(result.stdout || '').split('\n')) {
|
|
647
|
+
const candidate = line.trim()
|
|
648
|
+
if (!candidate || candidate.endsWith('/HEAD')) {
|
|
649
|
+
continue
|
|
650
|
+
}
|
|
651
|
+
unique.add(candidate)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return [...unique]
|
|
655
|
+
} catch {
|
|
656
|
+
return []
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* 生成 bash 补全脚本。
|
|
662
|
+
* 输入为空,输出为可直接 source 的 bash 脚本文本。
|
|
663
|
+
*/
|
|
664
|
+
function renderBashCompletionScript() {
|
|
665
|
+
return `# bash completion for icode
|
|
666
|
+
_icode_completion() {
|
|
667
|
+
local current_word
|
|
668
|
+
local index
|
|
669
|
+
local -a previous_words=()
|
|
670
|
+
|
|
671
|
+
COMPREPLY=()
|
|
672
|
+
current_word="\${COMP_WORDS[COMP_CWORD]}"
|
|
673
|
+
|
|
674
|
+
for ((index = 1; index < COMP_CWORD; index += 1)); do
|
|
675
|
+
previous_words+=("\${COMP_WORDS[index]}")
|
|
676
|
+
done
|
|
677
|
+
|
|
678
|
+
if ! mapfile -t COMPREPLY < <(command icode __complete --current "$current_word" -- "\${previous_words[@]}"); then
|
|
679
|
+
COMPREPLY=()
|
|
680
|
+
fi
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
complete -o bashdefault -o default -F _icode_completion icode
|
|
684
|
+
`
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* 生成 zsh 补全脚本。
|
|
689
|
+
* 输入为空,输出为可直接 source 的 zsh 脚本文本。
|
|
690
|
+
*/
|
|
691
|
+
function renderZshCompletionScript() {
|
|
692
|
+
return `#compdef icode
|
|
693
|
+
_icode_completion() {
|
|
694
|
+
emulate -L zsh
|
|
695
|
+
|
|
696
|
+
local current_word
|
|
697
|
+
local index
|
|
698
|
+
local -a previous_words
|
|
699
|
+
local -a completions
|
|
700
|
+
|
|
701
|
+
current_word="\${words[CURRENT]:-}"
|
|
702
|
+
previous_words=()
|
|
703
|
+
|
|
704
|
+
for ((index = 2; index < CURRENT; index += 1)); do
|
|
705
|
+
previous_words+=("\${words[index]}")
|
|
706
|
+
done
|
|
707
|
+
|
|
708
|
+
completions=("\${(@f)\$(command icode __complete --current "$current_word" -- "\${previous_words[@]}")}")
|
|
709
|
+
|
|
710
|
+
if (( \${#completions[@]} )); then
|
|
711
|
+
compadd -- "\${completions[@]}"
|
|
712
|
+
return 0
|
|
713
|
+
fi
|
|
714
|
+
|
|
715
|
+
_default
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
compdef _icode_completion icode
|
|
719
|
+
`
|
|
720
|
+
}
|