@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
package/src/cli.js CHANGED
@@ -4,20 +4,32 @@ import { runCodeReviewCommand } from './commands/codereview.js'
4
4
  import { runCompletionCommand, runHiddenCompletionCommand } from './commands/completion.js'
5
5
  import { runCheckoutCommand } from './commands/checkout.js'
6
6
  import { runConfigCommand } from './commands/config.js'
7
+ import { runAbortCommand, runContinueCommand } from './commands/continue.js'
7
8
  import { runDoctorCommand } from './commands/doctor.js'
8
9
  import { runExplainCommand } from './commands/explain.js'
9
10
  import { printMainHelp } from './commands/help.js'
11
+ import { runAmendCommand, runFixupCommand } from './commands/history.js'
10
12
  import { runInfoCommand } from './commands/info.js'
13
+ import { runLogCommand } from './commands/log.js'
11
14
  import { runMigrateCommand } from './commands/migrate.js'
15
+ import { runOpenCommand } from './commands/open.js'
16
+ import { runPrCommand } from './commands/pr.js'
12
17
  import { runPushCommand } from './commands/push.js'
13
18
  import { runReadyCommand } from './commands/ready.js'
19
+ import { runReleaseCommand } from './commands/release.js'
20
+ import { runRescueCommand } from './commands/rescue.js'
14
21
  import { runSaveCommand } from './commands/save.js'
22
+ import { runStashCommand } from './commands/stash.js'
23
+ import { runSwCommand } from './commands/sw.js'
15
24
  import { runSyncCommand } from './commands/sync.js'
16
25
  import { runTagCommand } from './commands/tag.js'
17
26
  import { runUndoCommand } from './commands/undo.js'
18
- import { asIcodeError } from './core/errors.js'
27
+ import { asIcodeError, IcodeError } from './core/errors.js'
19
28
  import { normalizeLegacyArgs } from './core/cli/args.js'
20
29
  import { notifyIfCliUpdateAvailable } from './core/cli/update-notifier.js'
30
+ import { chooseOne, isInteractiveTerminal } from './core/tools/interactive.js'
31
+ import { c } from './core/tools/theme.js'
32
+ import { inspectPausedWork } from './workflows/continue-workflow.js'
21
33
  import { logger } from './core/tools/logger.js'
22
34
 
23
35
  function isTruthy(value) {
@@ -25,11 +37,38 @@ function isTruthy(value) {
25
37
  return ['1', 'true', 'yes', 'y', 'on'].includes(normalized)
26
38
  }
27
39
 
40
+ function redactSensitiveMeta(value, seen = new WeakSet()) {
41
+ if (typeof value === 'string') {
42
+ return value
43
+ .replace(/(Bearer\s+)[^\s"']+/gi, '$1[REDACTED]')
44
+ .replace(/((?:api[_-]?key|private[_-]?token|authorization)\s*[=:]\s*)[^\s,;"']+/gi, '$1[REDACTED]')
45
+ }
46
+ if (!value || typeof value !== 'object') {
47
+ return value
48
+ }
49
+ if (seen.has(value)) {
50
+ return '[Circular]'
51
+ }
52
+
53
+ seen.add(value)
54
+ if (Array.isArray(value)) {
55
+ return value.map((item) => redactSensitiveMeta(item, seen))
56
+ }
57
+
58
+ const sanitized = {}
59
+ for (const [key, item] of Object.entries(value)) {
60
+ sanitized[key] = /api[_-]?key|token|authorization|secret|password/i.test(key)
61
+ ? '[REDACTED]'
62
+ : redactSensitiveMeta(item, seen)
63
+ }
64
+ return sanitized
65
+ }
66
+
28
67
  function serializeErrorMeta(meta) {
29
68
  try {
30
- return JSON.stringify(meta, null, 2)
69
+ return JSON.stringify(redactSensitiveMeta(meta), null, 2)
31
70
  } catch {
32
- return String(meta)
71
+ return '[无法序列化错误详情]'
33
72
  }
34
73
  }
35
74
 
@@ -44,8 +83,21 @@ const COMMANDS = {
44
83
  sync: runSyncCommand,
45
84
  clean: runCleanCommand,
46
85
  undo: runUndoCommand,
86
+ continue: runContinueCommand,
87
+ abort: runAbortCommand,
88
+ rescue: runRescueCommand,
89
+ stash: runStashCommand,
90
+ sw: runSwCommand,
91
+ switch: runSwCommand,
92
+ log: runLogCommand,
93
+ amend: runAmendCommand,
94
+ fixup: runFixupCommand,
47
95
  migrate: runMigrateCommand,
48
96
  tag: runTagCommand,
97
+ release: runReleaseCommand,
98
+ pr: runPrCommand,
99
+ mr: runPrCommand,
100
+ open: runOpenCommand,
49
101
  config: runConfigCommand,
50
102
  doctor: runDoctorCommand,
51
103
  explain: runExplainCommand,
@@ -58,6 +110,54 @@ const COMMANDS = {
58
110
 
59
111
  const NO_UPDATE_NOTIFICATION_COMMANDS = new Set(['help', 'completion', '__complete'])
60
112
 
113
+ const MAIN_MENU = [
114
+ { value: 'ready', label: 'ready', hint: '看看仓库现在什么状态' },
115
+ { value: 'save', label: 'save', hint: '提交改动(引导写提交信息)' },
116
+ { value: 'push', label: 'push', hint: '提交并推送,可选合并到其他分支' },
117
+ { value: 'sw', label: 'sw', hint: '切换分支' },
118
+ { value: 'checkout', label: 'checkout', hint: '切换或新建分支' },
119
+ { value: 'sync', label: 'sync', hint: '同步分支' },
120
+ { value: 'stash', label: 'stash', hint: '暂存 / 恢复改动' },
121
+ { value: 'log', label: 'log', hint: '最近的提交' },
122
+ { value: 'amend', label: 'amend', hint: '修改上一个提交' },
123
+ { value: 'fixup', label: 'fixup', hint: '把改动合进更早的提交' },
124
+ { value: 'undo', label: 'undo', hint: '撤销 / 回滚' },
125
+ { value: 'continue', label: 'continue', hint: '冲突解决后继续' },
126
+ { value: 'rescue', label: 'rescue', hint: '找回丢失的提交和改动' },
127
+ { value: 'clean', label: 'clean', hint: '清理已合并的分支' },
128
+ { value: 'pr', label: 'pr create', hint: '创建 PR / MR' },
129
+ { value: 'open', label: 'open', hint: '在浏览器打开仓库' },
130
+ { value: 'tag', label: 'tag', hint: '打 tag' },
131
+ { value: 'release', label: 'release', hint: '发版' },
132
+ { value: 'config', label: 'config', hint: '配置 token / AI / remote' },
133
+ { value: 'doctor', label: 'doctor', hint: '环境检查' },
134
+ { value: 'help', label: 'help', hint: '查看全部命令' }
135
+ ]
136
+
137
+ async function pickCommandFromMenu() {
138
+ // 有被冲突打断的操作时,把“继续 / 放弃”放在最前面。
139
+ let paused = null
140
+ try {
141
+ paused = await inspectPausedWork({})
142
+ } catch {
143
+ paused = null
144
+ }
145
+ const menu = paused
146
+ ? [
147
+ { value: 'continue', label: c.yellow('continue'), hint: `冲突解决后继续:${paused.description}` },
148
+ { value: 'abort', label: 'abort', hint: '放弃被中断的操作并切回原分支' },
149
+ ...MAIN_MENU
150
+ ]
151
+ : MAIN_MENU
152
+ const picked = await chooseOne(paused ? `有被中断的操作(${paused.description}),要做什么?` : '要做什么?', menu, 0)
153
+ // pr 从菜单进入时直接走创建流程。
154
+ if (picked === 'pr') {
155
+ await COMMANDS.pr(['create'])
156
+ return ''
157
+ }
158
+ return picked
159
+ }
160
+
61
161
  /**
62
162
  * 执行 CLI 主流程并在命令成功后触发升级提示检查。
63
163
  * 输入为命令行参数与可注入依赖,输出为建议的退出码。
@@ -74,13 +174,30 @@ export async function runCli(argv = process.argv.slice(2), dependencies = {}) {
74
174
  }
75
175
 
76
176
  if (!commandName) {
177
+ // 终端里直接运行 icode:显示常用操作菜单,选中后直接执行。
178
+ if (!dependencies.commands && isInteractiveTerminal()) {
179
+ const picked = await pickCommandFromMenu()
180
+ if (picked) {
181
+ await COMMANDS[picked]([])
182
+ }
183
+ return 0
184
+ }
77
185
  printHelp()
78
186
  return 0
79
187
  }
80
188
 
189
+ // --json 调用时 stdout 只输出结构化结果(config 的 --json 是取值参数,不算)。
190
+ if (commandName !== 'config' && commandArgs.includes('--json')) {
191
+ logger.setLogsToStderr(true)
192
+ }
193
+
81
194
  const command = commands[commandName]
82
195
  if (!command) {
83
- throw new Error(`未知命令: ${commandName}`)
196
+ throw new IcodeError(`未知命令: ${commandName}`, {
197
+ code: 'CLI_UNKNOWN_COMMAND',
198
+ exitCode: 2,
199
+ meta: { commandName }
200
+ })
84
201
  }
85
202
 
86
203
  await command(commandArgs)
@@ -92,9 +209,23 @@ export async function runCli(argv = process.argv.slice(2), dependencies = {}) {
92
209
  return 0
93
210
  }
94
211
 
212
+ // 需要跟一个值的全局参数(值不能被当成命令名)。
213
+ const GLOBAL_VALUE_FLAGS = new Set(['-R', '--remote'])
214
+
95
215
  function parseEntryArgs(argv) {
96
216
  const args = normalizeLegacyArgs(argv)
97
- const firstCommandIndex = args.findIndex((arg) => !arg.startsWith('-'))
217
+ let firstCommandIndex = -1
218
+ for (let index = 0; index < args.length; index += 1) {
219
+ const arg = args[index]
220
+ if (GLOBAL_VALUE_FLAGS.has(arg)) {
221
+ index += 1
222
+ continue
223
+ }
224
+ if (!arg.startsWith('-')) {
225
+ firstCommandIndex = index
226
+ break
227
+ }
228
+ }
98
229
 
99
230
  const globalArgs = firstCommandIndex === -1 ? args : args.slice(0, firstCommandIndex)
100
231
  const commandName = firstCommandIndex === -1 ? null : args[firstCommandIndex]
@@ -108,7 +239,24 @@ function parseEntryArgs(argv) {
108
239
  }
109
240
 
110
241
  function applyGlobalFlags(globalArgs) {
111
- for (const flag of globalArgs) {
242
+ for (let index = 0; index < globalArgs.length; index += 1) {
243
+ const flag = globalArgs[index]
244
+ if (flag === '-R' || flag === '--remote' || flag.startsWith('--remote=')) {
245
+ const value = flag.startsWith('--remote=') ? flag.slice('--remote='.length) : globalArgs[index + 1]
246
+ if (!flag.startsWith('--remote=')) {
247
+ index += 1
248
+ }
249
+ if (!value || value.startsWith('-')) {
250
+ throw new IcodeError('--remote 需要指定 remote 名称,例如: icode --remote upstream sync', {
251
+ code: 'CLI_REMOTE_REQUIRED',
252
+ exitCode: 2
253
+ })
254
+ }
255
+ // 通过环境变量传给所有命令的仓库上下文解析。
256
+ process.env.ICODE_REMOTE = value
257
+ continue
258
+ }
259
+
112
260
  if (flag === '-d' || flag === '--debug') {
113
261
  logger.setVerbose(true)
114
262
  process.env.ICODE_DEBUG = '1'
@@ -123,7 +271,11 @@ function applyGlobalFlags(globalArgs) {
123
271
  }
124
272
  }
125
273
 
126
- throw new Error(`未知全局参数: ${flag}`)
274
+ throw new IcodeError(`未知全局参数: ${flag}`, {
275
+ code: 'CLI_UNKNOWN_GLOBAL_FLAG',
276
+ exitCode: 2,
277
+ meta: { flag }
278
+ })
127
279
  }
128
280
 
129
281
  return {
@@ -144,8 +296,23 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
144
296
  * 统一处理 CLI 异常并输出对用户可读的错误信息。
145
297
  * 输入为任意异常对象,输出为进程退出。
146
298
  */
147
- export function handleCliError(error) {
299
+ export function handleCliError(error, argv = process.argv.slice(2)) {
148
300
  const normalized = asIcodeError(error)
301
+
302
+ // 带 --json 的调用(通常是脚本或 AI agent)同时在 stdout 输出结构化错误。
303
+ // config ai options set 的 --json 是取值参数,不算 JSON 输出模式。
304
+ const { commandName } = parseEntryArgs(argv)
305
+ if (argv.includes('--json') && commandName !== 'config') {
306
+ process.stdout.write(`${JSON.stringify({
307
+ ok: false,
308
+ error: {
309
+ code: normalized.code || 'UNKNOWN',
310
+ message: normalized.message,
311
+ exitCode: normalized.exitCode || 1
312
+ }
313
+ }, null, 2)}\n`)
314
+ }
315
+
149
316
  logger.error(normalized.message)
150
317
 
151
318
  if (normalized.code === 'AI_EMPTY_RESPONSE') {
@@ -1,21 +1,28 @@
1
1
  import { parseArgs } from 'node:util'
2
2
  import { normalizeLegacyArgs } from '../core/cli/args.js'
3
+ import { c, ui } from '../core/cli/render.js'
4
+ import { resolveGitContext } from '../core/git/context.js'
5
+ import { GitService } from '../core/git/service.js'
6
+ import { chooseOne, input, isInteractiveTerminal } from '../core/tools/interactive.js'
7
+ import { listSwitchCandidates } from '../workflows/switch-workflow.js'
3
8
  import { logger } from '../core/tools/logger.js'
4
9
  import { runCheckoutWorkflow } from '../workflows/checkout-workflow.js'
5
10
 
6
11
  function printHelp() {
7
12
  process.stdout.write(`
8
13
  Usage:
9
- icode checkout <branch> [base] [options]
14
+ icode checkout [branch] [base] [options]
10
15
 
11
16
  Arguments:
12
- <branch> 目标分支名(必填)
17
+ [branch] 目标分支名;在终端里不传时可直接选择或新建
13
18
  [base] 新建分支基线,默认主分支
14
19
 
15
20
  Options:
16
21
  -y, --yes 自动确认(跳过交互提示)
17
- --push-origin 新建分支后立即推送到 origin
22
+ --push-origin 新建分支后立即推送到远程(当前 remote)
18
23
  --pull-main 切换后同步主分支到当前分支
24
+ --autostash 有未提交改动时自动 stash,切换后恢复
25
+ --json 输出机器可读 JSON
19
26
  --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
20
27
  --no-verify 跳过 hook/husky 校验
21
28
  -h, --help 查看帮助
@@ -25,6 +32,44 @@ Notes:
25
32
  `)
26
33
  }
27
34
 
35
+ async function pickCheckoutTarget(repoMode) {
36
+ const context = await resolveGitContext({ repoMode })
37
+ const git = new GitService(context)
38
+ const candidates = (await listSwitchCandidates(git, { all: true })).filter((item) => item.name !== context.currentBranch)
39
+ const choice = await chooseOne('切换到哪个分支?', [
40
+ { value: '__new__', label: c.green('+ 新建分支…') },
41
+ ...candidates.map((item) => ({
42
+ value: item.name,
43
+ label: item.name,
44
+ hint: [item.kind === 'remote' ? '仅远程' : '', ui.sync(item.ahead, item.behind), item.relativeDate].filter(Boolean).join(' · ')
45
+ }))
46
+ ], candidates.length ? 1 : 0)
47
+
48
+ if (choice !== '__new__') {
49
+ return { branchName: choice }
50
+ }
51
+
52
+ const prefix = await chooseOne('分支类型', [
53
+ { value: 'feature/', label: 'feature/', hint: '新功能' },
54
+ { value: 'fix/', label: 'fix/', hint: '修复' },
55
+ { value: 'hotfix/', label: 'hotfix/', hint: '线上紧急修复' },
56
+ { value: 'chore/', label: 'chore/', hint: '杂项' },
57
+ { value: 'release/', label: 'release/', hint: '发版' },
58
+ { value: '', label: '不加前缀' }
59
+ ], 0, { filter: false })
60
+ const name = await input(`分支名 ${c.gray(prefix || '')}`, '')
61
+ if (!name) {
62
+ return null
63
+ }
64
+ const bases = [...new Set([context.defaultBranch, context.currentBranch, ...candidates.filter((item) => item.kind === 'local').map((item) => item.name)])].filter(Boolean)
65
+ const base = await chooseOne('基于哪个分支创建?', bases.map((item) => ({
66
+ value: item,
67
+ label: item,
68
+ hint: item === context.defaultBranch ? '主分支' : item === context.currentBranch ? '当前分支' : ''
69
+ })), 0)
70
+ return { branchName: `${prefix}${name.replace(/\s+/g, '-')}`, baseBranchName: base }
71
+ }
72
+
28
73
  export async function runCheckoutCommand(rawArgs) {
29
74
  const args = normalizeLegacyArgs(rawArgs)
30
75
  const parsed = parseArgs({
@@ -35,25 +80,47 @@ export async function runCheckoutCommand(rawArgs) {
35
80
  help: { type: 'boolean', short: 'h', default: false },
36
81
  'push-origin': { type: 'boolean', default: false },
37
82
  'pull-main': { type: 'boolean', default: false },
83
+ autostash: { type: 'boolean', default: false },
84
+ json: { type: 'boolean', default: false },
38
85
  'repo-mode': { type: 'string', default: 'auto' },
39
86
  'no-verify': { type: 'boolean', default: false }
40
87
  }
41
88
  })
42
89
 
43
- if (parsed.values.help || parsed.positionals.length < 1) {
90
+ if (parsed.values.help) {
44
91
  printHelp()
45
92
  return
46
93
  }
47
94
 
48
- const [branchName, baseBranchName] = parsed.positionals
95
+ let [branchName, baseBranchName] = parsed.positionals
96
+ if (!branchName) {
97
+ if (parsed.values.json || !isInteractiveTerminal()) {
98
+ printHelp()
99
+ return
100
+ }
101
+ // 不传分支名时直接选择:已有分支,或新建分支(只需输入名字,基线也用选的)。
102
+ const picked = await pickCheckoutTarget(parsed.values['repo-mode'])
103
+ if (!picked) {
104
+ return
105
+ }
106
+ ;({ branchName, baseBranchName } = picked)
107
+ }
49
108
  const result = await runCheckoutWorkflow({
50
109
  branchName,
51
110
  baseBranchName,
52
111
  pushOrigin: parsed.values['push-origin'],
53
112
  pullMain: parsed.values['pull-main'],
113
+ autostash: parsed.values.autostash,
54
114
  repoMode: parsed.values['repo-mode'],
55
115
  noVerify: parsed.values['no-verify']
56
116
  })
57
117
 
58
- logger.success(`checkout 完成: ${result.branchName}`)
118
+ if (parsed.values.json) {
119
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result }, null, 2)}\n`)
120
+ return result
121
+ }
122
+
123
+ const how = { local: '', tracking: c.gray(`(跟踪 ${result.remote}/${result.branchName})`), created: c.gray(`(基于 ${result.baseBranchName} 新建)`) }[result.mode] || ''
124
+ logger.success(`已切换到 ${c.bold(ui.branch(result.branchName))}${how}`)
125
+ return result
59
126
  }
@@ -1,5 +1,7 @@
1
1
  import { parseArgs } from 'node:util'
2
2
  import { normalizeLegacyArgs } from '../core/cli/args.js'
3
+ import { printJson } from '../core/cli/output.js'
4
+ import { c, print, table, title, ui } from '../core/cli/render.js'
3
5
  import { logger } from '../core/tools/logger.js'
4
6
  import { runCleanWorkflow } from '../workflows/clean-workflow.js'
5
7
 
@@ -9,19 +11,27 @@ Usage:
9
11
  icode clean [options]
10
12
 
11
13
  Options:
12
- --merged-target <branch> 基于该分支判断“已合并”状态,默认主分支
14
+ --merged-target <branch> 基于该分支判断“已合并”状态,默认主分支(优先用远程版本)
15
+ --merged 清理已合并分支(默认;与 --gone/--stale 同用时取并集)
16
+ --gone 清理远程分支已被删除的本地分支
17
+ --stale <days> 清理超过 N 天没有新提交的本地分支
13
18
  --keep <branch|csv> 额外保留分支,可重复使用或逗号分隔
14
- --remote 同时删除远程分支
19
+ --remote 同时删除远程分支(仅限已合并的分支)
15
20
  --force 强制删除本地分支(-D)
21
+ --dry-run 只列出将被删除的分支
22
+ --json 输出机器可读 JSON
16
23
  -y, --yes 自动确认(跳过确认提示)
17
24
  --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
18
25
  -h, --help 查看帮助
19
26
 
20
27
  Notes:
21
- 默认保护当前分支/主分支/受保护分支。
28
+ 默认保护当前分支/主分支/受保护分支。不会切换分支,工作区有改动也可以执行。
29
+ 删除前会自动创建恢复点,误删可用 icode rescue 找回。
22
30
 
23
31
  Examples:
24
32
  icode clean
33
+ icode clean --gone -y
34
+ icode clean --stale 90 --dry-run
25
35
  icode clean --merged-target main --keep release,hotfix
26
36
  icode clean --remote --force -y
27
37
  `)
@@ -35,9 +45,14 @@ export async function runCleanCommand(rawArgs) {
35
45
  allowPositionals: true,
36
46
  options: {
37
47
  'merged-target': { type: 'string' },
48
+ merged: { type: 'boolean', default: false },
49
+ gone: { type: 'boolean', default: false },
50
+ stale: { type: 'string' },
38
51
  keep: { type: 'string', multiple: true, default: [] },
39
52
  remote: { type: 'boolean', default: false },
40
53
  force: { type: 'boolean', default: false },
54
+ 'dry-run': { type: 'boolean', default: false },
55
+ json: { type: 'boolean', default: false },
41
56
  yes: { type: 'boolean', short: 'y', default: false },
42
57
  'repo-mode': { type: 'string', default: 'auto' },
43
58
  help: { type: 'boolean', short: 'h', default: false }
@@ -51,18 +66,43 @@ export async function runCleanCommand(rawArgs) {
51
66
 
52
67
  const result = await runCleanWorkflow({
53
68
  mergedTarget: parsed.values['merged-target'],
69
+ merged: parsed.values.merged,
70
+ gone: parsed.values.gone,
71
+ stale: parsed.values.stale,
54
72
  keep: parsed.values.keep,
55
73
  remote: parsed.values.remote,
56
74
  force: parsed.values.force,
75
+ dryRun: parsed.values['dry-run'],
76
+ json: parsed.values.json,
57
77
  yes: parsed.values.yes,
58
78
  repoMode: parsed.values['repo-mode']
59
79
  })
60
80
 
81
+ if (parsed.values.json) {
82
+ printJson(result)
83
+ return result
84
+ }
85
+
61
86
  if (result.canceled) {
62
- return
87
+ return result
63
88
  }
64
89
 
65
- logger.success(
66
- `clean 完成: 本地删除 ${result.deletedLocal.length} 个, 远程删除 ${result.deletedRemote.length} 个`
67
- )
90
+ if (result.dryRun) {
91
+ print(title(`将删除 ${result.candidates.length} 个分支(dry-run)`))
92
+ print(table(result.candidates, [
93
+ { key: 'branch', label: '分支', format: (value) => ui.branch(value) },
94
+ { key: 'reasons', label: '原因', format: (value) => c.gray(value.join(';')) }
95
+ ]))
96
+ return result
97
+ }
98
+
99
+ ;(result.failed || []).forEach((item) => logger.warn(`删除失败: ${ui.branch(item.branch)} ${c.gray(item.reason)}`))
100
+ if (!result.deletedLocal.length && !result.failed?.length) {
101
+ return result
102
+ }
103
+ logger.success(`已删除本地分支 ${c.bold(String(result.deletedLocal.length))} 个${result.deletedRemote.length ? `,远程分支 ${c.bold(String(result.deletedRemote.length))} 个` : ''}`)
104
+ if (result.backupId) {
105
+ logger.hint(`误删可用 icode rescue restore ${result.backupId} 找回`)
106
+ }
107
+ return result
68
108
  }