@icode-js/icode 3.1.0 → 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 (39) hide show
  1. package/README.md +105 -5
  2. package/package.json +1 -1
  3. package/src/cli.js +6 -0
  4. package/src/commands/clean.js +4 -1
  5. package/src/commands/codereview.js +4 -1
  6. package/src/commands/completion.js +9 -6
  7. package/src/commands/config.js +9 -6
  8. package/src/commands/doctor.js +80 -0
  9. package/src/commands/explain.js +4 -1
  10. package/src/commands/help.js +10 -0
  11. package/src/commands/info.js +4 -1
  12. package/src/commands/migrate.js +38 -1
  13. package/src/commands/push.js +45 -18
  14. package/src/commands/ready.js +110 -0
  15. package/src/commands/save.js +98 -0
  16. package/src/commands/undo.js +13 -1
  17. package/src/core/ai/diff-prompt.js +204 -0
  18. package/src/core/cli/args.js +23 -2
  19. package/src/core/cli/completion.js +33 -0
  20. package/src/core/config/store.js +2 -4
  21. package/src/core/git/context.js +14 -15
  22. package/src/core/git/preconditions.js +88 -0
  23. package/src/core/git/service.js +151 -1
  24. package/src/core/remote-merge/providers/gitlab.js +149 -90
  25. package/src/core/tools/interactive.js +209 -34
  26. package/src/core/tools/loading.js +5 -1
  27. package/src/core/tools/logger.js +5 -0
  28. package/src/workflows/ai-codereview-workflow.js +14 -2
  29. package/src/workflows/ai-commit-workflow.js +14 -6
  30. package/src/workflows/checkout-workflow.js +14 -2
  31. package/src/workflows/clean-workflow.js +87 -70
  32. package/src/workflows/doctor-workflow.js +131 -0
  33. package/src/workflows/migrate-workflow.js +265 -58
  34. package/src/workflows/push-workflow.js +232 -50
  35. package/src/workflows/ready-workflow.js +278 -0
  36. package/src/workflows/rollback-workflow.js +20 -5
  37. package/src/workflows/save-workflow.js +152 -0
  38. package/src/workflows/sync-workflow.js +18 -2
  39. package/src/workflows/undo-workflow.js +25 -7
@@ -9,6 +9,21 @@ function isRevertConflict(error) {
9
9
  return /revert is already in progress|could not revert|after resolving the conflicts|CONFLICT \(/i.test(output)
10
10
  }
11
11
 
12
+ // 静默模式下输出 info 日志,输入执行选项和日志文本,输出为空。
13
+ function logInfo(options, message) {
14
+ if (!options.silentLog) {
15
+ logger.info(message)
16
+ }
17
+ }
18
+
19
+ // 静默模式下输出 warn 日志,输入执行选项和日志文本,输出为空。
20
+ function logWarn(options, message) {
21
+ if (!options.silentLog) {
22
+ logger.warn(message)
23
+ }
24
+ }
25
+
26
+ // 执行底层回滚操作,输入 mode/ref/仓库选项,输出执行结果。
12
27
  export async function runRollbackWorkflow(options) {
13
28
  const mode = options.mode || 'revert'
14
29
  const ref = options.ref || (mode === 'revert' ? 'HEAD' : 'HEAD~1')
@@ -20,13 +35,13 @@ export async function runRollbackWorkflow(options) {
20
35
 
21
36
  const git = new GitService(context)
22
37
 
23
- logger.info(`仓库根目录: ${context.topLevelPath}`)
38
+ logInfo(options, `仓库根目录: ${context.topLevelPath}`)
24
39
  if (context.inheritedFromParent) {
25
- logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
40
+ logWarn(options, '当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
26
41
  }
27
42
 
28
43
  if (mode === 'revert') {
29
- logger.info(`执行回滚(revert): ${ref}`)
44
+ logInfo(options, `执行回滚(revert): ${ref}`)
30
45
  try {
31
46
  await git.revert(ref)
32
47
  } catch (error) {
@@ -63,7 +78,7 @@ export async function runRollbackWorkflow(options) {
63
78
  false
64
79
  )
65
80
  if (!accepted) {
66
- logger.warn('已取消 hard 回滚。')
81
+ logWarn(options, '已取消 hard 回滚。')
67
82
  return {
68
83
  canceled: true,
69
84
  mode,
@@ -73,7 +88,7 @@ export async function runRollbackWorkflow(options) {
73
88
  }
74
89
  }
75
90
 
76
- logger.info(`执行回滚(reset --${mode}): ${ref}`)
91
+ logInfo(options, `执行回滚(reset --${mode}): ${ref}`)
77
92
  await git.reset(mode, ref)
78
93
 
79
94
  return {
@@ -0,0 +1,152 @@
1
+ import { getAiProfile } from '../core/ai/config.js'
2
+ import { IcodeError } from '../core/errors.js'
3
+ import { resolveGitContext } from '../core/git/context.js'
4
+ import {
5
+ assertNoConflictedFiles,
6
+ assertNoPendingOperation,
7
+ assertOnBranch,
8
+ readGitSafetyState
9
+ } from '../core/git/preconditions.js'
10
+ import { GitService } from '../core/git/service.js'
11
+ import { runAiCommitWorkflow } from './ai-commit-workflow.js'
12
+
13
+ /**
14
+ * 清理提交信息。
15
+ * 输入为任意字符串,输出去掉首尾空白后的提交信息。
16
+ */
17
+ function normalizeMessage(message) {
18
+ return String(message || '').trim()
19
+ }
20
+
21
+ /**
22
+ * 校验 save 的提交信息来源。
23
+ * 输入为 workflow options,输出为空;发现冲突配置时抛错。
24
+ */
25
+ function validateMessageSource(options) {
26
+ const hasMessage = Boolean(normalizeMessage(options.message))
27
+ const useAiCommit = options.aiCommit === true
28
+
29
+ if (hasMessage && useAiCommit) {
30
+ throw new IcodeError('不能同时使用 --message 和 --ai-commit。请明确选择一种提交信息来源。', {
31
+ code: 'SAVE_MESSAGE_SOURCE_CONFLICT',
32
+ exitCode: 2
33
+ })
34
+ }
35
+
36
+ if (!hasMessage && !useAiCommit) {
37
+ throw new IcodeError('缺少提交信息。请使用 --message <msg>,或显式使用 --ai-commit。', {
38
+ code: 'SAVE_MESSAGE_REQUIRED',
39
+ exitCode: 2
40
+ })
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 校验当前仓库是否允许执行 save。
46
+ * 输入为当前分支、未完成操作和改动摘要,输出为空;不满足条件时抛出明确错误。
47
+ */
48
+ function validateSavePreconditions(currentBranch, operation, changes) {
49
+ assertOnBranch(currentBranch, 'save', 'SAVE_DETACHED_HEAD')
50
+ assertNoPendingOperation(operation, 'save', 'SAVE_GIT_OPERATION_IN_PROGRESS')
51
+ assertNoConflictedFiles(changes, 'save', 'SAVE_CONFLICTED_FILES')
52
+
53
+ if (changes.clean) {
54
+ throw new IcodeError('没有可提交的改动,save 已停止。', {
55
+ code: 'SAVE_NO_CHANGES',
56
+ exitCode: 2
57
+ })
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 构建 dry-run 结果。
63
+ * 输入为上下文、分支、改动和计划提交信息,输出不会产生副作用的预览结构。
64
+ */
65
+ function buildDryRunResult(context, currentBranch, changes, commitMessage, source) {
66
+ return {
67
+ applied: false,
68
+ dryRun: true,
69
+ commitId: '',
70
+ commitMessage,
71
+ source,
72
+ context: {
73
+ topLevelPath: context.topLevelPath,
74
+ currentBranch
75
+ },
76
+ changes
77
+ }
78
+ }
79
+
80
+ /**
81
+ * 执行安全快速提交。
82
+ * 输入为 save 参数,输出提交结果;关键假设:dry-run 不执行 add/commit,AI 模式必须使用有效 profile。
83
+ */
84
+ export async function runSaveWorkflow(options = {}) {
85
+ validateMessageSource(options)
86
+
87
+ const context = await resolveGitContext({
88
+ cwd: options.cwd,
89
+ repoMode: options.repoMode
90
+ })
91
+ const git = new GitService(context)
92
+ const { currentBranch, operation, changes } = await readGitSafetyState(git)
93
+
94
+ validateSavePreconditions(currentBranch, operation, changes)
95
+
96
+ if (options.aiCommit === true) {
97
+ // AI 模式必须先校验 profile 存在,避免 askAiJson 在缺配置时隐式选择其它配置。
98
+ getAiProfile(options.profile)
99
+ const aiResult = await runAiCommitWorkflow({
100
+ cwd: context.topLevelPath,
101
+ repoMode: 'strict',
102
+ profile: options.profile,
103
+ lang: options.lang || 'zh',
104
+ apply: options.dryRun !== true,
105
+ yes: options.yes === true,
106
+ noVerify: options.noVerify === true,
107
+ silentContextLog: true,
108
+ silentResultLog: true
109
+ })
110
+
111
+ if (options.dryRun === true) {
112
+ return buildDryRunResult(context, currentBranch, changes, aiResult.commitMessage, 'ai')
113
+ }
114
+
115
+ return {
116
+ applied: aiResult.applied,
117
+ dryRun: false,
118
+ commitId: aiResult.commitId || '',
119
+ commitMessage: aiResult.commitMessage,
120
+ source: 'ai',
121
+ context: {
122
+ topLevelPath: context.topLevelPath,
123
+ currentBranch
124
+ },
125
+ changes
126
+ }
127
+ }
128
+
129
+ const commitMessage = normalizeMessage(options.message)
130
+ if (options.dryRun === true) {
131
+ return buildDryRunResult(context, currentBranch, changes, commitMessage, 'message')
132
+ }
133
+
134
+ await git.stageAll()
135
+ await git.commit(commitMessage, {
136
+ noVerify: options.noVerify === true
137
+ })
138
+ const commitId = await git.revParseShort('HEAD')
139
+
140
+ return {
141
+ applied: true,
142
+ dryRun: false,
143
+ commitId,
144
+ commitMessage,
145
+ source: 'message',
146
+ context: {
147
+ topLevelPath: context.topLevelPath,
148
+ currentBranch
149
+ },
150
+ changes
151
+ }
152
+ }
@@ -1,5 +1,12 @@
1
1
  import { IcodeError } from '../core/errors.js'
2
2
  import { resolveGitContext } from '../core/git/context.js'
3
+ import {
4
+ assertCleanWorktree,
5
+ assertNoConflictedFiles,
6
+ assertNoPendingOperation,
7
+ assertOnBranch,
8
+ readGitSafetyState
9
+ } from '../core/git/preconditions.js'
3
10
  import { GitService } from '../core/git/service.js'
4
11
  import { confirm } from '../core/tools/interactive.js'
5
12
  import { logger } from '../core/tools/logger.js'
@@ -33,6 +40,10 @@ async function ensureBranchReady(git, branchName) {
33
40
  }
34
41
  }
35
42
 
43
+ /**
44
+ * 执行 sync 工作流。
45
+ * 输入为目标分支与同步选项,输出每个分支的同步摘要;实际切换分支前要求当前仓库处于干净状态。
46
+ */
36
47
  export async function runSyncWorkflow(options) {
37
48
  const context = await resolveGitContext({
38
49
  cwd: options.cwd,
@@ -40,7 +51,12 @@ export async function runSyncWorkflow(options) {
40
51
  })
41
52
 
42
53
  const git = new GitService(context)
43
- const currentBranch = (await git.getCurrentBranch()) || context.currentBranch
54
+ const safetyState = await readGitSafetyState(git)
55
+ const currentBranch = safetyState.currentBranch || context.currentBranch
56
+ assertOnBranch(currentBranch, 'sync', 'SYNC_DETACHED_HEAD')
57
+ assertNoPendingOperation(safetyState.operation, 'sync', 'SYNC_GIT_OPERATION_IN_PROGRESS')
58
+ assertNoConflictedFiles(safetyState.changes, 'sync', 'SYNC_CONFLICTED_FILES')
59
+ assertCleanWorktree(safetyState.changes, 'sync', 'SYNC_DIRTY_WORKTREE')
44
60
 
45
61
  let targetInput = options.branches?.length
46
62
  ? [...options.branches]
@@ -99,7 +115,7 @@ export async function runSyncWorkflow(options) {
99
115
  if (setup.remoteExists) {
100
116
  await git.pull(branchName, {
101
117
  noRebase: !options.rebase,
102
- allowUnrelatedHistories: true
118
+ rebase: options.rebase
103
119
  })
104
120
  }
105
121
 
@@ -35,6 +35,14 @@ const UNDO_REF_SUGGESTIONS = {
35
35
  hard: ['HEAD~1', 'HEAD~2', 'HEAD~3', 'HEAD~5', 'HEAD']
36
36
  }
37
37
 
38
+ // 静默模式下输出 warn 日志,输入执行选项和日志文本,输出为空。
39
+ function logWarn(options, message) {
40
+ if (!options.silentLog) {
41
+ logger.warn(message)
42
+ }
43
+ }
44
+
45
+ // 解析交互菜单选择,输入菜单 value,输出 mode/ref 或 canceled。
38
46
  function parseSelection(selection) {
39
47
  if (!selection || selection === 'cancel') {
40
48
  return {
@@ -49,10 +57,12 @@ function parseSelection(selection) {
49
57
  }
50
58
  }
51
59
 
60
+ // 解析指定模式的默认 ref,输入 mode,输出默认回滚目标。
52
61
  function resolveDefaultRef(mode) {
53
62
  return mode === 'revert' ? 'HEAD' : 'HEAD~1'
54
63
  }
55
64
 
65
+ // 构建 undo 的 ref 选择项,输入 workflow options 和 mode,输出交互候选项。
56
66
  async function buildUndoRefChoices(options, mode) {
57
67
  const fallbackRef = resolveDefaultRef(mode)
58
68
  const suggestedRefs = UNDO_REF_SUGGESTIONS[mode] || UNDO_REF_SUGGESTIONS.soft
@@ -98,6 +108,7 @@ async function buildUndoRefChoices(options, mode) {
98
108
  }
99
109
  }
100
110
 
111
+ // 校验并归一化 recover 参数,输入用户参数,输出 continue/abort/keep 或空字符串。
101
112
  function normalizeRecover(value) {
102
113
  const normalized = (value || '').trim().toLowerCase()
103
114
  if (!normalized) {
@@ -115,6 +126,7 @@ function normalizeRecover(value) {
115
126
  return normalized
116
127
  }
117
128
 
129
+ // 处理未完成的 merge/revert/cherry-pick 操作,输入 workflow options,输出恢复结果或 null。
118
130
  async function resolvePendingOperation(options) {
119
131
  const context = await resolveGitContext({
120
132
  cwd: options.cwd,
@@ -127,7 +139,7 @@ async function resolvePendingOperation(options) {
127
139
  return null
128
140
  }
129
141
 
130
- logger.warn(`检测到未完成的 ${pending} 操作。`)
142
+ logWarn(options, `检测到未完成的 ${pending} 操作。`)
131
143
 
132
144
  let action = normalizeRecover(options.recover)
133
145
  if (!action) {
@@ -153,7 +165,7 @@ async function resolvePendingOperation(options) {
153
165
  }
154
166
 
155
167
  if (action === 'keep') {
156
- logger.warn('保持现场,未执行继续/中止。')
168
+ logWarn(options, '保持现场,未执行继续/中止。')
157
169
  return {
158
170
  canceled: true,
159
171
  pendingOperation: pending
@@ -164,6 +176,8 @@ async function resolvePendingOperation(options) {
164
176
  try {
165
177
  if (pending === 'revert') {
166
178
  await git.revertContinue()
179
+ } else if (pending === 'merge') {
180
+ await git.mergeContinue()
167
181
  } else {
168
182
  await git.cherryPickContinue()
169
183
  }
@@ -186,6 +200,8 @@ async function resolvePendingOperation(options) {
186
200
 
187
201
  if (pending === 'revert') {
188
202
  await git.revertAbort()
203
+ } else if (pending === 'merge') {
204
+ await git.mergeAbort()
189
205
  } else {
190
206
  await git.abortCherryPick()
191
207
  }
@@ -196,6 +212,7 @@ async function resolvePendingOperation(options) {
196
212
  }
197
213
  }
198
214
 
215
+ // 处理 revert 冲突后的恢复策略,输入 options/mode/ref,输出恢复结果。
199
216
  async function handleRevertConflict(options, mode, ref) {
200
217
  let action = normalizeRecover(options.recover)
201
218
 
@@ -265,7 +282,7 @@ export async function runUndoWorkflow(options) {
265
282
  const selected = await chooseOne('请选择回滚策略:', UNDO_OPTIONS, 0)
266
283
  const parsed = parseSelection(selected)
267
284
  if (parsed.canceled) {
268
- logger.warn('已取消 undo。')
285
+ logWarn(options, '已取消 undo。')
269
286
  return {
270
287
  canceled: true
271
288
  }
@@ -285,7 +302,7 @@ export async function runUndoWorkflow(options) {
285
302
  const refChoice = await buildUndoRefChoices(options, mode)
286
303
  const selectedRef = await chooseOne('请选择要回滚的 ref:', refChoice.choices, refChoice.defaultIndex)
287
304
  if (selectedRef === 'cancel') {
288
- logger.warn('已取消 undo。')
305
+ logWarn(options, '已取消 undo。')
289
306
  return {
290
307
  canceled: true,
291
308
  mode,
@@ -307,7 +324,7 @@ export async function runUndoWorkflow(options) {
307
324
  mode !== 'hard' ? 0 : 1
308
325
  )) === 'yes'
309
326
  if (!accepted) {
310
- logger.warn('已取消 undo。')
327
+ logWarn(options, '已取消 undo。')
311
328
  return {
312
329
  canceled: true,
313
330
  mode,
@@ -322,14 +339,15 @@ export async function runUndoWorkflow(options) {
322
339
  mode,
323
340
  yes: options.yes,
324
341
  repoMode: options.repoMode,
325
- cwd: options.cwd
342
+ cwd: options.cwd,
343
+ silentLog: options.silentLog
326
344
  })
327
345
  } catch (error) {
328
346
  if (error?.code !== 'REVERT_CONFLICT') {
329
347
  throw error
330
348
  }
331
349
 
332
- logger.warn(error.message)
350
+ logWarn(options, error.message)
333
351
  return handleRevertConflict(options, mode, ref)
334
352
  }
335
353
  }