@icode-js/icode 3.0.2

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 (46) hide show
  1. package/README.md +346 -0
  2. package/bin/icode.js +6 -0
  3. package/package.json +34 -0
  4. package/src/cli.js +131 -0
  5. package/src/commands/ai.js +287 -0
  6. package/src/commands/checkout.js +59 -0
  7. package/src/commands/clean.js +65 -0
  8. package/src/commands/codereview.js +52 -0
  9. package/src/commands/config.js +513 -0
  10. package/src/commands/explain.js +80 -0
  11. package/src/commands/help.js +49 -0
  12. package/src/commands/info.js +57 -0
  13. package/src/commands/migrate.js +86 -0
  14. package/src/commands/push.js +125 -0
  15. package/src/commands/sync.js +74 -0
  16. package/src/commands/tag.js +53 -0
  17. package/src/commands/undo.js +66 -0
  18. package/src/core/ai-client.js +1125 -0
  19. package/src/core/ai-commit-summary.js +18 -0
  20. package/src/core/ai-config.js +342 -0
  21. package/src/core/ai-diff-range.js +117 -0
  22. package/src/core/args.js +47 -0
  23. package/src/core/commit-conventions.js +169 -0
  24. package/src/core/config-store.js +194 -0
  25. package/src/core/errors.js +25 -0
  26. package/src/core/git-context.js +105 -0
  27. package/src/core/git-service.js +428 -0
  28. package/src/core/hook-diagnostics.js +23 -0
  29. package/src/core/loading.js +36 -0
  30. package/src/core/logger.js +55 -0
  31. package/src/core/prompts.js +152 -0
  32. package/src/core/shell.js +77 -0
  33. package/src/workflows/ai-codereview-workflow.js +126 -0
  34. package/src/workflows/ai-commit-workflow.js +128 -0
  35. package/src/workflows/ai-conflict-workflow.js +102 -0
  36. package/src/workflows/ai-explain-workflow.js +116 -0
  37. package/src/workflows/ai-risk-review-workflow.js +49 -0
  38. package/src/workflows/checkout-workflow.js +85 -0
  39. package/src/workflows/clean-workflow.js +131 -0
  40. package/src/workflows/info-workflow.js +30 -0
  41. package/src/workflows/migrate-workflow.js +449 -0
  42. package/src/workflows/push-workflow.js +276 -0
  43. package/src/workflows/rollback-workflow.js +84 -0
  44. package/src/workflows/sync-workflow.js +141 -0
  45. package/src/workflows/tag-workflow.js +64 -0
  46. package/src/workflows/undo-workflow.js +328 -0
@@ -0,0 +1,276 @@
1
+ import { getRepoPolicy } from '../core/config-store.js'
2
+ import { IcodeError } from '../core/errors.js'
3
+ import { GitService } from '../core/git-service.js'
4
+ import { resolveGitContext } from '../core/git-context.js'
5
+ import { formatAiCommitSummary } from '../core/ai-commit-summary.js'
6
+ import { logger } from '../core/logger.js'
7
+ import { confirm, input } from '../core/prompts.js'
8
+ import { runAiCommitWorkflow } from './ai-commit-workflow.js'
9
+
10
+ function uniqueBranches(branches) {
11
+ return [...new Set(branches.map((item) => item.trim()).filter(Boolean))]
12
+ }
13
+
14
+ // Run AI commit before push and keep the full generated message in follow-up logs.
15
+ async function prepareAiCommitIfEnabled(inputOptions) {
16
+ if (!inputOptions.aiCommit) {
17
+ return {
18
+ enabled: false
19
+ }
20
+ }
21
+
22
+ if (inputOptions.message?.trim()) {
23
+ logger.warn('--ai-commit 已启用,将优先使用 AI 生成的提交信息。')
24
+ }
25
+
26
+ try {
27
+ const result = await runAiCommitWorkflow({
28
+ apply: true,
29
+ lang: inputOptions.aiCommitLang || 'zh',
30
+ profile: inputOptions.aiProfile,
31
+ repoMode: inputOptions.repoMode,
32
+ noVerify: inputOptions.noVerify,
33
+ yes: inputOptions.yes,
34
+ cwd: inputOptions.cwd,
35
+ silentContextLog: true
36
+ })
37
+
38
+ if (result.canceled) {
39
+ logger.warn('已取消 AI 自动提交。')
40
+ return {
41
+ enabled: true,
42
+ canceled: true
43
+ }
44
+ }
45
+
46
+ logger.success(`AI 自动提交完成:\n${formatAiCommitSummary(result.commitId, result.commitMessage)}`)
47
+ return {
48
+ enabled: true,
49
+ applied: true,
50
+ commitId: result.commitId,
51
+ commitMessage: result.commitMessage
52
+ }
53
+ } catch (error) {
54
+ if (error?.code === 'AI_COMMIT_EMPTY_DIFF') {
55
+ logger.info('未检测到可提交改动,跳过 --ai-commit。')
56
+ return {
57
+ enabled: true,
58
+ skipped: true,
59
+ reason: 'no-diff'
60
+ }
61
+ }
62
+
63
+ throw error
64
+ }
65
+ }
66
+
67
+ async function prepareCommitIfNeeded(git, options) {
68
+ const hasChanges = await git.hasChanges()
69
+ if (!hasChanges) {
70
+ logger.info('工作区无改动,跳过 commit。')
71
+ return false
72
+ }
73
+
74
+ let message = options.message?.trim()
75
+ if (!message) {
76
+ message = (await input('请输入提交信息', '')).trim()
77
+ }
78
+
79
+ if (!message) {
80
+ throw new IcodeError('检测到代码改动但未提供提交信息,请使用 -m 或 --message。', {
81
+ code: 'PUSH_COMMIT_MESSAGE_REQUIRED',
82
+ exitCode: 2
83
+ })
84
+ }
85
+
86
+ // 统一自动暂存,降低同学手动 add 的负担。
87
+ await git.stageAll()
88
+ await git.commit(message, {
89
+ noVerify: options.noVerify
90
+ })
91
+ logger.success(`提交完成: ${message}`)
92
+ return true
93
+ }
94
+
95
+ async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
96
+ const localExists = await git.branchExistsLocal(targetBranch)
97
+ const remoteExists = await git.branchExistsRemote(targetBranch)
98
+ let checkoutMode = 'local'
99
+
100
+ if (localExists) {
101
+ await git.checkout(targetBranch)
102
+ } else if (remoteExists) {
103
+ await git.checkoutTracking(targetBranch)
104
+ checkoutMode = 'tracking'
105
+ } else {
106
+ // 目标分支不存在时,默认从 source 分支切出,方便“临时发布分支”场景。
107
+ logger.warn(`目标分支 ${targetBranch} 不存在,将从 ${sourceBranch} 创建。`)
108
+ await git.checkoutNewBranch(targetBranch, sourceBranch)
109
+ checkoutMode = 'created'
110
+ }
111
+
112
+ if (remoteExists) {
113
+ await git.pull(targetBranch, {
114
+ allowUnrelatedHistories: true,
115
+ noRebase: true
116
+ })
117
+ }
118
+
119
+ return {
120
+ remoteExists,
121
+ checkoutMode
122
+ }
123
+ }
124
+
125
+ export async function runPushWorkflow(inputOptions) {
126
+ const context = await resolveGitContext({
127
+ cwd: inputOptions.cwd,
128
+ repoMode: inputOptions.repoMode
129
+ })
130
+
131
+ const git = new GitService(context)
132
+ const policy = getRepoPolicy(context.topLevelPath)
133
+ const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
134
+
135
+ logger.info(`仓库根目录: ${context.topLevelPath}`)
136
+ if (context.inheritedFromParent) {
137
+ logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
138
+ }
139
+
140
+ const currentBranch = (await git.getCurrentBranch()) || context.currentBranch
141
+ if (!currentBranch) {
142
+ throw new IcodeError('无法识别当前分支,请检查仓库状态。', {
143
+ code: 'PUSH_BRANCH_UNKNOWN',
144
+ exitCode: 2
145
+ })
146
+ }
147
+
148
+ const aiCommitResult = await prepareAiCommitIfEnabled(inputOptions)
149
+ if (aiCommitResult.canceled) {
150
+ return {
151
+ canceled: true,
152
+ reason: 'ai-commit-canceled',
153
+ repoRoot: context.topLevelPath,
154
+ currentBranch,
155
+ inheritedFromParent: context.inheritedFromParent
156
+ }
157
+ }
158
+
159
+ const shouldRunManualCommit = !aiCommitResult.applied && aiCommitResult.reason !== 'no-diff'
160
+ if (shouldRunManualCommit) {
161
+ await prepareCommitIfNeeded(git, inputOptions)
162
+ }
163
+ await git.fetch()
164
+
165
+ if (inputOptions.pullMain && context.defaultBranch !== currentBranch) {
166
+ logger.info(`先同步主分支 ${context.defaultBranch} 到当前分支 ${currentBranch}`)
167
+ await git.pull(context.defaultBranch, {
168
+ allowUnrelatedHistories: true,
169
+ noRebase: true
170
+ })
171
+ }
172
+
173
+ let branchTargets = uniqueBranches([
174
+ ...(inputOptions.notPushCurrent ? [] : [currentBranch]),
175
+ ...(inputOptions.targetBranches || [])
176
+ ])
177
+ if (inputOptions.notPushCurrent) {
178
+ branchTargets = branchTargets.filter((branchName) => branchName !== currentBranch)
179
+ }
180
+
181
+ if (!branchTargets.length) {
182
+ throw new IcodeError('没有可执行的目标分支。', {
183
+ code: 'PUSH_EMPTY_TARGETS',
184
+ exitCode: 2
185
+ })
186
+ }
187
+
188
+ if (!inputOptions.yes) {
189
+ const confirmed = await confirm(
190
+ `确认将 ${currentBranch} 推送/合并到以下分支: ${branchTargets.join(', ')} ?`,
191
+ true
192
+ )
193
+
194
+ if (!confirmed) {
195
+ logger.warn('已取消执行。')
196
+ return {
197
+ canceled: true,
198
+ branchTargets,
199
+ repoRoot: context.topLevelPath
200
+ }
201
+ }
202
+ }
203
+
204
+ const summary = []
205
+ const originalBranch = currentBranch
206
+
207
+ try {
208
+ for (const targetBranch of branchTargets) {
209
+ if (protectedBranches.has(targetBranch) && !inputOptions.forceProtected) {
210
+ logger.warn(`跳过受保护分支: ${targetBranch}(可用 --force-protected 覆盖)`)
211
+ summary.push({ branch: targetBranch, status: 'skipped-protected' })
212
+ continue
213
+ }
214
+
215
+ logger.info(`处理分支: ${targetBranch}`)
216
+
217
+ if (targetBranch === currentBranch) {
218
+ const remoteExists = await git.branchExistsRemote(targetBranch)
219
+ if (remoteExists) {
220
+ logger.info(`同步远程分支: ${targetBranch}`)
221
+ await git.pull(targetBranch, {
222
+ allowUnrelatedHistories: true,
223
+ noRebase: true
224
+ })
225
+ }
226
+
227
+ logger.info(`推送当前分支: ${targetBranch}`)
228
+ await git.push(targetBranch, {
229
+ setUpstream: !remoteExists,
230
+ noVerify: inputOptions.noVerify
231
+ })
232
+
233
+ logger.success(`推送成功: ${targetBranch}`)
234
+ summary.push({ branch: targetBranch, status: 'pushed' })
235
+ continue
236
+ }
237
+
238
+ logger.info(`切换到目标分支: ${targetBranch}`)
239
+ const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch)
240
+ logger.info(`目标分支准备完成: ${targetBranch} (${checkoutMode})`)
241
+
242
+ // 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
243
+ logger.info(`合并分支: ${currentBranch} -> ${targetBranch}`)
244
+ await git.merge(currentBranch, {
245
+ noFf: true,
246
+ noEdit: true
247
+ })
248
+ logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
249
+
250
+ logger.info(`推送目标分支: ${targetBranch}`)
251
+ await git.push(targetBranch, {
252
+ setUpstream: !remoteExists,
253
+ noVerify: inputOptions.noVerify
254
+ })
255
+
256
+ logger.success(`目标分支推送成功: ${targetBranch}`)
257
+ summary.push({ branch: targetBranch, status: 'merged-and-pushed' })
258
+ }
259
+ } finally {
260
+ const branchAfterWorkflow = await git.getCurrentBranch()
261
+ if (branchAfterWorkflow && branchAfterWorkflow !== originalBranch) {
262
+ try {
263
+ await git.checkout(originalBranch)
264
+ } catch (error) {
265
+ logger.warn(`未能自动切回原分支 ${originalBranch}: ${error.message}`)
266
+ }
267
+ }
268
+ }
269
+
270
+ return {
271
+ repoRoot: context.topLevelPath,
272
+ currentBranch,
273
+ summary,
274
+ inheritedFromParent: context.inheritedFromParent
275
+ }
276
+ }
@@ -0,0 +1,84 @@
1
+ import { IcodeError } from '../core/errors.js'
2
+ import { resolveGitContext } from '../core/git-context.js'
3
+ import { GitService } from '../core/git-service.js'
4
+ import { logger } from '../core/logger.js'
5
+ import { confirm } from '../core/prompts.js'
6
+
7
+ function isRevertConflict(error) {
8
+ const output = `${error?.meta?.stdout || ''}\n${error?.meta?.stderr || ''}\n${error?.message || ''}`
9
+ return /revert is already in progress|could not revert|after resolving the conflicts|CONFLICT \(/i.test(output)
10
+ }
11
+
12
+ export async function runRollbackWorkflow(options) {
13
+ const mode = options.mode || 'revert'
14
+ const ref = options.ref || (mode === 'revert' ? 'HEAD' : 'HEAD~1')
15
+
16
+ const context = await resolveGitContext({
17
+ cwd: options.cwd,
18
+ repoMode: options.repoMode
19
+ })
20
+
21
+ const git = new GitService(context)
22
+
23
+ logger.info(`仓库根目录: ${context.topLevelPath}`)
24
+ if (context.inheritedFromParent) {
25
+ logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
26
+ }
27
+
28
+ if (mode === 'revert') {
29
+ logger.info(`执行回滚(revert): ${ref}`)
30
+ try {
31
+ await git.revert(ref)
32
+ } catch (error) {
33
+ if (isRevertConflict(error)) {
34
+ throw new IcodeError(
35
+ 'revert 发生冲突。请解决冲突后执行 `git revert --continue`,或使用 `icode undo --recover abort` 直接中止。',
36
+ {
37
+ code: 'REVERT_CONFLICT',
38
+ cause: error,
39
+ meta: error.meta
40
+ }
41
+ )
42
+ }
43
+ throw error
44
+ }
45
+ return {
46
+ mode,
47
+ ref,
48
+ repoRoot: context.topLevelPath
49
+ }
50
+ }
51
+
52
+ if (!['soft', 'mixed', 'hard'].includes(mode)) {
53
+ throw new IcodeError('mode 仅支持: revert | soft | mixed | hard', {
54
+ code: 'ROLLBACK_MODE_INVALID',
55
+ exitCode: 2
56
+ })
57
+ }
58
+
59
+ if (mode === 'hard' && !options.yes) {
60
+ // hard reset 会直接丢弃工作区改动,这里强制确认一次降低误操作风险。
61
+ const accepted = await confirm(
62
+ `你将执行 git reset --hard ${ref},这会丢失未提交改动,是否继续?`,
63
+ false
64
+ )
65
+ if (!accepted) {
66
+ logger.warn('已取消 hard 回滚。')
67
+ return {
68
+ canceled: true,
69
+ mode,
70
+ ref,
71
+ repoRoot: context.topLevelPath
72
+ }
73
+ }
74
+ }
75
+
76
+ logger.info(`执行回滚(reset --${mode}): ${ref}`)
77
+ await git.reset(mode, ref)
78
+
79
+ return {
80
+ mode,
81
+ ref,
82
+ repoRoot: context.topLevelPath
83
+ }
84
+ }
@@ -0,0 +1,141 @@
1
+ import { IcodeError } from '../core/errors.js'
2
+ import { resolveGitContext } from '../core/git-context.js'
3
+ import { GitService } from '../core/git-service.js'
4
+ import { logger } from '../core/logger.js'
5
+ import { confirm } from '../core/prompts.js'
6
+
7
+ function unique(list) {
8
+ return [...new Set(list.map((item) => item.trim()).filter(Boolean))]
9
+ }
10
+
11
+ async function ensureBranchReady(git, branchName) {
12
+ const localExists = await git.branchExistsLocal(branchName)
13
+ const remoteExists = await git.branchExistsRemote(branchName)
14
+
15
+ if (!localExists && !remoteExists) {
16
+ return {
17
+ available: false,
18
+ localExists,
19
+ remoteExists
20
+ }
21
+ }
22
+
23
+ if (localExists) {
24
+ await git.checkout(branchName)
25
+ } else {
26
+ await git.checkoutTracking(branchName)
27
+ }
28
+
29
+ return {
30
+ available: true,
31
+ localExists,
32
+ remoteExists
33
+ }
34
+ }
35
+
36
+ export async function runSyncWorkflow(options) {
37
+ const context = await resolveGitContext({
38
+ cwd: options.cwd,
39
+ repoMode: options.repoMode
40
+ })
41
+
42
+ const git = new GitService(context)
43
+ const currentBranch = (await git.getCurrentBranch()) || context.currentBranch
44
+
45
+ let targetInput = options.branches?.length
46
+ ? [...options.branches]
47
+ : [currentBranch, context.defaultBranch]
48
+
49
+ if (options.allLocal) {
50
+ // all-local 场景下自动拉取全部本地分支,降低人工逐个输入分支名的成本。
51
+ const localBranches = await git.listLocalBranches()
52
+ targetInput = [...localBranches, ...targetInput]
53
+ }
54
+
55
+ const targets = unique(targetInput)
56
+
57
+ if (!targets.length) {
58
+ throw new IcodeError('没有可同步的分支。', {
59
+ code: 'SYNC_EMPTY_TARGETS',
60
+ exitCode: 2
61
+ })
62
+ }
63
+
64
+ logger.info(`仓库根目录: ${context.topLevelPath}`)
65
+ if (context.inheritedFromParent) {
66
+ logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
67
+ }
68
+
69
+ if (!options.yes) {
70
+ const ok = await confirm(`确认同步以下分支: ${targets.join(', ')} ?`, true)
71
+ if (!ok) {
72
+ logger.warn('已取消同步。')
73
+ return {
74
+ canceled: true,
75
+ targets,
76
+ repoRoot: context.topLevelPath
77
+ }
78
+ }
79
+ }
80
+
81
+ const summary = []
82
+ await git.fetch()
83
+ const originalBranch = currentBranch
84
+
85
+ try {
86
+ for (const branchName of targets) {
87
+ logger.info(`同步分支: ${branchName}`)
88
+ const setup = await ensureBranchReady(git, branchName)
89
+
90
+ if (!setup.available) {
91
+ logger.warn(`分支不存在(本地+远程): ${branchName}`)
92
+ summary.push({
93
+ branch: branchName,
94
+ status: 'missing'
95
+ })
96
+ continue
97
+ }
98
+
99
+ if (setup.remoteExists) {
100
+ await git.pull(branchName, {
101
+ noRebase: !options.rebase,
102
+ allowUnrelatedHistories: true
103
+ })
104
+ }
105
+
106
+ if (options.mergeMain && branchName !== context.defaultBranch) {
107
+ // 把最新主分支合入目标分支,降低后续提测/发布时的冲突概率。
108
+ await git.merge(context.defaultBranch, {
109
+ noFf: true,
110
+ noEdit: true
111
+ })
112
+ }
113
+
114
+ if (options.push) {
115
+ await git.push(branchName, {
116
+ setUpstream: !setup.remoteExists,
117
+ noVerify: options.noVerify
118
+ })
119
+ }
120
+
121
+ summary.push({
122
+ branch: branchName,
123
+ status: options.push ? 'synced-and-pushed' : 'synced'
124
+ })
125
+ }
126
+ } finally {
127
+ if (originalBranch) {
128
+ try {
129
+ await git.checkout(originalBranch)
130
+ } catch (error) {
131
+ logger.warn(`未能自动切回原分支 ${originalBranch}: ${error.message}`)
132
+ }
133
+ }
134
+ }
135
+
136
+ return {
137
+ repoRoot: context.topLevelPath,
138
+ targets,
139
+ summary
140
+ }
141
+ }
@@ -0,0 +1,64 @@
1
+ import { IcodeError } from '../core/errors.js'
2
+ import { GitService } from '../core/git-service.js'
3
+ import { resolveGitContext } from '../core/git-context.js'
4
+ import { logger } from '../core/logger.js'
5
+ import { input } from '../core/prompts.js'
6
+
7
+ function todayStamp() {
8
+ const now = new Date()
9
+ const year = String(now.getFullYear())
10
+ const month = String(now.getMonth() + 1).padStart(2, '0')
11
+ const date = String(now.getDate()).padStart(2, '0')
12
+ return `${year}${month}${date}`
13
+ }
14
+
15
+ function nextDailyTag(existingTags, dateStamp) {
16
+ const prefix = `v${dateStamp}_`
17
+ const serials = existingTags
18
+ .filter((tag) => tag.startsWith(prefix))
19
+ .map((tag) => Number(tag.split('_')[1]))
20
+ .filter((value) => Number.isFinite(value))
21
+
22
+ const max = serials.length ? Math.max(...serials) : 0
23
+ const next = String(max + 1).padStart(2, '0')
24
+ return `${prefix}${next}`
25
+ }
26
+
27
+ export async function runTagWorkflow(options) {
28
+ const context = await resolveGitContext({
29
+ cwd: options.cwd,
30
+ repoMode: options.repoMode
31
+ })
32
+
33
+ const git = new GitService(context)
34
+ const dateStamp = todayStamp()
35
+ const candidateTags = await git.listTags(`v${dateStamp}_*`)
36
+
37
+ let tagName = options.tagName?.trim() || nextDailyTag(candidateTags, dateStamp)
38
+ if (!options.tagName) {
39
+ tagName = (await input('请输入 tag 名称', tagName)).trim() || tagName
40
+ }
41
+
42
+ if (!tagName) {
43
+ throw new IcodeError('tag 名称不能为空。', {
44
+ code: 'TAG_NAME_REQUIRED',
45
+ exitCode: 2
46
+ })
47
+ }
48
+
49
+ const message = options.message?.trim() || `release: ${tagName}`
50
+
51
+ logger.info(`创建 tag: ${tagName}`)
52
+ await git.createAnnotatedTag(tagName, message, options.fromRef)
53
+
54
+ logger.info(`推送 tag: ${tagName}`)
55
+ await git.pushTag(tagName, {
56
+ noVerify: options.noVerify
57
+ })
58
+
59
+ return {
60
+ repoRoot: context.topLevelPath,
61
+ tagName,
62
+ message
63
+ }
64
+ }