@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { askAi } from '../core/ai/client.js'
|
|
2
|
+
import { buildLimitedDiffPrompt, formatDiffCoverage, summarizeDiffCoverage } from '../core/ai/diff-prompt.js'
|
|
2
3
|
import { resolveAiDiffRange } from '../core/ai/diff-range.js'
|
|
3
4
|
import { IcodeError } from '../core/errors.js'
|
|
4
5
|
import { resolveGitContext } from '../core/git/context.js'
|
|
@@ -19,6 +20,14 @@ function joinSections(sections) {
|
|
|
19
20
|
.join('\n\n')
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// 构建 AI code review prompt 中的 diff 片段,输入完整 diff,输出受限 diff 与覆盖范围。
|
|
24
|
+
function buildReviewDiffPrompt(diff) {
|
|
25
|
+
return buildLimitedDiffPrompt(diff, {
|
|
26
|
+
limit: 18000
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 执行 AI 代码审查工作流,输入 diff 范围和 AI 选项,输出审查文本与 diff 覆盖范围。
|
|
22
31
|
export async function runAiCodeReviewWorkflow(options) {
|
|
23
32
|
const context = await resolveGitContext({
|
|
24
33
|
cwd: options.cwd,
|
|
@@ -101,10 +110,12 @@ export async function runAiCodeReviewWorkflow(options) {
|
|
|
101
110
|
])
|
|
102
111
|
}
|
|
103
112
|
|
|
113
|
+
const limitedDiff = buildReviewDiffPrompt(diff)
|
|
114
|
+
|
|
104
115
|
const review = await askAi(
|
|
105
116
|
{
|
|
106
117
|
systemPrompt: '你是严格的软件代码审查工程师,请优先关注 bug、安全风险、行为回归、缺失测试。输出中文 Markdown。',
|
|
107
|
-
userPrompt: `请按如下结构输出:\n1. Findings(按严重度从高到低)\n2. Open Questions\n3. Summary\n\nFocus: ${options.focus || 'general'}\nRange: ${rangeSpec}\nDiff Source: ${diffSource}\n\nDiff Stat:\n${truncate(stat, 3000)}\n\nName Status:\n${truncate(nameStatus, 3000)}\n\nUnified Diff:\n${
|
|
118
|
+
userPrompt: `请按如下结构输出:\n1. Findings(按严重度从高到低)\n2. Open Questions\n3. Summary\n\nFocus: ${options.focus || 'general'}\nRange: ${rangeSpec}\nDiff Source: ${diffSource}\n\n${formatDiffCoverage(limitedDiff.coverage)}\n\nDiff Stat:\n${truncate(stat, 3000)}\n\nName Status:\n${truncate(nameStatus, 3000)}\n\nUnified Diff:\n${limitedDiff.diff}`
|
|
108
119
|
},
|
|
109
120
|
{
|
|
110
121
|
profile: options.profile,
|
|
@@ -121,6 +132,7 @@ export async function runAiCodeReviewWorkflow(options) {
|
|
|
121
132
|
|
|
122
133
|
return {
|
|
123
134
|
rangeSpec,
|
|
124
|
-
review
|
|
135
|
+
review,
|
|
136
|
+
diffCoverage: summarizeDiffCoverage(limitedDiff.coverage)
|
|
125
137
|
}
|
|
126
138
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
2
|
import { askAiJson } from '../core/ai/client.js'
|
|
3
|
+
import { buildLimitedDiffPrompt, formatDiffCoverage, summarizeDiffCoverage } from '../core/ai/diff-prompt.js'
|
|
3
4
|
import { resolveGitContext } from '../core/git/context.js'
|
|
4
5
|
import { scanCommitConventions } from '../core/git/commit-conventions.js'
|
|
5
6
|
import { GitService } from '../core/git/service.js'
|
|
@@ -74,7 +75,9 @@ export async function runAiCommitWorkflow(options) {
|
|
|
74
75
|
})
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
const limitedDiff = diff
|
|
78
|
+
const limitedDiff = buildLimitedDiffPrompt(diff, {
|
|
79
|
+
limit: 12000
|
|
80
|
+
})
|
|
78
81
|
const conventionContext = scanCommitConventions(context)
|
|
79
82
|
const conventionPrompt = conventionContext.hasConventions
|
|
80
83
|
? `Local commit conventions were detected from repository hooks/config files. Follow these local rules first when generating the commit message.\n\n${conventionContext.summary}\n\n`
|
|
@@ -89,7 +92,7 @@ export async function runAiCommitWorkflow(options) {
|
|
|
89
92
|
const { parsed, text } = await askAiJson(
|
|
90
93
|
{
|
|
91
94
|
systemPrompt: `You are a senior software engineer. Generate a concise Conventional Commit message. Output JSON only. Language: ${language}.`,
|
|
92
|
-
userPrompt: `${conventionPrompt}Based on the following git diff, return JSON with fields:\n{\"type\":\"feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert\",\"scope\":\"optional\",\"subject\":\"required one-line summary\",\"body\":\"optional details\"}\n\nDiff Source: ${diffSource}\n\nDiff:\n${limitedDiff}`
|
|
95
|
+
userPrompt: `${conventionPrompt}Based on the following git diff, return JSON with fields:\n{\"type\":\"feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert\",\"scope\":\"optional\",\"subject\":\"required one-line summary\",\"body\":\"optional details\"}\n\nDiff Source: ${diffSource}\n\n${formatDiffCoverage(limitedDiff.coverage)}\n\nDiff:\n${limitedDiff.diff}`
|
|
93
96
|
},
|
|
94
97
|
{
|
|
95
98
|
profile: options.profile
|
|
@@ -98,13 +101,16 @@ export async function runAiCommitWorkflow(options) {
|
|
|
98
101
|
|
|
99
102
|
const commitMessage = buildCommitMessage(parsed)
|
|
100
103
|
|
|
101
|
-
|
|
104
|
+
if (!options.silentResultLog) {
|
|
105
|
+
logger.success(`AI 建议提交信息:\n${commitMessage}`)
|
|
106
|
+
}
|
|
102
107
|
|
|
103
108
|
if (!options.apply) {
|
|
104
109
|
return {
|
|
105
110
|
applied: false,
|
|
106
111
|
commitMessage,
|
|
107
|
-
raw: text
|
|
112
|
+
raw: text,
|
|
113
|
+
diffCoverage: summarizeDiffCoverage(limitedDiff.coverage)
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -115,6 +121,7 @@ export async function runAiCommitWorkflow(options) {
|
|
|
115
121
|
applied: false,
|
|
116
122
|
commitMessage,
|
|
117
123
|
raw: text,
|
|
124
|
+
diffCoverage: summarizeDiffCoverage(limitedDiff.coverage),
|
|
118
125
|
canceled: true
|
|
119
126
|
}
|
|
120
127
|
}
|
|
@@ -130,7 +137,7 @@ export async function runAiCommitWorkflow(options) {
|
|
|
130
137
|
})
|
|
131
138
|
|
|
132
139
|
const commitId = await git.revParseShort('HEAD')
|
|
133
|
-
if (commitId) {
|
|
140
|
+
if (commitId && !options.silentResultLog) {
|
|
134
141
|
logger.success(`AI commit 已创建: ${commitId}`)
|
|
135
142
|
}
|
|
136
143
|
|
|
@@ -138,6 +145,7 @@ export async function runAiCommitWorkflow(options) {
|
|
|
138
145
|
applied: true,
|
|
139
146
|
commitId,
|
|
140
147
|
commitMessage,
|
|
141
|
-
raw: text
|
|
148
|
+
raw: text,
|
|
149
|
+
diffCoverage: summarizeDiffCoverage(limitedDiff.coverage)
|
|
142
150
|
}
|
|
143
151
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
2
|
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import {
|
|
4
|
+
assertCleanWorktree,
|
|
5
|
+
assertNoConflictedFiles,
|
|
6
|
+
assertNoPendingOperation,
|
|
7
|
+
readGitSafetyState
|
|
8
|
+
} from '../core/git/preconditions.js'
|
|
3
9
|
import { GitService } from '../core/git/service.js'
|
|
4
10
|
import { logger } from '../core/tools/logger.js'
|
|
5
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 执行 checkout 工作流。
|
|
14
|
+
* 输入为目标分支、基线分支和执行选项,输出分支切换结果;实际切换前要求工作区干净且没有未完成 Git 操作。
|
|
15
|
+
*/
|
|
6
16
|
export async function runCheckoutWorkflow(input) {
|
|
7
17
|
if (!input.branchName) {
|
|
8
18
|
throw new IcodeError('缺少分支名: icode checkout <branch> [base]', {
|
|
@@ -19,6 +29,10 @@ export async function runCheckoutWorkflow(input) {
|
|
|
19
29
|
const git = new GitService(context)
|
|
20
30
|
const branchName = input.branchName.trim()
|
|
21
31
|
const baseBranchName = input.baseBranchName?.trim() || context.defaultBranch
|
|
32
|
+
const safetyState = await readGitSafetyState(git)
|
|
33
|
+
assertNoPendingOperation(safetyState.operation, 'checkout', 'CHECKOUT_GIT_OPERATION_IN_PROGRESS')
|
|
34
|
+
assertNoConflictedFiles(safetyState.changes, 'checkout', 'CHECKOUT_CONFLICTED_FILES')
|
|
35
|
+
assertCleanWorktree(safetyState.changes, 'checkout', 'CHECKOUT_DIRTY_WORKTREE')
|
|
22
36
|
|
|
23
37
|
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
24
38
|
if (context.inheritedFromParent) {
|
|
@@ -55,7 +69,6 @@ export async function runCheckoutWorkflow(input) {
|
|
|
55
69
|
if (input.pullMain && context.defaultBranch !== branchName) {
|
|
56
70
|
logger.info(`同步主分支到当前分支: ${context.defaultBranch}`)
|
|
57
71
|
await git.pull(context.defaultBranch, {
|
|
58
|
-
allowUnrelatedHistories: true,
|
|
59
72
|
noRebase: true
|
|
60
73
|
})
|
|
61
74
|
}
|
|
@@ -63,7 +76,6 @@ export async function runCheckoutWorkflow(input) {
|
|
|
63
76
|
if (remoteExists) {
|
|
64
77
|
logger.info(`拉取远程分支: ${branchName}`)
|
|
65
78
|
await git.pull(branchName, {
|
|
66
|
-
allowUnrelatedHistories: true,
|
|
67
79
|
noRebase: true
|
|
68
80
|
})
|
|
69
81
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { getRepoPolicy } from '../core/config/store.js'
|
|
2
2
|
import { IcodeError } from '../core/errors.js'
|
|
3
3
|
import { resolveGitContext } from '../core/git/context.js'
|
|
4
|
+
import {
|
|
5
|
+
assertCleanWorktree,
|
|
6
|
+
assertNoConflictedFiles,
|
|
7
|
+
assertNoPendingOperation,
|
|
8
|
+
assertOnBranch,
|
|
9
|
+
readGitSafetyState
|
|
10
|
+
} from '../core/git/preconditions.js'
|
|
4
11
|
import { GitService } from '../core/git/service.js'
|
|
5
12
|
import { confirm } from '../core/tools/interactive.js'
|
|
6
13
|
import { logger } from '../core/tools/logger.js'
|
|
@@ -14,6 +21,10 @@ function normalizeKeepList(values = []) {
|
|
|
14
21
|
)
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
/**
|
|
25
|
+
* 执行 clean 工作流。
|
|
26
|
+
* 输入为清理基线、保留分支和删除选项,输出已删除分支;执行前要求工作区干净并在普通分支上。
|
|
27
|
+
*/
|
|
17
28
|
export async function runCleanWorkflow(options) {
|
|
18
29
|
const context = await resolveGitContext({
|
|
19
30
|
cwd: options.cwd,
|
|
@@ -22,8 +33,13 @@ export async function runCleanWorkflow(options) {
|
|
|
22
33
|
|
|
23
34
|
const git = new GitService(context)
|
|
24
35
|
const mergedTarget = (options.mergedTarget || context.defaultBranch).trim()
|
|
25
|
-
const
|
|
36
|
+
const safetyState = await readGitSafetyState(git)
|
|
37
|
+
const currentBranch = safetyState.currentBranch || context.currentBranch
|
|
26
38
|
const policy = getRepoPolicy(context.topLevelPath)
|
|
39
|
+
assertOnBranch(currentBranch, 'clean', 'CLEAN_DETACHED_HEAD')
|
|
40
|
+
assertNoPendingOperation(safetyState.operation, 'clean', 'CLEAN_GIT_OPERATION_IN_PROGRESS')
|
|
41
|
+
assertNoConflictedFiles(safetyState.changes, 'clean', 'CLEAN_CONFLICTED_FILES')
|
|
42
|
+
assertCleanWorktree(safetyState.changes, 'clean', 'CLEAN_DIRTY_WORKTREE')
|
|
27
43
|
|
|
28
44
|
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
29
45
|
if (context.inheritedFromParent) {
|
|
@@ -41,91 +57,92 @@ export async function runCleanWorkflow(options) {
|
|
|
41
57
|
})
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (targetRemoteExists) {
|
|
51
|
-
await git.pull(mergedTarget, {
|
|
52
|
-
allowUnrelatedHistories: true,
|
|
53
|
-
noRebase: true
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const mergedBranches = await git.listMergedLocalBranches(mergedTarget)
|
|
58
|
-
const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
|
|
59
|
-
const keepSet = normalizeKeepList(options.keep || [])
|
|
60
|
+
try {
|
|
61
|
+
if (targetLocalExists) {
|
|
62
|
+
await git.checkout(mergedTarget)
|
|
63
|
+
} else {
|
|
64
|
+
await git.checkoutTracking(mergedTarget)
|
|
65
|
+
}
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (currentBranch) {
|
|
67
|
-
keepSet.add(currentBranch)
|
|
68
|
-
}
|
|
69
|
-
protectedBranches.forEach((branch) => keepSet.add(branch))
|
|
67
|
+
if (targetRemoteExists) {
|
|
68
|
+
await git.pull(mergedTarget, {
|
|
69
|
+
noRebase: true
|
|
70
|
+
})
|
|
71
|
+
}
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
const mergedBranches = await git.listMergedLocalBranches(mergedTarget)
|
|
74
|
+
const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
|
|
75
|
+
const keepSet = normalizeKeepList(options.keep || [])
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
mergedTarget,
|
|
78
|
-
deletedLocal: [],
|
|
79
|
-
deletedRemote: []
|
|
77
|
+
// 这些分支永远不进入清理列表,避免误删核心分支。
|
|
78
|
+
keepSet.add(mergedTarget)
|
|
79
|
+
if (context.defaultBranch) {
|
|
80
|
+
keepSet.add(context.defaultBranch)
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
+
if (currentBranch) {
|
|
83
|
+
keepSet.add(currentBranch)
|
|
84
|
+
}
|
|
85
|
+
protectedBranches.forEach((branch) => keepSet.add(branch))
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
88
|
-
if (!accepted) {
|
|
89
|
-
logger.warn('已取消清理。')
|
|
87
|
+
const candidates = mergedBranches.filter((branch) => !keepSet.has(branch))
|
|
88
|
+
|
|
89
|
+
if (!candidates.length) {
|
|
90
|
+
logger.info('没有可清理的本地分支。')
|
|
90
91
|
return {
|
|
91
|
-
canceled: true,
|
|
92
92
|
repoRoot: context.topLevelPath,
|
|
93
93
|
mergedTarget,
|
|
94
|
-
|
|
94
|
+
deletedLocal: [],
|
|
95
|
+
deletedRemote: []
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
|
-
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
if (!options.yes) {
|
|
100
|
+
const accepted = await confirm(
|
|
101
|
+
`确认清理以下分支吗: ${candidates.join(', ')} ?`,
|
|
102
|
+
false
|
|
103
|
+
)
|
|
104
|
+
if (!accepted) {
|
|
105
|
+
logger.warn('已取消清理。')
|
|
106
|
+
return {
|
|
107
|
+
canceled: true,
|
|
108
|
+
repoRoot: context.topLevelPath,
|
|
109
|
+
mergedTarget,
|
|
110
|
+
candidates
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
const deletedLocal = []
|
|
116
|
+
const deletedRemote = []
|
|
117
|
+
|
|
118
|
+
for (const branch of candidates) {
|
|
119
|
+
await git.deleteLocalBranch(branch, {
|
|
120
|
+
force: options.force
|
|
121
|
+
})
|
|
122
|
+
deletedLocal.push(branch)
|
|
123
|
+
|
|
124
|
+
if (options.remote) {
|
|
125
|
+
const existsRemote = await git.branchExistsRemote(branch)
|
|
126
|
+
if (existsRemote) {
|
|
127
|
+
await git.deleteRemoteBranch(branch)
|
|
128
|
+
deletedRemote.push(branch)
|
|
129
|
+
}
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
|
-
}
|
|
116
132
|
|
|
117
|
-
|
|
133
|
+
return {
|
|
134
|
+
repoRoot: context.topLevelPath,
|
|
135
|
+
mergedTarget,
|
|
136
|
+
deletedLocal,
|
|
137
|
+
deletedRemote
|
|
138
|
+
}
|
|
139
|
+
} finally {
|
|
118
140
|
if (currentBranch && currentBranch !== mergedTarget) {
|
|
119
|
-
|
|
141
|
+
try {
|
|
142
|
+
await git.checkout(currentBranch)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger.warn(`未能切回原分支 ${currentBranch}: ${error.message}`)
|
|
145
|
+
}
|
|
120
146
|
}
|
|
121
|
-
} catch (error) {
|
|
122
|
-
logger.warn(`未能切回原分支 ${currentBranch}: ${error.message}`)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
repoRoot: context.topLevelPath,
|
|
127
|
-
mergedTarget,
|
|
128
|
-
deletedLocal,
|
|
129
|
-
deletedRemote
|
|
130
147
|
}
|
|
131
148
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getAiConfig } from '../core/ai/config.js'
|
|
2
|
+
import { getConfigFilePath, getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
|
|
3
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
4
|
+
import { GitService } from '../core/git/service.js'
|
|
5
|
+
import { runCommand } from '../core/commands/shell.js'
|
|
6
|
+
|
|
7
|
+
// 构建单条健康检查结果,输入检查项字段,输出统一结构。
|
|
8
|
+
function buildCheck(id, status, title, detail, suggestion = '') {
|
|
9
|
+
return {
|
|
10
|
+
id,
|
|
11
|
+
status,
|
|
12
|
+
title,
|
|
13
|
+
detail,
|
|
14
|
+
suggestion
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 汇总检查结果状态,输入 checks 数组,输出整体状态与计数。
|
|
19
|
+
function summarizeChecks(checks) {
|
|
20
|
+
const summary = {
|
|
21
|
+
status: 'pass',
|
|
22
|
+
pass: 0,
|
|
23
|
+
warn: 0,
|
|
24
|
+
fail: 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
checks.forEach((check) => {
|
|
28
|
+
if (check.status === 'fail') {
|
|
29
|
+
summary.fail += 1
|
|
30
|
+
summary.status = 'fail'
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (check.status === 'warn') {
|
|
34
|
+
summary.warn += 1
|
|
35
|
+
if (summary.status !== 'fail') {
|
|
36
|
+
summary.status = 'warn'
|
|
37
|
+
}
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
summary.pass += 1
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return summary
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 检查 git 命令可用性,输入仓库根目录,输出健康检查项。
|
|
47
|
+
async function checkGitVersion(repoRoot) {
|
|
48
|
+
const result = await runCommand('git', ['--version'], {
|
|
49
|
+
cwd: repoRoot,
|
|
50
|
+
allowFailure: true
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
if (result.exitCode !== 0) {
|
|
54
|
+
return buildCheck('git.version', 'fail', 'Git 命令不可用', '无法执行 git --version。', '请先安装 Git 并确认 PATH 配置正确。')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return buildCheck('git.version', 'pass', 'Git 命令可用', result.stdout.trim())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 检查 origin remote 是否存在,输入 GitService,输出健康检查项。
|
|
61
|
+
async function checkOriginRemote(git) {
|
|
62
|
+
const remoteUrl = await git.getRemoteUrl('origin')
|
|
63
|
+
if (!remoteUrl) {
|
|
64
|
+
return buildCheck('git.origin', 'warn', 'origin remote 未配置', '当前仓库没有 origin remote。', '需要 push 或远程合并前,请先配置 git remote add origin <url>。')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return buildCheck('git.origin', 'pass', 'origin remote 已配置', remoteUrl)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 检查 hook 配置,输入 git context,输出健康检查项。
|
|
71
|
+
function checkHooks(context) {
|
|
72
|
+
if (context.hasHuskyFolder || context.hasHookPath) {
|
|
73
|
+
return buildCheck('git.hooks', 'pass', 'Git hooks 已检测到', `hookPath=${context.hookPath}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return buildCheck('git.hooks', 'warn', '未检测到 Git hooks', '当前仓库没有 .husky 目录或 hooksPath。', '如团队依赖 commitlint/lint-staged,建议补齐 hooks 配置。')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 检查 AI profile 配置,输入 AI 配置对象,输出健康检查项。
|
|
80
|
+
function checkAiProfile(aiConfig) {
|
|
81
|
+
const profileCount = Object.keys(aiConfig.profiles || {}).length
|
|
82
|
+
if (!profileCount || !aiConfig.activeProfile) {
|
|
83
|
+
return buildCheck('ai.profile', 'warn', 'AI profile 未配置', '当前没有可用 AI profile。', '如需使用 ai commit/codereview/explain,请执行 icode config ai set ... --activate。')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return buildCheck('ai.profile', 'pass', 'AI profile 已配置', `activeProfile=${aiConfig.activeProfile}, profileCount=${profileCount}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 检查远程合并平台配置,输入平台配置与仓库策略,输出健康检查项。
|
|
90
|
+
function checkRemoteMerge(remoteMergePlatform, repoPolicy) {
|
|
91
|
+
const remoteMergePolicy = repoPolicy.remoteMerge && typeof repoPolicy.remoteMerge === 'object'
|
|
92
|
+
? repoPolicy.remoteMerge
|
|
93
|
+
: {}
|
|
94
|
+
|
|
95
|
+
if (remoteMergePolicy.enabled === false) {
|
|
96
|
+
return buildCheck('remoteMerge.config', 'pass', '远程合并已在仓库禁用', 'repositories.<repo>.remoteMerge.enabled=false')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!remoteMergePlatform.provider || !remoteMergePlatform.apiKey) {
|
|
100
|
+
return buildCheck('remoteMerge.config', 'warn', '远程合并平台未完整配置', '缺少 provider 或 apiKey。', '如需 icode push -r,请执行 icode config platform remote-merge set --provider gitlab --api-key <key>。')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return buildCheck('remoteMerge.config', 'pass', '远程合并平台已配置', `provider=${remoteMergePlatform.provider}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 执行项目健康检查,输入仓库选项,输出 context、checks 和 summary。
|
|
107
|
+
export async function runDoctorWorkflow(options = {}) {
|
|
108
|
+
const context = await resolveGitContext({
|
|
109
|
+
cwd: options.cwd,
|
|
110
|
+
repoMode: options.repoMode
|
|
111
|
+
})
|
|
112
|
+
const git = new GitService(context)
|
|
113
|
+
const repoPolicy = getRepoPolicy(context.topLevelPath)
|
|
114
|
+
const aiConfig = getAiConfig()
|
|
115
|
+
const remoteMergePlatform = getPlatformConfig('remoteMerge')
|
|
116
|
+
|
|
117
|
+
const checks = [
|
|
118
|
+
await checkGitVersion(context.topLevelPath),
|
|
119
|
+
await checkOriginRemote(git),
|
|
120
|
+
checkHooks(context),
|
|
121
|
+
checkAiProfile(aiConfig),
|
|
122
|
+
checkRemoteMerge(remoteMergePlatform, repoPolicy)
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
configPath: getConfigFilePath(),
|
|
127
|
+
context,
|
|
128
|
+
checks,
|
|
129
|
+
summary: summarizeChecks(checks)
|
|
130
|
+
}
|
|
131
|
+
}
|