@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,9 +1,16 @@
1
- import { getRepoPolicy } from '../core/config-store.js'
1
+ import { getRepoPolicy } from '../core/config/store.js'
2
2
  import { IcodeError } from '../core/errors.js'
3
- import { resolveGitContext } from '../core/git-context.js'
4
- import { GitService } from '../core/git-service.js'
5
- import { logger } from '../core/logger.js'
6
- import { confirm } from '../core/prompts.js'
3
+ import { resolveGitContext } from '../core/git/context.js'
4
+ import {
5
+ assertCleanWorktree,
6
+ assertNoConflictedFiles,
7
+ assertNoPendingOperation,
8
+ assertOnBranch,
9
+ readGitSafetyState
10
+ } from '../core/git/preconditions.js'
11
+ import { GitService } from '../core/git/service.js'
12
+ import { confirm } from '../core/tools/interactive.js'
13
+ import { logger } from '../core/tools/logger.js'
7
14
 
8
15
  function normalizeKeepList(values = []) {
9
16
  return new Set(
@@ -14,6 +21,10 @@ function normalizeKeepList(values = []) {
14
21
  )
15
22
  }
16
23
 
24
+ /**
25
+ * 执行 clean 工作流。
26
+ * 输入为清理基线、保留分支和删除选项,输出已删除分支;执行前要求工作区干净并在普通分支上。
27
+ */
17
28
  export async function runCleanWorkflow(options) {
18
29
  const context = await resolveGitContext({
19
30
  cwd: options.cwd,
@@ -22,8 +33,13 @@ export async function runCleanWorkflow(options) {
22
33
 
23
34
  const git = new GitService(context)
24
35
  const mergedTarget = (options.mergedTarget || context.defaultBranch).trim()
25
- const currentBranch = (await git.getCurrentBranch()) || context.currentBranch
36
+ const safetyState = await readGitSafetyState(git)
37
+ const currentBranch = safetyState.currentBranch || context.currentBranch
26
38
  const policy = getRepoPolicy(context.topLevelPath)
39
+ assertOnBranch(currentBranch, 'clean', 'CLEAN_DETACHED_HEAD')
40
+ assertNoPendingOperation(safetyState.operation, 'clean', 'CLEAN_GIT_OPERATION_IN_PROGRESS')
41
+ assertNoConflictedFiles(safetyState.changes, 'clean', 'CLEAN_CONFLICTED_FILES')
42
+ assertCleanWorktree(safetyState.changes, 'clean', 'CLEAN_DIRTY_WORKTREE')
27
43
 
28
44
  logger.info(`仓库根目录: ${context.topLevelPath}`)
29
45
  if (context.inheritedFromParent) {
@@ -41,91 +57,92 @@ export async function runCleanWorkflow(options) {
41
57
  })
42
58
  }
43
59
 
44
- if (targetLocalExists) {
45
- await git.checkout(mergedTarget)
46
- } else {
47
- await git.checkoutTracking(mergedTarget)
48
- }
49
-
50
- if (targetRemoteExists) {
51
- await git.pull(mergedTarget, {
52
- allowUnrelatedHistories: true,
53
- noRebase: true
54
- })
55
- }
56
-
57
- const mergedBranches = await git.listMergedLocalBranches(mergedTarget)
58
- const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
59
- const keepSet = normalizeKeepList(options.keep || [])
60
+ try {
61
+ if (targetLocalExists) {
62
+ await git.checkout(mergedTarget)
63
+ } else {
64
+ await git.checkoutTracking(mergedTarget)
65
+ }
60
66
 
61
- // 这些分支永远不进入清理列表,避免误删核心分支。
62
- keepSet.add(mergedTarget)
63
- if (context.defaultBranch) {
64
- keepSet.add(context.defaultBranch)
65
- }
66
- if (currentBranch) {
67
- keepSet.add(currentBranch)
68
- }
69
- protectedBranches.forEach((branch) => keepSet.add(branch))
67
+ if (targetRemoteExists) {
68
+ await git.pull(mergedTarget, {
69
+ noRebase: true
70
+ })
71
+ }
70
72
 
71
- const candidates = mergedBranches.filter((branch) => !keepSet.has(branch))
73
+ const mergedBranches = await git.listMergedLocalBranches(mergedTarget)
74
+ const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
75
+ const keepSet = normalizeKeepList(options.keep || [])
72
76
 
73
- if (!candidates.length) {
74
- logger.info('没有可清理的本地分支。')
75
- return {
76
- repoRoot: context.topLevelPath,
77
- mergedTarget,
78
- deletedLocal: [],
79
- deletedRemote: []
77
+ // 这些分支永远不进入清理列表,避免误删核心分支。
78
+ keepSet.add(mergedTarget)
79
+ if (context.defaultBranch) {
80
+ keepSet.add(context.defaultBranch)
80
81
  }
81
- }
82
+ if (currentBranch) {
83
+ keepSet.add(currentBranch)
84
+ }
85
+ protectedBranches.forEach((branch) => keepSet.add(branch))
82
86
 
83
- if (!options.yes) {
84
- const accepted = await confirm(
85
- `确认清理以下分支吗: ${candidates.join(', ')} ?`,
86
- false
87
- )
88
- if (!accepted) {
89
- logger.warn('已取消清理。')
87
+ const candidates = mergedBranches.filter((branch) => !keepSet.has(branch))
88
+
89
+ if (!candidates.length) {
90
+ logger.info('没有可清理的本地分支。')
90
91
  return {
91
- canceled: true,
92
92
  repoRoot: context.topLevelPath,
93
93
  mergedTarget,
94
- candidates
94
+ deletedLocal: [],
95
+ deletedRemote: []
95
96
  }
96
97
  }
97
- }
98
98
 
99
- const deletedLocal = []
100
- const deletedRemote = []
101
-
102
- for (const branch of candidates) {
103
- await git.deleteLocalBranch(branch, {
104
- force: options.force
105
- })
106
- deletedLocal.push(branch)
99
+ if (!options.yes) {
100
+ const accepted = await confirm(
101
+ `确认清理以下分支吗: ${candidates.join(', ')} ?`,
102
+ false
103
+ )
104
+ if (!accepted) {
105
+ logger.warn('已取消清理。')
106
+ return {
107
+ canceled: true,
108
+ repoRoot: context.topLevelPath,
109
+ mergedTarget,
110
+ candidates
111
+ }
112
+ }
113
+ }
107
114
 
108
- if (options.remote) {
109
- const existsRemote = await git.branchExistsRemote(branch)
110
- if (existsRemote) {
111
- await git.deleteRemoteBranch(branch)
112
- deletedRemote.push(branch)
115
+ const deletedLocal = []
116
+ const deletedRemote = []
117
+
118
+ for (const branch of candidates) {
119
+ await git.deleteLocalBranch(branch, {
120
+ force: options.force
121
+ })
122
+ deletedLocal.push(branch)
123
+
124
+ if (options.remote) {
125
+ const existsRemote = await git.branchExistsRemote(branch)
126
+ if (existsRemote) {
127
+ await git.deleteRemoteBranch(branch)
128
+ deletedRemote.push(branch)
129
+ }
113
130
  }
114
131
  }
115
- }
116
132
 
117
- try {
133
+ return {
134
+ repoRoot: context.topLevelPath,
135
+ mergedTarget,
136
+ deletedLocal,
137
+ deletedRemote
138
+ }
139
+ } finally {
118
140
  if (currentBranch && currentBranch !== mergedTarget) {
119
- await git.checkout(currentBranch)
141
+ try {
142
+ await git.checkout(currentBranch)
143
+ } catch (error) {
144
+ logger.warn(`未能切回原分支 ${currentBranch}: ${error.message}`)
145
+ }
120
146
  }
121
- } catch (error) {
122
- logger.warn(`未能切回原分支 ${currentBranch}: ${error.message}`)
123
- }
124
-
125
- return {
126
- repoRoot: context.topLevelPath,
127
- mergedTarget,
128
- deletedLocal,
129
- deletedRemote
130
147
  }
131
148
  }
@@ -0,0 +1,131 @@
1
+ import { getAiConfig } from '../core/ai/config.js'
2
+ import { getConfigFilePath, getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
3
+ import { resolveGitContext } from '../core/git/context.js'
4
+ import { GitService } from '../core/git/service.js'
5
+ import { runCommand } from '../core/commands/shell.js'
6
+
7
+ // 构建单条健康检查结果,输入检查项字段,输出统一结构。
8
+ function buildCheck(id, status, title, detail, suggestion = '') {
9
+ return {
10
+ id,
11
+ status,
12
+ title,
13
+ detail,
14
+ suggestion
15
+ }
16
+ }
17
+
18
+ // 汇总检查结果状态,输入 checks 数组,输出整体状态与计数。
19
+ function summarizeChecks(checks) {
20
+ const summary = {
21
+ status: 'pass',
22
+ pass: 0,
23
+ warn: 0,
24
+ fail: 0
25
+ }
26
+
27
+ checks.forEach((check) => {
28
+ if (check.status === 'fail') {
29
+ summary.fail += 1
30
+ summary.status = 'fail'
31
+ return
32
+ }
33
+ if (check.status === 'warn') {
34
+ summary.warn += 1
35
+ if (summary.status !== 'fail') {
36
+ summary.status = 'warn'
37
+ }
38
+ return
39
+ }
40
+ summary.pass += 1
41
+ })
42
+
43
+ return summary
44
+ }
45
+
46
+ // 检查 git 命令可用性,输入仓库根目录,输出健康检查项。
47
+ async function checkGitVersion(repoRoot) {
48
+ const result = await runCommand('git', ['--version'], {
49
+ cwd: repoRoot,
50
+ allowFailure: true
51
+ })
52
+
53
+ if (result.exitCode !== 0) {
54
+ return buildCheck('git.version', 'fail', 'Git 命令不可用', '无法执行 git --version。', '请先安装 Git 并确认 PATH 配置正确。')
55
+ }
56
+
57
+ return buildCheck('git.version', 'pass', 'Git 命令可用', result.stdout.trim())
58
+ }
59
+
60
+ // 检查 origin remote 是否存在,输入 GitService,输出健康检查项。
61
+ async function checkOriginRemote(git) {
62
+ const remoteUrl = await git.getRemoteUrl('origin')
63
+ if (!remoteUrl) {
64
+ return buildCheck('git.origin', 'warn', 'origin remote 未配置', '当前仓库没有 origin remote。', '需要 push 或远程合并前,请先配置 git remote add origin <url>。')
65
+ }
66
+
67
+ return buildCheck('git.origin', 'pass', 'origin remote 已配置', remoteUrl)
68
+ }
69
+
70
+ // 检查 hook 配置,输入 git context,输出健康检查项。
71
+ function checkHooks(context) {
72
+ if (context.hasHuskyFolder || context.hasHookPath) {
73
+ return buildCheck('git.hooks', 'pass', 'Git hooks 已检测到', `hookPath=${context.hookPath}`)
74
+ }
75
+
76
+ return buildCheck('git.hooks', 'warn', '未检测到 Git hooks', '当前仓库没有 .husky 目录或 hooksPath。', '如团队依赖 commitlint/lint-staged,建议补齐 hooks 配置。')
77
+ }
78
+
79
+ // 检查 AI profile 配置,输入 AI 配置对象,输出健康检查项。
80
+ function checkAiProfile(aiConfig) {
81
+ const profileCount = Object.keys(aiConfig.profiles || {}).length
82
+ if (!profileCount || !aiConfig.activeProfile) {
83
+ return buildCheck('ai.profile', 'warn', 'AI profile 未配置', '当前没有可用 AI profile。', '如需使用 ai commit/codereview/explain,请执行 icode config ai set ... --activate。')
84
+ }
85
+
86
+ return buildCheck('ai.profile', 'pass', 'AI profile 已配置', `activeProfile=${aiConfig.activeProfile}, profileCount=${profileCount}`)
87
+ }
88
+
89
+ // 检查远程合并平台配置,输入平台配置与仓库策略,输出健康检查项。
90
+ function checkRemoteMerge(remoteMergePlatform, repoPolicy) {
91
+ const remoteMergePolicy = repoPolicy.remoteMerge && typeof repoPolicy.remoteMerge === 'object'
92
+ ? repoPolicy.remoteMerge
93
+ : {}
94
+
95
+ if (remoteMergePolicy.enabled === false) {
96
+ return buildCheck('remoteMerge.config', 'pass', '远程合并已在仓库禁用', 'repositories.<repo>.remoteMerge.enabled=false')
97
+ }
98
+
99
+ if (!remoteMergePlatform.provider || !remoteMergePlatform.apiKey) {
100
+ return buildCheck('remoteMerge.config', 'warn', '远程合并平台未完整配置', '缺少 provider 或 apiKey。', '如需 icode push -r,请执行 icode config platform remote-merge set --provider gitlab --api-key <key>。')
101
+ }
102
+
103
+ return buildCheck('remoteMerge.config', 'pass', '远程合并平台已配置', `provider=${remoteMergePlatform.provider}`)
104
+ }
105
+
106
+ // 执行项目健康检查,输入仓库选项,输出 context、checks 和 summary。
107
+ export async function runDoctorWorkflow(options = {}) {
108
+ const context = await resolveGitContext({
109
+ cwd: options.cwd,
110
+ repoMode: options.repoMode
111
+ })
112
+ const git = new GitService(context)
113
+ const repoPolicy = getRepoPolicy(context.topLevelPath)
114
+ const aiConfig = getAiConfig()
115
+ const remoteMergePlatform = getPlatformConfig('remoteMerge')
116
+
117
+ const checks = [
118
+ await checkGitVersion(context.topLevelPath),
119
+ await checkOriginRemote(git),
120
+ checkHooks(context),
121
+ checkAiProfile(aiConfig),
122
+ checkRemoteMerge(remoteMergePlatform, repoPolicy)
123
+ ]
124
+
125
+ return {
126
+ configPath: getConfigFilePath(),
127
+ context,
128
+ checks,
129
+ summary: summarizeChecks(checks)
130
+ }
131
+ }
@@ -1,7 +1,7 @@
1
- import { getAiConfig } from '../core/ai-config.js'
2
- import { getConfigFilePath, getRepoPolicy } from '../core/config-store.js'
3
- import { resolveGitContext } from '../core/git-context.js'
4
- import { runCommand } from '../core/shell.js'
1
+ import { getAiConfig } from '../core/ai/config.js'
2
+ import { getConfigFilePath, getRepoPolicy } from '../core/config/store.js'
3
+ import { resolveGitContext } from '../core/git/context.js'
4
+ import { runCommand } from '../core/commands/shell.js'
5
5
 
6
6
  export async function runInfoWorkflow(options) {
7
7
  const context = await resolveGitContext({