@icode-js/icode 3.0.4 → 3.1.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -3
  3. package/bin/icode.js +4 -4
  4. package/package.json +1 -1
  5. package/src/cli.js +54 -23
  6. package/src/commands/ai.js +4 -4
  7. package/src/commands/checkout.js +2 -2
  8. package/src/commands/clean.js +1 -1
  9. package/src/commands/codereview.js +1 -1
  10. package/src/commands/completion.js +90 -0
  11. package/src/commands/config.js +117 -7
  12. package/src/commands/explain.js +2 -2
  13. package/src/commands/help.js +7 -0
  14. package/src/commands/info.js +1 -1
  15. package/src/commands/migrate.js +2 -2
  16. package/src/commands/push.js +29 -8
  17. package/src/commands/sync.js +2 -2
  18. package/src/commands/tag.js +2 -2
  19. package/src/commands/undo.js +60 -9
  20. package/src/core/{ai-client.js → ai/client.js} +3 -3
  21. package/src/core/{ai-config.js → ai/config.js} +2 -2
  22. package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
  23. package/src/core/cli/completion.js +687 -0
  24. package/src/core/cli/update-notifier.js +138 -0
  25. package/src/core/{shell.js → commands/shell.js} +13 -1
  26. package/src/core/{config-store.js → config/store.js} +55 -3
  27. package/src/core/{git-context.js → git/context.js} +25 -10
  28. package/src/core/{git-service.js → git/service.js} +44 -4
  29. package/src/core/remote-merge/client.js +42 -0
  30. package/src/core/remote-merge/providers/gitlab.js +537 -0
  31. package/src/core/remote-merge/shared.js +49 -0
  32. package/src/core/{prompts.js → tools/interactive.js} +46 -5
  33. package/src/workflows/ai-codereview-workflow.js +4 -4
  34. package/src/workflows/ai-commit-workflow.js +6 -6
  35. package/src/workflows/ai-conflict-workflow.js +4 -4
  36. package/src/workflows/ai-explain-workflow.js +4 -4
  37. package/src/workflows/ai-risk-review-workflow.js +1 -1
  38. package/src/workflows/checkout-workflow.js +3 -3
  39. package/src/workflows/clean-workflow.js +5 -5
  40. package/src/workflows/info-workflow.js +4 -4
  41. package/src/workflows/migrate-workflow.js +4 -4
  42. package/src/workflows/push-workflow.js +259 -24
  43. package/src/workflows/rollback-workflow.js +4 -4
  44. package/src/workflows/sync-workflow.js +4 -4
  45. package/src/workflows/tag-workflow.js +4 -4
  46. package/src/workflows/undo-workflow.js +12 -5
  47. /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
  48. /package/src/core/{args.js → cli/args.js} +0 -0
  49. /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
  50. /package/src/core/{hook-diagnostics.js → git/hook-diagnostics.js} +0 -0
  51. /package/src/core/{loading.js → tools/loading.js} +0 -0
  52. /package/src/core/{logger.js → tools/logger.js} +0 -0
@@ -1,10 +1,10 @@
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 { askAiJson } from '../core/ai-client.js'
6
- import { scanCommitConventions } from '../core/commit-conventions.js'
7
- import { confirm } from '../core/prompts.js'
2
+ import { askAiJson } from '../core/ai/client.js'
3
+ import { resolveGitContext } from '../core/git/context.js'
4
+ import { scanCommitConventions } from '../core/git/commit-conventions.js'
5
+ import { GitService } from '../core/git/service.js'
6
+ import { confirm } from '../core/tools/interactive.js'
7
+ import { logger } from '../core/tools/logger.js'
8
8
 
9
9
  function normalizeCommitType(value) {
10
10
  const allowed = new Set(['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'build', 'ci', 'revert'])
@@ -1,10 +1,10 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { askAi } from '../core/ai-client.js'
3
+ import { askAi } from '../core/ai/client.js'
4
4
  import { IcodeError } from '../core/errors.js'
5
- import { resolveGitContext } from '../core/git-context.js'
6
- import { GitService } from '../core/git-service.js'
7
- import { logger } from '../core/logger.js'
5
+ import { resolveGitContext } from '../core/git/context.js'
6
+ import { GitService } from '../core/git/service.js'
7
+ import { logger } from '../core/tools/logger.js'
8
8
 
9
9
  function extractConflictBlocks(content, maxBlocks = 3, maxLines = 80) {
10
10
  const lines = content.split('\n')
@@ -1,8 +1,8 @@
1
- import { askAi } from '../core/ai-client.js'
2
- import { resolveAiDiffRange } from '../core/ai-diff-range.js'
1
+ import { askAi } from '../core/ai/client.js'
2
+ import { resolveAiDiffRange } from '../core/ai/diff-range.js'
3
3
  import { IcodeError } from '../core/errors.js'
4
- import { resolveGitContext } from '../core/git-context.js'
5
- import { GitService } from '../core/git-service.js'
4
+ import { resolveGitContext } from '../core/git/context.js'
5
+ import { GitService } from '../core/git/service.js'
6
6
 
7
7
  function truncate(value, limit) {
8
8
  const text = value || ''
@@ -1,4 +1,4 @@
1
- import { askAiJson } from '../core/ai-client.js'
1
+ import { askAiJson } from '../core/ai/client.js'
2
2
 
3
3
  function normalizeDecision(value) {
4
4
  const normalized = (value || '').trim().toLowerCase()
@@ -1,7 +1,7 @@
1
1
  import { IcodeError } from '../core/errors.js'
2
- import { GitService } from '../core/git-service.js'
3
- import { resolveGitContext } from '../core/git-context.js'
4
- import { logger } from '../core/logger.js'
2
+ import { resolveGitContext } from '../core/git/context.js'
3
+ import { GitService } from '../core/git/service.js'
4
+ import { logger } from '../core/tools/logger.js'
5
5
 
6
6
  export async function runCheckoutWorkflow(input) {
7
7
  if (!input.branchName) {
@@ -1,9 +1,9 @@
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 { GitService } from '../core/git/service.js'
5
+ import { confirm } from '../core/tools/interactive.js'
6
+ import { logger } from '../core/tools/logger.js'
7
7
 
8
8
  function normalizeKeepList(values = []) {
9
9
  return new Set(
@@ -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({
@@ -1,8 +1,8 @@
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
 
@@ -1,16 +1,64 @@
1
- import { getRepoPolicy } from '../core/config-store.js'
1
+ import { formatAiCommitSummary } from '../core/ai/commit-summary.js'
2
+ import { getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
2
3
  import { IcodeError } from '../core/errors.js'
3
- import { GitService } from '../core/git-service.js'
4
- import { resolveGitContext } from '../core/git-context.js'
5
- import { formatAiCommitSummary } from '../core/ai-commit-summary.js'
6
- import { logger } from '../core/logger.js'
7
- import { confirm, input } from '../core/prompts.js'
4
+ import { resolveGitContext } from '../core/git/context.js'
5
+ import { GitService } from '../core/git/service.js'
6
+ import { confirm, input } from '../core/tools/interactive.js'
7
+ import { requestRemoteMerge, resolveRemoteMergeProjectUrl } from '../core/remote-merge/client.js'
8
+ import { logger } from '../core/tools/logger.js'
8
9
  import { runAiCommitWorkflow } from './ai-commit-workflow.js'
9
10
 
10
11
  function uniqueBranches(branches) {
11
12
  return [...new Set(branches.map((item) => item.trim()).filter(Boolean))]
12
13
  }
13
14
 
15
+ // 基于 commit 内容构建远程 MR 标题与正文,确保标题和内容都来自真实提交信息。
16
+ async function buildRemoteMergeCommitContent(git, sourceBranch, targetBranch) {
17
+ const rangeSpec = `origin/${targetBranch}..${sourceBranch}`
18
+ let commits = await git.listCommitMessages(rangeSpec)
19
+
20
+ if (!commits.length) {
21
+ const latestCommit = await git.getCommitMessage(sourceBranch)
22
+ if (latestCommit) {
23
+ commits = [latestCommit]
24
+ }
25
+ }
26
+
27
+ if (!commits.length) {
28
+ throw new IcodeError(`无法读取远程合并提交内容: ${sourceBranch} -> ${targetBranch}`, {
29
+ code: 'PUSH_REMOTE_MERGE_COMMIT_CONTENT_EMPTY',
30
+ exitCode: 2
31
+ })
32
+ }
33
+
34
+ const latestCommit = commits[commits.length - 1]
35
+ const description = commits
36
+ .map((commit) => {
37
+ if (commit.body) {
38
+ return `${commit.subject}\n\n${commit.body}`
39
+ }
40
+ return commit.subject
41
+ })
42
+ .join('\n\n')
43
+
44
+ return {
45
+ title: latestCommit.subject,
46
+ description
47
+ }
48
+ }
49
+
50
+ // 提取远程合并失败原因,优先使用 IcodeError.meta,其次回退到 error.message。
51
+ function resolveRemoteMergeFailureReason(error) {
52
+ const metaStep = typeof error?.meta?.step === 'string' ? error.meta.step.trim() : ''
53
+ const metaReason = typeof error?.meta?.reason === 'string' ? error.meta.reason.trim() : ''
54
+ if (metaReason) {
55
+ return metaStep ? `${metaReason} [step=${metaStep}]` : metaReason
56
+ }
57
+
58
+ const message = typeof error?.message === 'string' ? error.message.trim() : ''
59
+ return message || '远程合并失败'
60
+ }
61
+
14
62
  // Run AI commit before push and keep the full generated message in follow-up logs.
15
63
  async function prepareAiCommitIfEnabled(inputOptions) {
16
64
  if (!inputOptions.aiCommit) {
@@ -122,6 +170,162 @@ async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
122
170
  }
123
171
  }
124
172
 
173
+ function normalizeRemoteMergePolicy(policy = {}) {
174
+ const remoteMerge = policy.remoteMerge && typeof policy.remoteMerge === 'object' ? policy.remoteMerge : {}
175
+ return {
176
+ enabled: remoteMerge.enabled !== false,
177
+ projectUrl: typeof remoteMerge.projectUrl === 'string' ? remoteMerge.projectUrl.trim() : ''
178
+ }
179
+ }
180
+
181
+ // 归一化平台级远程合并配置,避免命令层拿到带空白的原始值。
182
+ function normalizeRemoteMergePlatformConfig(config = {}) {
183
+ return {
184
+ provider: typeof config.provider === 'string' ? config.provider.trim() : '',
185
+ apiKey: typeof config.apiKey === 'string' ? config.apiKey.trim() : ''
186
+ }
187
+ }
188
+
189
+ function buildRemoteMergePauseError(targetBranch, reason, meta = {}) {
190
+ return new IcodeError(`远程合并已暂停(${targetBranch}): ${reason}`, {
191
+ code: 'PUSH_REMOTE_MERGE_PAUSED',
192
+ exitCode: 2,
193
+ meta: {
194
+ targetBranch,
195
+ reason,
196
+ ...meta
197
+ }
198
+ })
199
+ }
200
+
201
+ // 统一把 provider 返回的 step 注入错误文案,便于定位 create/approve/merge/inspect 具体失败点。
202
+ function buildRemoteMergeResultMessage(prefix, result) {
203
+ const reason = typeof result?.reason === 'string' && result.reason.trim()
204
+ ? result.reason.trim()
205
+ : '未知原因'
206
+ const step = typeof result?.step === 'string' && result.step.trim()
207
+ ? result.step.trim()
208
+ : ''
209
+
210
+ return step ? `${prefix}(${step}): ${reason}` : `${prefix}: ${reason}`
211
+ }
212
+
213
+ // 构建批量远程合并暂停错误,明确指出具体失败分支与原因。
214
+ function buildRemoteMergeBatchPauseError(failures, summary) {
215
+ const branchesLabel = failures.map((item) => item.branch).join(', ')
216
+ const detail = failures.map((item) => `${item.branch}: ${item.reason}`).join(' | ')
217
+
218
+ return new IcodeError(`远程合并已暂停(${branchesLabel}): ${detail}`, {
219
+ code: 'PUSH_REMOTE_MERGE_PAUSED',
220
+ exitCode: 2,
221
+ meta: {
222
+ failures,
223
+ summary
224
+ }
225
+ })
226
+ }
227
+
228
+ // 基于 origin 推导 GitLab 项目地址并发起固定 API 的 MR 创建请求。
229
+ async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExists, inputOptions, remoteMergePolicy, remoteMergePlatform }) {
230
+ if (!remoteExists) {
231
+ throw buildRemoteMergePauseError(targetBranch, '远程合并要求目标分支已存在于 origin。', {
232
+ remoteExists: false
233
+ })
234
+ }
235
+
236
+ if (!remoteMergePolicy.enabled) {
237
+ throw buildRemoteMergePauseError(targetBranch, '仓库未启用远程合并配置,请先配置 repositories.<repo>.remoteMerge.enabled=true。')
238
+ }
239
+
240
+ if (!remoteMergePlatform.apiKey) {
241
+ throw buildRemoteMergePauseError(targetBranch, '平台未配置远程合并密钥,请先执行 icode config platform remote-merge set --provider <name> --api-key <key>。')
242
+ }
243
+
244
+ const remoteOriginUrl = await git.getRemoteUrl('origin')
245
+ const projectUrl = resolveRemoteMergeProjectUrl({
246
+ provider: remoteMergePlatform.provider,
247
+ remoteUrl: remoteOriginUrl
248
+ })
249
+ if (!projectUrl) {
250
+ throw buildRemoteMergePauseError(targetBranch, '无法从 origin 自动识别 GitLab 项目 URL,请检查 origin 是否指向 GitLab 仓库。', {
251
+ origin: remoteOriginUrl
252
+ })
253
+ }
254
+ logger.info(`已根据 origin 自动识别 GitLab 项目 URL: ${projectUrl}`)
255
+
256
+ logger.info(`发起远程合并: ${currentBranch} -> ${targetBranch}`)
257
+ const mergeRequestContent = await buildRemoteMergeCommitContent(git, currentBranch, targetBranch)
258
+ const result = await requestRemoteMerge({
259
+ provider: remoteMergePlatform.provider,
260
+ apiKey: remoteMergePlatform.apiKey,
261
+ projectUrl,
262
+ sourceBranch: currentBranch,
263
+ targetBranch,
264
+ mergeRequestTitle: mergeRequestContent.title,
265
+ mergeRequestDescription: mergeRequestContent.description,
266
+ repoRoot: git.cwd,
267
+ noVerify: inputOptions.noVerify
268
+ })
269
+
270
+ if (result.conflict) {
271
+ throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并冲突', result), {
272
+ sourceBranch: currentBranch,
273
+ step: result.step || '',
274
+ result
275
+ })
276
+ }
277
+
278
+ if (!result.ok) {
279
+ throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并失败', result), {
280
+ sourceBranch: currentBranch,
281
+ step: result.step || '',
282
+ result
283
+ })
284
+ }
285
+
286
+ const successReason = typeof result.reason === 'string' && result.reason.trim()
287
+ ? result.reason.trim()
288
+ : '远程合并已完成'
289
+
290
+ if (result.mergeRequestUrl) {
291
+ logger.success(`${successReason}: ${result.mergeRequestUrl}`)
292
+ } else {
293
+ logger.success(`${successReason}: ${currentBranch} -> ${targetBranch}`)
294
+ }
295
+
296
+ return result
297
+ }
298
+
299
+ // 并发执行多个远程合并目标分支,并把冲突/失败精确归档到对应分支。
300
+ async function runConcurrentRemoteMerges(options) {
301
+ const tasks = options.targetBranches.map(async (targetBranch) => {
302
+ try {
303
+ logger.info(`处理分支: ${targetBranch}`)
304
+ const remoteExists = await options.git.branchExistsRemote(targetBranch)
305
+ const remoteMergeResult = await performRemoteMerge({
306
+ ...options,
307
+ targetBranch,
308
+ remoteExists
309
+ })
310
+
311
+ return {
312
+ branch: targetBranch,
313
+ status: 'remote-merged-and-pushed',
314
+ mergeRequestUrl: remoteMergeResult.mergeRequestUrl || '',
315
+ mergeRequestId: remoteMergeResult.mergeRequestId || ''
316
+ }
317
+ } catch (error) {
318
+ return {
319
+ branch: targetBranch,
320
+ status: 'paused',
321
+ reason: resolveRemoteMergeFailureReason(error)
322
+ }
323
+ }
324
+ })
325
+
326
+ return Promise.all(tasks)
327
+ }
328
+
125
329
  export async function runPushWorkflow(inputOptions) {
126
330
  const context = await resolveGitContext({
127
331
  cwd: inputOptions.cwd,
@@ -131,6 +335,8 @@ export async function runPushWorkflow(inputOptions) {
131
335
  const git = new GitService(context)
132
336
  const policy = getRepoPolicy(context.topLevelPath)
133
337
  const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
338
+ const remoteMergePolicy = normalizeRemoteMergePolicy(policy)
339
+ const remoteMergePlatform = normalizeRemoteMergePlatformConfig(getPlatformConfig('remoteMerge'))
134
340
 
135
341
  logger.info(`仓库根目录: ${context.topLevelPath}`)
136
342
  if (context.inheritedFromParent) {
@@ -203,6 +409,7 @@ export async function runPushWorkflow(inputOptions) {
203
409
 
204
410
  const summary = []
205
411
  const originalBranch = currentBranch
412
+ const remoteMergeTargets = []
206
413
 
207
414
  try {
208
415
  for (const targetBranch of branchTargets) {
@@ -212,9 +419,8 @@ export async function runPushWorkflow(inputOptions) {
212
419
  continue
213
420
  }
214
421
 
215
- logger.info(`处理分支: ${targetBranch}`)
216
-
217
422
  if (targetBranch === currentBranch) {
423
+ logger.info(`处理分支: ${targetBranch}`)
218
424
  const remoteExists = await git.branchExistsRemote(targetBranch)
219
425
  if (remoteExists) {
220
426
  logger.info(`同步远程分支: ${targetBranch}`)
@@ -235,26 +441,55 @@ export async function runPushWorkflow(inputOptions) {
235
441
  continue
236
442
  }
237
443
 
238
- logger.info(`切换到目标分支: ${targetBranch}`)
239
- const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch)
240
- logger.info(`目标分支准备完成: ${targetBranch} (${checkoutMode})`)
444
+ if (inputOptions.remoteMerge) {
445
+ remoteMergeTargets.push(targetBranch)
446
+ } else {
447
+ logger.info(`处理分支: ${targetBranch}`)
448
+ logger.info(`切换到目标分支: ${targetBranch}`)
449
+ const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch)
450
+ logger.info(`目标分支准备完成: ${targetBranch} (${checkoutMode})`)
451
+
452
+ // 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
453
+ logger.info(`合并分支: ${currentBranch} -> ${targetBranch}`)
454
+ await git.merge(currentBranch, {
455
+ noFf: true,
456
+ noEdit: true
457
+ })
458
+ logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
241
459
 
242
- // 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
243
- logger.info(`合并分支: ${currentBranch} -> ${targetBranch}`)
244
- await git.merge(currentBranch, {
245
- noFf: true,
246
- noEdit: true
247
- })
248
- logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
460
+ logger.info(`推送目标分支: ${targetBranch}`)
461
+ await git.push(targetBranch, {
462
+ setUpstream: !remoteExists,
463
+ noVerify: inputOptions.noVerify
464
+ })
249
465
 
250
- logger.info(`推送目标分支: ${targetBranch}`)
251
- await git.push(targetBranch, {
252
- setUpstream: !remoteExists,
253
- noVerify: inputOptions.noVerify
466
+ logger.success(`目标分支推送成功: ${targetBranch}`)
467
+ summary.push({ branch: targetBranch, status: 'merged-and-pushed' })
468
+ }
469
+ }
470
+
471
+ if (inputOptions.remoteMerge && remoteMergeTargets.length) {
472
+ logger.info(`并发处理远程合并分支: ${remoteMergeTargets.join(', ')}`)
473
+ const remoteMergeSummary = await runConcurrentRemoteMerges({
474
+ git,
475
+ currentBranch,
476
+ targetBranches: remoteMergeTargets,
477
+ inputOptions,
478
+ remoteMergePolicy,
479
+ remoteMergePlatform
254
480
  })
481
+ summary.push(...remoteMergeSummary)
255
482
 
256
- logger.success(`目标分支推送成功: ${targetBranch}`)
257
- summary.push({ branch: targetBranch, status: 'merged-and-pushed' })
483
+ const failures = remoteMergeSummary
484
+ .filter((item) => item.status === 'paused')
485
+ .map((item) => ({
486
+ branch: item.branch,
487
+ reason: item.reason || '远程合并失败'
488
+ }))
489
+
490
+ if (failures.length) {
491
+ throw buildRemoteMergeBatchPauseError(failures, summary)
492
+ }
258
493
  }
259
494
  } finally {
260
495
  const branchAfterWorkflow = await git.getCurrentBranch()
@@ -1,8 +1,8 @@
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 { confirm } from '../core/prompts.js'
2
+ import { resolveGitContext } from '../core/git/context.js'
3
+ import { GitService } from '../core/git/service.js'
4
+ import { confirm } from '../core/tools/interactive.js'
5
+ import { logger } from '../core/tools/logger.js'
6
6
 
7
7
  function isRevertConflict(error) {
8
8
  const output = `${error?.meta?.stdout || ''}\n${error?.meta?.stderr || ''}\n${error?.message || ''}`
@@ -1,8 +1,8 @@
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 { confirm } from '../core/prompts.js'
2
+ import { resolveGitContext } from '../core/git/context.js'
3
+ import { GitService } from '../core/git/service.js'
4
+ import { confirm } from '../core/tools/interactive.js'
5
+ import { logger } from '../core/tools/logger.js'
6
6
 
7
7
  function unique(list) {
8
8
  return [...new Set(list.map((item) => item.trim()).filter(Boolean))]
@@ -1,8 +1,8 @@
1
1
  import { IcodeError } from '../core/errors.js'
2
- import { GitService } from '../core/git-service.js'
3
- import { resolveGitContext } from '../core/git-context.js'
4
- import { logger } from '../core/logger.js'
5
- import { input } from '../core/prompts.js'
2
+ import { resolveGitContext } from '../core/git/context.js'
3
+ import { GitService } from '../core/git/service.js'
4
+ import { input } from '../core/tools/interactive.js'
5
+ import { logger } from '../core/tools/logger.js'
6
6
 
7
7
  function todayStamp() {
8
8
  const now = new Date()
@@ -1,8 +1,8 @@
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 { chooseOne } from '../core/prompts.js'
2
+ import { resolveGitContext } from '../core/git/context.js'
3
+ import { GitService } from '../core/git/service.js'
4
+ import { chooseOne } from '../core/tools/interactive.js'
5
+ import { logger } from '../core/tools/logger.js'
6
6
  import { runRollbackWorkflow } from './rollback-workflow.js'
7
7
 
8
8
  const UNDO_OPTIONS = [
@@ -247,6 +247,10 @@ async function handleRevertConflict(options, mode, ref) {
247
247
  }
248
248
  }
249
249
 
250
+ /**
251
+ * 执行 undo 工作流。
252
+ * 输入为已归一化的回滚模式、目标 ref/hash、恢复策略和仓库选项,输出为执行结果或取消状态。
253
+ */
250
254
  export async function runUndoWorkflow(options) {
251
255
  const pendingResult = await resolvePendingOperation(options)
252
256
  if (pendingResult) {
@@ -255,6 +259,7 @@ export async function runUndoWorkflow(options) {
255
259
 
256
260
  let mode = options.mode?.trim()
257
261
  let ref = options.ref?.trim()
262
+ const hasExplicitRef = Boolean(ref)
258
263
 
259
264
  if (!mode) {
260
265
  const selected = await chooseOne('请选择回滚策略:', UNDO_OPTIONS, 0)
@@ -267,7 +272,9 @@ export async function runUndoWorkflow(options) {
267
272
  }
268
273
 
269
274
  mode = parsed.mode
270
- ref = parsed.ref
275
+ if (!hasExplicitRef) {
276
+ ref = parsed.ref
277
+ }
271
278
  }
272
279
 
273
280
  const defaultRef = resolveDefaultRef(mode)
File without changes
File without changes
File without changes