@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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -5
  3. package/bin/icode.js +4 -4
  4. package/package.json +1 -1
  5. package/src/cli.js +60 -23
  6. package/src/commands/ai.js +4 -4
  7. package/src/commands/checkout.js +2 -2
  8. package/src/commands/clean.js +5 -2
  9. package/src/commands/codereview.js +5 -2
  10. package/src/commands/completion.js +93 -0
  11. package/src/commands/config.js +123 -10
  12. package/src/commands/doctor.js +80 -0
  13. package/src/commands/explain.js +6 -3
  14. package/src/commands/help.js +17 -0
  15. package/src/commands/info.js +5 -2
  16. package/src/commands/migrate.js +40 -3
  17. package/src/commands/push.js +72 -24
  18. package/src/commands/ready.js +110 -0
  19. package/src/commands/save.js +98 -0
  20. package/src/commands/sync.js +2 -2
  21. package/src/commands/tag.js +2 -2
  22. package/src/commands/undo.js +73 -10
  23. package/src/core/{ai-client.js → ai/client.js} +3 -3
  24. package/src/core/{ai-config.js → ai/config.js} +2 -2
  25. package/src/core/ai/diff-prompt.js +204 -0
  26. package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
  27. package/src/core/{args.js → cli/args.js} +23 -2
  28. package/src/core/cli/completion.js +720 -0
  29. package/src/core/cli/update-notifier.js +138 -0
  30. package/src/core/{shell.js → commands/shell.js} +13 -1
  31. package/src/core/{config-store.js → config/store.js} +53 -3
  32. package/src/core/{git-context.js → git/context.js} +24 -10
  33. package/src/core/git/preconditions.js +88 -0
  34. package/src/core/{git-service.js → git/service.js} +195 -5
  35. package/src/core/remote-merge/client.js +42 -0
  36. package/src/core/remote-merge/providers/gitlab.js +596 -0
  37. package/src/core/remote-merge/shared.js +49 -0
  38. package/src/core/tools/interactive.js +368 -0
  39. package/src/core/{loading.js → tools/loading.js} +5 -1
  40. package/src/core/{logger.js → tools/logger.js} +5 -0
  41. package/src/workflows/ai-codereview-workflow.js +18 -6
  42. package/src/workflows/ai-commit-workflow.js +20 -12
  43. package/src/workflows/ai-conflict-workflow.js +4 -4
  44. package/src/workflows/ai-explain-workflow.js +4 -4
  45. package/src/workflows/ai-risk-review-workflow.js +1 -1
  46. package/src/workflows/checkout-workflow.js +17 -5
  47. package/src/workflows/clean-workflow.js +92 -75
  48. package/src/workflows/doctor-workflow.js +131 -0
  49. package/src/workflows/info-workflow.js +4 -4
  50. package/src/workflows/migrate-workflow.js +269 -62
  51. package/src/workflows/push-workflow.js +475 -58
  52. package/src/workflows/ready-workflow.js +278 -0
  53. package/src/workflows/rollback-workflow.js +24 -9
  54. package/src/workflows/save-workflow.js +152 -0
  55. package/src/workflows/sync-workflow.js +22 -6
  56. package/src/workflows/tag-workflow.js +4 -4
  57. package/src/workflows/undo-workflow.js +37 -12
  58. package/src/core/prompts.js +0 -152
  59. /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
  60. /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
  61. /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
+ }