@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
@@ -1,6 +1,7 @@
1
1
  import { parseArgs } from 'node:util'
2
2
  import { normalizeLegacyArgs } from '../core/cli/args.js'
3
3
  import { IcodeError } from '../core/errors.js'
4
+ import { c, keyValues, print, title, ui } from '../core/cli/render.js'
4
5
  import { logger } from '../core/tools/logger.js'
5
6
  import { runMigrateWorkflow } from '../workflows/migrate-workflow.js'
6
7
 
@@ -42,18 +43,26 @@ function printDryRunPlan(result) {
42
43
  const previewLimit = 10
43
44
  const previewCommits = result.commits.slice(0, previewLimit)
44
45
 
45
- logger.info(`[dry-run] source: ${result.sourceBranch} (${result.sourceRef})`)
46
- logger.info(`[dry-run] target: ${result.targetBranch} (${result.targetRef})`)
47
- logger.info(`[dry-run] range: ${result.rangeSpec}`)
48
- logger.info(`[dry-run] commits: ${result.migratedCommits}`)
49
- previewCommits.forEach((commit, index) => {
50
- logger.info(`[dry-run] commit ${index + 1}: ${commit.summary || commit.hash}`)
51
- })
46
+ print(title('迁移计划(dry-run)'))
47
+ print(keyValues([
48
+ ['来源', `${ui.branch(result.sourceBranch)} ${c.gray(`(${result.sourceRef})`)}`],
49
+ ['目标', `${ui.branch(result.targetBranch)} ${c.gray(`(${result.targetRef})`)}`],
50
+ ['范围', c.cyan(result.rangeSpec)],
51
+ ['提交数', c.bold(String(result.migratedCommits))]
52
+ ]))
53
+ if (previewCommits.length) {
54
+ print('')
55
+ previewCommits.forEach((commit) => {
56
+ const [hash, ...rest] = String(commit.summary || commit.hash).split(' ')
57
+ print(` ${ui.hash(hash)} ${ui.subject(rest.join(' '))}`)
58
+ })
59
+ }
52
60
  if (result.commits.length > previewLimit) {
53
- logger.info(`[dry-run] ... 还有 ${result.commits.length - previewLimit} 个提交未显示,可用 --json 查看完整计划`)
61
+ print(c.gray(` … 还有 ${result.commits.length - previewLimit} 个提交,可用 --json 查看完整计划`))
54
62
  }
63
+ print('')
55
64
  result.steps.forEach((step, index) => {
56
- logger.info(`[dry-run] ${index + 1}. ${step.action}: ${step.detail}`)
65
+ print(` ${c.gray(`${index + 1}.`)} ${step.detail}`)
57
66
  })
58
67
  }
59
68
 
@@ -117,7 +126,5 @@ export async function runMigrateCommand(rawArgs) {
117
126
  return result
118
127
  }
119
128
 
120
- logger.success(
121
- `migrate 完成: ${result.sourceBranch} -> ${result.targetBranch}, commits=${result.migratedCommits}, mode=${result.rangeMode || 'range'}`
122
- )
129
+ logger.success(`已迁移 ${c.bold(String(result.migratedCommits))} 个提交: ${ui.branch(result.sourceBranch)} ${c.gray('→')} ${ui.branch(result.targetBranch)}${result.pushed ? c.gray(',已推送') : ''}`)
123
130
  }
@@ -0,0 +1,138 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { IcodeError } from '../core/errors.js'
4
+ import { parseCommandArgs, printJson } from '../core/cli/output.js'
5
+ import { resolveGitContext } from '../core/git/context.js'
6
+ import { GitService } from '../core/git/service.js'
7
+ import { createPlatformClientForGit } from '../core/platform/index.js'
8
+ import { openInBrowser } from '../core/tools/browser.js'
9
+ import { logger } from '../core/tools/logger.js'
10
+
11
+ function printHelp() {
12
+ process.stdout.write(`
13
+ Usage:
14
+ icode open [target] [options]
15
+
16
+ Targets:
17
+ (不传) 当前分支页面(在主分支上时打开仓库首页)
18
+ repo 仓库首页
19
+ <file>[:line] 文件(当前分支),例如 src/cli.js:42
20
+ commit [ref] 提交页面(默认 HEAD)
21
+ compare [base] 当前分支与 base 的对比(默认主分支)
22
+ pr 当前分支的 PR/MR;没有时打开创建页面
23
+ prs PR/MR 列表
24
+ ci CI / 流水线页面
25
+ releases Release 页面
26
+
27
+ Options:
28
+ -p, --print 只打印链接,不打开浏览器
29
+ --branch <name> 指定分支(默认当前分支)
30
+ --json 输出机器可读 JSON
31
+ --repo-mode <mode> 仓库模式: auto | strict
32
+ -h, --help 查看帮助
33
+
34
+ Examples:
35
+ icode open
36
+ icode open src/cli.js:120
37
+ icode open pr
38
+ icode open compare release -p
39
+ `)
40
+ }
41
+
42
+ // 把用户给的文件路径转换成仓库根目录相对路径,并解析 :行号 后缀。
43
+ function resolveFileTarget(context, target) {
44
+ const match = String(target).match(/^(.*?)(?::(\d+))?$/)
45
+ const filePart = match?.[1] || target
46
+ const line = match?.[2] ? Number(match[2]) : 0
47
+ const absolute = path.resolve(process.cwd(), filePart)
48
+ if (!fs.existsSync(absolute)) {
49
+ return null
50
+ }
51
+ const relative = path.relative(context.topLevelPath, absolute).split(path.sep).join('/')
52
+ if (relative.startsWith('..')) {
53
+ return null
54
+ }
55
+ return { file: relative, line }
56
+ }
57
+
58
+ async function resolveUrl(git, context, client, target, options) {
59
+ const branch = options.branch || (await git.getCurrentBranch()) || context.defaultBranch
60
+ const urls = client.urls
61
+
62
+ switch (target) {
63
+ case '':
64
+ return branch && branch !== context.defaultBranch ? urls.branch(branch) : urls.repo()
65
+ case 'repo':
66
+ return urls.repo()
67
+ case 'prs':
68
+ case 'mrs':
69
+ return urls.prs()
70
+ case 'ci':
71
+ case 'actions':
72
+ case 'pipelines':
73
+ return urls.pipelines()
74
+ case 'releases':
75
+ return urls.releases()
76
+ case 'commit': {
77
+ const sha = await git.revParse(options.ref || 'HEAD')
78
+ if (!sha) {
79
+ throw new IcodeError(`提交不存在: ${options.ref}`, { code: 'OPEN_REF_NOT_FOUND', exitCode: 2 })
80
+ }
81
+ return urls.commit(sha)
82
+ }
83
+ case 'compare':
84
+ return urls.compare(options.ref || context.defaultBranch, branch)
85
+ case 'pr':
86
+ case 'mr': {
87
+ if (client.hasToken) {
88
+ try {
89
+ const [pr] = await client.listPullRequests({ head: branch, state: 'open' })
90
+ if (pr) {
91
+ return pr.url || urls.pr(pr.number)
92
+ }
93
+ } catch (error) {
94
+ logger.debug(`查询 PR 失败,改为打开创建页面: ${error.message}`)
95
+ }
96
+ }
97
+ return urls.newPr(context.defaultBranch, branch)
98
+ }
99
+ default: {
100
+ const fileTarget = resolveFileTarget(context, target)
101
+ if (!fileTarget) {
102
+ throw new IcodeError(`无法识别的目标: ${target}(不是已存在的文件,也不是 repo/pr/commit/compare/ci/releases)`, {
103
+ code: 'OPEN_TARGET_UNKNOWN',
104
+ exitCode: 2
105
+ })
106
+ }
107
+ return urls.file(branch, fileTarget.file, fileTarget.line)
108
+ }
109
+ }
110
+ }
111
+
112
+ export async function runOpenCommand(rawArgs) {
113
+ const { values, positionals } = parseCommandArgs(rawArgs, {
114
+ print: { type: 'boolean', short: 'p', default: false },
115
+ branch: { type: 'string' }
116
+ })
117
+ if (values.help) {
118
+ printHelp()
119
+ return
120
+ }
121
+
122
+ const context = await resolveGitContext({ repoMode: values['repo-mode'] })
123
+ const git = new GitService(context)
124
+ const client = await createPlatformClientForGit(git)
125
+ const [target = '', ref] = positionals
126
+ const url = await resolveUrl(git, context, client, target, { branch: values.branch, ref })
127
+
128
+ if (values.json) {
129
+ printJson({ url, provider: client.name })
130
+ return { url }
131
+ }
132
+
133
+ process.stdout.write(`${url}\n`)
134
+ if (!values.print) {
135
+ openInBrowser(url)
136
+ }
137
+ return { url }
138
+ }
@@ -0,0 +1,219 @@
1
+ import { IcodeError } from '../core/errors.js'
2
+ import { parseCommandArgs, printJson } from '../core/cli/output.js'
3
+ import { c, keyValues, print, table, ui } from '../core/cli/render.js'
4
+ import { openInBrowser } from '../core/tools/browser.js'
5
+ import { confirm, isInteractiveTerminal } from '../core/tools/interactive.js'
6
+ import { logger } from '../core/tools/logger.js'
7
+ import {
8
+ runPrCheckoutWorkflow,
9
+ runPrCreateWorkflow,
10
+ runPrListWorkflow,
11
+ runPrMergeWorkflow,
12
+ runPrViewWorkflow
13
+ } from '../workflows/pr-workflow.js'
14
+
15
+ function printHelp() {
16
+ process.stdout.write(`
17
+ Usage:
18
+ icode pr create [options] 为当前分支创建 PR/MR(会先推送当前分支)
19
+ icode pr list [options] 列出 PR/MR
20
+ icode pr view [number] 查看 PR/MR(默认当前分支对应的)
21
+ icode pr checkout <number> 检出 PR/MR 到本地分支 pr/<number>
22
+ icode pr merge [number] 合并 PR/MR(会确认)
23
+ icode pr open [number] 在浏览器打开 PR/MR(无编号时打开列表)
24
+
25
+ Options:
26
+ -b, --base <branch> 目标分支(默认主分支)
27
+ -t, --title <text> 标题(默认取提交信息)
28
+ --body <text> 正文
29
+ --draft 创建草稿
30
+ --ai 用 AI 生成标题和描述
31
+ --profile <name> AI profile(配合 --ai)
32
+ --lang <zh|en> AI 输出语言
33
+ --no-push 不自动推送当前分支
34
+ --web 不调 API,直接在浏览器打开创建页面
35
+ --state <state> list 的状态: open | closed | merged | all
36
+ --method <method> merge 方式: merge | squash | rebase
37
+ --branch <name> checkout 的本地分支名
38
+ --provider <name> 强制指定平台: github | gitlab | gitee | gitea
39
+ --json 输出机器可读 JSON
40
+ -y, --yes 自动确认
41
+ --repo-mode <mode> 仓库模式: auto | strict
42
+ -h, --help 查看帮助
43
+
44
+ Notes:
45
+ 平台按 remote 地址自动识别。需要 token: icode config platform token set <provider> <token>
46
+ (也可用 GITHUB_TOKEN / GITLAB_TOKEN 等环境变量)。没有 token 时 create 会打开网页创建页面。
47
+
48
+ Examples:
49
+ icode pr create
50
+ icode pr create -b release --ai
51
+ icode pr list --state all
52
+ icode pr checkout 42
53
+ icode pr merge 42 --method squash -y
54
+ `)
55
+ }
56
+
57
+ function formatState(pr) {
58
+ if (pr.merged) {
59
+ return c.magenta('已合并')
60
+ }
61
+ if (pr.state === 'open') {
62
+ return pr.draft ? c.gray('草稿') : c.green('打开')
63
+ }
64
+ return c.gray(pr.state === 'closed' ? '已关闭' : pr.state)
65
+ }
66
+
67
+ function printPr(pr, noun) {
68
+ print(` ${c.yellow(`${noun} #${pr.number}`)} ${c.bold(ui.subject(pr.title))}`)
69
+ print(keyValues([
70
+ ['分支', `${ui.branch(pr.head)} ${c.gray('→')} ${ui.branch(pr.base)}`],
71
+ ['状态', `${formatState(pr)}${pr.conflict ? ` ${c.red('存在冲突')}` : ''}`],
72
+ ['作者', pr.author ? ui.author(`@${pr.author}`) : ''],
73
+ ['链接', ui.url(pr.url)]
74
+ ], { indent: 4 }))
75
+ }
76
+
77
+ export async function runPrCommand(rawArgs) {
78
+ const { values, positionals } = parseCommandArgs(rawArgs, {
79
+ base: { type: 'string', short: 'b' },
80
+ title: { type: 'string', short: 't' },
81
+ body: { type: 'string' },
82
+ draft: { type: 'boolean', default: false },
83
+ ai: { type: 'boolean', default: false },
84
+ profile: { type: 'string' },
85
+ lang: { type: 'string', default: 'zh' },
86
+ 'no-push': { type: 'boolean', default: false },
87
+ 'no-verify': { type: 'boolean', default: false },
88
+ web: { type: 'boolean', default: false },
89
+ state: { type: 'string' },
90
+ method: { type: 'string' },
91
+ branch: { type: 'string' },
92
+ provider: { type: 'string' },
93
+ limit: { type: 'string' }
94
+ })
95
+
96
+ const [subcommand = '', target] = positionals
97
+ if (values.help || !subcommand) {
98
+ printHelp()
99
+ return
100
+ }
101
+
102
+ const base = { repoMode: values['repo-mode'], provider: values.provider }
103
+
104
+ if (subcommand === 'create') {
105
+ const result = await runPrCreateWorkflow({
106
+ ...base,
107
+ base: values.base,
108
+ title: values.title,
109
+ body: values.body,
110
+ draft: values.draft,
111
+ ai: values.ai,
112
+ profile: values.profile,
113
+ lang: values.lang,
114
+ noPush: values['no-push'],
115
+ noVerify: values['no-verify'],
116
+ web: values.web,
117
+ interactive: !values.json && !values.yes && isInteractiveTerminal()
118
+ })
119
+ if (values.json) {
120
+ printJson(result)
121
+ return result
122
+ }
123
+ if (result.canceled) {
124
+ logger.warn('已取消。')
125
+ return result
126
+ }
127
+ if (!result.created && !result.existed) {
128
+ if (result.reason === 'no-token') {
129
+ logger.warn(`未配置 ${result.provider} token,改为在浏览器中创建。配置: icode config platform token set ${result.provider} <token>`)
130
+ }
131
+ logger.info(`在浏览器中完成创建: ${ui.url(result.webUrl)}`)
132
+ openInBrowser(result.webUrl)
133
+ return result
134
+ }
135
+ if (result.existed) {
136
+ logger.info('该分支已有打开的 PR/MR:')
137
+ } else {
138
+ logger.success('已创建:')
139
+ }
140
+ printPr(result.pr, result.provider === 'gitlab' ? 'MR' : 'PR')
141
+ return result
142
+ }
143
+
144
+ if (subcommand === 'list' || subcommand === 'ls') {
145
+ const result = await runPrListWorkflow({ ...base, state: values.state, base: values.base, limit: values.limit ? Number(values.limit) : undefined })
146
+ if (values.json) {
147
+ printJson(result)
148
+ return result
149
+ }
150
+ if (!result.items.length) {
151
+ logger.info(`没有${values.state && values.state !== 'open' ? '' : '打开的'} ${result.prNoun}。`)
152
+ return result
153
+ }
154
+ print(table(result.items, [
155
+ { key: 'number', label: '#', format: (value) => c.yellow(`#${value}`) },
156
+ { key: 'title', label: '标题', maxWidth: 50, format: (value) => ui.subject(value) },
157
+ { key: 'head', label: '分支', format: (_, pr) => `${ui.branch(pr.head)} ${c.gray('→')} ${ui.branch(pr.base)}` },
158
+ { key: 'state', label: '状态', format: (_, pr) => formatState(pr) + (pr.conflict ? ` ${c.red('冲突')}` : '') },
159
+ { key: 'author', label: '作者', format: (value) => ui.author(value ? `@${value}` : '') }
160
+ ]))
161
+ return result
162
+ }
163
+
164
+ if (subcommand === 'view') {
165
+ const result = await runPrViewWorkflow({ ...base, number: target })
166
+ if (values.json) {
167
+ printJson(result)
168
+ return result
169
+ }
170
+ printPr(result.pr, result.prNoun)
171
+ return result
172
+ }
173
+
174
+ if (subcommand === 'open') {
175
+ // 有编号打开该 PR;没有编号时先找当前分支的 PR,找不到就打开列表页。
176
+ const result = await runPrViewWorkflow({ ...base, number: target }).catch((error) => {
177
+ if (!target && error.code === 'PR_NOT_FOUND') {
178
+ return null
179
+ }
180
+ throw error
181
+ })
182
+ const url = result ? result.pr.url : (await runPrListWorkflow(base)).webUrl
183
+ logger.info(ui.url(url))
184
+ openInBrowser(url)
185
+ return { url }
186
+ }
187
+
188
+ if (subcommand === 'checkout' || subcommand === 'co') {
189
+ if (!target) {
190
+ throw new IcodeError('缺少编号: icode pr checkout <number>', { code: 'PR_NUMBER_REQUIRED', exitCode: 2 })
191
+ }
192
+ const result = await runPrCheckoutWorkflow({ ...base, number: target, branch: values.branch })
193
+ if (values.json) {
194
+ printJson(result)
195
+ return result
196
+ }
197
+ logger.success(`已检出到本地分支 ${ui.branch(result.branch)}`)
198
+ return result
199
+ }
200
+
201
+ if (subcommand === 'merge') {
202
+ if (!values.yes) {
203
+ const accepted = await confirm(`确认合并 ${target ? `#${target}` : '当前分支的 PR/MR'}?`, false)
204
+ if (!accepted) {
205
+ logger.warn('已取消。')
206
+ return
207
+ }
208
+ }
209
+ const result = await runPrMergeWorkflow({ ...base, number: target, method: values.method })
210
+ if (values.json) {
211
+ printJson(result)
212
+ return result
213
+ }
214
+ logger.success(result.alreadyMerged ? `#${result.number} 之前已合并。` : `#${result.number} 已合并。`)
215
+ return result
216
+ }
217
+
218
+ throw new IcodeError(`未知 pr 子命令: ${subcommand}`, { code: 'PR_SUBCOMMAND_UNKNOWN', exitCode: 2 })
219
+ }
@@ -1,6 +1,7 @@
1
1
  import { parseArgs } from 'node:util'
2
2
  import { getAiCommandOptions } from '../core/ai/config.js'
3
3
  import { normalizeLegacyArgs } from '../core/cli/args.js'
4
+ import { c, keyValues, print, renderBranchResults, title, ui } from '../core/cli/render.js'
4
5
  import { logger } from '../core/tools/logger.js'
5
6
  import { runPushWorkflow } from '../workflows/push-workflow.js'
6
7
 
@@ -16,22 +17,14 @@ const PUSH_PARSE_OPTIONS = {
16
17
  'pull-main': { type: 'boolean' },
17
18
  'not-push-current': { type: 'boolean' },
18
19
  'force-protected': { type: 'boolean' },
20
+ force: { type: 'boolean', short: 'f' },
21
+ 'create-target': { type: 'boolean' },
22
+ 'allow-risky': { type: 'boolean' },
23
+ json: { type: 'boolean' },
19
24
  'repo-mode': { type: 'string' },
20
25
  'no-verify': { type: 'boolean' }
21
26
  }
22
27
 
23
- function formatBranchStatus(status) {
24
- const map = {
25
- pushed: '已推送',
26
- 'merged-and-pushed': '已合并并推送',
27
- 'remote-merged-and-pushed': '已远程合并并推送',
28
- 'skipped-protected': '已跳过(受保护)',
29
- 'skipped-missing-remote': '已跳过(远程分支不存在)',
30
- paused: '已暂停'
31
- }
32
-
33
- return map[status] || status
34
- }
35
28
 
36
29
  function printHelp() {
37
30
  process.stdout.write(`
@@ -52,6 +45,10 @@ Options:
52
45
  --pull-main 提交前将主分支同步到当前分支
53
46
  --not-push-current 不推送当前分支,只处理目标分支
54
47
  --force-protected 强制处理配置里的受保护分支
48
+ -f, --force 安全强推当前分支(只用 --force-with-lease,拒绝主分支/受保护分支)
49
+ --create-target 目标分支不存在时允许从当前分支新建(默认会询问或报错)
50
+ --allow-risky 检测到敏感文件/疑似密钥/大文件时仍继续提交
51
+ --json 输出机器可读 JSON
55
52
  --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
56
53
  --no-verify 跳过 hook/husky 校验
57
54
  -h, --help 查看帮助
@@ -62,6 +59,14 @@ Notes:
62
59
  远程 merge 通过 PR/MR API 发起,不会本地切换到目标分支。
63
60
  远程 merge 若出现冲突会暂停流程,并保留明确失败原因。
64
61
  布尔开关仅在命令行显式传入时生效(如 --ai-commit / --pull-main / --no-verify / -y)。
62
+ 本地 merge 和强推前会自动创建恢复点,可用 icode rescue 找回。
63
+
64
+ Examples:
65
+ icode push -m "feat: login"
66
+ icode push release test -y
67
+ icode push release -r -y
68
+ icode push --force -y
69
+ icode --remote upstream push -m "fix: typo"
65
70
  `)
66
71
  }
67
72
 
@@ -91,23 +96,46 @@ function resolveStringOption(cliValue, configValue, fallback = '') {
91
96
  return fallback
92
97
  }
93
98
 
99
+ const STATUS_LABELS = {
100
+ pushed: '已推送',
101
+ 'merged-and-pushed': '已合并并推送',
102
+ 'remote-merged-and-pushed': '已远程合并',
103
+ 'force-pushed': '已强推 (--force-with-lease)',
104
+ 'skipped-protected': '已跳过(受保护分支,可加 --force-protected)',
105
+ 'skipped-missing-remote': '已跳过(远程分支不存在)',
106
+ paused: '已暂停'
107
+ }
108
+
94
109
  // 统一输出 push 结果摘要,失败分支会追加具体原因,方便定位问题目标分支。
95
110
  function printPushSummary(summary = []) {
96
- summary.forEach((item) => {
97
- const reason = typeof item.reason === 'string' && item.reason.trim() ? ` - ${item.reason.trim()}` : ''
98
- logger.info(`[结果] ${item.branch}: ${formatBranchStatus(item.status)}${reason}`)
99
- })
111
+ print('')
112
+ renderBranchResults(summary, STATUS_LABELS)
100
113
  }
101
114
 
102
115
  // 输出 dry-run 计划摘要,输入 workflow 返回的计划,输出对用户可读的阶段列表。
103
116
  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'}`)
117
+ const actionLabels = {
118
+ 'ai-commit': 'AI 生成提交信息并提交',
119
+ 'commit-if-needed': '有改动时提交',
120
+ 'pull-main': '同步主分支',
121
+ 'skip-protected': '跳过受保护分支',
122
+ 'push-current': '推送当前分支',
123
+ 'force-push-current': '强推当前分支',
124
+ 'local-merge': '本地 merge 后推送',
125
+ 'remote-merge': '创建并合并 PR/MR'
126
+ }
127
+ print(title('push 计划(dry-run)'))
128
+ print(keyValues([
129
+ ['当前分支', ui.branch(plan.currentBranch)],
130
+ ['remote', ui.remote(plan.remote || 'origin')],
131
+ ['目标分支', plan.branchTargets.map(ui.branch).join(', ')],
132
+ ['合并方式', plan.remoteMerge ? '远程 PR/MR' : '本地 merge']
133
+ ]))
134
+ print('')
107
135
  plan.steps.forEach((step, index) => {
108
- const reason = step.reason ? ` - ${step.reason}` : ''
109
- logger.info(`[dry-run] ${index + 1}. ${step.action}: ${step.branch}${reason}`)
136
+ print(` ${c.gray(`${index + 1}.`)} ${actionLabels[step.action] || step.action} ${ui.branch(step.branch)} ${c.gray(step.reason || '')}`)
110
137
  })
138
+ print(c.gray('\n 未执行任何操作。去掉 --dry-run 即可执行。'))
111
139
  }
112
140
 
113
141
  export function resolvePushWorkflowOptions(parsedValues, parsedPositionals, scopedOptions = {}) {
@@ -124,6 +152,9 @@ export function resolvePushWorkflowOptions(parsedValues, parsedPositionals, scop
124
152
  pullMain: resolveBooleanOption(parsedValues['pull-main'], false),
125
153
  notPushCurrent: resolveBooleanOption(parsedValues['not-push-current'], false),
126
154
  forceProtected: resolveBooleanOption(parsedValues['force-protected'], false),
155
+ force: resolveBooleanOption(parsedValues.force, false),
156
+ createTarget: resolveBooleanOption(parsedValues['create-target'], false),
157
+ allowRisky: resolveBooleanOption(parsedValues['allow-risky'], false),
127
158
  repoMode: resolveStringOption(parsedValues['repo-mode'], undefined, 'auto'),
128
159
  noVerify: resolveBooleanOption(parsedValues['no-verify'], false)
129
160
  }
@@ -159,6 +190,11 @@ export async function runPushCommand(rawArgs) {
159
190
  throw error
160
191
  }
161
192
 
193
+ if (parsed.values.json) {
194
+ process.stdout.write(`${JSON.stringify({ ok: !result.canceled, ...result }, null, 2)}\n`)
195
+ return result
196
+ }
197
+
162
198
  if (result.canceled) {
163
199
  return
164
200
  }
@@ -169,5 +205,9 @@ export async function runPushCommand(rawArgs) {
169
205
  }
170
206
 
171
207
  printPushSummary(result.summary)
172
- logger.success(`push 完成,共处理 ${result.summary.length} 个分支。`)
208
+ print('')
209
+ logger.success(`push 完成,共处理 ${result.summary.length} 个分支`)
210
+ if (result.backupId) {
211
+ logger.hint(`已创建恢复点 ${result.backupId},需要时可用 icode rescue 找回`)
212
+ }
173
213
  }
@@ -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 { c, formatChanges, keyValues, print, renderChecks, renderOverallStatus, title, ui } from '../core/cli/render.js'
4
4
  import { withSpinner } from '../core/tools/loading.js'
5
5
  import { runReadyWorkflow } from '../workflows/ready-workflow.js'
6
6
 
@@ -23,55 +23,25 @@ Notes:
23
23
  `)
24
24
  }
25
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
26
  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
- }
27
+ const upstream = result.upstream.configured
28
+ ? `${ui.remote(result.upstream.upstream)} ${ui.sync(result.upstream.ahead, result.upstream.behind) || c.gray('已同步')}`
29
+ : c.yellow('未配置')
72
30
 
73
- result.checks.forEach((check) => {
74
- printCheck(check)
31
+ print(title('仓库状态'))
32
+ print(keyValues([
33
+ ['仓库', ui.path(result.context.topLevelPath)],
34
+ ['分支', result.context.currentBranch ? ui.branch(result.context.currentBranch) : c.red('DETACHED')],
35
+ ['主分支', ui.branch(result.context.defaultBranch)],
36
+ ['upstream', upstream],
37
+ ['改动', formatChanges(result.changes)]
38
+ ]))
39
+ print('')
40
+ renderChecks(result.checks)
41
+ renderOverallStatus(result.status, {
42
+ 通过: result.summary?.pass,
43
+ 注意: result.summary?.warn,
44
+ 失败: result.summary?.fail
75
45
  })
76
46
  }
77
47