@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.
- package/LICENSE +21 -0
- package/README.md +142 -5
- package/bin/icode.js +4 -4
- package/package.json +1 -1
- package/src/cli.js +60 -23
- package/src/commands/ai.js +4 -4
- package/src/commands/checkout.js +2 -2
- package/src/commands/clean.js +5 -2
- package/src/commands/codereview.js +5 -2
- package/src/commands/completion.js +93 -0
- package/src/commands/config.js +123 -10
- package/src/commands/doctor.js +80 -0
- package/src/commands/explain.js +6 -3
- package/src/commands/help.js +17 -0
- package/src/commands/info.js +5 -2
- package/src/commands/migrate.js +40 -3
- package/src/commands/push.js +72 -24
- package/src/commands/ready.js +110 -0
- package/src/commands/save.js +98 -0
- package/src/commands/sync.js +2 -2
- package/src/commands/tag.js +2 -2
- package/src/commands/undo.js +73 -10
- package/src/core/{ai-client.js → ai/client.js} +3 -3
- package/src/core/{ai-config.js → ai/config.js} +2 -2
- package/src/core/ai/diff-prompt.js +204 -0
- package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
- package/src/core/{args.js → cli/args.js} +23 -2
- package/src/core/cli/completion.js +720 -0
- package/src/core/cli/update-notifier.js +138 -0
- package/src/core/{shell.js → commands/shell.js} +13 -1
- package/src/core/{config-store.js → config/store.js} +53 -3
- package/src/core/{git-context.js → git/context.js} +24 -10
- package/src/core/git/preconditions.js +88 -0
- package/src/core/{git-service.js → git/service.js} +195 -5
- package/src/core/remote-merge/client.js +42 -0
- package/src/core/remote-merge/providers/gitlab.js +596 -0
- package/src/core/remote-merge/shared.js +49 -0
- package/src/core/tools/interactive.js +368 -0
- package/src/core/{loading.js → tools/loading.js} +5 -1
- package/src/core/{logger.js → tools/logger.js} +5 -0
- package/src/workflows/ai-codereview-workflow.js +18 -6
- package/src/workflows/ai-commit-workflow.js +20 -12
- package/src/workflows/ai-conflict-workflow.js +4 -4
- package/src/workflows/ai-explain-workflow.js +4 -4
- package/src/workflows/ai-risk-review-workflow.js +1 -1
- package/src/workflows/checkout-workflow.js +17 -5
- package/src/workflows/clean-workflow.js +92 -75
- package/src/workflows/doctor-workflow.js +131 -0
- package/src/workflows/info-workflow.js +4 -4
- package/src/workflows/migrate-workflow.js +269 -62
- package/src/workflows/push-workflow.js +475 -58
- package/src/workflows/ready-workflow.js +278 -0
- package/src/workflows/rollback-workflow.js +24 -9
- package/src/workflows/save-workflow.js +152 -0
- package/src/workflows/sync-workflow.js +22 -6
- package/src/workflows/tag-workflow.js +4 -4
- package/src/workflows/undo-workflow.js +37 -12
- package/src/core/prompts.js +0 -152
- /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
- /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
- /package/src/core/{hook-diagnostics.js → git/hook-diagnostics.js} +0 -0
|
@@ -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/sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
|
-
import { normalizeLegacyArgs } from '../core/args.js'
|
|
3
|
-
import { logger } from '../core/logger.js'
|
|
2
|
+
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
3
|
+
import { logger } from '../core/tools/logger.js'
|
|
4
4
|
import { runSyncWorkflow } from '../workflows/sync-workflow.js'
|
|
5
5
|
|
|
6
6
|
function printHelp() {
|
package/src/commands/tag.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
|
-
import { normalizeLegacyArgs } from '../core/args.js'
|
|
3
|
-
import { logger } from '../core/logger.js'
|
|
2
|
+
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
3
|
+
import { logger } from '../core/tools/logger.js'
|
|
4
4
|
import { runTagWorkflow } from '../workflows/tag-workflow.js'
|
|
5
5
|
|
|
6
6
|
function printHelp() {
|
package/src/commands/undo.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
3
|
+
import { IcodeError } from '../core/errors.js'
|
|
4
|
+
import { logger } from '../core/tools/logger.js'
|
|
3
5
|
import { runUndoWorkflow } from '../workflows/undo-workflow.js'
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 输出 undo 命令帮助文本。
|
|
9
|
+
* 输入为空,输出为标准输出文本。
|
|
10
|
+
*/
|
|
5
11
|
function printHelp() {
|
|
6
12
|
process.stdout.write(`
|
|
7
13
|
Usage:
|
|
8
|
-
icode undo [options]
|
|
14
|
+
icode undo [ref] [options]
|
|
9
15
|
|
|
10
16
|
Options:
|
|
11
17
|
--mode <mode> 回滚模式: revert | soft | mixed | hard
|
|
12
18
|
--ref <ref> 回滚目标,默认按 mode 自动给出
|
|
19
|
+
--hash <hash> 按 commit hash 指定回滚目标(等同 --ref)
|
|
13
20
|
--recover <action> 冲突恢复策略: continue | abort | keep
|
|
21
|
+
--json 输出机器可读 JSON,适合 AI agent 或脚本消费
|
|
14
22
|
-y, --yes 自动确认(跳过确认提示)
|
|
15
23
|
--repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
|
|
16
24
|
-h, --help 查看帮助
|
|
@@ -20,20 +28,76 @@ Notes:
|
|
|
20
28
|
|
|
21
29
|
Examples:
|
|
22
30
|
icode undo
|
|
31
|
+
icode undo a1b2c3d
|
|
23
32
|
icode undo --mode revert --ref HEAD~2
|
|
33
|
+
icode undo --mode hard --hash a1b2c3d -y
|
|
24
34
|
icode undo --recover abort
|
|
35
|
+
icode undo --mode revert --ref HEAD --json
|
|
25
36
|
icode undo --mode hard --ref HEAD~1 -y
|
|
26
37
|
`)
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
/**
|
|
41
|
+
* 归一化 undo 命令参数。
|
|
42
|
+
* 输入为 parseArgs 的 values、positionals 与默认配置,输出为 workflow 可直接使用的显式选项。
|
|
43
|
+
*/
|
|
44
|
+
export function resolveUndoWorkflowOptions(values, positionals, defaults = {}) {
|
|
45
|
+
if (positionals.length > 1) {
|
|
46
|
+
throw new IcodeError('undo 最多支持一个位置参数作为回滚目标 ref/hash', {
|
|
47
|
+
code: 'UNDO_POSITIONAL_INVALID',
|
|
48
|
+
exitCode: 2
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const refCandidates = [
|
|
53
|
+
{ source: '--ref', value: values.ref },
|
|
54
|
+
{ source: '--hash', value: values.hash },
|
|
55
|
+
{ source: 'position', value: positionals[0] }
|
|
56
|
+
].filter((candidate) => typeof candidate.value === 'string' && candidate.value.trim())
|
|
57
|
+
|
|
58
|
+
const normalizedRefs = refCandidates.map((candidate) => ({
|
|
59
|
+
source: candidate.source,
|
|
60
|
+
value: candidate.value.trim()
|
|
61
|
+
}))
|
|
62
|
+
const firstRef = normalizedRefs[0]?.value
|
|
63
|
+
const conflict = normalizedRefs.find((candidate) => candidate.value !== firstRef)
|
|
64
|
+
|
|
65
|
+
if (conflict) {
|
|
66
|
+
throw new IcodeError(
|
|
67
|
+
`undo 回滚目标冲突: ${normalizedRefs.map((item) => `${item.source}=${item.value}`).join(', ')}`,
|
|
68
|
+
{
|
|
69
|
+
code: 'UNDO_REF_CONFLICT',
|
|
70
|
+
exitCode: 2
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
mode: values.mode,
|
|
77
|
+
ref: firstRef,
|
|
78
|
+
recover: values.recover,
|
|
79
|
+
json: values.json === true,
|
|
80
|
+
silentLog: values.json === true,
|
|
81
|
+
yes: values.yes === true,
|
|
82
|
+
repoMode: values['repo-mode'] || defaults.repoMode || 'auto'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 执行 undo 命令。
|
|
88
|
+
* 输入为子命令参数数组,输出为空;内部会按参数调用 undo workflow 并打印结果。
|
|
89
|
+
*/
|
|
29
90
|
export async function runUndoCommand(rawArgs) {
|
|
91
|
+
const args = normalizeLegacyArgs(rawArgs)
|
|
30
92
|
const parsed = parseArgs({
|
|
31
|
-
args
|
|
93
|
+
args,
|
|
32
94
|
allowPositionals: true,
|
|
33
95
|
options: {
|
|
34
96
|
mode: { type: 'string' },
|
|
35
97
|
ref: { type: 'string' },
|
|
98
|
+
hash: { type: 'string' },
|
|
36
99
|
recover: { type: 'string' },
|
|
100
|
+
json: { type: 'boolean', default: false },
|
|
37
101
|
yes: { type: 'boolean', short: 'y', default: false },
|
|
38
102
|
'repo-mode': { type: 'string', default: 'auto' },
|
|
39
103
|
help: { type: 'boolean', short: 'h', default: false }
|
|
@@ -45,13 +109,12 @@ export async function runUndoCommand(rawArgs) {
|
|
|
45
109
|
return
|
|
46
110
|
}
|
|
47
111
|
|
|
48
|
-
const result = await runUndoWorkflow(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
})
|
|
112
|
+
const result = await runUndoWorkflow(resolveUndoWorkflowOptions(parsed.values, parsed.positionals))
|
|
113
|
+
|
|
114
|
+
if (parsed.values.json) {
|
|
115
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
116
|
+
return result
|
|
117
|
+
}
|
|
55
118
|
|
|
56
119
|
if (result.canceled) {
|
|
57
120
|
return
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { IcodeError } from '
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { IcodeError } from '../errors.js'
|
|
2
|
+
import { withSpinner } from '../tools/loading.js'
|
|
3
|
+
import { getAiProfile } from './config.js'
|
|
4
4
|
|
|
5
5
|
function trimSlash(value) {
|
|
6
6
|
return value.replace(/\/+$/, '')
|
|
@@ -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) =>
|
|
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) {
|