@icode-js/icode 3.2.0 → 3.4.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 (75) hide show
  1. package/README.md +287 -24
  2. package/bin/icode.js +10 -0
  3. package/package.json +3 -3
  4. package/src/cli.js +175 -8
  5. package/src/commands/checkout.js +73 -6
  6. package/src/commands/clean.js +47 -7
  7. package/src/commands/config-ux.js +292 -0
  8. package/src/commands/config.js +199 -48
  9. package/src/commands/continue.js +104 -0
  10. package/src/commands/doctor.js +15 -29
  11. package/src/commands/help.js +54 -34
  12. package/src/commands/history.js +147 -0
  13. package/src/commands/info.js +39 -4
  14. package/src/commands/log.js +142 -0
  15. package/src/commands/migrate.js +19 -12
  16. package/src/commands/open.js +138 -0
  17. package/src/commands/pr.js +219 -0
  18. package/src/commands/push.js +62 -22
  19. package/src/commands/ready.js +18 -48
  20. package/src/commands/release.js +112 -0
  21. package/src/commands/rescue.js +164 -0
  22. package/src/commands/save.js +52 -11
  23. package/src/commands/stash.js +210 -0
  24. package/src/commands/sw.js +89 -0
  25. package/src/commands/sync.js +19 -2
  26. package/src/commands/tag.js +40 -1
  27. package/src/commands/undo.js +9 -3
  28. package/src/core/ai/diff-range.js +1 -1
  29. package/src/core/cli/completion.js +237 -1
  30. package/src/core/cli/output.js +46 -0
  31. package/src/core/cli/render.js +121 -0
  32. package/src/core/commands/shell.js +9 -1
  33. package/src/core/config/repo-config.js +93 -0
  34. package/src/core/config/store.js +60 -10
  35. package/src/core/git/autostash.js +46 -0
  36. package/src/core/git/backup.js +170 -0
  37. package/src/core/git/conflicts.js +58 -0
  38. package/src/core/git/context.js +72 -5
  39. package/src/core/git/file-guard.js +193 -0
  40. package/src/core/git/operation-state.js +63 -0
  41. package/src/core/git/operations.js +32 -0
  42. package/src/core/git/semver.js +194 -0
  43. package/src/core/git/service.js +454 -24
  44. package/src/core/platform/http.js +53 -0
  45. package/src/core/platform/index.js +165 -0
  46. package/src/core/platform/providers.js +454 -0
  47. package/src/core/platform/remote-url.js +92 -0
  48. package/src/core/remote-merge/client.js +33 -3
  49. package/src/core/tools/browser.js +28 -0
  50. package/src/core/tools/commit-prompt.js +137 -0
  51. package/src/core/tools/interactive.js +272 -40
  52. package/src/core/tools/logger.js +33 -23
  53. package/src/core/tools/theme.js +228 -0
  54. package/src/workflows/ai-commit-workflow.js +52 -20
  55. package/src/workflows/checkout-workflow.js +24 -10
  56. package/src/workflows/clean-workflow.js +135 -83
  57. package/src/workflows/continue-workflow.js +233 -0
  58. package/src/workflows/doctor-workflow.js +10 -7
  59. package/src/workflows/history-workflow.js +211 -0
  60. package/src/workflows/migrate-workflow.js +39 -14
  61. package/src/workflows/pr-workflow.js +252 -0
  62. package/src/workflows/push/commit-step.js +123 -0
  63. package/src/workflows/push/local-merge-step.js +213 -0
  64. package/src/workflows/push/plan.js +63 -0
  65. package/src/workflows/push/remote-merge-step.js +224 -0
  66. package/src/workflows/push-workflow.js +185 -492
  67. package/src/workflows/ready-workflow.js +14 -10
  68. package/src/workflows/release-workflow.js +189 -0
  69. package/src/workflows/rescue-workflow.js +137 -0
  70. package/src/workflows/save-workflow.js +106 -41
  71. package/src/workflows/switch-workflow.js +171 -0
  72. package/src/workflows/sync-workflow.js +143 -44
  73. package/src/workflows/tag-workflow.js +103 -17
  74. package/src/workflows/undo-workflow.js +113 -34
  75. package/src/workflows/rollback-workflow.js +0 -99
@@ -1,15 +1,15 @@
1
1
  import { getRepoPolicy } from '../core/config/store.js'
2
+ import { ui, shortenHome } from '../core/tools/theme.js'
2
3
  import { IcodeError } from '../core/errors.js'
4
+ import { createBackup } from '../core/git/backup.js'
3
5
  import { resolveGitContext } from '../core/git/context.js'
4
6
  import {
5
- assertCleanWorktree,
6
- assertNoConflictedFiles,
7
7
  assertNoPendingOperation,
8
8
  assertOnBranch,
9
9
  readGitSafetyState
10
10
  } from '../core/git/preconditions.js'
11
11
  import { GitService } from '../core/git/service.js'
12
- import { confirm } from '../core/tools/interactive.js'
12
+ import { chooseMany, confirm, isInteractiveTerminal } from '../core/tools/interactive.js'
13
13
  import { logger } from '../core/tools/logger.js'
14
14
 
15
15
  function normalizeKeepList(values = []) {
@@ -21,14 +21,28 @@ function normalizeKeepList(values = []) {
21
21
  )
22
22
  }
23
23
 
24
+ function parseStaleDays(value) {
25
+ if (value == null || value === '') {
26
+ return 0
27
+ }
28
+ const days = Number(value)
29
+ if (!Number.isFinite(days) || days <= 0) {
30
+ throw new IcodeError('--stale 需要正数天数,例如 --stale 90', { code: 'CLEAN_STALE_INVALID', exitCode: 2 })
31
+ }
32
+ return days
33
+ }
34
+
24
35
  /**
25
36
  * 执行 clean 工作流。
26
- * 输入为清理基线、保留分支和删除选项,输出已删除分支;执行前要求工作区干净并在普通分支上。
37
+ * 清理范围:已合并(默认)、--gone(远程分支已删除)、--stale <天>(超过 N 天没有提交)。
38
+ * 不切换分支、不要求工作区干净;删除前会为所有待删分支创建恢复点。
39
+ * 输出 { candidates: [{ branch, reasons }], deletedLocal, deletedRemote, backupId }。
27
40
  */
28
41
  export async function runCleanWorkflow(options) {
29
42
  const context = await resolveGitContext({
30
43
  cwd: options.cwd,
31
- repoMode: options.repoMode
44
+ repoMode: options.repoMode,
45
+ remote: options.remote
32
46
  })
33
47
 
34
48
  const git = new GitService(context)
@@ -38,111 +52,149 @@ export async function runCleanWorkflow(options) {
38
52
  const policy = getRepoPolicy(context.topLevelPath)
39
53
  assertOnBranch(currentBranch, 'clean', 'CLEAN_DETACHED_HEAD')
40
54
  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')
43
55
 
44
- logger.info(`仓库根目录: ${context.topLevelPath}`)
45
- if (context.inheritedFromParent) {
46
- logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
56
+ const staleDays = parseStaleDays(options.stale)
57
+ const selectGone = options.gone === true
58
+ // 只传 --gone/--stale 时只清理这些;同时传 --merged 时三者合并。
59
+ const selectMerged = options.merged === true || (!selectGone && !staleDays)
60
+
61
+ if (!options.json) {
62
+ logger.hint(`仓库 ${shortenHome(context.topLevelPath)}`)
47
63
  }
48
64
 
49
65
  await git.fetch()
50
66
 
51
- const targetLocalExists = await git.branchExistsLocal(mergedTarget)
52
- const targetRemoteExists = await git.branchExistsRemote(mergedTarget)
53
- if (!targetLocalExists && !targetRemoteExists) {
54
- throw new IcodeError(`清理基线分支不存在: ${mergedTarget}`, {
55
- code: 'CLEAN_TARGET_MISSING',
56
- exitCode: 2
57
- })
58
- }
59
-
60
- try {
61
- if (targetLocalExists) {
62
- await git.checkout(mergedTarget)
67
+ // 优先用远程的基线分支判断,避免本地主分支落后导致漏判。
68
+ let mergedRef = ''
69
+ if (selectMerged) {
70
+ const remoteTarget = git.remoteRef(mergedTarget)
71
+ if (await git.refExists(remoteTarget)) {
72
+ mergedRef = remoteTarget
73
+ } else if (await git.branchExistsLocal(mergedTarget)) {
74
+ mergedRef = mergedTarget
63
75
  } else {
64
- await git.checkoutTracking(mergedTarget)
65
- }
66
-
67
- if (targetRemoteExists) {
68
- await git.pull(mergedTarget, {
69
- noRebase: true
76
+ throw new IcodeError(`清理基线分支不存在: ${mergedTarget}`, {
77
+ code: 'CLEAN_TARGET_MISSING',
78
+ exitCode: 2
70
79
  })
71
80
  }
81
+ }
72
82
 
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 || [])
83
+ const protectedBranches = [...(policy.protectedBranches || []), ...(context.repoConfig?.protectedBranches || [])]
84
+ const keepSet = normalizeKeepList(options.keep || [])
85
+ // 这些分支永远不进入清理列表,避免误删核心分支。
86
+ ;[mergedTarget, context.defaultBranch, currentBranch, ...protectedBranches]
87
+ .map((branch) => String(branch || '').trim())
88
+ .filter(Boolean)
89
+ .forEach((branch) => keepSet.add(branch))
90
+
91
+ const reasonsByBranch = new Map()
92
+ const addReason = (branch, reason) => {
93
+ if (keepSet.has(branch)) {
94
+ return
95
+ }
96
+ if (!reasonsByBranch.has(branch)) {
97
+ reasonsByBranch.set(branch, [])
98
+ }
99
+ reasonsByBranch.get(branch).push(reason)
100
+ }
76
101
 
77
- // 这些分支永远不进入清理列表,避免误删核心分支。
78
- keepSet.add(mergedTarget)
79
- if (context.defaultBranch) {
80
- keepSet.add(context.defaultBranch)
102
+ if (selectMerged) {
103
+ for (const branch of await git.listMergedLocalBranches(mergedRef)) {
104
+ addReason(branch, `已合并到 ${mergedRef}`)
81
105
  }
82
- if (currentBranch) {
83
- keepSet.add(currentBranch)
106
+ }
107
+
108
+ if (selectGone || staleDays) {
109
+ const now = Date.now() / 1000
110
+ for (const detail of await git.listBranchDetails()) {
111
+ if (selectGone && detail.gone) {
112
+ addReason(detail.name, '远程分支已删除')
113
+ }
114
+ if (staleDays && detail.timestamp && now - detail.timestamp > staleDays * 86400) {
115
+ addReason(detail.name, `超过 ${staleDays} 天未提交(${detail.relativeDate})`)
116
+ }
84
117
  }
85
- protectedBranches.forEach((branch) => keepSet.add(branch))
118
+ }
86
119
 
87
- const candidates = mergedBranches.filter((branch) => !keepSet.has(branch))
120
+ const candidates = [...reasonsByBranch.entries()].map(([branch, reasons]) => ({ branch, reasons }))
121
+ const baseResult = {
122
+ repoRoot: context.topLevelPath,
123
+ mergedTarget,
124
+ candidates,
125
+ deletedLocal: [],
126
+ deletedRemote: []
127
+ }
88
128
 
89
- if (!candidates.length) {
129
+ if (!candidates.length) {
130
+ if (!options.json) {
90
131
  logger.info('没有可清理的本地分支。')
91
- return {
92
- repoRoot: context.topLevelPath,
93
- mergedTarget,
94
- deletedLocal: [],
95
- deletedRemote: []
96
- }
97
132
  }
133
+ return baseResult
134
+ }
135
+
136
+ if (options.dryRun) {
137
+ return { ...baseResult, dryRun: true }
138
+ }
98
139
 
99
- if (!options.yes) {
100
- const accepted = await confirm(
101
- `确认清理以下分支吗: ${candidates.join(', ')} ?`,
102
- false
103
- )
140
+ let selected = candidates
141
+ if (!options.yes) {
142
+ if (isInteractiveTerminal()) {
143
+ // 勾选要删的分支(默认全选),不想删的取消勾选即可。
144
+ const picked = await chooseMany(`选择要删除的分支(共 ${candidates.length} 个)`, candidates.map((item) => ({
145
+ value: item.branch,
146
+ label: item.branch,
147
+ hint: item.reasons.join(';')
148
+ })), { defaultValues: candidates.map((item) => item.branch), doneLabel: '删除' })
149
+ if (!picked || !picked.length) {
150
+ logger.warn('已取消清理。')
151
+ return { ...baseResult, canceled: true }
152
+ }
153
+ selected = candidates.filter((item) => picked.includes(item.branch))
154
+ } else {
155
+ const accepted = await confirm(`确认删除 ${candidates.length} 个分支吗?`, false)
104
156
  if (!accepted) {
105
157
  logger.warn('已取消清理。')
106
- return {
107
- canceled: true,
108
- repoRoot: context.topLevelPath,
109
- mergedTarget,
110
- candidates
111
- }
158
+ return { ...baseResult, canceled: true }
112
159
  }
113
160
  }
161
+ }
114
162
 
115
- const deletedLocal = []
116
- const deletedRemote = []
163
+ // 有恢复点兜底,未合并的 gone/stale 分支也可以安全地用 -D 删除。
164
+ const backup = await createBackup(git, {
165
+ reason: `clean ${selected.length} branches`,
166
+ branches: selected.map((item) => item.branch),
167
+ silent: options.json
168
+ })
117
169
 
118
- for (const branch of candidates) {
119
- await git.deleteLocalBranch(branch, {
120
- force: options.force
121
- })
122
- deletedLocal.push(branch)
170
+ const deletedLocal = []
171
+ const deletedRemote = []
172
+ const failed = []
123
173
 
124
- if (options.remote) {
125
- const existsRemote = await git.branchExistsRemote(branch)
126
- if (existsRemote) {
127
- await git.deleteRemoteBranch(branch)
128
- deletedRemote.push(branch)
129
- }
130
- }
174
+ for (const { branch, reasons } of selected) {
175
+ const merged = reasons.some((reason) => reason.startsWith('已合并'))
176
+ try {
177
+ await git.deleteLocalBranch(branch, { force: options.force || !merged })
178
+ deletedLocal.push(branch)
179
+ } catch (error) {
180
+ failed.push({ branch, reason: error.message.split('\n')[0] })
181
+ continue
131
182
  }
132
183
 
133
- return {
134
- repoRoot: context.topLevelPath,
135
- mergedTarget,
136
- deletedLocal,
137
- deletedRemote
138
- }
139
- } finally {
140
- if (currentBranch && currentBranch !== mergedTarget) {
141
- try {
142
- await git.checkout(currentBranch)
143
- } catch (error) {
144
- logger.warn(`未能切回原分支 ${currentBranch}: ${error.message}`)
184
+ if (options.remote && merged) {
185
+ const existsRemote = await git.branchExistsRemote(branch)
186
+ if (existsRemote) {
187
+ await git.deleteRemoteBranch(branch)
188
+ deletedRemote.push(branch)
145
189
  }
146
190
  }
147
191
  }
192
+
193
+ return {
194
+ ...baseResult,
195
+ deletedLocal,
196
+ deletedRemote,
197
+ failed,
198
+ backupId: backup.entries.length ? backup.id : ''
199
+ }
148
200
  }
@@ -0,0 +1,233 @@
1
+ import { IcodeError } from '../core/errors.js'
2
+ import { inspectConflicts, stageResolvedConflicts } from '../core/git/conflicts.js'
3
+ import { resolveGitContext } from '../core/git/context.js'
4
+ import { clearOperationState, describeOperationState, loadOperationState, saveOperationState } from '../core/git/operation-state.js'
5
+ import { abortOperation, continueOperation, OPERATION_LABELS } from '../core/git/operations.js'
6
+ import { GitService } from '../core/git/service.js'
7
+ import { logger } from '../core/tools/logger.js'
8
+ import { ui } from '../core/tools/theme.js'
9
+ import { mergeIntoTargetAndPush, pushCurrentBranch } from './push/local-merge-step.js'
10
+ import { runSyncBranches } from './sync-workflow.js'
11
+
12
+ async function openGit(options) {
13
+ const context = await resolveGitContext({ cwd: options.cwd, repoMode: options.repoMode })
14
+ return { context, git: new GitService(context) }
15
+ }
16
+
17
+ function formatUnresolved(unresolved) {
18
+ return unresolved
19
+ .map((item) => ` - ${item.file}(第 ${item.lines.slice(0, 5).join(', ')}${item.lines.length > 5 ? ' …' : ''} 行)`)
20
+ .join('\n')
21
+ }
22
+
23
+ /**
24
+ * 读取当前是否有需要继续/放弃的内容,供 ready / 主菜单提示。
25
+ * 输出 { operation, state, description } 或 null。
26
+ */
27
+ export async function inspectPausedWork(options = {}) {
28
+ const { context, git } = await openGit(options)
29
+ const operation = await git.getInProgressOperation()
30
+ const state = loadOperationState(context)
31
+ if (!operation && !state) {
32
+ return null
33
+ }
34
+ return {
35
+ operation,
36
+ state,
37
+ description: [operation ? OPERATION_LABELS[operation] || operation : '', describeOperationState(state)].filter(Boolean).join(' · ')
38
+ }
39
+ }
40
+
41
+ // 冲突解决后把 push 剩下的步骤做完:推送已合并的目标分支、处理剩余目标、切回原分支。
42
+ async function resumePush(git, context, state) {
43
+ const summary = [...(state.summary || [])]
44
+ const options = { ...(state.options || {}), yes: true }
45
+ const remaining = [...(state.remainingTargets || [])]
46
+
47
+ if (state.pendingPush) {
48
+ const remoteExists = await git.branchExistsRemote(state.pendingPush)
49
+ logger.info(`推送目标分支: ${ui.branch(state.pendingPush)}`)
50
+ await git.push(state.pendingPush, { setUpstream: !remoteExists, noVerify: options.noVerify })
51
+ summary.push({ branch: state.pendingPush, status: 'merged-and-pushed' })
52
+ }
53
+
54
+ for (let index = 0; index < remaining.length; index += 1) {
55
+ const branch = remaining[index]
56
+ try {
57
+ if (branch === state.sourceBranch) {
58
+ await git.checkout(state.sourceBranch)
59
+ summary.push(await pushCurrentBranch(git, branch, options, summary))
60
+ } else {
61
+ summary.push(await mergeIntoTargetAndPush(git, state.sourceBranch, branch, options, summary))
62
+ }
63
+ } catch (error) {
64
+ // 又遇到冲突:更新进度,等下一次 icode continue。
65
+ if (error?.code === 'PUSH_LOCAL_MERGE_PAUSED' && await git.getInProgressOperation()) {
66
+ const mergedTarget = error.meta?.step === 'merge-target'
67
+ saveOperationState(context, {
68
+ ...state,
69
+ pendingPush: mergedTarget ? branch : null,
70
+ remainingTargets: remaining.slice(mergedTarget ? index + 1 : index),
71
+ summary
72
+ })
73
+ }
74
+ throw error
75
+ }
76
+ }
77
+
78
+ return summary
79
+ }
80
+
81
+ async function resumeMigrate(git, state) {
82
+ const summary = []
83
+ if (state.push) {
84
+ logger.info(`推送目标分支: ${ui.branch(state.targetBranch)}`)
85
+ await git.push(state.targetBranch, { setUpstream: !state.targetExistsRemote, noVerify: state.options?.noVerify })
86
+ summary.push({ branch: state.targetBranch, status: 'pushed' })
87
+ }
88
+ return summary
89
+ }
90
+
91
+ async function resumeSync(git, context, state) {
92
+ const targets = [state.currentBranch, ...(state.remainingTargets || [])].filter(Boolean)
93
+ // runSyncBranches 结束时会自己切回原分支,并在再次冲突时更新进度。
94
+ clearOperationState(context)
95
+ const summary = await runSyncBranches(git, context, targets, state.options || {}, state.originalBranch, {
96
+ branch: state.currentBranch,
97
+ startAt: state.startAt || 'push'
98
+ })
99
+ return [...(state.summary || []), ...summary]
100
+ }
101
+
102
+ async function returnToOriginalBranch(git, state) {
103
+ const original = state?.originalBranch
104
+ if (!original) {
105
+ return ''
106
+ }
107
+ const current = await git.getCurrentBranch()
108
+ if (current !== original) {
109
+ await git.checkout(original)
110
+ }
111
+ return original
112
+ }
113
+
114
+ /**
115
+ * icode continue:检查冲突是否都已解决 -> 自动暂存 -> 继续 Git 操作 -> 接着完成 push/migrate/sync 剩下的步骤。
116
+ * 输出 { operation, staged, resumed, summary, returnedTo }。
117
+ */
118
+ export async function runContinueWorkflow(options = {}) {
119
+ const { context, git } = await openGit(options)
120
+ const operation = await git.getInProgressOperation()
121
+ const state = loadOperationState(context)
122
+
123
+ if (!operation && !state) {
124
+ throw new IcodeError('没有需要继续的操作。', { code: 'CONTINUE_NOTHING', exitCode: 2 })
125
+ }
126
+
127
+ let staged = []
128
+ if (operation) {
129
+ const { resolved, unresolved } = await inspectConflicts(git)
130
+ if (unresolved.length) {
131
+ throw new IcodeError(
132
+ `还有 ${unresolved.length} 个文件存在冲突标记,请先处理:\n${formatUnresolved(unresolved)}\n处理完再执行 icode continue;想放弃可执行 icode abort。`,
133
+ { code: 'CONTINUE_UNRESOLVED_CONFLICTS', exitCode: 2, meta: { unresolved } }
134
+ )
135
+ }
136
+ staged = await stageResolvedConflicts(git, resolved)
137
+ if (staged.length && !options.silentLog) {
138
+ logger.info(`已自动暂存 ${staged.length} 个已解决的文件: ${staged.map(ui.path).join(' ')}`)
139
+ }
140
+
141
+ try {
142
+ await continueOperation(git, operation)
143
+ } catch (error) {
144
+ // rebase / cherry-pick 的下一个提交可能又冲突。
145
+ const still = await git.getInProgressOperation()
146
+ const next = still ? await inspectConflicts(git) : { unresolved: [] }
147
+ if (next.unresolved.length) {
148
+ throw new IcodeError(
149
+ `下一个提交又出现冲突:\n${formatUnresolved(next.unresolved)}\n处理完再执行 icode continue。`,
150
+ { code: 'CONTINUE_NEXT_CONFLICT', exitCode: 2, meta: { unresolved: next.unresolved }, cause: error }
151
+ )
152
+ }
153
+ throw new IcodeError(`继续 ${operation} 失败:${error.message.split('\n').slice(0, 4).join('\n')}`, {
154
+ code: 'CONTINUE_FAILED',
155
+ exitCode: 2,
156
+ cause: error
157
+ })
158
+ }
159
+ }
160
+
161
+ if (operation && !options.silentLog) {
162
+ logger.success(`${OPERATION_LABELS[operation] || operation} 已完成`)
163
+ }
164
+
165
+ let summary = []
166
+ if (state) {
167
+ if (state.command === 'push') {
168
+ summary = await resumePush(git, context, state)
169
+ } else if (state.command === 'migrate') {
170
+ summary = await resumeMigrate(git, state)
171
+ } else if (state.command === 'sync') {
172
+ summary = await resumeSync(git, context, state)
173
+ }
174
+ }
175
+
176
+ // 进度只在全部完成后清除;中途再次冲突时上面已经抛错并更新了进度。
177
+ let returnedTo = ''
178
+ if (state) {
179
+ if (state.command !== 'sync') {
180
+ returnedTo = await returnToOriginalBranch(git, state)
181
+ } else {
182
+ returnedTo = state.originalBranch || ''
183
+ }
184
+ clearOperationState(context)
185
+ }
186
+
187
+ return {
188
+ operation,
189
+ staged,
190
+ resumed: state ? state.command : '',
191
+ description: describeOperationState(state),
192
+ summary,
193
+ returnedTo,
194
+ repoRoot: context.topLevelPath
195
+ }
196
+ }
197
+
198
+ /**
199
+ * icode abort:中止进行中的 Git 操作,清除中断进度并切回原分支。
200
+ * 已经推送成功的分支不会被撤回。
201
+ */
202
+ export async function runAbortWorkflow(options = {}) {
203
+ const { context, git } = await openGit(options)
204
+ const operation = await git.getInProgressOperation()
205
+ const state = loadOperationState(context)
206
+
207
+ if (!operation && !state) {
208
+ throw new IcodeError('没有需要放弃的操作。', { code: 'ABORT_NOTHING', exitCode: 2 })
209
+ }
210
+
211
+ if (operation) {
212
+ await abortOperation(git, operation)
213
+ }
214
+
215
+ let returnedTo = ''
216
+ if (state) {
217
+ try {
218
+ returnedTo = await returnToOriginalBranch(git, state)
219
+ } catch (error) {
220
+ logger.warn(`未能切回 ${state.originalBranch}: ${error.message.split('\n')[0]}`)
221
+ }
222
+ clearOperationState(context)
223
+ }
224
+
225
+ return {
226
+ operation,
227
+ aborted: state ? state.command : '',
228
+ description: describeOperationState(state),
229
+ done: (state?.summary || []).filter((item) => /pushed/.test(item.status)).map((item) => item.branch),
230
+ returnedTo,
231
+ repoRoot: context.topLevelPath
232
+ }
233
+ }
@@ -1,5 +1,6 @@
1
+ import { resolveRemoteMergePlatform } from './push/remote-merge-step.js'
1
2
  import { getAiConfig } from '../core/ai/config.js'
2
- import { getConfigFilePath, getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
3
+ import { getConfigFilePath, getRepoPolicy } from '../core/config/store.js'
3
4
  import { resolveGitContext } from '../core/git/context.js'
4
5
  import { GitService } from '../core/git/service.js'
5
6
  import { runCommand } from '../core/commands/shell.js'
@@ -57,14 +58,16 @@ async function checkGitVersion(repoRoot) {
57
58
  return buildCheck('git.version', 'pass', 'Git 命令可用', result.stdout.trim())
58
59
  }
59
60
 
60
- // 检查 origin remote 是否存在,输入 GitService,输出健康检查项。
61
+ // 检查当前使用的 remote 是否存在,输入 GitService,输出健康检查项(id 保持 git.origin 兼容旧脚本)。
61
62
  async function checkOriginRemote(git) {
62
- const remoteUrl = await git.getRemoteUrl('origin')
63
+ const remoteName = git.remote
64
+ const remoteUrl = await git.getRemoteUrl(remoteName)
63
65
  if (!remoteUrl) {
64
- return buildCheck('git.origin', 'warn', 'origin remote 未配置', '当前仓库没有 origin remote。', '需要 push 或远程合并前,请先配置 git remote add origin <url>。')
66
+ return buildCheck('git.origin', 'warn', `${remoteName} remote 未配置`, `当前仓库没有 ${remoteName} remote。`, `需要 push 或远程合并前,请先配置 git remote add ${remoteName} <url>,或用 icode --remote <name> 指定其它 remote。`)
65
67
  }
66
68
 
67
- return buildCheck('git.origin', 'pass', 'origin remote 已配置', remoteUrl)
69
+ const source = git.context.remoteSource && git.context.remoteSource !== 'default' ? `,来源: ${git.context.remoteSource}` : ''
70
+ return buildCheck('git.origin', 'pass', `${remoteName} remote 已配置${source}`, remoteUrl)
68
71
  }
69
72
 
70
73
  // 检查 hook 配置,输入 git context,输出健康检查项。
@@ -97,7 +100,7 @@ function checkRemoteMerge(remoteMergePlatform, repoPolicy) {
97
100
  }
98
101
 
99
102
  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>。')
103
+ return buildCheck('remoteMerge.config', 'warn', '远程合并平台未完整配置', remoteMergePlatform.provider ? `未找到 ${remoteMergePlatform.provider} token。` : '无法从 remote 识别代码托管平台。', `如需 icode push -r / icode pr,请执行 icode config platform token set ${remoteMergePlatform.provider || '<provider>'} <token>。`)
101
104
  }
102
105
 
103
106
  return buildCheck('remoteMerge.config', 'pass', '远程合并平台已配置', `provider=${remoteMergePlatform.provider}`)
@@ -112,7 +115,7 @@ export async function runDoctorWorkflow(options = {}) {
112
115
  const git = new GitService(context)
113
116
  const repoPolicy = getRepoPolicy(context.topLevelPath)
114
117
  const aiConfig = getAiConfig()
115
- const remoteMergePlatform = getPlatformConfig('remoteMerge')
118
+ const remoteMergePlatform = await resolveRemoteMergePlatform(git)
116
119
 
117
120
  const checks = [
118
121
  await checkGitVersion(context.topLevelPath),