@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
@@ -1,19 +1,22 @@
1
1
  import { IcodeError } from '../core/errors.js'
2
- import { resolveGitContext } from '../core/git-context.js'
3
- import { GitService } from '../core/git-service.js'
4
- import { logger } from '../core/logger.js'
5
- import { chooseMany, chooseOne, isInteractiveTerminal } from '../core/prompts.js'
2
+ import { resolveGitContext } from '../core/git/context.js'
3
+ import { GitService } from '../core/git/service.js'
4
+ import { chooseMany, chooseOne, isInteractiveTerminal } from '../core/tools/interactive.js'
5
+ import { logger } from '../core/tools/logger.js'
6
6
 
7
7
  const INTERACTIVE_COMMIT_LIMIT = 30
8
8
 
9
+ // 归一化分支名,输入为用户参数,输出去除首尾空白后的分支名。
9
10
  function normalizeBranchName(value) {
10
11
  return (value || '').trim()
11
12
  }
12
13
 
14
+ // 归一化提交范围,输入为用户参数,输出去除首尾空白后的 range 表达式。
13
15
  function normalizeRangeSpec(value) {
14
16
  return (value || '').trim()
15
17
  }
16
18
 
19
+ // 构建最近 N 条提交的交互选项,输入最大提交数,输出可供 chooseOne 使用的选项。
17
20
  function buildRecentCountChoices(maxCount) {
18
21
  const candidates = [1, 2, 3, 5, 8, 10]
19
22
  .filter((count) => count <= maxCount)
@@ -32,10 +35,12 @@ function buildRecentCountChoices(maxCount) {
32
35
  return candidates
33
36
  }
34
37
 
38
+ // 去重并排序分支候选,输入本地/远程分支列表,输出稳定顺序的分支名数组。
35
39
  function uniqueBranches(branches) {
36
40
  return Array.from(new Set((branches || []).map((item) => item.trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b))
37
41
  }
38
42
 
43
+ // 读取迁移交互模式的分支候选,输入 GitService,输出本地与 origin 远程分支名。
39
44
  async function listBranchCandidates(git) {
40
45
  const [localBranches, remoteBranches] = await Promise.all([
41
46
  git.listLocalBranches(),
@@ -44,6 +49,7 @@ async function listBranchCandidates(git) {
44
49
  return uniqueBranches([...localBranches, ...remoteBranches])
45
50
  }
46
51
 
52
+ // 选择 source/target 分支,输入候选和默认值,输出用户选择的分支名。
47
53
  async function pickBranch({ label, candidates, defaultValue, excludedBranch = '' }) {
48
54
  const excluded = normalizeBranchName(excludedBranch)
49
55
  const normalizedDefault = normalizeBranchName(defaultValue)
@@ -82,6 +88,7 @@ async function pickBranch({ label, candidates, defaultValue, excludedBranch = ''
82
88
  return normalizeBranchName(selected)
83
89
  }
84
90
 
91
+ // 手动选择要 cherry-pick 的提交,输入提交 hash 列表,输出被选择的 hash 列表。
85
92
  async function pickManualCommits(git, commits) {
86
93
  const displayCommits = commits.length > INTERACTIVE_COMMIT_LIMIT
87
94
  ? commits.slice(-INTERACTIVE_COMMIT_LIMIT)
@@ -109,12 +116,13 @@ async function pickManualCommits(git, commits) {
109
116
  return Array.isArray(selected) ? selected : []
110
117
  }
111
118
 
119
+ // 解析交互模式迁移计划,输入 GitService 和初始参数,输出 source/target/range/selectedCommits。
112
120
  async function resolveInteractivePlan(git, options) {
113
121
  const candidates = await listBranchCandidates(git)
114
122
  const sourceBranch = await pickBranch({
115
123
  label: 'source 分支',
116
124
  candidates,
117
- defaultValue: options.sourceBranch
125
+ defaultValue: options.sourceBranch || options.currentBranch
118
126
  })
119
127
 
120
128
  const targetBranch = await pickBranch({
@@ -260,6 +268,189 @@ async function resolveInteractivePlan(git, options) {
260
268
  }
261
269
  }
262
270
 
271
+ // 静默模式下输出 info 日志,输入执行选项和日志文本,输出为空。
272
+ function logInfo(options, message) {
273
+ if (!options.silentLog) {
274
+ logger.info(message)
275
+ }
276
+ }
277
+
278
+ // 静默模式下输出 warn 日志,输入执行选项和日志文本,输出为空。
279
+ function logWarn(options, message) {
280
+ if (!options.silentLog) {
281
+ logger.warn(message)
282
+ }
283
+ }
284
+
285
+ // 检查 migrate 执行前置条件,输入 GitService,输出当前分支和改动摘要。
286
+ async function assertMigratePreconditions(git) {
287
+ const [currentBranch, operation, changes] = await Promise.all([
288
+ git.getCurrentBranch(),
289
+ git.getInProgressOperation(),
290
+ git.getStatusSummary()
291
+ ])
292
+
293
+ if (!currentBranch) {
294
+ throw new IcodeError('当前不在普通分支上,migrate 已停止。', {
295
+ code: 'MIGRATE_DETACHED_HEAD',
296
+ exitCode: 2
297
+ })
298
+ }
299
+
300
+ if (operation) {
301
+ throw new IcodeError(`检测到未完成的 Git 操作: ${operation}。请先完成或中止该操作。`, {
302
+ code: 'MIGRATE_GIT_OPERATION_IN_PROGRESS',
303
+ exitCode: 2,
304
+ meta: {
305
+ operation
306
+ }
307
+ })
308
+ }
309
+
310
+ if (!changes.clean) {
311
+ throw new IcodeError('工作区存在未提交改动,migrate 已停止。请先执行 save 或手动清理。', {
312
+ code: 'MIGRATE_DIRTY_WORKTREE',
313
+ exitCode: 2,
314
+ meta: {
315
+ changes
316
+ }
317
+ })
318
+ }
319
+
320
+ return {
321
+ currentBranch,
322
+ changes
323
+ }
324
+ }
325
+
326
+ // 将本地/远程分支存在性解析为可用于 rev-list 的 ref,输入分支名和存在性,输出明确 ref。
327
+ function resolveBranchRef(branchName, existsLocal, existsRemote, label) {
328
+ if (existsLocal) {
329
+ return branchName
330
+ }
331
+
332
+ if (existsRemote) {
333
+ return `origin/${branchName}`
334
+ }
335
+
336
+ throw new IcodeError(`${label} 分支不存在: ${branchName}`, {
337
+ code: label === 'source' ? 'MIGRATE_SOURCE_MISSING' : 'MIGRATE_TARGET_MISSING',
338
+ exitCode: 2
339
+ })
340
+ }
341
+
342
+ // 构建提交展示计划,输入提交 hash 列表,输出 hash 与提交摘要列表。
343
+ async function buildCommitPlan(git, commits) {
344
+ const plan = []
345
+
346
+ for (const commitHash of commits) {
347
+ plan.push({
348
+ hash: commitHash,
349
+ summary: await git.showCommitSummary(commitHash)
350
+ })
351
+ }
352
+
353
+ return plan
354
+ }
355
+
356
+ // 构建 migrate 执行步骤,输入计划上下文,输出人类和 agent 都能读取的步骤数组。
357
+ function buildMigrateSteps(plan) {
358
+ const steps = [
359
+ {
360
+ action: 'checkout-target',
361
+ detail: plan.targetExistsLocal
362
+ ? `切换到本地目标分支 ${plan.targetBranch}`
363
+ : `从 origin/${plan.targetBranch} 创建 tracking 分支`
364
+ }
365
+ ]
366
+
367
+ if (plan.targetExistsRemote) {
368
+ steps.push({
369
+ action: 'pull-target',
370
+ detail: `同步 origin/${plan.targetBranch} 到目标分支`
371
+ })
372
+ }
373
+
374
+ steps.push({
375
+ action: 'cherry-pick',
376
+ detail: `迁移 ${plan.commits.length} 个提交`
377
+ })
378
+
379
+ if (plan.push) {
380
+ steps.push({
381
+ action: 'push-target',
382
+ detail: `推送目标分支 ${plan.targetBranch}`
383
+ })
384
+ }
385
+
386
+ if (plan.originalBranch && plan.originalBranch !== plan.targetBranch) {
387
+ steps.push({
388
+ action: 'restore-branch',
389
+ detail: `切回原分支 ${plan.originalBranch}`
390
+ })
391
+ }
392
+
393
+ return steps
394
+ }
395
+
396
+ // 输出迁移计划预览,输入计划对象,输出用户确认前需要看到的关键信息。
397
+ function logMigratePlan(options, plan) {
398
+ const previewLimit = 10
399
+ const previewCommits = plan.commits.slice(0, previewLimit)
400
+
401
+ logInfo(options, '迁移计划:')
402
+ logInfo(options, ` source: ${plan.sourceBranch} (${plan.sourceRef})`)
403
+ logInfo(options, ` target: ${plan.targetBranch} (${plan.targetRef})`)
404
+ logInfo(options, ` range: ${plan.rangeSpec}`)
405
+ logInfo(options, ` commits: ${plan.migratedCommits}`)
406
+
407
+ if (previewCommits.length) {
408
+ logInfo(options, '提交预览:')
409
+ previewCommits.forEach((commit, index) => {
410
+ logInfo(options, ` ${index + 1}. ${commit.summary || commit.hash}`)
411
+ })
412
+ }
413
+
414
+ if (plan.commits.length > previewLimit) {
415
+ logInfo(options, ` ... 还有 ${plan.commits.length - previewLimit} 个提交未显示,可用 --dry-run --json 查看完整计划。`)
416
+ }
417
+
418
+ logInfo(options, '执行步骤:')
419
+ plan.steps.forEach((step, index) => {
420
+ logInfo(options, ` ${index + 1}. ${step.action}: ${step.detail}`)
421
+ })
422
+ }
423
+
424
+ // 构建迁移计划结果,输入已解析的分支、范围和提交,输出稳定 JSON 结构。
425
+ async function buildMigratePlan(git, values) {
426
+ const commits = values.selectedCommits.length
427
+ ? values.selectedCommits
428
+ : await git.revList(values.effectiveRangeSpec)
429
+ const commitPlan = await buildCommitPlan(git, commits)
430
+ const plan = {
431
+ sourceBranch: values.sourceBranch,
432
+ targetBranch: values.targetBranch,
433
+ sourceRef: values.sourceRef,
434
+ targetRef: values.targetRef,
435
+ originalBranch: values.originalBranch,
436
+ rangeSpec: values.effectiveRangeSpec,
437
+ rangeMode: values.rangeMode,
438
+ selectedCommits: values.selectedCommits,
439
+ commits: commitPlan,
440
+ migratedCommits: commits.length,
441
+ targetExistsLocal: values.targetExistsLocal,
442
+ targetExistsRemote: values.targetExistsRemote,
443
+ push: values.push,
444
+ repoRoot: values.repoRoot
445
+ }
446
+
447
+ return {
448
+ ...plan,
449
+ steps: buildMigrateSteps(plan)
450
+ }
451
+ }
452
+
453
+ // 执行提交迁移工作流,输入 source/target/range/push/dryRun 等选项,输出迁移结果或计划。
263
454
  export async function runMigrateWorkflow(options) {
264
455
  let sourceBranch = normalizeBranchName(options.sourceBranch)
265
456
  let targetBranch = normalizeBranchName(options.targetBranch)
@@ -276,11 +467,12 @@ export async function runMigrateWorkflow(options) {
276
467
  })
277
468
 
278
469
  const git = new GitService(context)
279
- const originalBranch = await git.getCurrentBranch()
470
+ const precheck = await assertMigratePreconditions(git)
471
+ const originalBranch = precheck.currentBranch
280
472
 
281
- logger.info(`仓库根目录: ${context.topLevelPath}`)
473
+ logInfo(options, `仓库根目录: ${context.topLevelPath}`)
282
474
  if (context.inheritedFromParent) {
283
- logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
475
+ logWarn(options, '当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
284
476
  }
285
477
 
286
478
  await git.fetch()
@@ -296,6 +488,7 @@ export async function runMigrateWorkflow(options) {
296
488
  const interactivePlan = await resolveInteractivePlan(git, {
297
489
  sourceBranch,
298
490
  targetBranch,
491
+ currentBranch: originalBranch,
299
492
  range: rangeSpec
300
493
  })
301
494
 
@@ -334,20 +527,72 @@ export async function runMigrateWorkflow(options) {
334
527
 
335
528
  const sourceExistsLocal = await git.branchExistsLocal(sourceBranch)
336
529
  const sourceExistsRemote = await git.branchExistsRemote(sourceBranch)
337
- if (!sourceExistsLocal && !sourceExistsRemote) {
338
- throw new IcodeError(`source 分支不存在: ${sourceBranch}`, {
339
- code: 'MIGRATE_SOURCE_MISSING',
340
- exitCode: 2
341
- })
342
- }
530
+ const sourceRef = resolveBranchRef(sourceBranch, sourceExistsLocal, sourceExistsRemote, 'source')
343
531
 
344
532
  const targetExistsLocal = await git.branchExistsLocal(targetBranch)
345
533
  const targetExistsRemote = await git.branchExistsRemote(targetBranch)
346
- if (!targetExistsLocal && !targetExistsRemote) {
347
- throw new IcodeError(`target 分支不存在: ${targetBranch}`, {
348
- code: 'MIGRATE_TARGET_MISSING',
349
- exitCode: 2
350
- })
534
+ const targetRef = resolveBranchRef(targetBranch, targetExistsLocal, targetExistsRemote, 'target')
535
+
536
+ // 默认迁移 source 相对 target 的增量提交;交互模式可选择最近 N 条或手动挑选提交。
537
+ const effectiveRangeSpec = rangeSpec || `${targetRef}..${sourceRef}`
538
+ const plan = await buildMigratePlan(git, {
539
+ sourceBranch,
540
+ targetBranch,
541
+ sourceRef,
542
+ targetRef,
543
+ originalBranch,
544
+ rangeMode,
545
+ selectedCommits,
546
+ effectiveRangeSpec,
547
+ targetExistsLocal,
548
+ targetExistsRemote,
549
+ push: Boolean(options.push),
550
+ repoRoot: context.topLevelPath
551
+ })
552
+
553
+ if (options.dryRun) {
554
+ return {
555
+ dryRun: true,
556
+ ...plan
557
+ }
558
+ }
559
+
560
+ const commits = plan.commits.map((commit) => commit.hash)
561
+
562
+ if (!commits.length) {
563
+ logWarn(options, '没有可迁移的提交。')
564
+ return {
565
+ dryRun: false,
566
+ sourceBranch,
567
+ targetBranch,
568
+ migratedCommits: 0,
569
+ rangeSpec: effectiveRangeSpec,
570
+ rangeMode,
571
+ repoRoot: context.topLevelPath
572
+ }
573
+ }
574
+
575
+ if (!options.yes) {
576
+ logMigratePlan(options, plan)
577
+ const accepted = (await chooseOne(
578
+ '确认执行以上迁移计划吗?',
579
+ [
580
+ { value: 'yes', label: '确认执行' },
581
+ { value: 'no', label: '取消迁移' }
582
+ ],
583
+ 0
584
+ )) === 'yes'
585
+ if (!accepted) {
586
+ logger.warn('已取消迁移。')
587
+ return {
588
+ canceled: true,
589
+ sourceBranch,
590
+ targetBranch,
591
+ rangeSpec: effectiveRangeSpec,
592
+ rangeMode,
593
+ repoRoot: context.topLevelPath
594
+ }
595
+ }
351
596
  }
352
597
 
353
598
  try {
@@ -359,60 +604,21 @@ export async function runMigrateWorkflow(options) {
359
604
 
360
605
  if (targetExistsRemote) {
361
606
  await git.pull(targetBranch, {
362
- allowUnrelatedHistories: true,
363
607
  noRebase: true
364
608
  })
365
609
  }
366
610
 
367
- // 默认迁移 source 相对 target 的增量提交;交互模式可选择最近 N 条或手动挑选提交。
368
- const effectiveRangeSpec = rangeSpec || `${targetBranch}..${sourceBranch}`
369
- const commits = selectedCommits.length ? selectedCommits : await git.revList(effectiveRangeSpec)
370
-
371
611
  if (selectedCommits.length) {
372
- logger.info(`迁移范围: 手动选择 ${selectedCommits.length} 个提交(mode=${rangeMode})`)
612
+ logInfo(options, `迁移范围: 手动选择 ${selectedCommits.length} 个提交(mode=${rangeMode})`)
373
613
  } else {
374
- logger.info(`迁移范围: ${effectiveRangeSpec}`)
375
- }
376
-
377
- if (!commits.length) {
378
- logger.warn('没有可迁移的提交。')
379
- return {
380
- sourceBranch,
381
- targetBranch,
382
- migratedCommits: 0,
383
- rangeSpec: effectiveRangeSpec,
384
- rangeMode,
385
- repoRoot: context.topLevelPath
386
- }
387
- }
388
-
389
- if (!options.yes) {
390
- const accepted = (await chooseOne(
391
- `确认将 ${commits.length} 个提交从 ${sourceBranch} 迁移到 ${targetBranch} 吗?`,
392
- [
393
- { value: 'yes', label: '确认迁移' },
394
- { value: 'no', label: '取消' }
395
- ],
396
- 0
397
- )) === 'yes'
398
- if (!accepted) {
399
- logger.warn('已取消迁移。')
400
- return {
401
- canceled: true,
402
- sourceBranch,
403
- targetBranch,
404
- rangeSpec: effectiveRangeSpec,
405
- rangeMode,
406
- repoRoot: context.topLevelPath
407
- }
408
- }
614
+ logInfo(options, `迁移范围: ${effectiveRangeSpec}`)
409
615
  }
410
616
 
411
617
  try {
412
618
  await git.cherryPick(commits)
413
619
  } catch (error) {
414
620
  throw new IcodeError(
415
- '迁移失败: cherry-pick 发生冲突。请先解决冲突后执行 `git cherry-pick --continue`,或执行 `git cherry-pick --abort` 回滚。',
621
+ '迁移失败: cherry-pick 发生冲突。请先解决冲突后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 回滚。',
416
622
  {
417
623
  code: 'MIGRATE_CHERRY_PICK_FAILED',
418
624
  cause: error,
@@ -429,6 +635,7 @@ export async function runMigrateWorkflow(options) {
429
635
  }
430
636
 
431
637
  return {
638
+ dryRun: false,
432
639
  sourceBranch,
433
640
  targetBranch,
434
641
  migratedCommits: commits.length,
@@ -442,7 +649,7 @@ export async function runMigrateWorkflow(options) {
442
649
  try {
443
650
  await git.checkout(originalBranch)
444
651
  } catch (error) {
445
- logger.warn(`未能自动切回原分支 ${originalBranch}: ${error.message}`)
652
+ logWarn(options, `未能自动切回原分支 ${originalBranch}: ${error.message}`)
446
653
  }
447
654
  }
448
655
  }