@icode-js/icode 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +287 -24
  2. package/bin/icode.js +10 -0
  3. package/package.json +3 -3
  4. package/src/cli.js +175 -8
  5. package/src/commands/checkout.js +73 -6
  6. package/src/commands/clean.js +47 -7
  7. package/src/commands/config-ux.js +292 -0
  8. package/src/commands/config.js +199 -48
  9. package/src/commands/continue.js +104 -0
  10. package/src/commands/doctor.js +15 -29
  11. package/src/commands/help.js +54 -34
  12. package/src/commands/history.js +147 -0
  13. package/src/commands/info.js +39 -4
  14. package/src/commands/log.js +142 -0
  15. package/src/commands/migrate.js +19 -12
  16. package/src/commands/open.js +138 -0
  17. package/src/commands/pr.js +219 -0
  18. package/src/commands/push.js +62 -22
  19. package/src/commands/ready.js +18 -48
  20. package/src/commands/release.js +112 -0
  21. package/src/commands/rescue.js +164 -0
  22. package/src/commands/save.js +52 -11
  23. package/src/commands/stash.js +210 -0
  24. package/src/commands/sw.js +89 -0
  25. package/src/commands/sync.js +19 -2
  26. package/src/commands/tag.js +40 -1
  27. package/src/commands/undo.js +9 -3
  28. package/src/core/ai/diff-range.js +1 -1
  29. package/src/core/cli/completion.js +237 -1
  30. package/src/core/cli/output.js +46 -0
  31. package/src/core/cli/render.js +121 -0
  32. package/src/core/commands/shell.js +9 -1
  33. package/src/core/config/repo-config.js +93 -0
  34. package/src/core/config/store.js +60 -10
  35. package/src/core/git/autostash.js +46 -0
  36. package/src/core/git/backup.js +170 -0
  37. package/src/core/git/conflicts.js +58 -0
  38. package/src/core/git/context.js +72 -5
  39. package/src/core/git/file-guard.js +193 -0
  40. package/src/core/git/operation-state.js +63 -0
  41. package/src/core/git/operations.js +32 -0
  42. package/src/core/git/semver.js +194 -0
  43. package/src/core/git/service.js +454 -24
  44. package/src/core/platform/http.js +53 -0
  45. package/src/core/platform/index.js +165 -0
  46. package/src/core/platform/providers.js +454 -0
  47. package/src/core/platform/remote-url.js +92 -0
  48. package/src/core/remote-merge/client.js +33 -3
  49. package/src/core/tools/browser.js +28 -0
  50. package/src/core/tools/commit-prompt.js +137 -0
  51. package/src/core/tools/interactive.js +272 -40
  52. package/src/core/tools/logger.js +33 -23
  53. package/src/core/tools/theme.js +228 -0
  54. package/src/workflows/ai-commit-workflow.js +52 -20
  55. package/src/workflows/checkout-workflow.js +24 -10
  56. package/src/workflows/clean-workflow.js +135 -83
  57. package/src/workflows/continue-workflow.js +233 -0
  58. package/src/workflows/doctor-workflow.js +10 -7
  59. package/src/workflows/history-workflow.js +211 -0
  60. package/src/workflows/migrate-workflow.js +39 -14
  61. package/src/workflows/pr-workflow.js +252 -0
  62. package/src/workflows/push/commit-step.js +123 -0
  63. package/src/workflows/push/local-merge-step.js +213 -0
  64. package/src/workflows/push/plan.js +63 -0
  65. package/src/workflows/push/remote-merge-step.js +224 -0
  66. package/src/workflows/push-workflow.js +185 -492
  67. package/src/workflows/ready-workflow.js +14 -10
  68. package/src/workflows/release-workflow.js +189 -0
  69. package/src/workflows/rescue-workflow.js +137 -0
  70. package/src/workflows/save-workflow.js +106 -41
  71. package/src/workflows/switch-workflow.js +171 -0
  72. package/src/workflows/sync-workflow.js +143 -44
  73. package/src/workflows/tag-workflow.js +103 -17
  74. package/src/workflows/undo-workflow.js +113 -34
  75. package/src/workflows/rollback-workflow.js +0 -99
@@ -0,0 +1,104 @@
1
+ import { parseCommandArgs, printJson } from '../core/cli/output.js'
2
+ import { c, print, renderBranchResults } from '../core/cli/render.js'
3
+ import { confirm, isInteractiveTerminal } from '../core/tools/interactive.js'
4
+ import { logger } from '../core/tools/logger.js'
5
+ import { inspectPausedWork, runAbortWorkflow, runContinueWorkflow } from '../workflows/continue-workflow.js'
6
+
7
+ const RESULT_LABELS = {
8
+ pushed: '已推送',
9
+ 'merged-and-pushed': '已合并并推送',
10
+ 'force-pushed': '已强推',
11
+ 'skipped-protected': '已跳过(受保护分支)',
12
+ synced: '已同步',
13
+ 'synced-and-pushed': '已同步并推送',
14
+ missing: '本地和远程都不存在'
15
+ }
16
+
17
+ function printContinueHelp() {
18
+ process.stdout.write(`
19
+ Usage:
20
+ icode continue [options]
21
+
22
+ 解决冲突后继续刚才被打断的操作:
23
+ 1. 检查冲突文件里是否还有 <<<<<<< / >>>>>>> 标记,有就列出文件和行号
24
+ 2. 自动 git add 已解决的文件
25
+ 3. 继续 merge / rebase / cherry-pick / revert
26
+ 4. 如果是 push / migrate / sync 中途冲突,接着把剩下的步骤做完(推送、处理剩余分支、切回原分支)
27
+
28
+ Options:
29
+ --json 输出机器可读 JSON
30
+ --repo-mode <mode> 仓库模式: auto | strict
31
+ -h, --help 查看帮助
32
+ `)
33
+ }
34
+
35
+ function printAbortHelp() {
36
+ process.stdout.write(`
37
+ Usage:
38
+ icode abort [options]
39
+
40
+ 放弃刚才被冲突打断的操作:中止 merge / rebase / cherry-pick / revert,并切回原分支。
41
+ 已经推送成功的分支不会被撤回。
42
+
43
+ Options:
44
+ -y, --yes 不再确认
45
+ --json 输出机器可读 JSON
46
+ --repo-mode <mode> 仓库模式: auto | strict
47
+ -h, --help 查看帮助
48
+ `)
49
+ }
50
+
51
+ export async function runContinueCommand(rawArgs) {
52
+ const { values } = parseCommandArgs(rawArgs)
53
+ if (values.help) {
54
+ printContinueHelp()
55
+ return
56
+ }
57
+
58
+ const result = await runContinueWorkflow({ repoMode: values['repo-mode'], silentLog: values.json })
59
+ if (values.json) {
60
+ printJson(result)
61
+ return result
62
+ }
63
+
64
+ if (result.summary.length) {
65
+ print('')
66
+ renderBranchResults(result.summary, RESULT_LABELS)
67
+ print('')
68
+ }
69
+ if (result.resumed) {
70
+ logger.success(`${result.description} 已全部完成${result.returnedTo ? c.gray(`,已切回 ${result.returnedTo}`) : ''}`)
71
+ }
72
+ return result
73
+ }
74
+
75
+ export async function runAbortCommand(rawArgs) {
76
+ const { values } = parseCommandArgs(rawArgs)
77
+ if (values.help) {
78
+ printAbortHelp()
79
+ return
80
+ }
81
+
82
+ if (!values.yes && !values.json && isInteractiveTerminal()) {
83
+ const paused = await inspectPausedWork({ repoMode: values['repo-mode'] })
84
+ if (paused) {
85
+ const accepted = await confirm(`放弃 ${paused.description}?冲突处理到一半的改动会丢弃`, false)
86
+ if (!accepted) {
87
+ logger.hint('已保留现场。处理完冲突后执行 icode continue。')
88
+ return
89
+ }
90
+ }
91
+ }
92
+
93
+ const result = await runAbortWorkflow({ repoMode: values['repo-mode'] })
94
+ if (values.json) {
95
+ printJson(result)
96
+ return result
97
+ }
98
+
99
+ logger.success(`已放弃 ${result.description || result.operation}${result.returnedTo ? c.gray(`,已切回 ${result.returnedTo}`) : ''}`)
100
+ if (result.done.length) {
101
+ logger.hint(`这些分支在中断前已推送,不会撤回: ${result.done.join(', ')}`)
102
+ }
103
+ return result
104
+ }
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from 'node:util'
2
2
  import { normalizeLegacyArgs } from '../core/cli/args.js'
3
- import { logger } from '../core/tools/logger.js'
3
+ import { keyValues, print, renderChecks, renderOverallStatus, title, ui } from '../core/cli/render.js'
4
4
  import { runDoctorWorkflow } from '../workflows/doctor-workflow.js'
5
5
 
6
6
  // 输出 doctor 子命令帮助,输入为空,输出标准帮助文本。
@@ -15,31 +15,10 @@ Options:
15
15
  -h, --help 查看帮助
16
16
 
17
17
  Notes:
18
- 检查 Git、origin、hooks、AI profile 与远程合并配置,并给出修复建议。
18
+ 检查 Git、当前 remote、hooks、AI profile 与平台 token,并给出修复建议。
19
19
  `)
20
20
  }
21
21
 
22
- // 根据检查状态选择输出函数,输入状态,输出 logger 方法。
23
- function resolveLogMethod(status) {
24
- if (status === 'fail') {
25
- return logger.error
26
- }
27
- if (status === 'warn') {
28
- return logger.warn
29
- }
30
- return logger.success
31
- }
32
-
33
- // 输出单条 doctor 检查结果,输入检查项,输出彩色日志。
34
- function printCheck(check) {
35
- const log = resolveLogMethod(check.status).bind(logger)
36
- const statusLabel = check.status.toUpperCase()
37
- log(`[${statusLabel}] ${check.title}: ${check.detail}`)
38
- if (check.suggestion) {
39
- logger.info(`建议: ${check.suggestion}`)
40
- }
41
- }
42
-
43
22
  // 执行 doctor 子命令,输入原始参数,输出健康检查结果。
44
23
  // 执行 doctor 子命令,输入原始参数,输出健康检查结果;参数会先归一化。
45
24
  export async function runDoctorCommand(rawArgs) {
@@ -68,13 +47,20 @@ export async function runDoctorCommand(rawArgs) {
68
47
  return result
69
48
  }
70
49
 
71
- logger.info(`仓库根目录: ${result.context.topLevelPath}`)
72
- logger.info(`配置文件: ${result.configPath}`)
73
- result.checks.forEach((check) => {
74
- printCheck(check)
75
- })
50
+ print(title('icode doctor'))
51
+ print(keyValues([
52
+ ['仓库', ui.path(result.context.topLevelPath)],
53
+ ['remote', ui.remote(result.context.remote || 'origin')],
54
+ ['配置文件', ui.path(result.configPath)]
55
+ ]))
56
+ print('')
57
+ renderChecks(result.checks)
76
58
 
77
59
  const summary = result.summary
78
- logger.info(`doctor 完成: pass=${summary.pass}, warn=${summary.warn}, fail=${summary.fail}`)
60
+ renderOverallStatus(summary.fail ? 'fail' : summary.warn ? 'warn' : 'pass', {
61
+ 通过: summary.pass,
62
+ 注意: summary.warn,
63
+ 失败: summary.fail
64
+ })
79
65
  return result
80
66
  }
@@ -7,60 +7,80 @@ export function printMainHelp() {
7
7
  icode v3 - Git workflow CLI
8
8
 
9
9
  Usage:
10
- icode <command> [options]
10
+ icode [global options] <command> [options]
11
11
 
12
- Commands:
13
- ai AI 助手能力(提交信息/冲突建议/代码评审)
14
- codereview AI 代码评审(不带 ai 前缀)
15
- checkout 切换/创建分支(本地/远程自动识别)
12
+ 日常:
16
13
  ready 检查仓库是否适合继续 save/push(适合 AI agent 读取)
17
- save 安全快速提交本地改动(检查 + add + commit)
18
- push 提交并推送,可合并到多个目标分支
14
+ save 安全提交(风险扫描 + add + commit),可只提交指定路径
15
+ push 提交并推送,可合并到多个目标分支;--force 安全强推
16
+ sw 快速切换分支(模糊匹配 / 最近使用 / 切回上一个)
17
+ checkout 切换/创建分支(本地/远程自动识别)
18
+ stash 查看、暂存、恢复 stash
19
+ log 更易读的提交历史(--mine / --unpushed / --file)
19
20
  sync 批量同步分支(fetch + pull)
20
- clean 清理已合并分支(可删远程)
21
- undo 向导式撤销/回滚(revert/reset)
21
+
22
+ 整理历史:
23
+ amend 修改上一个提交(追加改动 / 改提交信息)
24
+ fixup 把改动合进更早的某个提交(自动 autosquash)
22
25
  migrate 迁移分支提交(cherry-pick)
23
- tag 创建并推送 tag(支持自动命名)
24
- config 查看和修改本地配置(含 AI profile)
25
- doctor 检查 Git/AI/远程合并配置健康状态
26
+ clean 清理已合并 / 远程已删除 / 长期未动的分支
27
+
28
+ 兜底:
29
+ continue 冲突解决后继续(自动暂存,并接着完成被打断的 push/migrate/sync)
30
+ abort 放弃被冲突打断的操作并切回原分支
31
+ undo 向导式撤销/回滚(revert/reset)
32
+ rescue 找回恢复点和 reflog 里的提交
33
+
34
+ 协作与发布:
35
+ pr 创建/查看/检出/合并 PR、MR(GitHub/GitLab/Gitee/Gitea)
36
+ open 在浏览器打开仓库、分支、文件、PR、CI
37
+ tag 创建并推送 tag(日期命名或语义化版本)
38
+ release 一条命令发版(版本号 + changelog + tag + 平台 Release)
39
+
40
+ AI:
41
+ ai AI 助手能力(提交信息/冲突建议/代码评审)
42
+ codereview AI 代码评审(不带 ai 前缀)
26
43
  explain AI 解释 Git diff(自然语言)
44
+
45
+ 配置与环境:
46
+ config 查看和修改本地配置(AI profile / 平台 token / 受保护分支)
47
+ doctor 检查 Git/AI/平台配置健康状态
27
48
  info 查看当前 git 与配置环境
28
49
  completion 生成 bash/zsh tab 补全脚本
29
50
  help 查看帮助(命令总览)
30
51
 
31
52
  Global options:
53
+ -R, --remote <name> 指定使用的 remote(默认按 upstream / .icoderc / origin 自动选择)
32
54
  -d, --debug 开启调试日志(输出更多细节)
33
55
  -h, --help 查看帮助
34
56
 
35
57
  Tips:
58
+ 直接运行 icode 会显示常用操作菜单;多数命令不带参数时会给出可选项,方向键选择即可。
36
59
  icode <command> -h 查看子命令完整参数说明
60
+ 大多数命令支持 --json,便于脚本和 AI agent 读取;出错时也会输出 {"ok":false,...}
61
+ 会改写历史或删除分支的操作都会自动创建恢复点,可用 icode rescue 找回。
62
+ 仓库根目录的 .icoderc.json 可以共享 remote、主分支、受保护分支和提交扫描规则。
37
63
 
38
64
  Examples:
39
- icode checkout feature/login main --push-origin
40
65
  icode ready
41
- icode ready --json
42
66
  icode save -m "feat: add login form"
43
- icode save --ai-commit -y
44
- icode save -m "fix: adjust copy" --dry-run
45
- icode ai commit --apply -y
46
- icode ai codereview
47
- icode codereview --base origin/main --head HEAD
48
- icode explain --base origin/main --head HEAD
67
+ icode save -m "fix: typo" src/login.vue
68
+ icode sw login
49
69
  icode push release test -m "feat: batch publish" -y
50
- icode push release test --ai-commit -y
51
- icode push release test -m "feat: keep merge commit" --local-merge -y
52
- icode push release test --dry-run
53
- icode sync --all-local --merge-main
54
- icode clean --remote --force -y
55
- icode undo a1b2c3d --mode revert -y
56
- icode undo --mode revert --ref HEAD~1 -y
57
- icode undo --recover abort
58
- icode migrate feature/login release --push -y
59
- icode migrate --interactive
60
- icode push --no-verify -m "chore: bypass hooks"
61
- icode config protect add main release
62
- icode doctor
70
+ icode push --force -y
71
+ icode amend -a
72
+ icode fixup -a
73
+ icode log --unpushed
74
+ icode stash save -m "wip"
75
+ icode clean --gone -y
76
+ icode continue
77
+ icode undo --mode hard --ref HEAD~1
78
+ icode rescue
79
+ icode pr create --ai
80
+ icode open src/cli.js:42
81
+ icode release --dry-run
82
+ icode --remote upstream sync
83
+ icode config platform token set github <token>
63
84
  source <(icode completion zsh)
64
- icode info
65
85
  `)
66
86
  }
@@ -0,0 +1,147 @@
1
+ import { parseCommandArgs, printJson } from '../core/cli/output.js'
2
+ import { c, ui } from '../core/cli/render.js'
3
+ import { isInteractiveTerminal } from '../core/tools/interactive.js'
4
+ import { logger } from '../core/tools/logger.js'
5
+ import { runAmendWorkflow, runFixupWorkflow } from '../workflows/history-workflow.js'
6
+
7
+ const SHARED_OPTIONS = {
8
+ all: { type: 'boolean', short: 'a', default: false },
9
+ 'allow-risky': { type: 'boolean', default: false },
10
+ 'no-verify': { type: 'boolean', default: false }
11
+ }
12
+
13
+ function printAmendHelp() {
14
+ process.stdout.write(`
15
+ Usage:
16
+ icode amend [path...] [options]
17
+
18
+ Arguments:
19
+ [path...] 把这些路径的改动追加到上一个提交
20
+
21
+ Options:
22
+ -m, --message <msg> 修改上一个提交的信息
23
+ -a, --all 追加全部未提交改动
24
+ --allow-risky 检测到敏感文件/疑似密钥/大文件时仍继续
25
+ --no-verify 跳过 hook 校验
26
+ --json 输出机器可读 JSON
27
+ -y, --yes 已推送时也不再确认
28
+ --repo-mode <mode> 仓库模式: auto | strict
29
+ -h, --help 查看帮助
30
+
31
+ Notes:
32
+ 不传 -a 或路径时只追加已暂存的改动。上一个提交已推送时会提示,之后需要 icode push --force。
33
+ 执行前会自动创建恢复点。
34
+
35
+ Examples:
36
+ icode amend -a
37
+ icode amend -m "feat: 更准确的描述"
38
+ icode amend src/login.js
39
+ `)
40
+ }
41
+
42
+ function printFixupHelp() {
43
+ process.stdout.write(`
44
+ Usage:
45
+ icode fixup [commit] [options]
46
+
47
+ Arguments:
48
+ [commit] 要修正的提交;不传则从当前分支未合入主分支的提交里选择
49
+
50
+ Options:
51
+ -a, --all 先暂存全部未提交改动
52
+ --no-rebase 只生成 fixup! 提交,不自动整理历史
53
+ --allow-risky 检测到敏感文件/疑似密钥/大文件时仍继续
54
+ --no-verify 跳过 hook 校验
55
+ --json 输出机器可读 JSON
56
+ -y, --yes 已推送时也不再确认
57
+ --repo-mode <mode> 仓库模式: auto | strict
58
+ -h, --help 查看帮助
59
+
60
+ Notes:
61
+ 等同于 git commit --fixup=<commit> && git rebase -i --autosquash(不会打开编辑器)。
62
+ 发生冲突时用 icode continue / icode abort 处理。执行前会自动创建恢复点。
63
+
64
+ Examples:
65
+ icode fixup -a
66
+ icode fixup a1b2c3d
67
+ `)
68
+ }
69
+
70
+ function printForceHint(result) {
71
+ if (result.needsForcePush) {
72
+ logger.warn('改写的提交之前已推送,更新远程请执行: icode push --force')
73
+ }
74
+ }
75
+
76
+ export async function runAmendCommand(rawArgs) {
77
+ const { values, positionals } = parseCommandArgs(rawArgs, {
78
+ ...SHARED_OPTIONS,
79
+ message: { type: 'string', short: 'm' }
80
+ })
81
+ if (values.help) {
82
+ printAmendHelp()
83
+ return
84
+ }
85
+
86
+ const result = await runAmendWorkflow({
87
+ message: values.message,
88
+ all: values.all,
89
+ paths: positionals,
90
+ interactive: !values.json && !values.yes && isInteractiveTerminal(),
91
+ allowRisky: values['allow-risky'],
92
+ noVerify: values['no-verify'],
93
+ yes: values.yes,
94
+ repoMode: values['repo-mode'],
95
+ silentLog: values.json
96
+ })
97
+
98
+ if (values.json) {
99
+ printJson(result)
100
+ return result
101
+ }
102
+ if (result.canceled) {
103
+ logger.warn('已取消。')
104
+ return result
105
+ }
106
+ logger.success(`已修改上一个提交 ${ui.hash(result.previousCommitId)} ${c.gray('→')} ${ui.hash(result.commitId)} ${ui.subject(result.message.split('\n')[0])}`)
107
+ printForceHint(result)
108
+ return result
109
+ }
110
+
111
+ export async function runFixupCommand(rawArgs) {
112
+ const { values, positionals } = parseCommandArgs(rawArgs, {
113
+ ...SHARED_OPTIONS,
114
+ 'no-rebase': { type: 'boolean', default: false }
115
+ })
116
+ if (values.help) {
117
+ printFixupHelp()
118
+ return
119
+ }
120
+
121
+ const result = await runFixupWorkflow({
122
+ commit: positionals[0],
123
+ all: values.all,
124
+ noRebase: values['no-rebase'],
125
+ allowRisky: values['allow-risky'],
126
+ noVerify: values['no-verify'],
127
+ yes: values.yes,
128
+ repoMode: values['repo-mode'],
129
+ silentLog: values.json
130
+ })
131
+
132
+ if (values.json) {
133
+ printJson(result)
134
+ return result
135
+ }
136
+ if (result.canceled) {
137
+ logger.warn('已取消。')
138
+ return result
139
+ }
140
+ if (result.rebased) {
141
+ logger.success(`已把改动合进提交 ${ui.hash(result.target)}`)
142
+ } else {
143
+ logger.success(`已创建 fixup 提交 ${result.fixupCommit}(目标 ${result.target}),之后可执行 git rebase -i --autosquash`)
144
+ }
145
+ printForceHint(result)
146
+ return result
147
+ }
@@ -1,6 +1,7 @@
1
1
  import { parseArgs } from 'node:util'
2
2
  import { normalizeLegacyArgs } from '../core/cli/args.js'
3
- import { logger } from '../core/tools/logger.js'
3
+ import { printJson } from '../core/cli/output.js'
4
+ import { c, keyValues, print, title, ui } from '../core/cli/render.js'
4
5
  import { runInfoWorkflow } from '../workflows/info-workflow.js'
5
6
 
6
7
  function printHelp() {
@@ -10,6 +11,7 @@ Usage:
10
11
 
11
12
  Options:
12
13
  --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
14
+ --json 输出机器可读 JSON
13
15
  -h, --help 查看帮助
14
16
 
15
17
  Notes:
@@ -25,6 +27,7 @@ export async function runInfoCommand(rawArgs) {
25
27
  allowPositionals: true,
26
28
  options: {
27
29
  'repo-mode': { type: 'string', default: 'auto' },
30
+ json: { type: 'boolean', default: false },
28
31
  help: { type: 'boolean', short: 'h', default: false }
29
32
  }
30
33
  })
@@ -46,15 +49,47 @@ export async function runInfoCommand(rawArgs) {
46
49
  inheritedFromParent: result.context.inheritedFromParent,
47
50
  currentBranch: result.context.currentBranch,
48
51
  defaultBranch: result.context.defaultBranch,
52
+ remote: result.context.remote,
53
+ remoteSource: result.context.remoteSource,
54
+ remotes: result.context.remotes,
55
+ repoConfigFile: result.context.repoConfig?.filePath || '',
49
56
  hookPath: result.context.hookPath,
50
57
  hasHookPath: result.context.hasHookPath,
51
58
  hasHuskyFolder: result.context.hasHuskyFolder,
52
59
  isSubmodule: result.context.isSubmodule,
53
60
  superprojectPath: result.context.superprojectPath,
54
- protectedBranches: result.repoPolicy.protectedBranches || [],
61
+ protectedBranches: [...new Set([...(result.repoPolicy.protectedBranches || []), ...(result.context.repoConfig?.protectedBranches || [])])],
55
62
  ai: result.aiConfig
56
63
  }
57
64
 
58
- process.stdout.write(`${JSON.stringify(infoBlock, null, 2)}\n`)
59
- logger.success('info 输出完成')
65
+ if (parsed.values.json) {
66
+ printJson(infoBlock)
67
+ return infoBlock
68
+ }
69
+
70
+ const remoteSourceLabel = {
71
+ option: '命令行参数', env: '环境变量', 'repo-config': '.icoderc.json', config: '全局配置',
72
+ upstream: '分支 upstream', default: '默认', 'first-remote': '第一个 remote'
73
+ }[infoBlock.remoteSource] || infoBlock.remoteSource
74
+
75
+ print(title('仓库'))
76
+ print(keyValues([
77
+ ['根目录', ui.path(infoBlock.repoRoot)],
78
+ ['当前目录', infoBlock.inheritedFromParent ? `${ui.path(infoBlock.cwd)} ${c.yellow('(继承父级仓库)')}` : ''],
79
+ ['当前分支', infoBlock.currentBranch ? ui.branch(infoBlock.currentBranch) : c.red('DETACHED')],
80
+ ['主分支', ui.branch(infoBlock.defaultBranch)],
81
+ ['remote', `${ui.remote(infoBlock.remote)} ${c.gray(`(${remoteSourceLabel})`)}${infoBlock.remotes?.length > 1 ? c.gray(` 可用: ${infoBlock.remotes.join(', ')}`) : ''}`],
82
+ ['受保护分支', infoBlock.protectedBranches.length ? infoBlock.protectedBranches.map(ui.branch).join(', ') : c.gray('无')],
83
+ ['子模块', infoBlock.isSubmodule ? `是,父仓库 ${ui.path(infoBlock.superprojectPath)}` : '']
84
+ ]))
85
+ print('')
86
+ print(title('环境'))
87
+ print(keyValues([
88
+ ['Git', infoBlock.gitVersion.replace(/^git version /, '')],
89
+ ['Hooks', infoBlock.hasHuskyFolder ? c.green('husky') : infoBlock.hasHookPath ? `${c.green('已配置')} ${c.gray(infoBlock.hookPath)}` : c.gray('未检测到')],
90
+ ['AI profile', infoBlock.ai.activeProfile ? `${c.green(infoBlock.ai.activeProfile)} ${c.gray(`(共 ${infoBlock.ai.profileCount} 个)`)}` : c.gray('未配置')],
91
+ ['全局配置', ui.path(infoBlock.configPath)],
92
+ ['仓库配置', infoBlock.repoConfigFile ? ui.path(infoBlock.repoConfigFile) : c.gray('无(icode config init 生成)')]
93
+ ]))
94
+ return infoBlock
60
95
  }
@@ -0,0 +1,142 @@
1
+ import { IcodeError } from '../core/errors.js'
2
+ import { parseCommandArgs, printJson } from '../core/cli/output.js'
3
+ import { renderCommits } from '../core/cli/render.js'
4
+ import { runCommand } from '../core/commands/shell.js'
5
+ import { resolveGitContext } from '../core/git/context.js'
6
+ import { GitService } from '../core/git/service.js'
7
+ import { logger } from '../core/tools/logger.js'
8
+
9
+ function printHelp() {
10
+ process.stdout.write(`
11
+ Usage:
12
+ icode log [range] [options]
13
+
14
+ Arguments:
15
+ [range] 提交范围或分支,例如 main..HEAD、origin/main、v1.2.0..
16
+
17
+ Options:
18
+ -n, --limit <n> 显示条数(默认 20)
19
+ --mine 只看自己的提交(按 git config user.email)
20
+ --author <name> 按作者过滤
21
+ --since <date> 起始时间,例如 "2 weeks ago"、2026-10-01
22
+ --grep <text> 按提交信息过滤(忽略大小写)
23
+ --file <path> 只看某个文件的历史(跟踪重命名)
24
+ --unpushed 只看还没推送到远程的提交
25
+ --no-merges 不显示 merge commit
26
+ --graph 显示分支图(调用 git log --graph)
27
+ --json 输出机器可读 JSON
28
+ --repo-mode <mode> 仓库模式: auto | strict
29
+ -h, --help 查看帮助
30
+
31
+ Examples:
32
+ icode log
33
+ icode log --mine --since "1 week ago"
34
+ icode log --unpushed
35
+ icode log --file src/cli.js -n 5
36
+ icode log main..HEAD --json
37
+ `)
38
+ }
39
+
40
+ function parseLimit(value) {
41
+ if (value == null) {
42
+ return 20
43
+ }
44
+ const number = Number(value)
45
+ if (!Number.isInteger(number) || number <= 0) {
46
+ throw new IcodeError('--limit 需要正整数', { code: 'LOG_LIMIT_INVALID', exitCode: 2 })
47
+ }
48
+ return number
49
+ }
50
+
51
+ /**
52
+ * 解析 --unpushed 对应的范围:有 upstream 用 @{u}..HEAD,否则用 <remote>/<默认分支>..HEAD。
53
+ */
54
+ async function resolveUnpushedRange(git, context) {
55
+ const upstream = await git.getUpstreamRef()
56
+ if (upstream) {
57
+ return `${upstream}..HEAD`
58
+ }
59
+ const fallback = git.remoteRef(context.defaultBranch)
60
+ if (await git.refExists(fallback)) {
61
+ return `${fallback}..HEAD`
62
+ }
63
+ return 'HEAD'
64
+ }
65
+
66
+ export async function runLogCommand(rawArgs) {
67
+ const { values, positionals } = parseCommandArgs(rawArgs, {
68
+ limit: { type: 'string', short: 'n' },
69
+ mine: { type: 'boolean', default: false },
70
+ author: { type: 'string' },
71
+ since: { type: 'string' },
72
+ grep: { type: 'string' },
73
+ file: { type: 'string' },
74
+ unpushed: { type: 'boolean', default: false },
75
+ 'no-merges': { type: 'boolean', default: false },
76
+ graph: { type: 'boolean', default: false }
77
+ })
78
+
79
+ if (values.help) {
80
+ printHelp()
81
+ return
82
+ }
83
+
84
+ const context = await resolveGitContext({ repoMode: values['repo-mode'] })
85
+ const git = new GitService(context)
86
+ const limit = parseLimit(values.limit)
87
+
88
+ let author = values.author || ''
89
+ if (values.mine) {
90
+ author = (await git.getConfigValue('user.email')) || (await git.getConfigValue('user.name'))
91
+ if (!author) {
92
+ throw new IcodeError('未配置 git user.email,无法使用 --mine。', { code: 'LOG_USER_UNKNOWN', exitCode: 2 })
93
+ }
94
+ }
95
+
96
+ const range = values.unpushed ? await resolveUnpushedRange(git, context) : positionals[0] || ''
97
+
98
+ if (values.graph && !values.json) {
99
+ // 分支图直接交给 git 渲染,颜色和对齐都更可靠。
100
+ const args = ['log', '--graph', '--color=always', '--format=%C(yellow)%h%C(reset) %C(cyan)%ar%C(reset) %C(green)%an%C(reset)%C(auto)%d%C(reset) %s', `-${limit}`]
101
+ if (author) {
102
+ args.push(`--author=${author}`)
103
+ }
104
+ if (values.since) {
105
+ args.push(`--since=${values.since}`)
106
+ }
107
+ if (range) {
108
+ args.push(range)
109
+ } else {
110
+ args.push('--all')
111
+ }
112
+ const result = await runCommand('git', args, { cwd: context.topLevelPath })
113
+ process.stdout.write(result.stdout)
114
+ return
115
+ }
116
+
117
+ const entries = await git.logEntries({
118
+ range,
119
+ limit,
120
+ author,
121
+ since: values.since,
122
+ grep: values.grep,
123
+ file: values.file,
124
+ noMerges: values['no-merges']
125
+ })
126
+
127
+ if (values.json) {
128
+ printJson({ range, commits: entries })
129
+ return entries
130
+ }
131
+
132
+ if (!entries.length) {
133
+ logger.info(values.unpushed ? '没有未推送的提交。' : '没有匹配的提交。')
134
+ return entries
135
+ }
136
+
137
+ renderCommits(entries)
138
+ if (entries.length === limit) {
139
+ logger.hint(`只显示最近 ${limit} 条,可用 -n 调整`)
140
+ }
141
+ return entries
142
+ }