@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
@@ -2,6 +2,11 @@ import { formatAiCommitSummary } from '../core/ai/commit-summary.js'
2
2
  import { getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
3
3
  import { IcodeError } from '../core/errors.js'
4
4
  import { resolveGitContext } from '../core/git/context.js'
5
+ import {
6
+ assertNoConflictedFiles,
7
+ assertNoPendingOperation,
8
+ readGitSafetyState
9
+ } from '../core/git/preconditions.js'
5
10
  import { GitService } from '../core/git/service.js'
6
11
  import { confirm, input } from '../core/tools/interactive.js'
7
12
  import { requestRemoteMerge, resolveRemoteMergeProjectUrl } from '../core/remote-merge/client.js'
@@ -112,9 +117,9 @@ async function prepareAiCommitIfEnabled(inputOptions) {
112
117
  }
113
118
  }
114
119
 
115
- async function prepareCommitIfNeeded(git, options) {
116
- const hasChanges = await git.hasChanges()
117
- if (!hasChanges) {
120
+ // 在 push 场景下按需创建本地提交,输入 GitService、命令参数和已读取的改动摘要,输出是否提交。
121
+ async function prepareCommitIfNeeded(git, options, changes) {
122
+ if (changes.clean) {
118
123
  logger.info('工作区无改动,跳过 commit。')
119
124
  return false
120
125
  }
@@ -140,7 +145,52 @@ async function prepareCommitIfNeeded(git, options) {
140
145
  return true
141
146
  }
142
147
 
143
- async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
148
+ // 提取底层 Git 失败细节,输入异常对象,输出用于暂停错误的简洁原因。
149
+ function resolveGitFailureReason(error) {
150
+ const stderr = typeof error?.meta?.stderr === 'string' ? error.meta.stderr.trim() : ''
151
+ const stdout = typeof error?.meta?.stdout === 'string' ? error.meta.stdout.trim() : ''
152
+ const output = [stderr, stdout].filter(Boolean).join('\n').trim()
153
+ const message = typeof error?.message === 'string' ? error.message.trim() : ''
154
+ const reason = output || message || 'Git 操作失败'
155
+
156
+ return reason.length > 2000 ? `${reason.slice(0, 2000)}...` : reason
157
+ }
158
+
159
+ // 判断 Git 失败是否来自合并冲突,输入异常对象,输出布尔结果。
160
+ function isGitConflictFailure(error) {
161
+ const text = [
162
+ error?.meta?.stderr || '',
163
+ error?.meta?.stdout || '',
164
+ error?.message || ''
165
+ ].join('\n')
166
+
167
+ return /CONFLICT \(|Automatic merge failed|fix conflicts|unmerged files|needs merge|you need to resolve your current index first/i.test(text)
168
+ }
169
+
170
+ // 构建本地合并暂停错误,输入分支、阶段和原始异常,输出可被命令层汇总的 IcodeError。
171
+ function buildLocalMergePauseError({ sourceBranch, targetBranch, step, error, summary = [] }) {
172
+ const reason = resolveGitFailureReason(error)
173
+ const conflict = isGitConflictFailure(error)
174
+ const prefix = conflict ? '本地合并冲突' : '本地合并失败'
175
+ const nextStep = conflict
176
+ ? '请解决冲突后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 中止本次合并。'
177
+ : '请根据上面的 Git 错误修复后重试。'
178
+
179
+ return new IcodeError(`${prefix}(${step}): ${sourceBranch} -> ${targetBranch}\n${reason}\n${nextStep}`, {
180
+ code: 'PUSH_LOCAL_MERGE_PAUSED',
181
+ exitCode: 2,
182
+ cause: error,
183
+ meta: {
184
+ sourceBranch,
185
+ targetBranch,
186
+ step,
187
+ reason,
188
+ summary
189
+ }
190
+ })
191
+ }
192
+
193
+ async function checkoutTargetBranch(git, targetBranch, sourceBranch, summary = []) {
144
194
  const localExists = await git.branchExistsLocal(targetBranch)
145
195
  const remoteExists = await git.branchExistsRemote(targetBranch)
146
196
  let checkoutMode = 'local'
@@ -158,10 +208,19 @@ async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
158
208
  }
159
209
 
160
210
  if (remoteExists) {
161
- await git.pull(targetBranch, {
162
- allowUnrelatedHistories: true,
163
- noRebase: true
164
- })
211
+ try {
212
+ await git.pull(targetBranch, {
213
+ noRebase: true
214
+ })
215
+ } catch (error) {
216
+ throw buildLocalMergePauseError({
217
+ sourceBranch,
218
+ targetBranch,
219
+ step: 'pull-target',
220
+ error,
221
+ summary
222
+ })
223
+ }
165
224
  }
166
225
 
167
226
  return {
@@ -173,8 +232,7 @@ async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
173
232
  function normalizeRemoteMergePolicy(policy = {}) {
174
233
  const remoteMerge = policy.remoteMerge && typeof policy.remoteMerge === 'object' ? policy.remoteMerge : {}
175
234
  return {
176
- enabled: remoteMerge.enabled !== false,
177
- projectUrl: typeof remoteMerge.projectUrl === 'string' ? remoteMerge.projectUrl.trim() : ''
235
+ enabled: remoteMerge.enabled !== false
178
236
  }
179
237
  }
180
238
 
@@ -198,7 +256,7 @@ function buildRemoteMergePauseError(targetBranch, reason, meta = {}) {
198
256
  })
199
257
  }
200
258
 
201
- // 统一把 provider 返回的 step 注入错误文案,便于定位 create/approve/merge/inspect 具体失败点。
259
+ // 统一把 provider 返回的 step 注入错误文案,便于定位 create/merge/inspect 具体失败点。
202
260
  function buildRemoteMergeResultMessage(prefix, result) {
203
261
  const reason = typeof result?.reason === 'string' && result.reason.trim()
204
262
  ? result.reason.trim()
@@ -225,6 +283,77 @@ function buildRemoteMergeBatchPauseError(failures, summary) {
225
283
  })
226
284
  }
227
285
 
286
+ // 构建 push dry-run 步骤,输入分支目标和策略,输出不会产生副作用的计划列表。
287
+ function buildPushDryRunPlan({ currentBranch, branchTargets, protectedBranches, inputOptions }) {
288
+ const steps = []
289
+
290
+ if (inputOptions.aiCommit) {
291
+ steps.push({
292
+ action: 'ai-commit',
293
+ branch: currentBranch,
294
+ reason: '将生成并提交 AI commit'
295
+ })
296
+ } else {
297
+ steps.push({
298
+ action: 'commit-if-needed',
299
+ branch: currentBranch,
300
+ reason: inputOptions.message ? '检测到改动时使用 -m/--message 提交' : '检测到改动时会要求输入提交信息'
301
+ })
302
+ }
303
+
304
+ if (inputOptions.pullMain) {
305
+ steps.push({
306
+ action: 'pull-main',
307
+ branch: currentBranch,
308
+ reason: '将主分支同步到当前分支'
309
+ })
310
+ }
311
+
312
+ branchTargets.forEach((targetBranch) => {
313
+ if (protectedBranches.has(targetBranch) && !inputOptions.forceProtected) {
314
+ steps.push({
315
+ action: 'skip-protected',
316
+ branch: targetBranch,
317
+ reason: '受保护分支,未传 --force-protected'
318
+ })
319
+ return
320
+ }
321
+
322
+ if (targetBranch === currentBranch) {
323
+ steps.push({
324
+ action: 'push-current',
325
+ branch: targetBranch,
326
+ reason: '将同步并推送当前分支'
327
+ })
328
+ return
329
+ }
330
+
331
+ steps.push({
332
+ action: inputOptions.remoteMerge ? 'remote-merge' : 'local-merge',
333
+ branch: targetBranch,
334
+ reason: inputOptions.remoteMerge ? '将创建并合并远程 MR' : '将切换目标分支、本地 merge 后推送'
335
+ })
336
+ })
337
+
338
+ return {
339
+ currentBranch,
340
+ branchTargets,
341
+ remoteMerge: Boolean(inputOptions.remoteMerge),
342
+ steps
343
+ }
344
+ }
345
+
346
+ // 根据 origin 推导远程合并项目 URL,输入平台配置与 remote,输出项目 URL。
347
+ function resolveRemoteMergeProjectUrlFromOrigin(remoteMergePlatform, remoteOriginUrl) {
348
+ return {
349
+ projectUrl: resolveRemoteMergeProjectUrl({
350
+ provider: remoteMergePlatform.provider,
351
+ remoteUrl: remoteOriginUrl
352
+ }),
353
+ source: 'origin'
354
+ }
355
+ }
356
+
228
357
  // 基于 origin 推导 GitLab 项目地址并发起固定 API 的 MR 创建请求。
229
358
  async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExists, inputOptions, remoteMergePolicy, remoteMergePlatform }) {
230
359
  if (!remoteExists) {
@@ -242,18 +371,15 @@ async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExis
242
371
  }
243
372
 
244
373
  const remoteOriginUrl = await git.getRemoteUrl('origin')
245
- const projectUrl = resolveRemoteMergeProjectUrl({
246
- provider: remoteMergePlatform.provider,
247
- remoteUrl: remoteOriginUrl
248
- })
374
+ const { projectUrl, source } = resolveRemoteMergeProjectUrlFromOrigin(remoteMergePlatform, remoteOriginUrl)
249
375
  if (!projectUrl) {
250
376
  throw buildRemoteMergePauseError(targetBranch, '无法从 origin 自动识别 GitLab 项目 URL,请检查 origin 是否指向 GitLab 仓库。', {
251
377
  origin: remoteOriginUrl
252
378
  })
253
379
  }
254
- logger.info(`已根据 origin 自动识别 GitLab 项目 URL: ${projectUrl}`)
380
+ logger.info(`GitLab 项目 URL(${source}): ${logger.color('green', projectUrl)}`)
255
381
 
256
- logger.info(`发起远程合并: ${currentBranch} -> ${targetBranch}`)
382
+ logger.info(`发起远程合并: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
257
383
  const mergeRequestContent = await buildRemoteMergeCommitContent(git, currentBranch, targetBranch)
258
384
  const result = await requestRemoteMerge({
259
385
  provider: remoteMergePlatform.provider,
@@ -288,9 +414,9 @@ async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExis
288
414
  : '远程合并已完成'
289
415
 
290
416
  if (result.mergeRequestUrl) {
291
- logger.success(`${successReason}: ${result.mergeRequestUrl}`)
417
+ logger.success(`${successReason}: ${logger.color('green', result.mergeRequestUrl)}`)
292
418
  } else {
293
- logger.success(`${successReason}: ${currentBranch} -> ${targetBranch}`)
419
+ logger.success(`${successReason}: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
294
420
  }
295
421
 
296
422
  return result
@@ -326,6 +452,10 @@ async function runConcurrentRemoteMerges(options) {
326
452
  return Promise.all(tasks)
327
453
  }
328
454
 
455
+ /**
456
+ * 执行 push 工作流。
457
+ * 输入为 push 命令选项,输出推送/合并摘要;关键假设是实际执行前必须没有未完成操作和冲突文件。
458
+ */
329
459
  export async function runPushWorkflow(inputOptions) {
330
460
  const context = await resolveGitContext({
331
461
  cwd: inputOptions.cwd,
@@ -351,6 +481,40 @@ export async function runPushWorkflow(inputOptions) {
351
481
  })
352
482
  }
353
483
 
484
+ let branchTargets = uniqueBranches([
485
+ ...(inputOptions.notPushCurrent ? [] : [currentBranch]),
486
+ ...(inputOptions.targetBranches || [])
487
+ ])
488
+ if (inputOptions.notPushCurrent) {
489
+ branchTargets = branchTargets.filter((branchName) => branchName !== currentBranch)
490
+ }
491
+
492
+ if (!branchTargets.length) {
493
+ throw new IcodeError('没有可执行的目标分支。', {
494
+ code: 'PUSH_EMPTY_TARGETS',
495
+ exitCode: 2
496
+ })
497
+ }
498
+
499
+ if (inputOptions.dryRun) {
500
+ return {
501
+ dryRun: true,
502
+ repoRoot: context.topLevelPath,
503
+ currentBranch,
504
+ inheritedFromParent: context.inheritedFromParent,
505
+ plan: buildPushDryRunPlan({
506
+ currentBranch,
507
+ branchTargets,
508
+ protectedBranches,
509
+ inputOptions
510
+ })
511
+ }
512
+ }
513
+
514
+ const safetyState = await readGitSafetyState(git)
515
+ assertNoPendingOperation(safetyState.operation, 'push', 'PUSH_GIT_OPERATION_IN_PROGRESS')
516
+ assertNoConflictedFiles(safetyState.changes, 'push', 'PUSH_CONFLICTED_FILES')
517
+
354
518
  const aiCommitResult = await prepareAiCommitIfEnabled(inputOptions)
355
519
  if (aiCommitResult.canceled) {
356
520
  return {
@@ -364,31 +528,27 @@ export async function runPushWorkflow(inputOptions) {
364
528
 
365
529
  const shouldRunManualCommit = !aiCommitResult.applied && aiCommitResult.reason !== 'no-diff'
366
530
  if (shouldRunManualCommit) {
367
- await prepareCommitIfNeeded(git, inputOptions)
531
+ await prepareCommitIfNeeded(git, inputOptions, safetyState.changes)
368
532
  }
369
533
  await git.fetch()
370
534
 
535
+ const summary = []
536
+
371
537
  if (inputOptions.pullMain && context.defaultBranch !== currentBranch) {
372
538
  logger.info(`先同步主分支 ${context.defaultBranch} 到当前分支 ${currentBranch}`)
373
- await git.pull(context.defaultBranch, {
374
- allowUnrelatedHistories: true,
375
- noRebase: true
376
- })
377
- }
378
-
379
- let branchTargets = uniqueBranches([
380
- ...(inputOptions.notPushCurrent ? [] : [currentBranch]),
381
- ...(inputOptions.targetBranches || [])
382
- ])
383
- if (inputOptions.notPushCurrent) {
384
- branchTargets = branchTargets.filter((branchName) => branchName !== currentBranch)
385
- }
386
-
387
- if (!branchTargets.length) {
388
- throw new IcodeError('没有可执行的目标分支。', {
389
- code: 'PUSH_EMPTY_TARGETS',
390
- exitCode: 2
391
- })
539
+ try {
540
+ await git.pull(context.defaultBranch, {
541
+ noRebase: true
542
+ })
543
+ } catch (error) {
544
+ throw buildLocalMergePauseError({
545
+ sourceBranch: context.defaultBranch,
546
+ targetBranch: currentBranch,
547
+ step: 'pull-main',
548
+ error,
549
+ summary
550
+ })
551
+ }
392
552
  }
393
553
 
394
554
  if (!inputOptions.yes) {
@@ -407,7 +567,6 @@ export async function runPushWorkflow(inputOptions) {
407
567
  }
408
568
  }
409
569
 
410
- const summary = []
411
570
  const originalBranch = currentBranch
412
571
  const remoteMergeTargets = []
413
572
 
@@ -424,10 +583,19 @@ export async function runPushWorkflow(inputOptions) {
424
583
  const remoteExists = await git.branchExistsRemote(targetBranch)
425
584
  if (remoteExists) {
426
585
  logger.info(`同步远程分支: ${targetBranch}`)
427
- await git.pull(targetBranch, {
428
- allowUnrelatedHistories: true,
429
- noRebase: true
430
- })
586
+ try {
587
+ await git.pull(targetBranch, {
588
+ noRebase: true
589
+ })
590
+ } catch (error) {
591
+ throw buildLocalMergePauseError({
592
+ sourceBranch: `origin/${targetBranch}`,
593
+ targetBranch,
594
+ step: 'pull-current',
595
+ error,
596
+ summary
597
+ })
598
+ }
431
599
  }
432
600
 
433
601
  logger.info(`推送当前分支: ${targetBranch}`)
@@ -446,15 +614,25 @@ export async function runPushWorkflow(inputOptions) {
446
614
  } else {
447
615
  logger.info(`处理分支: ${targetBranch}`)
448
616
  logger.info(`切换到目标分支: ${targetBranch}`)
449
- const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch)
617
+ const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch, summary)
450
618
  logger.info(`目标分支准备完成: ${targetBranch} (${checkoutMode})`)
451
619
 
452
620
  // 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
453
621
  logger.info(`合并分支: ${currentBranch} -> ${targetBranch}`)
454
- await git.merge(currentBranch, {
455
- noFf: true,
456
- noEdit: true
457
- })
622
+ try {
623
+ await git.merge(currentBranch, {
624
+ noFf: true,
625
+ noEdit: true
626
+ })
627
+ } catch (error) {
628
+ throw buildLocalMergePauseError({
629
+ sourceBranch: currentBranch,
630
+ targetBranch,
631
+ step: 'merge-target',
632
+ error,
633
+ summary
634
+ })
635
+ }
458
636
  logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
459
637
 
460
638
  logger.info(`推送目标分支: ${targetBranch}`)
@@ -493,7 +671,11 @@ export async function runPushWorkflow(inputOptions) {
493
671
  }
494
672
  } finally {
495
673
  const branchAfterWorkflow = await git.getCurrentBranch()
496
- if (branchAfterWorkflow && branchAfterWorkflow !== originalBranch) {
674
+ const operationAfterWorkflow = await git.getInProgressOperation()
675
+ if (operationAfterWorkflow === 'merge') {
676
+ logger.warn(`检测到未完成的 merge,已保留在当前分支 ${branchAfterWorkflow || '(unknown)'}。`)
677
+ logger.warn('解决冲突后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 中止。')
678
+ } else if (branchAfterWorkflow && branchAfterWorkflow !== originalBranch) {
497
679
  try {
498
680
  await git.checkout(originalBranch)
499
681
  } catch (error) {
@@ -0,0 +1,278 @@
1
+ import { getAiConfig } from '../core/ai/config.js'
2
+ import { getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
3
+ import { resolveGitContext } from '../core/git/context.js'
4
+ import { GitService } from '../core/git/service.js'
5
+
6
+ /**
7
+ * 构建 ready 检查项。
8
+ * 输入为检查字段,输出稳定的检查项对象;关键假设是 status 只能是 pass/warn/fail。
9
+ */
10
+ function buildCheck(id, status, title, detail, nextStep = '') {
11
+ return {
12
+ id,
13
+ status,
14
+ title,
15
+ detail,
16
+ nextStep
17
+ }
18
+ }
19
+
20
+ /**
21
+ * 汇总 ready 检查状态。
22
+ * 输入为检查项数组,输出整体 ok/status 和计数。
23
+ */
24
+ function summarizeReadyChecks(checks) {
25
+ const summary = {
26
+ ok: true,
27
+ status: 'pass',
28
+ pass: 0,
29
+ warn: 0,
30
+ fail: 0
31
+ }
32
+
33
+ for (const check of checks) {
34
+ if (check.status === 'fail') {
35
+ summary.ok = false
36
+ summary.status = 'fail'
37
+ summary.fail += 1
38
+ continue
39
+ }
40
+
41
+ if (check.status === 'warn') {
42
+ summary.warn += 1
43
+ if (summary.status !== 'fail') {
44
+ summary.status = 'warn'
45
+ }
46
+ continue
47
+ }
48
+
49
+ summary.pass += 1
50
+ }
51
+
52
+ return summary
53
+ }
54
+
55
+ /**
56
+ * 从检查项提取下一步建议。
57
+ * 输入为检查项数组,输出去重后的建议文本数组。
58
+ */
59
+ function collectNextSteps(checks) {
60
+ const nextSteps = []
61
+ const seen = new Set()
62
+
63
+ for (const check of checks) {
64
+ const nextStep = String(check.nextStep || '').trim()
65
+ if (!nextStep || seen.has(nextStep)) {
66
+ continue
67
+ }
68
+ seen.add(nextStep)
69
+ nextSteps.push(nextStep)
70
+ }
71
+
72
+ return nextSteps
73
+ }
74
+
75
+ /**
76
+ * 检查当前分支是否存在。
77
+ * 输入为分支名,输出 ready 检查项。
78
+ */
79
+ function checkCurrentBranch(currentBranch) {
80
+ if (!currentBranch) {
81
+ return buildCheck(
82
+ 'git.branch',
83
+ 'fail',
84
+ '当前不在普通分支上',
85
+ '检测到 detached HEAD 或无法读取当前分支。',
86
+ '请先切换到明确分支,例如: icode checkout <branch>'
87
+ )
88
+ }
89
+
90
+ return buildCheck('git.branch', 'pass', '当前分支明确', currentBranch)
91
+ }
92
+
93
+ /**
94
+ * 检查是否存在未完成的 Git 操作。
95
+ * 输入为 operation 字符串,输出 ready 检查项。
96
+ */
97
+ function checkInProgressOperation(operation) {
98
+ if (!operation) {
99
+ return buildCheck('git.operation', 'pass', '没有未完成的 Git 操作', '工作区未处于 merge/rebase/cherry-pick/revert 流程中。')
100
+ }
101
+
102
+ return buildCheck(
103
+ 'git.operation',
104
+ 'fail',
105
+ '存在未完成的 Git 操作',
106
+ `当前操作: ${operation}`,
107
+ `请先完成或中止 ${operation},再继续执行 save/push。`
108
+ )
109
+ }
110
+
111
+ /**
112
+ * 检查工作区改动情况。
113
+ * 输入为结构化改动摘要,输出 ready 检查项。
114
+ */
115
+ function checkChanges(changes) {
116
+ if (changes.conflicted > 0) {
117
+ return buildCheck(
118
+ 'git.changes',
119
+ 'fail',
120
+ '存在未解决冲突',
121
+ `冲突文件 ${changes.conflicted} 个,全部改动 ${changes.total} 个。`,
122
+ '请先解决冲突;如需要建议可执行: icode ai conflict'
123
+ )
124
+ }
125
+
126
+ if (!changes.clean) {
127
+ return buildCheck(
128
+ 'git.changes',
129
+ 'warn',
130
+ '存在未提交改动',
131
+ `staged=${changes.staged}, unstaged=${changes.unstaged}, untracked=${changes.untracked}`,
132
+ '如需安全快速提交,可执行: icode save -m "type: subject"'
133
+ )
134
+ }
135
+
136
+ return buildCheck('git.changes', 'pass', '工作区干净', '没有暂存、未暂存或未跟踪改动。')
137
+ }
138
+
139
+ /**
140
+ * 检查 upstream 关系。
141
+ * 输入为 upstream 状态,输出 ready 检查项。
142
+ */
143
+ function checkUpstream(upstreamStatus) {
144
+ if (!upstreamStatus.configured) {
145
+ return buildCheck(
146
+ 'git.upstream',
147
+ 'warn',
148
+ '当前分支未设置 upstream',
149
+ '无法判断本地与远程领先/落后关系。',
150
+ '如需建立远程跟踪关系,可在首次推送时使用: icode push -m "type: subject"'
151
+ )
152
+ }
153
+
154
+ if (upstreamStatus.behind > 0) {
155
+ return buildCheck(
156
+ 'git.upstream',
157
+ 'warn',
158
+ '当前分支落后 upstream',
159
+ `${upstreamStatus.upstream}: ahead=${upstreamStatus.ahead}, behind=${upstreamStatus.behind}`,
160
+ '建议先同步远程变更: icode sync'
161
+ )
162
+ }
163
+
164
+ if (upstreamStatus.ahead > 0) {
165
+ return buildCheck(
166
+ 'git.upstream',
167
+ 'warn',
168
+ '当前分支有未推送提交',
169
+ `${upstreamStatus.upstream}: ahead=${upstreamStatus.ahead}, behind=${upstreamStatus.behind}`,
170
+ '如需发布当前分支,可执行: icode push'
171
+ )
172
+ }
173
+
174
+ return buildCheck(
175
+ 'git.upstream',
176
+ 'pass',
177
+ 'upstream 已同步',
178
+ `${upstreamStatus.upstream}: ahead=0, behind=0`
179
+ )
180
+ }
181
+
182
+ /**
183
+ * 检查 AI profile 是否可用于 AI 场景。
184
+ * 输入为 AI 配置,输出 ready 检查项。
185
+ */
186
+ function checkAiProfile(aiConfig) {
187
+ const activeProfile = String(aiConfig.activeProfile || '').trim()
188
+ const profileCount = Object.keys(aiConfig.profiles || {}).length
189
+
190
+ if (!activeProfile || profileCount === 0) {
191
+ return buildCheck(
192
+ 'ai.profile',
193
+ 'warn',
194
+ 'AI profile 未配置',
195
+ 'save --ai-commit、codereview、explain 等能力不可用。',
196
+ '如需 AI 能力,请先执行: icode config ai set <name> ... --activate'
197
+ )
198
+ }
199
+
200
+ return buildCheck('ai.profile', 'pass', 'AI profile 可用', `activeProfile=${activeProfile}, profileCount=${profileCount}`)
201
+ }
202
+
203
+ /**
204
+ * 检查远程合并配置是否可用于 remote-merge 场景。
205
+ * 输入为平台配置和仓库策略,输出 ready 检查项。
206
+ */
207
+ function checkRemoteMerge(remoteMergePlatform, repoPolicy) {
208
+ const repoRemoteMerge = repoPolicy.remoteMerge && typeof repoPolicy.remoteMerge === 'object'
209
+ ? repoPolicy.remoteMerge
210
+ : {}
211
+
212
+ if (repoRemoteMerge.enabled === false) {
213
+ return buildCheck('remoteMerge.config', 'pass', '远程合并已禁用', '当前仓库策略明确禁用 remote merge。')
214
+ }
215
+
216
+ if (!remoteMergePlatform.provider || !remoteMergePlatform.apiKey) {
217
+ return buildCheck(
218
+ 'remoteMerge.config',
219
+ 'warn',
220
+ '远程合并配置不完整',
221
+ '缺少 provider 或 apiKey,push -r 不可用。',
222
+ '如需远程合并,请执行: icode config platform remote-merge set --provider gitlab --api-key <key>'
223
+ )
224
+ }
225
+
226
+ return buildCheck('remoteMerge.config', 'pass', '远程合并配置可用', `provider=${remoteMergePlatform.provider}`)
227
+ }
228
+
229
+ /**
230
+ * 执行场景化就绪检查。
231
+ * 输入为仓库选项,输出面向人类和 AI agent 的稳定状态结构。
232
+ */
233
+ export async function runReadyWorkflow(options = {}) {
234
+ const context = await resolveGitContext({
235
+ cwd: options.cwd,
236
+ repoMode: options.repoMode
237
+ })
238
+ const git = new GitService(context)
239
+ const [currentBranch, operation, changes, upstream] = await Promise.all([
240
+ git.getCurrentBranch(),
241
+ git.getInProgressOperation(),
242
+ git.getStatusSummary(),
243
+ git.getUpstreamStatus()
244
+ ])
245
+ const aiConfig = getAiConfig()
246
+ const repoPolicy = getRepoPolicy(context.topLevelPath)
247
+ const remoteMergePlatform = getPlatformConfig('remoteMerge')
248
+
249
+ const checks = [
250
+ checkCurrentBranch(currentBranch),
251
+ checkInProgressOperation(operation),
252
+ checkChanges(changes),
253
+ checkUpstream(upstream),
254
+ checkAiProfile(aiConfig),
255
+ checkRemoteMerge(remoteMergePlatform, repoPolicy)
256
+ ]
257
+ const summary = summarizeReadyChecks(checks)
258
+
259
+ return {
260
+ ok: summary.ok,
261
+ status: summary.status,
262
+ summary,
263
+ context: {
264
+ cwd: context.cwd,
265
+ topLevelPath: context.topLevelPath,
266
+ repoMode: context.repoMode,
267
+ currentBranch,
268
+ defaultBranch: context.defaultBranch,
269
+ inheritedFromParent: context.inheritedFromParent,
270
+ isSubmodule: context.isSubmodule
271
+ },
272
+ operation,
273
+ changes,
274
+ upstream,
275
+ checks,
276
+ nextSteps: collectNextSteps(checks)
277
+ }
278
+ }