@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.
- package/README.md +287 -24
- package/bin/icode.js +10 -0
- package/package.json +3 -3
- package/src/cli.js +175 -8
- package/src/commands/checkout.js +73 -6
- package/src/commands/clean.js +47 -7
- package/src/commands/config-ux.js +292 -0
- package/src/commands/config.js +199 -48
- package/src/commands/continue.js +104 -0
- package/src/commands/doctor.js +15 -29
- package/src/commands/help.js +54 -34
- package/src/commands/history.js +147 -0
- package/src/commands/info.js +39 -4
- package/src/commands/log.js +142 -0
- package/src/commands/migrate.js +19 -12
- package/src/commands/open.js +138 -0
- package/src/commands/pr.js +219 -0
- package/src/commands/push.js +62 -22
- package/src/commands/ready.js +18 -48
- package/src/commands/release.js +112 -0
- package/src/commands/rescue.js +164 -0
- package/src/commands/save.js +52 -11
- package/src/commands/stash.js +210 -0
- package/src/commands/sw.js +89 -0
- package/src/commands/sync.js +19 -2
- package/src/commands/tag.js +40 -1
- package/src/commands/undo.js +9 -3
- package/src/core/ai/diff-range.js +1 -1
- package/src/core/cli/completion.js +237 -1
- package/src/core/cli/output.js +46 -0
- package/src/core/cli/render.js +121 -0
- package/src/core/commands/shell.js +9 -1
- package/src/core/config/repo-config.js +93 -0
- package/src/core/config/store.js +60 -10
- package/src/core/git/autostash.js +46 -0
- package/src/core/git/backup.js +170 -0
- package/src/core/git/conflicts.js +58 -0
- package/src/core/git/context.js +72 -5
- package/src/core/git/file-guard.js +193 -0
- package/src/core/git/operation-state.js +63 -0
- package/src/core/git/operations.js +32 -0
- package/src/core/git/semver.js +194 -0
- package/src/core/git/service.js +454 -24
- package/src/core/platform/http.js +53 -0
- package/src/core/platform/index.js +165 -0
- package/src/core/platform/providers.js +454 -0
- package/src/core/platform/remote-url.js +92 -0
- package/src/core/remote-merge/client.js +33 -3
- package/src/core/tools/browser.js +28 -0
- package/src/core/tools/commit-prompt.js +137 -0
- package/src/core/tools/interactive.js +272 -40
- package/src/core/tools/logger.js +33 -23
- package/src/core/tools/theme.js +228 -0
- package/src/workflows/ai-commit-workflow.js +52 -20
- package/src/workflows/checkout-workflow.js +24 -10
- package/src/workflows/clean-workflow.js +135 -83
- package/src/workflows/continue-workflow.js +233 -0
- package/src/workflows/doctor-workflow.js +10 -7
- package/src/workflows/history-workflow.js +211 -0
- package/src/workflows/migrate-workflow.js +39 -14
- package/src/workflows/pr-workflow.js +252 -0
- package/src/workflows/push/commit-step.js +123 -0
- package/src/workflows/push/local-merge-step.js +213 -0
- package/src/workflows/push/plan.js +63 -0
- package/src/workflows/push/remote-merge-step.js +224 -0
- package/src/workflows/push-workflow.js +185 -492
- package/src/workflows/ready-workflow.js +14 -10
- package/src/workflows/release-workflow.js +189 -0
- package/src/workflows/rescue-workflow.js +137 -0
- package/src/workflows/save-workflow.js +106 -41
- package/src/workflows/switch-workflow.js +171 -0
- package/src/workflows/sync-workflow.js +143 -44
- package/src/workflows/tag-workflow.js +103 -17
- package/src/workflows/undo-workflow.js +113 -34
- package/src/workflows/rollback-workflow.js +0 -99
package/src/cli.js
CHANGED
|
@@ -4,20 +4,32 @@ import { runCodeReviewCommand } from './commands/codereview.js'
|
|
|
4
4
|
import { runCompletionCommand, runHiddenCompletionCommand } from './commands/completion.js'
|
|
5
5
|
import { runCheckoutCommand } from './commands/checkout.js'
|
|
6
6
|
import { runConfigCommand } from './commands/config.js'
|
|
7
|
+
import { runAbortCommand, runContinueCommand } from './commands/continue.js'
|
|
7
8
|
import { runDoctorCommand } from './commands/doctor.js'
|
|
8
9
|
import { runExplainCommand } from './commands/explain.js'
|
|
9
10
|
import { printMainHelp } from './commands/help.js'
|
|
11
|
+
import { runAmendCommand, runFixupCommand } from './commands/history.js'
|
|
10
12
|
import { runInfoCommand } from './commands/info.js'
|
|
13
|
+
import { runLogCommand } from './commands/log.js'
|
|
11
14
|
import { runMigrateCommand } from './commands/migrate.js'
|
|
15
|
+
import { runOpenCommand } from './commands/open.js'
|
|
16
|
+
import { runPrCommand } from './commands/pr.js'
|
|
12
17
|
import { runPushCommand } from './commands/push.js'
|
|
13
18
|
import { runReadyCommand } from './commands/ready.js'
|
|
19
|
+
import { runReleaseCommand } from './commands/release.js'
|
|
20
|
+
import { runRescueCommand } from './commands/rescue.js'
|
|
14
21
|
import { runSaveCommand } from './commands/save.js'
|
|
22
|
+
import { runStashCommand } from './commands/stash.js'
|
|
23
|
+
import { runSwCommand } from './commands/sw.js'
|
|
15
24
|
import { runSyncCommand } from './commands/sync.js'
|
|
16
25
|
import { runTagCommand } from './commands/tag.js'
|
|
17
26
|
import { runUndoCommand } from './commands/undo.js'
|
|
18
|
-
import { asIcodeError } from './core/errors.js'
|
|
27
|
+
import { asIcodeError, IcodeError } from './core/errors.js'
|
|
19
28
|
import { normalizeLegacyArgs } from './core/cli/args.js'
|
|
20
29
|
import { notifyIfCliUpdateAvailable } from './core/cli/update-notifier.js'
|
|
30
|
+
import { chooseOne, isInteractiveTerminal } from './core/tools/interactive.js'
|
|
31
|
+
import { c } from './core/tools/theme.js'
|
|
32
|
+
import { inspectPausedWork } from './workflows/continue-workflow.js'
|
|
21
33
|
import { logger } from './core/tools/logger.js'
|
|
22
34
|
|
|
23
35
|
function isTruthy(value) {
|
|
@@ -25,11 +37,38 @@ function isTruthy(value) {
|
|
|
25
37
|
return ['1', 'true', 'yes', 'y', 'on'].includes(normalized)
|
|
26
38
|
}
|
|
27
39
|
|
|
40
|
+
function redactSensitiveMeta(value, seen = new WeakSet()) {
|
|
41
|
+
if (typeof value === 'string') {
|
|
42
|
+
return value
|
|
43
|
+
.replace(/(Bearer\s+)[^\s"']+/gi, '$1[REDACTED]')
|
|
44
|
+
.replace(/((?:api[_-]?key|private[_-]?token|authorization)\s*[=:]\s*)[^\s,;"']+/gi, '$1[REDACTED]')
|
|
45
|
+
}
|
|
46
|
+
if (!value || typeof value !== 'object') {
|
|
47
|
+
return value
|
|
48
|
+
}
|
|
49
|
+
if (seen.has(value)) {
|
|
50
|
+
return '[Circular]'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
seen.add(value)
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
return value.map((item) => redactSensitiveMeta(item, seen))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sanitized = {}
|
|
59
|
+
for (const [key, item] of Object.entries(value)) {
|
|
60
|
+
sanitized[key] = /api[_-]?key|token|authorization|secret|password/i.test(key)
|
|
61
|
+
? '[REDACTED]'
|
|
62
|
+
: redactSensitiveMeta(item, seen)
|
|
63
|
+
}
|
|
64
|
+
return sanitized
|
|
65
|
+
}
|
|
66
|
+
|
|
28
67
|
function serializeErrorMeta(meta) {
|
|
29
68
|
try {
|
|
30
|
-
return JSON.stringify(meta, null, 2)
|
|
69
|
+
return JSON.stringify(redactSensitiveMeta(meta), null, 2)
|
|
31
70
|
} catch {
|
|
32
|
-
return
|
|
71
|
+
return '[无法序列化错误详情]'
|
|
33
72
|
}
|
|
34
73
|
}
|
|
35
74
|
|
|
@@ -44,8 +83,21 @@ const COMMANDS = {
|
|
|
44
83
|
sync: runSyncCommand,
|
|
45
84
|
clean: runCleanCommand,
|
|
46
85
|
undo: runUndoCommand,
|
|
86
|
+
continue: runContinueCommand,
|
|
87
|
+
abort: runAbortCommand,
|
|
88
|
+
rescue: runRescueCommand,
|
|
89
|
+
stash: runStashCommand,
|
|
90
|
+
sw: runSwCommand,
|
|
91
|
+
switch: runSwCommand,
|
|
92
|
+
log: runLogCommand,
|
|
93
|
+
amend: runAmendCommand,
|
|
94
|
+
fixup: runFixupCommand,
|
|
47
95
|
migrate: runMigrateCommand,
|
|
48
96
|
tag: runTagCommand,
|
|
97
|
+
release: runReleaseCommand,
|
|
98
|
+
pr: runPrCommand,
|
|
99
|
+
mr: runPrCommand,
|
|
100
|
+
open: runOpenCommand,
|
|
49
101
|
config: runConfigCommand,
|
|
50
102
|
doctor: runDoctorCommand,
|
|
51
103
|
explain: runExplainCommand,
|
|
@@ -58,6 +110,54 @@ const COMMANDS = {
|
|
|
58
110
|
|
|
59
111
|
const NO_UPDATE_NOTIFICATION_COMMANDS = new Set(['help', 'completion', '__complete'])
|
|
60
112
|
|
|
113
|
+
const MAIN_MENU = [
|
|
114
|
+
{ value: 'ready', label: 'ready', hint: '看看仓库现在什么状态' },
|
|
115
|
+
{ value: 'save', label: 'save', hint: '提交改动(引导写提交信息)' },
|
|
116
|
+
{ value: 'push', label: 'push', hint: '提交并推送,可选合并到其他分支' },
|
|
117
|
+
{ value: 'sw', label: 'sw', hint: '切换分支' },
|
|
118
|
+
{ value: 'checkout', label: 'checkout', hint: '切换或新建分支' },
|
|
119
|
+
{ value: 'sync', label: 'sync', hint: '同步分支' },
|
|
120
|
+
{ value: 'stash', label: 'stash', hint: '暂存 / 恢复改动' },
|
|
121
|
+
{ value: 'log', label: 'log', hint: '最近的提交' },
|
|
122
|
+
{ value: 'amend', label: 'amend', hint: '修改上一个提交' },
|
|
123
|
+
{ value: 'fixup', label: 'fixup', hint: '把改动合进更早的提交' },
|
|
124
|
+
{ value: 'undo', label: 'undo', hint: '撤销 / 回滚' },
|
|
125
|
+
{ value: 'continue', label: 'continue', hint: '冲突解决后继续' },
|
|
126
|
+
{ value: 'rescue', label: 'rescue', hint: '找回丢失的提交和改动' },
|
|
127
|
+
{ value: 'clean', label: 'clean', hint: '清理已合并的分支' },
|
|
128
|
+
{ value: 'pr', label: 'pr create', hint: '创建 PR / MR' },
|
|
129
|
+
{ value: 'open', label: 'open', hint: '在浏览器打开仓库' },
|
|
130
|
+
{ value: 'tag', label: 'tag', hint: '打 tag' },
|
|
131
|
+
{ value: 'release', label: 'release', hint: '发版' },
|
|
132
|
+
{ value: 'config', label: 'config', hint: '配置 token / AI / remote' },
|
|
133
|
+
{ value: 'doctor', label: 'doctor', hint: '环境检查' },
|
|
134
|
+
{ value: 'help', label: 'help', hint: '查看全部命令' }
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
async function pickCommandFromMenu() {
|
|
138
|
+
// 有被冲突打断的操作时,把“继续 / 放弃”放在最前面。
|
|
139
|
+
let paused = null
|
|
140
|
+
try {
|
|
141
|
+
paused = await inspectPausedWork({})
|
|
142
|
+
} catch {
|
|
143
|
+
paused = null
|
|
144
|
+
}
|
|
145
|
+
const menu = paused
|
|
146
|
+
? [
|
|
147
|
+
{ value: 'continue', label: c.yellow('continue'), hint: `冲突解决后继续:${paused.description}` },
|
|
148
|
+
{ value: 'abort', label: 'abort', hint: '放弃被中断的操作并切回原分支' },
|
|
149
|
+
...MAIN_MENU
|
|
150
|
+
]
|
|
151
|
+
: MAIN_MENU
|
|
152
|
+
const picked = await chooseOne(paused ? `有被中断的操作(${paused.description}),要做什么?` : '要做什么?', menu, 0)
|
|
153
|
+
// pr 从菜单进入时直接走创建流程。
|
|
154
|
+
if (picked === 'pr') {
|
|
155
|
+
await COMMANDS.pr(['create'])
|
|
156
|
+
return ''
|
|
157
|
+
}
|
|
158
|
+
return picked
|
|
159
|
+
}
|
|
160
|
+
|
|
61
161
|
/**
|
|
62
162
|
* 执行 CLI 主流程并在命令成功后触发升级提示检查。
|
|
63
163
|
* 输入为命令行参数与可注入依赖,输出为建议的退出码。
|
|
@@ -74,13 +174,30 @@ export async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
74
174
|
}
|
|
75
175
|
|
|
76
176
|
if (!commandName) {
|
|
177
|
+
// 终端里直接运行 icode:显示常用操作菜单,选中后直接执行。
|
|
178
|
+
if (!dependencies.commands && isInteractiveTerminal()) {
|
|
179
|
+
const picked = await pickCommandFromMenu()
|
|
180
|
+
if (picked) {
|
|
181
|
+
await COMMANDS[picked]([])
|
|
182
|
+
}
|
|
183
|
+
return 0
|
|
184
|
+
}
|
|
77
185
|
printHelp()
|
|
78
186
|
return 0
|
|
79
187
|
}
|
|
80
188
|
|
|
189
|
+
// --json 调用时 stdout 只输出结构化结果(config 的 --json 是取值参数,不算)。
|
|
190
|
+
if (commandName !== 'config' && commandArgs.includes('--json')) {
|
|
191
|
+
logger.setLogsToStderr(true)
|
|
192
|
+
}
|
|
193
|
+
|
|
81
194
|
const command = commands[commandName]
|
|
82
195
|
if (!command) {
|
|
83
|
-
throw new
|
|
196
|
+
throw new IcodeError(`未知命令: ${commandName}`, {
|
|
197
|
+
code: 'CLI_UNKNOWN_COMMAND',
|
|
198
|
+
exitCode: 2,
|
|
199
|
+
meta: { commandName }
|
|
200
|
+
})
|
|
84
201
|
}
|
|
85
202
|
|
|
86
203
|
await command(commandArgs)
|
|
@@ -92,9 +209,23 @@ export async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
92
209
|
return 0
|
|
93
210
|
}
|
|
94
211
|
|
|
212
|
+
// 需要跟一个值的全局参数(值不能被当成命令名)。
|
|
213
|
+
const GLOBAL_VALUE_FLAGS = new Set(['-R', '--remote'])
|
|
214
|
+
|
|
95
215
|
function parseEntryArgs(argv) {
|
|
96
216
|
const args = normalizeLegacyArgs(argv)
|
|
97
|
-
|
|
217
|
+
let firstCommandIndex = -1
|
|
218
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
219
|
+
const arg = args[index]
|
|
220
|
+
if (GLOBAL_VALUE_FLAGS.has(arg)) {
|
|
221
|
+
index += 1
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if (!arg.startsWith('-')) {
|
|
225
|
+
firstCommandIndex = index
|
|
226
|
+
break
|
|
227
|
+
}
|
|
228
|
+
}
|
|
98
229
|
|
|
99
230
|
const globalArgs = firstCommandIndex === -1 ? args : args.slice(0, firstCommandIndex)
|
|
100
231
|
const commandName = firstCommandIndex === -1 ? null : args[firstCommandIndex]
|
|
@@ -108,7 +239,24 @@ function parseEntryArgs(argv) {
|
|
|
108
239
|
}
|
|
109
240
|
|
|
110
241
|
function applyGlobalFlags(globalArgs) {
|
|
111
|
-
for (
|
|
242
|
+
for (let index = 0; index < globalArgs.length; index += 1) {
|
|
243
|
+
const flag = globalArgs[index]
|
|
244
|
+
if (flag === '-R' || flag === '--remote' || flag.startsWith('--remote=')) {
|
|
245
|
+
const value = flag.startsWith('--remote=') ? flag.slice('--remote='.length) : globalArgs[index + 1]
|
|
246
|
+
if (!flag.startsWith('--remote=')) {
|
|
247
|
+
index += 1
|
|
248
|
+
}
|
|
249
|
+
if (!value || value.startsWith('-')) {
|
|
250
|
+
throw new IcodeError('--remote 需要指定 remote 名称,例如: icode --remote upstream sync', {
|
|
251
|
+
code: 'CLI_REMOTE_REQUIRED',
|
|
252
|
+
exitCode: 2
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
// 通过环境变量传给所有命令的仓库上下文解析。
|
|
256
|
+
process.env.ICODE_REMOTE = value
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
|
|
112
260
|
if (flag === '-d' || flag === '--debug') {
|
|
113
261
|
logger.setVerbose(true)
|
|
114
262
|
process.env.ICODE_DEBUG = '1'
|
|
@@ -123,7 +271,11 @@ function applyGlobalFlags(globalArgs) {
|
|
|
123
271
|
}
|
|
124
272
|
}
|
|
125
273
|
|
|
126
|
-
throw new
|
|
274
|
+
throw new IcodeError(`未知全局参数: ${flag}`, {
|
|
275
|
+
code: 'CLI_UNKNOWN_GLOBAL_FLAG',
|
|
276
|
+
exitCode: 2,
|
|
277
|
+
meta: { flag }
|
|
278
|
+
})
|
|
127
279
|
}
|
|
128
280
|
|
|
129
281
|
return {
|
|
@@ -144,8 +296,23 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
144
296
|
* 统一处理 CLI 异常并输出对用户可读的错误信息。
|
|
145
297
|
* 输入为任意异常对象,输出为进程退出。
|
|
146
298
|
*/
|
|
147
|
-
export function handleCliError(error) {
|
|
299
|
+
export function handleCliError(error, argv = process.argv.slice(2)) {
|
|
148
300
|
const normalized = asIcodeError(error)
|
|
301
|
+
|
|
302
|
+
// 带 --json 的调用(通常是脚本或 AI agent)同时在 stdout 输出结构化错误。
|
|
303
|
+
// config ai options set 的 --json 是取值参数,不算 JSON 输出模式。
|
|
304
|
+
const { commandName } = parseEntryArgs(argv)
|
|
305
|
+
if (argv.includes('--json') && commandName !== 'config') {
|
|
306
|
+
process.stdout.write(`${JSON.stringify({
|
|
307
|
+
ok: false,
|
|
308
|
+
error: {
|
|
309
|
+
code: normalized.code || 'UNKNOWN',
|
|
310
|
+
message: normalized.message,
|
|
311
|
+
exitCode: normalized.exitCode || 1
|
|
312
|
+
}
|
|
313
|
+
}, null, 2)}\n`)
|
|
314
|
+
}
|
|
315
|
+
|
|
149
316
|
logger.error(normalized.message)
|
|
150
317
|
|
|
151
318
|
if (normalized.code === 'AI_EMPTY_RESPONSE') {
|
package/src/commands/checkout.js
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
2
|
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
3
|
+
import { c, ui } from '../core/cli/render.js'
|
|
4
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
5
|
+
import { GitService } from '../core/git/service.js'
|
|
6
|
+
import { chooseOne, input, isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
7
|
+
import { listSwitchCandidates } from '../workflows/switch-workflow.js'
|
|
3
8
|
import { logger } from '../core/tools/logger.js'
|
|
4
9
|
import { runCheckoutWorkflow } from '../workflows/checkout-workflow.js'
|
|
5
10
|
|
|
6
11
|
function printHelp() {
|
|
7
12
|
process.stdout.write(`
|
|
8
13
|
Usage:
|
|
9
|
-
icode checkout
|
|
14
|
+
icode checkout [branch] [base] [options]
|
|
10
15
|
|
|
11
16
|
Arguments:
|
|
12
|
-
|
|
17
|
+
[branch] 目标分支名;在终端里不传时可直接选择或新建
|
|
13
18
|
[base] 新建分支基线,默认主分支
|
|
14
19
|
|
|
15
20
|
Options:
|
|
16
21
|
-y, --yes 自动确认(跳过交互提示)
|
|
17
|
-
--push-origin
|
|
22
|
+
--push-origin 新建分支后立即推送到远程(当前 remote)
|
|
18
23
|
--pull-main 切换后同步主分支到当前分支
|
|
24
|
+
--autostash 有未提交改动时自动 stash,切换后恢复
|
|
25
|
+
--json 输出机器可读 JSON
|
|
19
26
|
--repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
|
|
20
27
|
--no-verify 跳过 hook/husky 校验
|
|
21
28
|
-h, --help 查看帮助
|
|
@@ -25,6 +32,44 @@ Notes:
|
|
|
25
32
|
`)
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
async function pickCheckoutTarget(repoMode) {
|
|
36
|
+
const context = await resolveGitContext({ repoMode })
|
|
37
|
+
const git = new GitService(context)
|
|
38
|
+
const candidates = (await listSwitchCandidates(git, { all: true })).filter((item) => item.name !== context.currentBranch)
|
|
39
|
+
const choice = await chooseOne('切换到哪个分支?', [
|
|
40
|
+
{ value: '__new__', label: c.green('+ 新建分支…') },
|
|
41
|
+
...candidates.map((item) => ({
|
|
42
|
+
value: item.name,
|
|
43
|
+
label: item.name,
|
|
44
|
+
hint: [item.kind === 'remote' ? '仅远程' : '', ui.sync(item.ahead, item.behind), item.relativeDate].filter(Boolean).join(' · ')
|
|
45
|
+
}))
|
|
46
|
+
], candidates.length ? 1 : 0)
|
|
47
|
+
|
|
48
|
+
if (choice !== '__new__') {
|
|
49
|
+
return { branchName: choice }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const prefix = await chooseOne('分支类型', [
|
|
53
|
+
{ value: 'feature/', label: 'feature/', hint: '新功能' },
|
|
54
|
+
{ value: 'fix/', label: 'fix/', hint: '修复' },
|
|
55
|
+
{ value: 'hotfix/', label: 'hotfix/', hint: '线上紧急修复' },
|
|
56
|
+
{ value: 'chore/', label: 'chore/', hint: '杂项' },
|
|
57
|
+
{ value: 'release/', label: 'release/', hint: '发版' },
|
|
58
|
+
{ value: '', label: '不加前缀' }
|
|
59
|
+
], 0, { filter: false })
|
|
60
|
+
const name = await input(`分支名 ${c.gray(prefix || '')}`, '')
|
|
61
|
+
if (!name) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
const bases = [...new Set([context.defaultBranch, context.currentBranch, ...candidates.filter((item) => item.kind === 'local').map((item) => item.name)])].filter(Boolean)
|
|
65
|
+
const base = await chooseOne('基于哪个分支创建?', bases.map((item) => ({
|
|
66
|
+
value: item,
|
|
67
|
+
label: item,
|
|
68
|
+
hint: item === context.defaultBranch ? '主分支' : item === context.currentBranch ? '当前分支' : ''
|
|
69
|
+
})), 0)
|
|
70
|
+
return { branchName: `${prefix}${name.replace(/\s+/g, '-')}`, baseBranchName: base }
|
|
71
|
+
}
|
|
72
|
+
|
|
28
73
|
export async function runCheckoutCommand(rawArgs) {
|
|
29
74
|
const args = normalizeLegacyArgs(rawArgs)
|
|
30
75
|
const parsed = parseArgs({
|
|
@@ -35,25 +80,47 @@ export async function runCheckoutCommand(rawArgs) {
|
|
|
35
80
|
help: { type: 'boolean', short: 'h', default: false },
|
|
36
81
|
'push-origin': { type: 'boolean', default: false },
|
|
37
82
|
'pull-main': { type: 'boolean', default: false },
|
|
83
|
+
autostash: { type: 'boolean', default: false },
|
|
84
|
+
json: { type: 'boolean', default: false },
|
|
38
85
|
'repo-mode': { type: 'string', default: 'auto' },
|
|
39
86
|
'no-verify': { type: 'boolean', default: false }
|
|
40
87
|
}
|
|
41
88
|
})
|
|
42
89
|
|
|
43
|
-
if (parsed.values.help
|
|
90
|
+
if (parsed.values.help) {
|
|
44
91
|
printHelp()
|
|
45
92
|
return
|
|
46
93
|
}
|
|
47
94
|
|
|
48
|
-
|
|
95
|
+
let [branchName, baseBranchName] = parsed.positionals
|
|
96
|
+
if (!branchName) {
|
|
97
|
+
if (parsed.values.json || !isInteractiveTerminal()) {
|
|
98
|
+
printHelp()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
// 不传分支名时直接选择:已有分支,或新建分支(只需输入名字,基线也用选的)。
|
|
102
|
+
const picked = await pickCheckoutTarget(parsed.values['repo-mode'])
|
|
103
|
+
if (!picked) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
;({ branchName, baseBranchName } = picked)
|
|
107
|
+
}
|
|
49
108
|
const result = await runCheckoutWorkflow({
|
|
50
109
|
branchName,
|
|
51
110
|
baseBranchName,
|
|
52
111
|
pushOrigin: parsed.values['push-origin'],
|
|
53
112
|
pullMain: parsed.values['pull-main'],
|
|
113
|
+
autostash: parsed.values.autostash,
|
|
54
114
|
repoMode: parsed.values['repo-mode'],
|
|
55
115
|
noVerify: parsed.values['no-verify']
|
|
56
116
|
})
|
|
57
117
|
|
|
58
|
-
|
|
118
|
+
if (parsed.values.json) {
|
|
119
|
+
process.stdout.write(`${JSON.stringify({ ok: true, ...result }, null, 2)}\n`)
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const how = { local: '', tracking: c.gray(`(跟踪 ${result.remote}/${result.branchName})`), created: c.gray(`(基于 ${result.baseBranchName} 新建)`) }[result.mode] || ''
|
|
124
|
+
logger.success(`已切换到 ${c.bold(ui.branch(result.branchName))}${how}`)
|
|
125
|
+
return result
|
|
59
126
|
}
|
package/src/commands/clean.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
2
|
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
3
|
+
import { printJson } from '../core/cli/output.js'
|
|
4
|
+
import { c, print, table, title, ui } from '../core/cli/render.js'
|
|
3
5
|
import { logger } from '../core/tools/logger.js'
|
|
4
6
|
import { runCleanWorkflow } from '../workflows/clean-workflow.js'
|
|
5
7
|
|
|
@@ -9,19 +11,27 @@ Usage:
|
|
|
9
11
|
icode clean [options]
|
|
10
12
|
|
|
11
13
|
Options:
|
|
12
|
-
--merged-target <branch>
|
|
14
|
+
--merged-target <branch> 基于该分支判断“已合并”状态,默认主分支(优先用远程版本)
|
|
15
|
+
--merged 清理已合并分支(默认;与 --gone/--stale 同用时取并集)
|
|
16
|
+
--gone 清理远程分支已被删除的本地分支
|
|
17
|
+
--stale <days> 清理超过 N 天没有新提交的本地分支
|
|
13
18
|
--keep <branch|csv> 额外保留分支,可重复使用或逗号分隔
|
|
14
|
-
--remote
|
|
19
|
+
--remote 同时删除远程分支(仅限已合并的分支)
|
|
15
20
|
--force 强制删除本地分支(-D)
|
|
21
|
+
--dry-run 只列出将被删除的分支
|
|
22
|
+
--json 输出机器可读 JSON
|
|
16
23
|
-y, --yes 自动确认(跳过确认提示)
|
|
17
24
|
--repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
|
|
18
25
|
-h, --help 查看帮助
|
|
19
26
|
|
|
20
27
|
Notes:
|
|
21
|
-
|
|
28
|
+
默认保护当前分支/主分支/受保护分支。不会切换分支,工作区有改动也可以执行。
|
|
29
|
+
删除前会自动创建恢复点,误删可用 icode rescue 找回。
|
|
22
30
|
|
|
23
31
|
Examples:
|
|
24
32
|
icode clean
|
|
33
|
+
icode clean --gone -y
|
|
34
|
+
icode clean --stale 90 --dry-run
|
|
25
35
|
icode clean --merged-target main --keep release,hotfix
|
|
26
36
|
icode clean --remote --force -y
|
|
27
37
|
`)
|
|
@@ -35,9 +45,14 @@ export async function runCleanCommand(rawArgs) {
|
|
|
35
45
|
allowPositionals: true,
|
|
36
46
|
options: {
|
|
37
47
|
'merged-target': { type: 'string' },
|
|
48
|
+
merged: { type: 'boolean', default: false },
|
|
49
|
+
gone: { type: 'boolean', default: false },
|
|
50
|
+
stale: { type: 'string' },
|
|
38
51
|
keep: { type: 'string', multiple: true, default: [] },
|
|
39
52
|
remote: { type: 'boolean', default: false },
|
|
40
53
|
force: { type: 'boolean', default: false },
|
|
54
|
+
'dry-run': { type: 'boolean', default: false },
|
|
55
|
+
json: { type: 'boolean', default: false },
|
|
41
56
|
yes: { type: 'boolean', short: 'y', default: false },
|
|
42
57
|
'repo-mode': { type: 'string', default: 'auto' },
|
|
43
58
|
help: { type: 'boolean', short: 'h', default: false }
|
|
@@ -51,18 +66,43 @@ export async function runCleanCommand(rawArgs) {
|
|
|
51
66
|
|
|
52
67
|
const result = await runCleanWorkflow({
|
|
53
68
|
mergedTarget: parsed.values['merged-target'],
|
|
69
|
+
merged: parsed.values.merged,
|
|
70
|
+
gone: parsed.values.gone,
|
|
71
|
+
stale: parsed.values.stale,
|
|
54
72
|
keep: parsed.values.keep,
|
|
55
73
|
remote: parsed.values.remote,
|
|
56
74
|
force: parsed.values.force,
|
|
75
|
+
dryRun: parsed.values['dry-run'],
|
|
76
|
+
json: parsed.values.json,
|
|
57
77
|
yes: parsed.values.yes,
|
|
58
78
|
repoMode: parsed.values['repo-mode']
|
|
59
79
|
})
|
|
60
80
|
|
|
81
|
+
if (parsed.values.json) {
|
|
82
|
+
printJson(result)
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
if (result.canceled) {
|
|
62
|
-
return
|
|
87
|
+
return result
|
|
63
88
|
}
|
|
64
89
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
if (result.dryRun) {
|
|
91
|
+
print(title(`将删除 ${result.candidates.length} 个分支(dry-run)`))
|
|
92
|
+
print(table(result.candidates, [
|
|
93
|
+
{ key: 'branch', label: '分支', format: (value) => ui.branch(value) },
|
|
94
|
+
{ key: 'reasons', label: '原因', format: (value) => c.gray(value.join(';')) }
|
|
95
|
+
]))
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
;(result.failed || []).forEach((item) => logger.warn(`删除失败: ${ui.branch(item.branch)} ${c.gray(item.reason)}`))
|
|
100
|
+
if (!result.deletedLocal.length && !result.failed?.length) {
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
103
|
+
logger.success(`已删除本地分支 ${c.bold(String(result.deletedLocal.length))} 个${result.deletedRemote.length ? `,远程分支 ${c.bold(String(result.deletedRemote.length))} 个` : ''}`)
|
|
104
|
+
if (result.backupId) {
|
|
105
|
+
logger.hint(`误删可用 icode rescue restore ${result.backupId} 找回`)
|
|
106
|
+
}
|
|
107
|
+
return result
|
|
68
108
|
}
|