@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
@@ -4,6 +4,22 @@ import { normalizeLegacyArgs } from '../core/cli/args.js'
4
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: '已推送',
@@ -29,7 +45,8 @@ Options:
29
45
  -m, --message <msg> 提交信息(未填会提示输入)
30
46
  -y, --yes 自动确认(跳过确认提示)
31
47
  --local-merge 使用本地 merge 模式(默认,会切换分支并生成 merge commit)
32
- --remote-merge 使用远程 PR/MR 合并模式(默认启用项目远程合并)
48
+ -r, --remote-merge 使用远程 PR/MR 合并模式(默认启用项目远程合并)
49
+ --dry-run 只展示计划,不执行 commit/fetch/pull/push/merge/MR
33
50
  --ai-commit push 前自动执行 AI commit(会参考本地 hook/commitlint 规范)
34
51
  --ai-profile <name> 指定 AI profile(用于 --ai-commit)
35
52
  --pull-main 提交前将主分支同步到当前分支
@@ -82,6 +99,17 @@ function printPushSummary(summary = []) {
82
99
  })
83
100
  }
84
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
+
85
113
  export function resolvePushWorkflowOptions(parsedValues, parsedPositionals, scopedOptions = {}) {
86
114
  return {
87
115
  targetBranches: parsedPositionals,
@@ -89,6 +117,7 @@ export function resolvePushWorkflowOptions(parsedValues, parsedPositionals, scop
89
117
  // 显式传入开关才生效,避免配置项隐式开启 push 行为。
90
118
  yes: resolveBooleanOption(parsedValues.yes, false),
91
119
  remoteMerge: resolveBooleanOption(parsedValues['remote-merge'], false),
120
+ dryRun: resolveBooleanOption(parsedValues['dry-run'], false),
92
121
  aiCommit: resolveBooleanOption(parsedValues['ai-commit'], false),
93
122
  aiCommitLang: resolveStringOption(undefined, scopedOptions.aiCommitLang, 'zh'),
94
123
  aiProfile: resolveStringOption(parsedValues['ai-profile'], scopedOptions.aiProfile, ''),
@@ -100,27 +129,20 @@ export function resolvePushWorkflowOptions(parsedValues, parsedPositionals, scop
100
129
  }
101
130
  }
102
131
 
103
- export async function runPushCommand(rawArgs) {
132
+ // 解析 push 子命令参数;rawArgs 是不含命令名的参数数组,返回 node:util parseArgs 的结构化结果。
133
+ export function parsePushCommandArgs(rawArgs) {
104
134
  const args = normalizeLegacyArgs(rawArgs)
105
- const scopedOptions = getAiCommandOptions('push')
106
- const parsed = parseArgs({
135
+ return parseArgs({
107
136
  args,
108
137
  allowPositionals: true,
109
- options: {
110
- message: { type: 'string', short: 'm' },
111
- yes: { type: 'boolean', short: 'y' },
112
- 'local-merge': { type: 'boolean' },
113
- 'remote-merge': { type: 'boolean' },
114
- 'ai-commit': { type: 'boolean' },
115
- 'ai-profile': { type: 'string' },
116
- help: { type: 'boolean', short: 'h' },
117
- 'pull-main': { type: 'boolean' },
118
- 'not-push-current': { type: 'boolean' },
119
- 'force-protected': { type: 'boolean' },
120
- 'repo-mode': { type: 'string' },
121
- 'no-verify': { type: 'boolean' }
122
- }
138
+ options: PUSH_PARSE_OPTIONS
123
139
  })
140
+ }
141
+
142
+ // 执行 push 子命令;rawArgs 是不含命令名的参数数组,输出结果摘要并保留 workflow 抛出的失败信息。
143
+ export async function runPushCommand(rawArgs) {
144
+ const scopedOptions = getAiCommandOptions('push')
145
+ const parsed = parsePushCommandArgs(rawArgs)
124
146
 
125
147
  if (parsed.values.help) {
126
148
  printHelp()
@@ -141,6 +163,11 @@ export async function runPushCommand(rawArgs) {
141
163
  return
142
164
  }
143
165
 
166
+ if (result.dryRun) {
167
+ printDryRunPlan(result.plan)
168
+ return
169
+ }
170
+
144
171
  printPushSummary(result.summary)
145
172
  logger.success(`push 完成,共处理 ${result.summary.length} 个分支。`)
146
173
  }
@@ -0,0 +1,110 @@
1
+ import { parseArgs } from 'node:util'
2
+ import { normalizeLegacyArgs } from '../core/cli/args.js'
3
+ import { logger } from '../core/tools/logger.js'
4
+ import { withSpinner } from '../core/tools/loading.js'
5
+ import { runReadyWorkflow } from '../workflows/ready-workflow.js'
6
+
7
+ /**
8
+ * 输出 ready 子命令帮助。
9
+ * 输入为空,输出标准帮助文本。
10
+ */
11
+ function printHelp() {
12
+ process.stdout.write(`
13
+ Usage:
14
+ icode ready [options]
15
+
16
+ Options:
17
+ --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
18
+ --json 输出机器可读 JSON,适合 AI agent 或脚本消费
19
+ -h, --help 查看帮助
20
+
21
+ Notes:
22
+ ready 用于判断当前仓库是否适合继续 save/push/remote-merge,不会修改 Git 状态。
23
+ `)
24
+ }
25
+
26
+ /**
27
+ * 根据检查状态选择彩色输出函数。
28
+ * 输入为 pass/warn/fail,输出 logger 方法。
29
+ */
30
+ function resolveLogMethod(status) {
31
+ if (status === 'fail') {
32
+ return logger.error
33
+ }
34
+ if (status === 'warn') {
35
+ return logger.warn
36
+ }
37
+ return logger.success
38
+ }
39
+
40
+ /**
41
+ * 输出单条 ready 检查结果。
42
+ * 输入为检查项,输出彩色日志和下一步建议。
43
+ */
44
+ function printCheck(check) {
45
+ const log = resolveLogMethod(check.status).bind(logger)
46
+ const statusLabel = check.status.toUpperCase()
47
+ log(`[${statusLabel}] ${check.title}: ${check.detail}`)
48
+ if (check.nextStep) {
49
+ logger.info(`下一步: ${check.nextStep}`)
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 输出 ready 的人类可读摘要。
55
+ * 输入为 workflow 返回结果,输出分色日志。
56
+ */
57
+ function printReadyResult(result) {
58
+ const branchText = logger.color('green', result.context.currentBranch || 'DETACHED')
59
+ const statusColor = result.status === 'fail' ? 'red' : result.status === 'warn' ? 'yellow' : 'green'
60
+ const statusText = logger.color(statusColor, result.status.toUpperCase())
61
+
62
+ logger.info(`仓库根目录: ${result.context.topLevelPath}`)
63
+ logger.info(`当前分支: ${branchText}`)
64
+ logger.info(`整体状态: ${statusText}`)
65
+ logger.info(`改动统计: staged=${result.changes.staged}, unstaged=${result.changes.unstaged}, untracked=${result.changes.untracked}, conflicted=${result.changes.conflicted}`)
66
+
67
+ if (result.upstream.configured) {
68
+ logger.info(`upstream: ${result.upstream.upstream} (ahead=${result.upstream.ahead}, behind=${result.upstream.behind})`)
69
+ } else {
70
+ logger.warn('upstream: 未配置')
71
+ }
72
+
73
+ result.checks.forEach((check) => {
74
+ printCheck(check)
75
+ })
76
+ }
77
+
78
+ /**
79
+ * 执行 ready 子命令。
80
+ * 输入为原始参数,输出 ready workflow 结果;关键假设是该命令只读,不产生 Git 副作用。
81
+ */
82
+ export async function runReadyCommand(rawArgs) {
83
+ const args = normalizeLegacyArgs(rawArgs)
84
+ const parsed = parseArgs({
85
+ args,
86
+ allowPositionals: true,
87
+ options: {
88
+ 'repo-mode': { type: 'string', default: 'auto' },
89
+ json: { type: 'boolean', default: false },
90
+ help: { type: 'boolean', short: 'h', default: false }
91
+ }
92
+ })
93
+
94
+ if (parsed.values.help) {
95
+ printHelp()
96
+ return
97
+ }
98
+
99
+ const result = await withSpinner('检查仓库就绪状态', () => runReadyWorkflow({
100
+ repoMode: parsed.values['repo-mode']
101
+ }))
102
+
103
+ if (parsed.values.json) {
104
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
105
+ return result
106
+ }
107
+
108
+ printReadyResult(result)
109
+ return result
110
+ }
@@ -0,0 +1,98 @@
1
+ import { parseArgs } from 'node:util'
2
+ import { normalizeLegacyArgs } from '../core/cli/args.js'
3
+ import { logger } from '../core/tools/logger.js'
4
+ import { runSaveWorkflow } from '../workflows/save-workflow.js'
5
+
6
+ /**
7
+ * 输出 save 子命令帮助。
8
+ * 输入为空,输出标准帮助文本。
9
+ */
10
+ function printHelp() {
11
+ process.stdout.write(`
12
+ Usage:
13
+ icode save (--message <msg> | --ai-commit) [options]
14
+
15
+ Options:
16
+ -m, --message <msg> 指定提交信息
17
+ --ai-commit 使用 AI 根据全部未提交改动生成提交信息
18
+ --profile <name> 指定 AI profile(仅 --ai-commit 使用)
19
+ --lang <zh|en> AI 提交信息语言,默认 zh
20
+ --dry-run 只检查并展示将提交的信息,不执行 add/commit
21
+ --json 输出机器可读 JSON
22
+ --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
23
+ --no-verify commit 时跳过 hook/husky 校验
24
+ -y, --yes 自动确认 AI 提交(仅 --ai-commit 且非 dry-run 时有意义)
25
+ -h, --help 查看帮助
26
+
27
+ Notes:
28
+ save 是安全快速提交场景:检查状态 -> 暂存全部改动 -> commit。不会 push。
29
+ `)
30
+ }
31
+
32
+ /**
33
+ * 输出 save 人类可读结果。
34
+ * 输入为 workflow 结果,输出分色摘要。
35
+ */
36
+ function printSaveResult(result) {
37
+ logger.info(`仓库根目录: ${result.context.topLevelPath}`)
38
+ logger.info(`当前分支: ${logger.color('green', result.context.currentBranch)}`)
39
+ logger.info(`提交来源: ${result.source === 'ai' ? logger.color('cyan', 'AI') : logger.color('cyan', 'message')}`)
40
+ logger.info(`改动统计: staged=${result.changes.staged}, unstaged=${result.changes.unstaged}, untracked=${result.changes.untracked}`)
41
+
42
+ if (result.dryRun) {
43
+ logger.warn('dry-run: 未执行 git add 或 git commit')
44
+ logger.info(`计划提交信息:\n${logger.color('yellow', result.commitMessage)}`)
45
+ return
46
+ }
47
+
48
+ logger.success(`本地提交已创建: ${logger.color('green', result.commitId)}`)
49
+ logger.info(`提交信息:\n${result.commitMessage}`)
50
+ }
51
+
52
+ /**
53
+ * 执行 save 子命令。
54
+ * 输入为原始参数,输出 save workflow 结果;关键假设是提交信息来源必须显式且唯一。
55
+ */
56
+ export async function runSaveCommand(rawArgs) {
57
+ const args = normalizeLegacyArgs(rawArgs)
58
+ const parsed = parseArgs({
59
+ args,
60
+ allowPositionals: true,
61
+ options: {
62
+ message: { type: 'string', short: 'm' },
63
+ 'ai-commit': { type: 'boolean', default: false },
64
+ profile: { type: 'string', default: '' },
65
+ lang: { type: 'string', default: 'zh' },
66
+ 'dry-run': { type: 'boolean', default: false },
67
+ json: { type: 'boolean', default: false },
68
+ 'repo-mode': { type: 'string', default: 'auto' },
69
+ 'no-verify': { type: 'boolean', default: false },
70
+ yes: { type: 'boolean', short: 'y', default: false },
71
+ help: { type: 'boolean', short: 'h', default: false }
72
+ }
73
+ })
74
+
75
+ if (parsed.values.help) {
76
+ printHelp()
77
+ return
78
+ }
79
+
80
+ const result = await runSaveWorkflow({
81
+ message: parsed.values.message,
82
+ aiCommit: parsed.values['ai-commit'],
83
+ profile: parsed.values.profile,
84
+ lang: parsed.values.lang,
85
+ dryRun: parsed.values['dry-run'],
86
+ repoMode: parsed.values['repo-mode'],
87
+ noVerify: parsed.values['no-verify'],
88
+ yes: parsed.values.yes
89
+ })
90
+
91
+ if (parsed.values.json) {
92
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
93
+ return result
94
+ }
95
+
96
+ printSaveResult(result)
97
+ return result
98
+ }
@@ -1,4 +1,5 @@
1
1
  import { parseArgs } from 'node:util'
2
+ import { normalizeLegacyArgs } from '../core/cli/args.js'
2
3
  import { IcodeError } from '../core/errors.js'
3
4
  import { logger } from '../core/tools/logger.js'
4
5
  import { runUndoWorkflow } from '../workflows/undo-workflow.js'
@@ -17,6 +18,7 @@ Options:
17
18
  --ref <ref> 回滚目标,默认按 mode 自动给出
18
19
  --hash <hash> 按 commit hash 指定回滚目标(等同 --ref)
19
20
  --recover <action> 冲突恢复策略: continue | abort | keep
21
+ --json 输出机器可读 JSON,适合 AI agent 或脚本消费
20
22
  -y, --yes 自动确认(跳过确认提示)
21
23
  --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
22
24
  -h, --help 查看帮助
@@ -30,6 +32,7 @@ Examples:
30
32
  icode undo --mode revert --ref HEAD~2
31
33
  icode undo --mode hard --hash a1b2c3d -y
32
34
  icode undo --recover abort
35
+ icode undo --mode revert --ref HEAD --json
33
36
  icode undo --mode hard --ref HEAD~1 -y
34
37
  `)
35
38
  }
@@ -73,6 +76,8 @@ export function resolveUndoWorkflowOptions(values, positionals, defaults = {}) {
73
76
  mode: values.mode,
74
77
  ref: firstRef,
75
78
  recover: values.recover,
79
+ json: values.json === true,
80
+ silentLog: values.json === true,
76
81
  yes: values.yes === true,
77
82
  repoMode: values['repo-mode'] || defaults.repoMode || 'auto'
78
83
  }
@@ -83,14 +88,16 @@ export function resolveUndoWorkflowOptions(values, positionals, defaults = {}) {
83
88
  * 输入为子命令参数数组,输出为空;内部会按参数调用 undo workflow 并打印结果。
84
89
  */
85
90
  export async function runUndoCommand(rawArgs) {
91
+ const args = normalizeLegacyArgs(rawArgs)
86
92
  const parsed = parseArgs({
87
- args: rawArgs,
93
+ args,
88
94
  allowPositionals: true,
89
95
  options: {
90
96
  mode: { type: 'string' },
91
97
  ref: { type: 'string' },
92
98
  hash: { type: 'string' },
93
99
  recover: { type: 'string' },
100
+ json: { type: 'boolean', default: false },
94
101
  yes: { type: 'boolean', short: 'y', default: false },
95
102
  'repo-mode': { type: 'string', default: 'auto' },
96
103
  help: { type: 'boolean', short: 'h', default: false }
@@ -104,6 +111,11 @@ export async function runUndoCommand(rawArgs) {
104
111
 
105
112
  const result = await runUndoWorkflow(resolveUndoWorkflowOptions(parsed.values, parsed.positionals))
106
113
 
114
+ if (parsed.values.json) {
115
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
116
+ return result
117
+ }
118
+
107
119
  if (result.canceled) {
108
120
  return
109
121
  }
@@ -0,0 +1,204 @@
1
+ // 归一化 diff 字符预算,输入任意 limit,输出正整数预算。
2
+ function normalizeLimit(limit) {
3
+ const value = Number(limit)
4
+ return Number.isInteger(value) && value > 0 ? value : 12000
5
+ }
6
+
7
+ // 从 diff 文件头中提取展示路径,输入 `diff --git` 头,输出新路径优先的文件路径。
8
+ function extractFilePathFromDiffHeader(header) {
9
+ const match = String(header || '').match(/^diff --git\s+a\/(.+?)\s+b\/(.+)$/)
10
+ if (!match) {
11
+ return ''
12
+ }
13
+
14
+ const oldPath = match[1]
15
+ const newPath = match[2]
16
+ if (newPath && newPath !== 'dev/null') {
17
+ return newPath
18
+ }
19
+ return oldPath || ''
20
+ }
21
+
22
+ // 将 unified diff 拆成文件级块,输入为 diff 文本,输出前缀和按文件划分的块;关键假设是 git diff 使用标准 `diff --git` 文件头。
23
+ export function splitUnifiedDiffByFile(diff) {
24
+ const text = String(diff || '')
25
+ const markerPattern = /^diff --git .+$/gm
26
+ const markers = [...text.matchAll(markerPattern)]
27
+
28
+ if (!markers.length) {
29
+ return {
30
+ prefix: '',
31
+ blocks: []
32
+ }
33
+ }
34
+
35
+ const prefix = text.slice(0, markers[0].index).trim()
36
+ const blocks = markers.map((marker, index) => {
37
+ const start = marker.index
38
+ const end = index + 1 < markers.length ? markers[index + 1].index : text.length
39
+ const content = text.slice(start, end).trimEnd()
40
+ return {
41
+ path: extractFilePathFromDiffHeader(marker[0]),
42
+ content,
43
+ length: content.length
44
+ }
45
+ })
46
+
47
+ return {
48
+ prefix,
49
+ blocks
50
+ }
51
+ }
52
+
53
+ // 在剩余预算内追加文件 diff 块,输入输出片段、文件块和预算,输出是否完整包含及部分内容。
54
+ function appendBlockWithinBudget(parts, block, remainingBudget) {
55
+ if (block.content.length <= remainingBudget) {
56
+ parts.push(block.content)
57
+ return {
58
+ included: true,
59
+ partialContent: ''
60
+ }
61
+ }
62
+
63
+ if (remainingBudget <= 120) {
64
+ return {
65
+ included: false,
66
+ partialContent: ''
67
+ }
68
+ }
69
+
70
+ const partialContent = `${block.content.slice(0, remainingBudget - 40).trimEnd()}\n...<file diff truncated>`
71
+ parts.push(partialContent)
72
+ return {
73
+ included: false,
74
+ partialContent
75
+ }
76
+ }
77
+
78
+ // 构建 diff 覆盖范围说明,输入截断状态,输出模型可读的覆盖声明。
79
+ export function formatDiffCoverage(coverage) {
80
+ if (!coverage.truncated) {
81
+ return [
82
+ 'Diff Coverage: full',
83
+ `Total files: ${coverage.totalFiles}`,
84
+ `Original chars: ${coverage.originalChars}`
85
+ ].join('\n')
86
+ }
87
+
88
+ const lines = [
89
+ 'Diff Coverage: partial',
90
+ `Original chars: ${coverage.originalChars}`,
91
+ `Prompt chars: ${coverage.promptChars}`,
92
+ `Total files: ${coverage.totalFiles}`,
93
+ `Included full file diffs: ${coverage.includedFiles.length ? coverage.includedFiles.join(', ') : '(none)'}`,
94
+ `Included partial file diffs: ${coverage.partialFiles.length ? coverage.partialFiles.join(', ') : '(none)'}`,
95
+ `Omitted file diffs: ${coverage.omittedFiles.length ? coverage.omittedFiles.join(', ') : '(none)'}`,
96
+ 'Instruction: treat omitted or partial file diffs as unknown; do not claim they were reviewed in detail.'
97
+ ]
98
+
99
+ return lines.join('\n')
100
+ }
101
+
102
+ // 按文件边界限制 diff 长度,输入完整 diff 和字符预算,输出截断后的 diff 与覆盖元信息。
103
+ export function buildLimitedDiffPrompt(diff, options = {}) {
104
+ const text = String(diff || '').trimEnd()
105
+ const limit = normalizeLimit(options.limit)
106
+
107
+ if (text.length <= limit) {
108
+ const parsed = splitUnifiedDiffByFile(text)
109
+ return {
110
+ diff: text,
111
+ coverage: {
112
+ truncated: false,
113
+ originalChars: text.length,
114
+ promptChars: text.length,
115
+ totalFiles: parsed.blocks.length,
116
+ includedFiles: parsed.blocks.map((block) => block.path).filter(Boolean),
117
+ partialFiles: [],
118
+ omittedFiles: []
119
+ }
120
+ }
121
+ }
122
+
123
+ const parsed = splitUnifiedDiffByFile(text)
124
+ if (!parsed.blocks.length) {
125
+ const limitedText = `${text.slice(0, limit).trimEnd()}\n...<diff truncated>`
126
+ return {
127
+ diff: limitedText,
128
+ coverage: {
129
+ truncated: true,
130
+ originalChars: text.length,
131
+ promptChars: limitedText.length,
132
+ totalFiles: 0,
133
+ includedFiles: [],
134
+ partialFiles: ['unknown'],
135
+ omittedFiles: ['unknown']
136
+ }
137
+ }
138
+ }
139
+
140
+ const parts = []
141
+ const includedFiles = []
142
+ const partialFiles = []
143
+ const omittedFiles = []
144
+ let usedChars = 0
145
+
146
+ if (parsed.prefix) {
147
+ const prefix = parsed.prefix.length > 300 ? `${parsed.prefix.slice(0, 300).trimEnd()}\n...<diff section prefix truncated>` : parsed.prefix
148
+ parts.push(prefix)
149
+ usedChars += prefix.length + 2
150
+ }
151
+
152
+ for (let index = 0; index < parsed.blocks.length; index += 1) {
153
+ const block = parsed.blocks[index]
154
+ const separatorCost = parts.length ? 2 : 0
155
+ const remainingBudget = limit - usedChars - separatorCost
156
+ const result = appendBlockWithinBudget(parts, block, remainingBudget)
157
+
158
+ if (result.included) {
159
+ includedFiles.push(block.path || '(unknown)')
160
+ usedChars += block.content.length + separatorCost
161
+ continue
162
+ }
163
+
164
+ if (result.partialContent) {
165
+ partialFiles.push(block.path || '(unknown)')
166
+ usedChars += result.partialContent.length + separatorCost
167
+ } else {
168
+ omittedFiles.push(block.path || '(unknown)')
169
+ }
170
+
171
+ const remainingBlocks = parsed.blocks.slice(index + 1)
172
+ remainingBlocks.forEach((item) => {
173
+ omittedFiles.push(item.path || '(unknown)')
174
+ })
175
+ break
176
+ }
177
+
178
+ const limitedText = parts.join('\n\n').trimEnd()
179
+ return {
180
+ diff: limitedText,
181
+ coverage: {
182
+ truncated: true,
183
+ originalChars: text.length,
184
+ promptChars: limitedText.length,
185
+ totalFiles: parsed.blocks.length,
186
+ includedFiles,
187
+ partialFiles,
188
+ omittedFiles
189
+ }
190
+ }
191
+ }
192
+
193
+ // 从覆盖信息中生成简短统计,输入 coverage,输出便于命令结果或测试读取的结构。
194
+ export function summarizeDiffCoverage(coverage) {
195
+ return {
196
+ truncated: coverage.truncated,
197
+ totalFiles: coverage.totalFiles,
198
+ includedFiles: coverage.includedFiles.length,
199
+ partialFiles: coverage.partialFiles.length,
200
+ omittedFiles: coverage.omittedFiles.length,
201
+ originalChars: coverage.originalChars,
202
+ promptChars: coverage.promptChars
203
+ }
204
+ }
@@ -1,19 +1,40 @@
1
1
  const LEGACY_ALIAS_MAP = new Map([
2
2
  ['-pm', '--pull-main'],
3
+ ['--pullMain', '--pull-main'],
3
4
  ['--pullMainBranch', '--pull-main'],
4
5
  ['--pushOrigin', '--push-origin'],
6
+ ['--localMerge', '--local-merge'],
5
7
  ['--notPushCurrent', '--not-push-current'],
8
+ ['--forceProtected', '--force-protected'],
6
9
  ['--repoMode', '--repo-mode'],
7
10
  ['--noVerify', '--no-verify'],
8
11
  ['--allLocal', '--all-local'],
9
12
  ['--mergeMain', '--merge-main'],
10
13
  ['--aiReview', '--ai-review'],
11
14
  ['--aiProfile', '--ai-profile'],
12
- ['--aiCommit', '--ai-commit']
15
+ ['--aiCommit', '--ai-commit'],
16
+ ['--remoteMerge', '--remote-merge'],
17
+ ['--dryRun', '--dry-run'],
18
+ ['--dumpResponse', '--dump-response'],
19
+ ['--mergedTarget', '--merged-target'],
20
+ ['--baseUrl', '--base-url'],
21
+ ['--apiKey', '--api-key'],
22
+ ['--maxTokens', '--max-tokens'],
23
+ ['--requestBody', '--request-body']
13
24
  ])
14
25
 
26
+ // 统一历史 camelCase 参数到当前 kebab-case 参数;输入原始 argv,输出可直接交给 parseArgs 的参数。
15
27
  export function normalizeLegacyArgs(argv = []) {
16
- return argv.map((arg) => LEGACY_ALIAS_MAP.get(arg) || arg)
28
+ return argv.map((arg) => {
29
+ const separatorIndex = String(arg).indexOf('=')
30
+ if (separatorIndex === -1) {
31
+ return LEGACY_ALIAS_MAP.get(arg) || arg
32
+ }
33
+
34
+ const key = arg.slice(0, separatorIndex)
35
+ const value = arg.slice(separatorIndex)
36
+ return `${LEGACY_ALIAS_MAP.get(key) || key}${value}`
37
+ })
17
38
  }
18
39
 
19
40
  export function parseConfigValue(raw) {
@@ -87,12 +87,35 @@ const ROOT_COMPLETION_SPEC = {
87
87
  { long: '--help', short: '-h' }
88
88
  ]
89
89
  },
90
+ ready: {
91
+ options: [
92
+ { long: '--repo-mode', valueSource: 'repo-mode' },
93
+ { long: '--json' },
94
+ { long: '--help', short: '-h' }
95
+ ]
96
+ },
97
+ save: {
98
+ options: [
99
+ { long: '--message', short: '-m', valueSource: 'text' },
100
+ { long: '--ai-commit' },
101
+ { long: '--profile', valueSource: 'ai-profile' },
102
+ { long: '--lang', valueSource: 'ai-commit-lang' },
103
+ { long: '--dry-run' },
104
+ { long: '--json' },
105
+ { long: '--repo-mode', valueSource: 'repo-mode' },
106
+ { long: '--no-verify' },
107
+ { long: '--yes', short: '-y' },
108
+ { long: '--help', short: '-h' }
109
+ ]
110
+ },
90
111
  push: {
91
112
  positionals: [{ valueSource: 'branch', repeat: true }],
92
113
  options: [
93
114
  { long: '--message', short: '-m', valueSource: 'text' },
94
115
  { long: '--yes', short: '-y' },
95
116
  { long: '--local-merge' },
117
+ { long: '--remote-merge', short: '-r' },
118
+ { long: '--dry-run' },
96
119
  { long: '--ai-commit' },
97
120
  { long: '--ai-profile', valueSource: 'ai-profile' },
98
121
  { long: '--help', short: '-h' },
@@ -134,6 +157,7 @@ const ROOT_COMPLETION_SPEC = {
134
157
  { long: '--ref', valueSource: 'ref' },
135
158
  { long: '--hash', valueSource: 'ref' },
136
159
  { long: '--recover', valueSource: 'undo-recover' },
160
+ { long: '--json' },
137
161
  { long: '--yes', short: '-y' },
138
162
  { long: '--repo-mode', valueSource: 'repo-mode' },
139
163
  { long: '--help', short: '-h' }
@@ -147,6 +171,8 @@ const ROOT_COMPLETION_SPEC = {
147
171
  options: [
148
172
  { long: '--interactive', short: '-i' },
149
173
  { long: '--range', valueSource: 'text' },
174
+ { long: '--dry-run' },
175
+ { long: '--json' },
150
176
  { long: '--push' },
151
177
  { long: '--yes', short: '-y' },
152
178
  { long: '--repo-mode', valueSource: 'repo-mode' },
@@ -248,6 +274,13 @@ const ROOT_COMPLETION_SPEC = {
248
274
  }
249
275
  }
250
276
  },
277
+ doctor: {
278
+ options: [
279
+ { long: '--repo-mode', valueSource: 'repo-mode' },
280
+ { long: '--json' },
281
+ { long: '--help', short: '-h' }
282
+ ]
283
+ },
251
284
  explain: EXPLAIN_NODE,
252
285
  info: {
253
286
  options: [