@icode-js/icode 3.0.4 → 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 (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -5
  3. package/bin/icode.js +4 -4
  4. package/package.json +1 -1
  5. package/src/cli.js +60 -23
  6. package/src/commands/ai.js +4 -4
  7. package/src/commands/checkout.js +2 -2
  8. package/src/commands/clean.js +5 -2
  9. package/src/commands/codereview.js +5 -2
  10. package/src/commands/completion.js +93 -0
  11. package/src/commands/config.js +123 -10
  12. package/src/commands/doctor.js +80 -0
  13. package/src/commands/explain.js +6 -3
  14. package/src/commands/help.js +17 -0
  15. package/src/commands/info.js +5 -2
  16. package/src/commands/migrate.js +40 -3
  17. package/src/commands/push.js +72 -24
  18. package/src/commands/ready.js +110 -0
  19. package/src/commands/save.js +98 -0
  20. package/src/commands/sync.js +2 -2
  21. package/src/commands/tag.js +2 -2
  22. package/src/commands/undo.js +73 -10
  23. package/src/core/{ai-client.js → ai/client.js} +3 -3
  24. package/src/core/{ai-config.js → ai/config.js} +2 -2
  25. package/src/core/ai/diff-prompt.js +204 -0
  26. package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
  27. package/src/core/{args.js → cli/args.js} +23 -2
  28. package/src/core/cli/completion.js +720 -0
  29. package/src/core/cli/update-notifier.js +138 -0
  30. package/src/core/{shell.js → commands/shell.js} +13 -1
  31. package/src/core/{config-store.js → config/store.js} +53 -3
  32. package/src/core/{git-context.js → git/context.js} +24 -10
  33. package/src/core/git/preconditions.js +88 -0
  34. package/src/core/{git-service.js → git/service.js} +195 -5
  35. package/src/core/remote-merge/client.js +42 -0
  36. package/src/core/remote-merge/providers/gitlab.js +596 -0
  37. package/src/core/remote-merge/shared.js +49 -0
  38. package/src/core/tools/interactive.js +368 -0
  39. package/src/core/{loading.js → tools/loading.js} +5 -1
  40. package/src/core/{logger.js → tools/logger.js} +5 -0
  41. package/src/workflows/ai-codereview-workflow.js +18 -6
  42. package/src/workflows/ai-commit-workflow.js +20 -12
  43. package/src/workflows/ai-conflict-workflow.js +4 -4
  44. package/src/workflows/ai-explain-workflow.js +4 -4
  45. package/src/workflows/ai-risk-review-workflow.js +1 -1
  46. package/src/workflows/checkout-workflow.js +17 -5
  47. package/src/workflows/clean-workflow.js +92 -75
  48. package/src/workflows/doctor-workflow.js +131 -0
  49. package/src/workflows/info-workflow.js +4 -4
  50. package/src/workflows/migrate-workflow.js +269 -62
  51. package/src/workflows/push-workflow.js +475 -58
  52. package/src/workflows/ready-workflow.js +278 -0
  53. package/src/workflows/rollback-workflow.js +24 -9
  54. package/src/workflows/save-workflow.js +152 -0
  55. package/src/workflows/sync-workflow.js +22 -6
  56. package/src/workflows/tag-workflow.js +4 -4
  57. package/src/workflows/undo-workflow.js +37 -12
  58. package/src/core/prompts.js +0 -152
  59. /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
  60. /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
  61. /package/src/core/{hook-diagnostics.js → git/hook-diagnostics.js} +0 -0
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path'
2
2
  import { parseArgs } from 'node:util'
3
- import { askAi } from '../core/ai-client.js'
3
+ import { askAi } from '../core/ai/client.js'
4
4
  import {
5
5
  getAiCommandOptions,
6
6
  getAiProfileForDisplay,
@@ -11,28 +11,31 @@ import {
11
11
  upsertAiCommandOptions,
12
12
  upsertAiProfile,
13
13
  useAiProfile
14
- } from '../core/ai-config.js'
14
+ } from '../core/ai/config.js'
15
15
  import {
16
16
  deleteValue,
17
17
  getConfigFilePath,
18
+ getPlatformConfig,
18
19
  getRepoPolicy,
19
20
  getValue,
20
21
  readConfig,
22
+ setPlatformConfig,
21
23
  setRepoPolicy,
22
24
  setValue
23
- } from '../core/config-store.js'
24
- import { parseConfigValue } from '../core/args.js'
25
+ } from '../core/config/store.js'
26
+ import { normalizeLegacyArgs, parseConfigValue } from '../core/cli/args.js'
25
27
  import { IcodeError } from '../core/errors.js'
26
- import { resolveGitContext } from '../core/git-context.js'
27
- import { logger } from '../core/logger.js'
28
+ import { resolveGitContext } from '../core/git/context.js'
29
+ import { logger } from '../core/tools/logger.js'
28
30
 
31
+ // 输出 config 命令帮助,并展示当前支持的远程合并配置方式。
29
32
  function printHelp() {
30
33
  process.stdout.write(`
31
34
  Usage:
32
35
  icode config <command> [...args]
33
36
 
34
37
  Arguments:
35
- <command> 子命令(list/get/set/delete/ai/protect)
38
+ <command> 子命令(list/get/set/delete/ai/protect/platform)
36
39
 
37
40
  Commands:
38
41
  list 查看全部配置
@@ -43,6 +46,7 @@ Commands:
43
46
  protect list 查看受保护分支
44
47
  protect add <branch...> 添加受保护分支
45
48
  protect remove <branch...> 移除受保护分支
49
+ platform <subcommand> 平台级配置(如远程合并密钥)
46
50
 
47
51
  Options:
48
52
  --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承),仅影响 protect
@@ -52,11 +56,112 @@ Examples:
52
56
  icode config set defaults.repoMode strict
53
57
  icode config get defaults.repoMode
54
58
  icode config protect add main release
59
+ icode config platform remote-merge set --provider gitlab --api-key rm_xxx
55
60
  icode config ai set openai --format openai --base-url https://api.openai.com/v1 --api-key sk-xxx --model gpt-4o-mini --activate
56
61
  icode config ai set ollama --format ollama --base-url http://127.0.0.1:11434 --model qwen2.5:7b --activate
57
62
  `)
58
63
  }
59
64
 
65
+ function maskSecret(secret) {
66
+ const raw = String(secret || '').trim()
67
+ if (!raw) {
68
+ return ''
69
+ }
70
+ if (raw.length <= 8) {
71
+ return `${raw.slice(0, 2)}****${raw.slice(-1)}`
72
+ }
73
+ return `${raw.slice(0, 4)}****${raw.slice(-4)}`
74
+ }
75
+
76
+ // 输出 platform 子命令帮助,明确 remote-merge 仅需平台名与密钥。
77
+ function printPlatformHelp() {
78
+ process.stdout.write(`
79
+ Usage:
80
+ icode config platform <subcommand> [options]
81
+
82
+ Subcommands:
83
+ remote-merge show
84
+ remote-merge set --provider gitlab --api-key <key>
85
+
86
+ Examples:
87
+ icode config platform remote-merge show
88
+ icode config platform remote-merge set --provider gitlab --api-key rm_xxx
89
+ `)
90
+ }
91
+
92
+ // 处理平台级远程合并配置,当前只接受 GitLab 平台密钥写入。
93
+ async function runPlatformConfigCommand(args) {
94
+ if (!args.length || args[0] === '-h' || args[0] === '--help') {
95
+ printPlatformHelp()
96
+ return
97
+ }
98
+
99
+ const [domain, action, ...rest] = args
100
+ if (domain !== 'remote-merge') {
101
+ throw new IcodeError(`未知 platform 子命令: ${domain}`, {
102
+ code: 'CONFIG_PLATFORM_UNKNOWN_SUBCOMMAND',
103
+ exitCode: 2
104
+ })
105
+ }
106
+
107
+ if (action === 'show') {
108
+ const config = getPlatformConfig('remoteMerge')
109
+ process.stdout.write(`${JSON.stringify({
110
+ provider: config.provider || '',
111
+ hasApiKey: Boolean(config.apiKey),
112
+ apiKeyMasked: maskSecret(config.apiKey)
113
+ }, null, 2)}\n`)
114
+ return
115
+ }
116
+
117
+ if (action === 'set') {
118
+ const parsed = parseArgs({
119
+ args: rest,
120
+ allowPositionals: true,
121
+ options: {
122
+ provider: { type: 'string' },
123
+ 'api-key': { type: 'string' }
124
+ }
125
+ })
126
+
127
+ const provider = String(parsed.values.provider || '').trim()
128
+ const apiKey = String(parsed.values['api-key'] || '').trim()
129
+
130
+ if (!provider) {
131
+ throw new IcodeError('缺少 --provider,示例: icode config platform remote-merge set --provider gitlab --api-key xxx', {
132
+ code: 'CONFIG_PLATFORM_PROVIDER_REQUIRED',
133
+ exitCode: 2
134
+ })
135
+ }
136
+
137
+ if (!apiKey) {
138
+ throw new IcodeError('缺少 --api-key,示例: icode config platform remote-merge set --provider gitlab --api-key xxx', {
139
+ code: 'CONFIG_PLATFORM_API_KEY_REQUIRED',
140
+ exitCode: 2
141
+ })
142
+ }
143
+
144
+ if (provider !== 'gitlab') {
145
+ throw new IcodeError(`platform remote-merge 当前仅支持 gitlab,收到 provider=${provider}`, {
146
+ code: 'CONFIG_PLATFORM_PROVIDER_UNSUPPORTED',
147
+ exitCode: 2
148
+ })
149
+ }
150
+
151
+ setPlatformConfig('remoteMerge', {
152
+ provider,
153
+ apiKey
154
+ })
155
+ logger.success(`平台远程合并密钥已写入: ${provider}`)
156
+ return
157
+ }
158
+
159
+ throw new IcodeError('platform remote-merge 仅支持: show | set', {
160
+ code: 'CONFIG_PLATFORM_INVALID_ACTION',
161
+ exitCode: 2
162
+ })
163
+ }
164
+
60
165
  function printAiHelp() {
61
166
  process.stdout.write(`
62
167
  Usage:
@@ -380,14 +485,22 @@ async function resolvePolicyRoot(repoMode) {
380
485
  }
381
486
  }
382
487
 
488
+ // 执行 config 子命令,输入原始参数,输出配置读写结果;参数会先归一化。
383
489
  export async function runConfigCommand(rawArgs) {
384
- if (rawArgs[0] === 'ai') {
385
- await runAiConfigCommand(rawArgs.slice(1))
490
+ const args = normalizeLegacyArgs(rawArgs)
491
+
492
+ if (args[0] === 'ai') {
493
+ await runAiConfigCommand(args.slice(1))
494
+ return
495
+ }
496
+
497
+ if (args[0] === 'platform') {
498
+ await runPlatformConfigCommand(args.slice(1))
386
499
  return
387
500
  }
388
501
 
389
502
  const parsed = parseArgs({
390
- args: rawArgs,
503
+ args,
391
504
  allowPositionals: true,
392
505
  options: {
393
506
  help: { type: 'boolean', short: 'h', default: false },
@@ -0,0 +1,80 @@
1
+ import { parseArgs } from 'node:util'
2
+ import { normalizeLegacyArgs } from '../core/cli/args.js'
3
+ import { logger } from '../core/tools/logger.js'
4
+ import { runDoctorWorkflow } from '../workflows/doctor-workflow.js'
5
+
6
+ // 输出 doctor 子命令帮助,输入为空,输出标准帮助文本。
7
+ function printHelp() {
8
+ process.stdout.write(`
9
+ Usage:
10
+ icode doctor [options]
11
+
12
+ Options:
13
+ --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
14
+ --json 输出机器可读 JSON
15
+ -h, --help 查看帮助
16
+
17
+ Notes:
18
+ 检查 Git、origin、hooks、AI profile 与远程合并配置,并给出修复建议。
19
+ `)
20
+ }
21
+
22
+ // 根据检查状态选择输出函数,输入状态,输出 logger 方法。
23
+ function resolveLogMethod(status) {
24
+ if (status === 'fail') {
25
+ return logger.error
26
+ }
27
+ if (status === 'warn') {
28
+ return logger.warn
29
+ }
30
+ return logger.success
31
+ }
32
+
33
+ // 输出单条 doctor 检查结果,输入检查项,输出彩色日志。
34
+ function printCheck(check) {
35
+ const log = resolveLogMethod(check.status).bind(logger)
36
+ const statusLabel = check.status.toUpperCase()
37
+ log(`[${statusLabel}] ${check.title}: ${check.detail}`)
38
+ if (check.suggestion) {
39
+ logger.info(`建议: ${check.suggestion}`)
40
+ }
41
+ }
42
+
43
+ // 执行 doctor 子命令,输入原始参数,输出健康检查结果。
44
+ // 执行 doctor 子命令,输入原始参数,输出健康检查结果;参数会先归一化。
45
+ export async function runDoctorCommand(rawArgs) {
46
+ const args = normalizeLegacyArgs(rawArgs)
47
+ const parsed = parseArgs({
48
+ args,
49
+ allowPositionals: true,
50
+ options: {
51
+ 'repo-mode': { type: 'string', default: 'auto' },
52
+ json: { type: 'boolean', default: false },
53
+ help: { type: 'boolean', short: 'h', default: false }
54
+ }
55
+ })
56
+
57
+ if (parsed.values.help) {
58
+ printHelp()
59
+ return
60
+ }
61
+
62
+ const result = await runDoctorWorkflow({
63
+ repoMode: parsed.values['repo-mode']
64
+ })
65
+
66
+ if (parsed.values.json) {
67
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
68
+ return result
69
+ }
70
+
71
+ logger.info(`仓库根目录: ${result.context.topLevelPath}`)
72
+ logger.info(`配置文件: ${result.configPath}`)
73
+ result.checks.forEach((check) => {
74
+ printCheck(check)
75
+ })
76
+
77
+ const summary = result.summary
78
+ logger.info(`doctor 完成: pass=${summary.pass}, warn=${summary.warn}, fail=${summary.fail}`)
79
+ return result
80
+ }
@@ -1,6 +1,7 @@
1
1
  import { parseArgs } from 'node:util'
2
- import { getAiCommandOptions } from '../core/ai-config.js'
3
- import { logger } from '../core/logger.js'
2
+ import { getAiCommandOptions } from '../core/ai/config.js'
3
+ import { normalizeLegacyArgs } from '../core/cli/args.js'
4
+ import { logger } from '../core/tools/logger.js'
4
5
  import { runAiExplainWorkflow } from '../workflows/ai-explain-workflow.js'
5
6
 
6
7
  function printHelp() {
@@ -47,10 +48,12 @@ function resolveStringOption(cliValue, configValue, fallback = '') {
47
48
  return fallback
48
49
  }
49
50
 
51
+ // 执行 explain 子命令,输入原始参数,输出 AI 对 diff 的解释;参数会先归一化。
50
52
  export async function runExplainCommand(rawArgs) {
51
53
  const scopedOptions = getAiCommandOptions('explain')
54
+ const args = normalizeLegacyArgs(rawArgs)
52
55
  const parsed = parseArgs({
53
- args: rawArgs,
56
+ args,
54
57
  allowPositionals: true,
55
58
  options: {
56
59
  base: { type: 'string' },
@@ -1,3 +1,7 @@
1
+ /**
2
+ * 输出主帮助文本,集中展示命令总览与常用示例。
3
+ * 输入为空,输出为标准输出文本。
4
+ */
1
5
  export function printMainHelp() {
2
6
  process.stdout.write(`
3
7
  icode v3 - Git workflow CLI
@@ -9,6 +13,8 @@ Commands:
9
13
  ai AI 助手能力(提交信息/冲突建议/代码评审)
10
14
  codereview AI 代码评审(不带 ai 前缀)
11
15
  checkout 切换/创建分支(本地/远程自动识别)
16
+ ready 检查仓库是否适合继续 save/push(适合 AI agent 读取)
17
+ save 安全快速提交本地改动(检查 + add + commit)
12
18
  push 提交并推送,可合并到多个目标分支
13
19
  sync 批量同步分支(fetch + pull)
14
20
  clean 清理已合并分支(可删远程)
@@ -16,8 +22,10 @@ Commands:
16
22
  migrate 迁移分支提交(cherry-pick)
17
23
  tag 创建并推送 tag(支持自动命名)
18
24
  config 查看和修改本地配置(含 AI profile)
25
+ doctor 检查 Git/AI/远程合并配置健康状态
19
26
  explain AI 解释 Git diff(自然语言)
20
27
  info 查看当前 git 与配置环境
28
+ completion 生成 bash/zsh tab 补全脚本
21
29
  help 查看帮助(命令总览)
22
30
 
23
31
  Global options:
@@ -29,6 +37,11 @@ Tips:
29
37
 
30
38
  Examples:
31
39
  icode checkout feature/login main --push-origin
40
+ icode ready
41
+ icode ready --json
42
+ icode save -m "feat: add login form"
43
+ icode save --ai-commit -y
44
+ icode save -m "fix: adjust copy" --dry-run
32
45
  icode ai commit --apply -y
33
46
  icode ai codereview
34
47
  icode codereview --base origin/main --head HEAD
@@ -36,14 +49,18 @@ Examples:
36
49
  icode push release test -m "feat: batch publish" -y
37
50
  icode push release test --ai-commit -y
38
51
  icode push release test -m "feat: keep merge commit" --local-merge -y
52
+ icode push release test --dry-run
39
53
  icode sync --all-local --merge-main
40
54
  icode clean --remote --force -y
55
+ icode undo a1b2c3d --mode revert -y
41
56
  icode undo --mode revert --ref HEAD~1 -y
42
57
  icode undo --recover abort
43
58
  icode migrate feature/login release --push -y
44
59
  icode migrate --interactive
45
60
  icode push --no-verify -m "chore: bypass hooks"
46
61
  icode config protect add main release
62
+ icode doctor
63
+ source <(icode completion zsh)
47
64
  icode info
48
65
  `)
49
66
  }
@@ -1,5 +1,6 @@
1
1
  import { parseArgs } from 'node:util'
2
- import { logger } from '../core/logger.js'
2
+ import { normalizeLegacyArgs } from '../core/cli/args.js'
3
+ import { logger } from '../core/tools/logger.js'
3
4
  import { runInfoWorkflow } from '../workflows/info-workflow.js'
4
5
 
5
6
  function printHelp() {
@@ -16,9 +17,11 @@ Notes:
16
17
  `)
17
18
  }
18
19
 
20
+ // 执行 info 子命令,输入原始参数,输出仓库与配置概览;参数会先归一化。
19
21
  export async function runInfoCommand(rawArgs) {
22
+ const args = normalizeLegacyArgs(rawArgs)
20
23
  const parsed = parseArgs({
21
- args: rawArgs,
24
+ args,
22
25
  allowPositionals: true,
23
26
  options: {
24
27
  'repo-mode': { type: 'string', default: 'auto' },
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from 'node:util'
2
- import { normalizeLegacyArgs } from '../core/args.js'
2
+ import { normalizeLegacyArgs } from '../core/cli/args.js'
3
3
  import { IcodeError } from '../core/errors.js'
4
- import { logger } from '../core/logger.js'
4
+ import { logger } from '../core/tools/logger.js'
5
5
  import { runMigrateWorkflow } from '../workflows/migrate-workflow.js'
6
6
 
7
7
  function printHelp() {
@@ -17,6 +17,8 @@ Arguments:
17
17
  Options:
18
18
  -i, --interactive 交互模式(单选/多选 source/target 与迁移范围)
19
19
  --range <spec> 指定提交范围,例如 main..feature-x(交互模式下若提供则直接使用)
20
+ --dry-run 只展示迁移计划,不 checkout/pull/cherry-pick/push
21
+ --json 输出机器可读 JSON,适合 AI agent 或脚本消费
20
22
  --push 迁移后自动推送 target 分支
21
23
  -y, --yes 自动确认(跳过确认提示)
22
24
  --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
@@ -29,11 +31,32 @@ Notes:
29
31
  Examples:
30
32
  icode migrate feature/login release
31
33
  icode migrate --interactive
34
+ icode migrate feature/login release --dry-run --json
32
35
  icode migrate feature/login release --range HEAD~3..feature/login
33
36
  icode migrate feature/login release --push -y
34
37
  `)
35
38
  }
36
39
 
40
+ // 输出 migrate dry-run 计划,输入 workflow 结果,输出人类可读步骤。
41
+ function printDryRunPlan(result) {
42
+ const previewLimit = 10
43
+ const previewCommits = result.commits.slice(0, previewLimit)
44
+
45
+ logger.info(`[dry-run] source: ${result.sourceBranch} (${result.sourceRef})`)
46
+ logger.info(`[dry-run] target: ${result.targetBranch} (${result.targetRef})`)
47
+ logger.info(`[dry-run] range: ${result.rangeSpec}`)
48
+ logger.info(`[dry-run] commits: ${result.migratedCommits}`)
49
+ previewCommits.forEach((commit, index) => {
50
+ logger.info(`[dry-run] commit ${index + 1}: ${commit.summary || commit.hash}`)
51
+ })
52
+ if (result.commits.length > previewLimit) {
53
+ logger.info(`[dry-run] ... 还有 ${result.commits.length - previewLimit} 个提交未显示,可用 --json 查看完整计划`)
54
+ }
55
+ result.steps.forEach((step, index) => {
56
+ logger.info(`[dry-run] ${index + 1}. ${step.action}: ${step.detail}`)
57
+ })
58
+ }
59
+
37
60
  export async function runMigrateCommand(rawArgs) {
38
61
  const args = normalizeLegacyArgs(rawArgs)
39
62
  const parsed = parseArgs({
@@ -42,6 +65,8 @@ export async function runMigrateCommand(rawArgs) {
42
65
  options: {
43
66
  interactive: { type: 'boolean', short: 'i', default: false },
44
67
  range: { type: 'string' },
68
+ 'dry-run': { type: 'boolean', default: false },
69
+ json: { type: 'boolean', default: false },
45
70
  push: { type: 'boolean', default: false },
46
71
  yes: { type: 'boolean', short: 'y', default: false },
47
72
  'repo-mode': { type: 'string', default: 'auto' },
@@ -70,16 +95,28 @@ export async function runMigrateCommand(rawArgs) {
70
95
  targetBranch,
71
96
  interactive,
72
97
  range: parsed.values.range,
98
+ dryRun: parsed.values['dry-run'],
73
99
  push: parsed.values.push,
74
100
  yes: parsed.values.yes,
75
101
  repoMode: parsed.values['repo-mode'],
76
- noVerify: parsed.values['no-verify']
102
+ noVerify: parsed.values['no-verify'],
103
+ silentLog: parsed.values.json
77
104
  })
78
105
 
106
+ if (parsed.values.json) {
107
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
108
+ return result
109
+ }
110
+
79
111
  if (result.canceled) {
80
112
  return
81
113
  }
82
114
 
115
+ if (result.dryRun) {
116
+ printDryRunPlan(result)
117
+ return result
118
+ }
119
+
83
120
  logger.success(
84
121
  `migrate 完成: ${result.sourceBranch} -> ${result.targetBranch}, commits=${result.migratedCommits}, mode=${result.rangeMode || 'range'}`
85
122
  )
@@ -1,15 +1,33 @@
1
1
  import { parseArgs } from 'node:util'
2
- import { normalizeLegacyArgs } from '../core/args.js'
3
- import { getAiCommandOptions } from '../core/ai-config.js'
4
- import { logger } from '../core/logger.js'
2
+ import { getAiCommandOptions } from '../core/ai/config.js'
3
+ import { normalizeLegacyArgs } from '../core/cli/args.js'
4
+ import { logger } from '../core/tools/logger.js'
5
5
  import { runPushWorkflow } from '../workflows/push-workflow.js'
6
6
 
7
+ const PUSH_PARSE_OPTIONS = {
8
+ message: { type: 'string', short: 'm' },
9
+ yes: { type: 'boolean', short: 'y' },
10
+ 'local-merge': { type: 'boolean' },
11
+ 'remote-merge': { type: 'boolean', short: 'r' },
12
+ 'dry-run': { type: 'boolean' },
13
+ 'ai-commit': { type: 'boolean' },
14
+ 'ai-profile': { type: 'string' },
15
+ help: { type: 'boolean', short: 'h' },
16
+ 'pull-main': { type: 'boolean' },
17
+ 'not-push-current': { type: 'boolean' },
18
+ 'force-protected': { type: 'boolean' },
19
+ 'repo-mode': { type: 'string' },
20
+ 'no-verify': { type: 'boolean' }
21
+ }
22
+
7
23
  function formatBranchStatus(status) {
8
24
  const map = {
9
25
  pushed: '已推送',
10
26
  'merged-and-pushed': '已合并并推送',
27
+ 'remote-merged-and-pushed': '已远程合并并推送',
11
28
  'skipped-protected': '已跳过(受保护)',
12
- 'skipped-missing-remote': '已跳过(远程分支不存在)'
29
+ 'skipped-missing-remote': '已跳过(远程分支不存在)',
30
+ paused: '已暂停'
13
31
  }
14
32
 
15
33
  return map[status] || status
@@ -27,6 +45,8 @@ Options:
27
45
  -m, --message <msg> 提交信息(未填会提示输入)
28
46
  -y, --yes 自动确认(跳过确认提示)
29
47
  --local-merge 使用本地 merge 模式(默认,会切换分支并生成 merge commit)
48
+ -r, --remote-merge 使用远程 PR/MR 合并模式(默认启用项目远程合并)
49
+ --dry-run 只展示计划,不执行 commit/fetch/pull/push/merge/MR
30
50
  --ai-commit push 前自动执行 AI commit(会参考本地 hook/commitlint 规范)
31
51
  --ai-profile <name> 指定 AI profile(用于 --ai-commit)
32
52
  --pull-main 提交前将主分支同步到当前分支
@@ -39,6 +59,8 @@ Options:
39
59
  Notes:
40
60
  默认使用本地 merge 模式。
41
61
  未指定 target 时默认处理当前分支。
62
+ 远程 merge 通过 PR/MR API 发起,不会本地切换到目标分支。
63
+ 远程 merge 若出现冲突会暂停流程,并保留明确失败原因。
42
64
  布尔开关仅在命令行显式传入时生效(如 --ai-commit / --pull-main / --no-verify / -y)。
43
65
  `)
44
66
  }
@@ -69,12 +91,33 @@ function resolveStringOption(cliValue, configValue, fallback = '') {
69
91
  return fallback
70
92
  }
71
93
 
94
+ // 统一输出 push 结果摘要,失败分支会追加具体原因,方便定位问题目标分支。
95
+ function printPushSummary(summary = []) {
96
+ summary.forEach((item) => {
97
+ const reason = typeof item.reason === 'string' && item.reason.trim() ? ` - ${item.reason.trim()}` : ''
98
+ logger.info(`[结果] ${item.branch}: ${formatBranchStatus(item.status)}${reason}`)
99
+ })
100
+ }
101
+
102
+ // 输出 dry-run 计划摘要,输入 workflow 返回的计划,输出对用户可读的阶段列表。
103
+ function printDryRunPlan(plan) {
104
+ logger.info(`[dry-run] 当前分支: ${plan.currentBranch}`)
105
+ logger.info(`[dry-run] 目标分支: ${plan.branchTargets.join(', ')}`)
106
+ logger.info(`[dry-run] 合并模式: ${plan.remoteMerge ? 'remote-merge' : 'local-merge'}`)
107
+ plan.steps.forEach((step, index) => {
108
+ const reason = step.reason ? ` - ${step.reason}` : ''
109
+ logger.info(`[dry-run] ${index + 1}. ${step.action}: ${step.branch}${reason}`)
110
+ })
111
+ }
112
+
72
113
  export function resolvePushWorkflowOptions(parsedValues, parsedPositionals, scopedOptions = {}) {
73
114
  return {
74
115
  targetBranches: parsedPositionals,
75
116
  message: parsedValues.message,
76
117
  // 显式传入开关才生效,避免配置项隐式开启 push 行为。
77
118
  yes: resolveBooleanOption(parsedValues.yes, false),
119
+ remoteMerge: resolveBooleanOption(parsedValues['remote-merge'], false),
120
+ dryRun: resolveBooleanOption(parsedValues['dry-run'], false),
78
121
  aiCommit: resolveBooleanOption(parsedValues['ai-commit'], false),
79
122
  aiCommitLang: resolveStringOption(undefined, scopedOptions.aiCommitLang, 'zh'),
80
123
  aiProfile: resolveStringOption(parsedValues['ai-profile'], scopedOptions.aiProfile, ''),
@@ -86,40 +129,45 @@ export function resolvePushWorkflowOptions(parsedValues, parsedPositionals, scop
86
129
  }
87
130
  }
88
131
 
89
- export async function runPushCommand(rawArgs) {
132
+ // 解析 push 子命令参数;rawArgs 是不含命令名的参数数组,返回 node:util parseArgs 的结构化结果。
133
+ export function parsePushCommandArgs(rawArgs) {
90
134
  const args = normalizeLegacyArgs(rawArgs)
91
- const scopedOptions = getAiCommandOptions('push')
92
- const parsed = parseArgs({
135
+ return parseArgs({
93
136
  args,
94
137
  allowPositionals: true,
95
- options: {
96
- message: { type: 'string', short: 'm' },
97
- yes: { type: 'boolean', short: 'y' },
98
- 'local-merge': { type: 'boolean' },
99
- 'ai-commit': { type: 'boolean' },
100
- 'ai-profile': { type: 'string' },
101
- help: { type: 'boolean', short: 'h' },
102
- 'pull-main': { type: 'boolean' },
103
- 'not-push-current': { type: 'boolean' },
104
- 'force-protected': { type: 'boolean' },
105
- 'repo-mode': { type: 'string' },
106
- 'no-verify': { type: 'boolean' }
107
- }
138
+ options: PUSH_PARSE_OPTIONS
108
139
  })
140
+ }
141
+
142
+ // 执行 push 子命令;rawArgs 是不含命令名的参数数组,输出结果摘要并保留 workflow 抛出的失败信息。
143
+ export async function runPushCommand(rawArgs) {
144
+ const scopedOptions = getAiCommandOptions('push')
145
+ const parsed = parsePushCommandArgs(rawArgs)
109
146
 
110
147
  if (parsed.values.help) {
111
148
  printHelp()
112
149
  return
113
150
  }
114
151
 
115
- const result = await runPushWorkflow(resolvePushWorkflowOptions(parsed.values, parsed.positionals, scopedOptions))
152
+ let result
153
+ try {
154
+ result = await runPushWorkflow(resolvePushWorkflowOptions(parsed.values, parsed.positionals, scopedOptions))
155
+ } catch (error) {
156
+ if (Array.isArray(error?.meta?.summary) && error.meta.summary.length) {
157
+ printPushSummary(error.meta.summary)
158
+ }
159
+ throw error
160
+ }
116
161
 
117
162
  if (result.canceled) {
118
163
  return
119
164
  }
120
165
 
121
- result.summary.forEach((item) => {
122
- logger.info(`[结果] ${item.branch}: ${formatBranchStatus(item.status)}`)
123
- })
166
+ if (result.dryRun) {
167
+ printDryRunPlan(result.plan)
168
+ return
169
+ }
170
+
171
+ printPushSummary(result.summary)
124
172
  logger.success(`push 完成,共处理 ${result.summary.length} 个分支。`)
125
173
  }