@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.
- package/README.md +105 -5
- package/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/clean.js +4 -1
- package/src/commands/codereview.js +4 -1
- package/src/commands/completion.js +9 -6
- package/src/commands/config.js +9 -6
- package/src/commands/doctor.js +80 -0
- package/src/commands/explain.js +4 -1
- package/src/commands/help.js +10 -0
- package/src/commands/info.js +4 -1
- package/src/commands/migrate.js +38 -1
- package/src/commands/push.js +45 -18
- package/src/commands/ready.js +110 -0
- package/src/commands/save.js +98 -0
- package/src/commands/undo.js +13 -1
- package/src/core/ai/diff-prompt.js +204 -0
- package/src/core/cli/args.js +23 -2
- package/src/core/cli/completion.js +33 -0
- package/src/core/config/store.js +2 -4
- package/src/core/git/context.js +14 -15
- package/src/core/git/preconditions.js +88 -0
- package/src/core/git/service.js +151 -1
- package/src/core/remote-merge/providers/gitlab.js +149 -90
- package/src/core/tools/interactive.js +209 -34
- package/src/core/tools/loading.js +5 -1
- package/src/core/tools/logger.js +5 -0
- package/src/workflows/ai-codereview-workflow.js +14 -2
- package/src/workflows/ai-commit-workflow.js +14 -6
- package/src/workflows/checkout-workflow.js +14 -2
- package/src/workflows/clean-workflow.js +87 -70
- package/src/workflows/doctor-workflow.js +131 -0
- package/src/workflows/migrate-workflow.js +265 -58
- package/src/workflows/push-workflow.js +232 -50
- package/src/workflows/ready-workflow.js +278 -0
- package/src/workflows/rollback-workflow.js +20 -5
- package/src/workflows/save-workflow.js +152 -0
- package/src/workflows/sync-workflow.js +18 -2
- package/src/workflows/undo-workflow.js +25 -7
package/src/commands/push.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
132
|
+
// 解析 push 子命令参数;rawArgs 是不含命令名的参数数组,返回 node:util parseArgs 的结构化结果。
|
|
133
|
+
export function parsePushCommandArgs(rawArgs) {
|
|
104
134
|
const args = normalizeLegacyArgs(rawArgs)
|
|
105
|
-
|
|
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
|
+
}
|
package/src/commands/undo.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/src/core/cli/args.js
CHANGED
|
@@ -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) =>
|
|
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: [
|