@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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { parseCommandArgs, printJson } from '../core/cli/output.js'
|
|
2
|
+
import { c, keyValues, print, renderMarkdown, title, ui } from '../core/cli/render.js'
|
|
3
|
+
import { logger } from '../core/tools/logger.js'
|
|
4
|
+
|
|
5
|
+
import { runReleaseWorkflow } from '../workflows/release-workflow.js'
|
|
6
|
+
|
|
7
|
+
function printHelp() {
|
|
8
|
+
process.stdout.write(`
|
|
9
|
+
Usage:
|
|
10
|
+
icode release [options]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
-b, --bump <kind> 版本升级: auto(默认,按提交推断) | patch | minor | major | prerelease
|
|
14
|
+
--preid <id> 预发布标识,例如 beta、rc
|
|
15
|
+
-n, --name <tag> 直接指定 tag 名(不自动计算)
|
|
16
|
+
--write-changelog 把 changelog 写到 CHANGELOG.md 顶部并提交
|
|
17
|
+
--changelog-file <path> 指定 changelog 文件(隐含 --write-changelog)
|
|
18
|
+
--update-package 同步更新 package.json 的 version(会一起提交)
|
|
19
|
+
--no-platform 不创建 GitHub/GitLab/Gitee/Gitea Release
|
|
20
|
+
--draft 平台 Release 设为草稿
|
|
21
|
+
--prerelease 平台 Release 标记为预发布
|
|
22
|
+
--no-push 只在本地打 tag,不推送
|
|
23
|
+
--dry-run 只展示版本号和 changelog
|
|
24
|
+
--json 输出机器可读 JSON
|
|
25
|
+
-y, --yes 自动确认
|
|
26
|
+
--no-verify 跳过 hook 校验
|
|
27
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
28
|
+
-h, --help 查看帮助
|
|
29
|
+
|
|
30
|
+
Notes:
|
|
31
|
+
流程: 计算版本 -> 生成 changelog -> (可选)写文件并提交 -> 打 tag 并推送 -> 创建平台 Release。
|
|
32
|
+
版本推断基于 Conventional Commits:破坏性变更 -> major,feat -> minor,其它 -> patch。
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
icode release --dry-run
|
|
36
|
+
icode release -y
|
|
37
|
+
icode release --bump minor --write-changelog --update-package -y
|
|
38
|
+
icode release --bump prerelease --preid rc
|
|
39
|
+
`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function runReleaseCommand(rawArgs) {
|
|
43
|
+
const { values } = parseCommandArgs(rawArgs, {
|
|
44
|
+
bump: { type: 'string', short: 'b' },
|
|
45
|
+
preid: { type: 'string' },
|
|
46
|
+
name: { type: 'string', short: 'n' },
|
|
47
|
+
'write-changelog': { type: 'boolean', default: false },
|
|
48
|
+
'changelog-file': { type: 'string' },
|
|
49
|
+
'update-package': { type: 'boolean', default: false },
|
|
50
|
+
'no-platform': { type: 'boolean', default: false },
|
|
51
|
+
draft: { type: 'boolean', default: false },
|
|
52
|
+
prerelease: { type: 'boolean', default: false },
|
|
53
|
+
'no-push': { type: 'boolean', default: false },
|
|
54
|
+
'dry-run': { type: 'boolean', default: false },
|
|
55
|
+
'no-verify': { type: 'boolean', default: false }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (values.help) {
|
|
59
|
+
printHelp()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const result = await runReleaseWorkflow({
|
|
64
|
+
bump: values.bump,
|
|
65
|
+
preid: values.preid,
|
|
66
|
+
name: values.name,
|
|
67
|
+
writeChangelog: values['write-changelog'],
|
|
68
|
+
changelogFile: values['changelog-file'],
|
|
69
|
+
updatePackage: values['update-package'],
|
|
70
|
+
platform: !values['no-platform'],
|
|
71
|
+
draft: values.draft,
|
|
72
|
+
prerelease: values.prerelease,
|
|
73
|
+
noPush: values['no-push'],
|
|
74
|
+
dryRun: values['dry-run'],
|
|
75
|
+
noVerify: values['no-verify'],
|
|
76
|
+
yes: values.yes,
|
|
77
|
+
repoMode: values['repo-mode']
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (values.json) {
|
|
81
|
+
printJson(result)
|
|
82
|
+
return result
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (result.canceled) {
|
|
86
|
+
logger.warn('已取消发版。')
|
|
87
|
+
return result
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (result.dryRun) {
|
|
91
|
+
print(title('发版预览(dry-run)'))
|
|
92
|
+
print(keyValues([
|
|
93
|
+
['版本', `${result.previousTag ? ui.tag(result.previousTag) : c.gray('(无)')} ${c.gray('→')} ${c.bold(ui.tag(result.tagName))} ${c.gray(`${result.bump} · ${result.commitCount} 个提交`)}`],
|
|
94
|
+
['分支', ui.branch(result.branch)],
|
|
95
|
+
['changelog 文件', result.changelogFile ? ui.path(result.changelogFile) : c.gray('不写入(--write-changelog 开启)')],
|
|
96
|
+
['平台 Release', result.platform ? (result.platformToken ? c.green(result.platform) : `${c.yellow(result.platform)} ${c.gray('(未配置 token,只给出链接)')}`) : c.gray('不创建')]
|
|
97
|
+
]))
|
|
98
|
+
print('')
|
|
99
|
+
print(renderMarkdown(result.changelog))
|
|
100
|
+
logger.hint('确认无误后去掉 --dry-run 执行')
|
|
101
|
+
return result
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
logger.success(`已发布 ${c.bold(ui.tag(result.tagName))}${result.pushed ? '' : c.yellow('(仅本地,未推送)')}`)
|
|
105
|
+
if (result.releaseUrl) {
|
|
106
|
+
logger.info(`Release: ${ui.url(result.releaseUrl)}`)
|
|
107
|
+
}
|
|
108
|
+
if (result.platformError) {
|
|
109
|
+
logger.warn(result.platformError)
|
|
110
|
+
}
|
|
111
|
+
return result
|
|
112
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { IcodeError } from '../core/errors.js'
|
|
2
|
+
import { parseCommandArgs, printJson } from '../core/cli/output.js'
|
|
3
|
+
import { c, print, table, title, ui } from '../core/cli/render.js'
|
|
4
|
+
import { pad } from '../core/tools/theme.js'
|
|
5
|
+
import { chooseOne, isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
6
|
+
import { logger } from '../core/tools/logger.js'
|
|
7
|
+
import {
|
|
8
|
+
runRescueListWorkflow,
|
|
9
|
+
runRescuePruneWorkflow,
|
|
10
|
+
runRescueRestoreWorkflow
|
|
11
|
+
} from '../workflows/rescue-workflow.js'
|
|
12
|
+
|
|
13
|
+
function printHelp() {
|
|
14
|
+
process.stdout.write(`
|
|
15
|
+
Usage:
|
|
16
|
+
icode rescue [list] 查看恢复点和最近的 HEAD 历史(reflog)
|
|
17
|
+
icode rescue restore <id[:分支]|commit> [options] 找回内容(默认新建分支,不动当前分支)
|
|
18
|
+
icode rescue prune [--keep <n>] 只保留最近 n 个恢复点(默认 20)
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--branch <name> restore 时新建的分支名(默认 rescue/<分支>-<随机>)
|
|
22
|
+
--reset restore 时把当前分支硬重置到目标(会先给当前状态建恢复点)
|
|
23
|
+
--limit <n> list 时显示的 reflog 条数(默认 20)
|
|
24
|
+
--keep <n> prune 保留的恢复点数量
|
|
25
|
+
--json 输出机器可读 JSON
|
|
26
|
+
-y, --yes 自动确认
|
|
27
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
28
|
+
-h, --help 查看帮助
|
|
29
|
+
|
|
30
|
+
Notes:
|
|
31
|
+
undo(reset)、push(本地 merge/强推)、sync、migrate、amend、fixup、clean 执行前都会自动创建恢复点。
|
|
32
|
+
恢复点保存在 refs/icode/backup/ 下,不会被 push,也不会出现在分支列表里。
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
icode rescue
|
|
36
|
+
icode rescue restore 20261102-153012-120
|
|
37
|
+
icode rescue restore 20261102-153012-120:feature/login --reset
|
|
38
|
+
icode rescue restore HEAD@{3} --branch fix/lost-work
|
|
39
|
+
`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printList(result) {
|
|
43
|
+
print(title('恢复点'))
|
|
44
|
+
if (!result.backups.length) {
|
|
45
|
+
print(c.gray(' 暂无。undo / push / amend / fixup / clean 等操作前会自动创建。'))
|
|
46
|
+
} else {
|
|
47
|
+
for (const group of result.backups) {
|
|
48
|
+
print(` ${c.yellow(group.id)} ${c.bold(group.reason || '')}`)
|
|
49
|
+
for (const entry of group.entries) {
|
|
50
|
+
const name = entry.worktree ? c.cyan('未提交改动') : ui.branch(entry.branch)
|
|
51
|
+
print(` ${pad(name, 30)} ${ui.hash(entry.sha)}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (result.reflog.length) {
|
|
57
|
+
print('')
|
|
58
|
+
print(title('最近的 HEAD 历史 (reflog)'))
|
|
59
|
+
print(table(result.reflog, [
|
|
60
|
+
{ key: 'selector', label: '选择器', format: (value) => c.cyan(value) },
|
|
61
|
+
{ key: 'shortHash', label: '提交', format: (value) => ui.hash(value) },
|
|
62
|
+
{ key: 'relativeDate', label: '时间', format: (value) => ui.time(value) },
|
|
63
|
+
{ key: 'subject', label: '操作', maxWidth: 70 }
|
|
64
|
+
]))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
print('')
|
|
68
|
+
logger.hint('找回: icode rescue restore <恢复点id 或 HEAD@{n}>')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function printRestoreResult(result) {
|
|
72
|
+
if (result.action === 'apply-worktree') {
|
|
73
|
+
logger.success('已把备份的未提交改动应用回工作区。')
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (result.action === 'reset') {
|
|
77
|
+
logger.success(`已把 ${ui.branch(result.branch)} 重置到 ${ui.hash(result.target.sha)}`)
|
|
78
|
+
logger.hint(`重置前的状态已存为恢复点 ${result.backupId}`)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
logger.success(`已新建分支 ${ui.branch(result.branch)} → ${ui.hash(result.target.sha)}`)
|
|
82
|
+
logger.hint(`切换过去查看: icode sw ${result.branch}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toInteger(value, label) {
|
|
86
|
+
if (value == null) {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
const number = Number(value)
|
|
90
|
+
if (!Number.isInteger(number) || number < 0) {
|
|
91
|
+
throw new IcodeError(`${label} 需要非负整数`, { code: 'RESCUE_ARG_INVALID', exitCode: 2 })
|
|
92
|
+
}
|
|
93
|
+
return number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runRescueCommand(rawArgs) {
|
|
97
|
+
const parsed = parseCommandArgs(rawArgs, {
|
|
98
|
+
branch: { type: 'string' },
|
|
99
|
+
reset: { type: 'boolean', default: false },
|
|
100
|
+
limit: { type: 'string' },
|
|
101
|
+
keep: { type: 'string' },
|
|
102
|
+
entry: { type: 'string' }
|
|
103
|
+
})
|
|
104
|
+
const { values, positionals } = parsed
|
|
105
|
+
|
|
106
|
+
if (values.help) {
|
|
107
|
+
printHelp()
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const [subcommand = 'list', target] = positionals
|
|
112
|
+
const base = { repoMode: values['repo-mode'], yes: values.yes }
|
|
113
|
+
|
|
114
|
+
if (subcommand === 'list') {
|
|
115
|
+
const result = await runRescueListWorkflow({ ...base, limit: toInteger(values.limit, '--limit') })
|
|
116
|
+
if (values.json) {
|
|
117
|
+
printJson(result)
|
|
118
|
+
return result
|
|
119
|
+
}
|
|
120
|
+
printList(result)
|
|
121
|
+
|
|
122
|
+
// 交互终端里直接选择要恢复的恢复点,省去复制 id。
|
|
123
|
+
if (!values.yes && isInteractiveTerminal() && result.backups.length) {
|
|
124
|
+
const choice = await chooseOne('要找回其中一个恢复点吗?', [
|
|
125
|
+
{ value: '', label: '不用了' },
|
|
126
|
+
...result.backups.slice(0, 15).map((group) => ({ value: group.id, label: group.reason || group.id, hint: `${group.id} · ${group.entries.map((entry) => entry.branch).join(', ')}` }))
|
|
127
|
+
], 0)
|
|
128
|
+
if (choice) {
|
|
129
|
+
const restored = await runRescueRestoreWorkflow({ ...base, target: choice })
|
|
130
|
+
printRestoreResult(restored)
|
|
131
|
+
return restored
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (subcommand === 'restore') {
|
|
138
|
+
const result = await runRescueRestoreWorkflow({
|
|
139
|
+
...base,
|
|
140
|
+
target,
|
|
141
|
+
branch: values.branch,
|
|
142
|
+
reset: values.reset,
|
|
143
|
+
entry: values.entry
|
|
144
|
+
})
|
|
145
|
+
if (values.json) {
|
|
146
|
+
printJson(result)
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
printRestoreResult(result)
|
|
150
|
+
return result
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (subcommand === 'prune') {
|
|
154
|
+
const result = await runRescuePruneWorkflow({ ...base, keep: toInteger(values.keep, '--keep') })
|
|
155
|
+
if (values.json) {
|
|
156
|
+
printJson(result)
|
|
157
|
+
return result
|
|
158
|
+
}
|
|
159
|
+
logger.success(`已清理 ${result.removed.length} 个旧恢复点,保留最近 ${result.keep} 个。`)
|
|
160
|
+
return result
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
throw new IcodeError(`未知 rescue 子命令: ${subcommand}`, { code: 'RESCUE_SUBCOMMAND_UNKNOWN', exitCode: 2 })
|
|
164
|
+
}
|
package/src/commands/save.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
2
|
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
3
|
+
import { formatRiskFindings } from '../core/git/file-guard.js'
|
|
4
|
+
import { c, formatChanges, keyValues, print, ui } from '../core/cli/render.js'
|
|
5
|
+
import { isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
3
6
|
import { logger } from '../core/tools/logger.js'
|
|
4
7
|
import { runSaveWorkflow } from '../workflows/save-workflow.js'
|
|
5
8
|
|
|
@@ -10,14 +13,19 @@ import { runSaveWorkflow } from '../workflows/save-workflow.js'
|
|
|
10
13
|
function printHelp() {
|
|
11
14
|
process.stdout.write(`
|
|
12
15
|
Usage:
|
|
13
|
-
icode save
|
|
16
|
+
icode save [--message <msg> | --ai-commit] [path...] [options]
|
|
17
|
+
|
|
18
|
+
Arguments:
|
|
19
|
+
[path...] 只提交这些路径(文件或目录);不传则提交全部改动
|
|
14
20
|
|
|
15
21
|
Options:
|
|
16
22
|
-m, --message <msg> 指定提交信息
|
|
17
23
|
--ai-commit 使用 AI 根据全部未提交改动生成提交信息
|
|
18
24
|
--profile <name> 指定 AI profile(仅 --ai-commit 使用)
|
|
19
25
|
--lang <zh|en> AI 提交信息语言,默认 zh
|
|
20
|
-
--
|
|
26
|
+
--staged-only 只提交暂存区(不自动 git add)
|
|
27
|
+
--allow-risky 检测到敏感文件/疑似密钥/大文件时仍继续提交
|
|
28
|
+
--dry-run 只检查并展示将提交的信息与风险,不执行 add/commit
|
|
21
29
|
--json 输出机器可读 JSON
|
|
22
30
|
--repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
|
|
23
31
|
--no-verify commit 时跳过 hook/husky 校验
|
|
@@ -25,7 +33,15 @@ Options:
|
|
|
25
33
|
-h, --help 查看帮助
|
|
26
34
|
|
|
27
35
|
Notes:
|
|
28
|
-
|
|
36
|
+
在终端里不传 -m 时会进入提交信息向导(AI 生成 / 选类型+范围 / 手动输入)。
|
|
37
|
+
save 是安全快速提交场景:检查状态 -> 风险扫描 -> 暂存 -> commit。不会 push。
|
|
38
|
+
风险规则可在仓库根目录 .icoderc.json 的 save 字段调整(maxFileSizeMB / sensitivePatterns / allowPatterns)。
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
icode save -m "feat: login"
|
|
42
|
+
icode save -m "fix: typo" src/pages/login.vue
|
|
43
|
+
icode save --staged-only -m "chore: partial"
|
|
44
|
+
icode save --ai-commit --dry-run --json
|
|
29
45
|
`)
|
|
30
46
|
}
|
|
31
47
|
|
|
@@ -34,19 +50,33 @@ Notes:
|
|
|
34
50
|
* 输入为 workflow 结果,输出分色摘要。
|
|
35
51
|
*/
|
|
36
52
|
function printSaveResult(result) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
53
|
+
const scopeText = result.scope === 'staged'
|
|
54
|
+
? '仅暂存区'
|
|
55
|
+
: result.scope === 'paths'
|
|
56
|
+
? `指定路径 ${result.paths.map(ui.path).join(' ')}`
|
|
57
|
+
: '全部改动'
|
|
58
|
+
|
|
59
|
+
print(keyValues([
|
|
60
|
+
['分支', ui.branch(result.context.currentBranch)],
|
|
61
|
+
['改动', formatChanges(result.changes)],
|
|
62
|
+
['范围', `${scopeText} ${c.gray(`(${result.files?.length || 0} 个文件)`)}`],
|
|
63
|
+
['来源', result.source === 'ai' ? c.magenta('AI 生成') : '手动填写']
|
|
64
|
+
]))
|
|
65
|
+
|
|
66
|
+
if (result.risks?.length) {
|
|
67
|
+
print('')
|
|
68
|
+
logger.warn(`发现 ${result.risks.length} 个风险文件:\n${formatRiskFindings(result.risks)}`)
|
|
69
|
+
}
|
|
41
70
|
|
|
71
|
+
print('')
|
|
42
72
|
if (result.dryRun) {
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
print(` ${c.yellow('dry-run')} ${c.gray('未执行 git add / git commit')}`)
|
|
74
|
+
print(` ${ui.subject(result.commitMessage.split('\n')[0])}`)
|
|
45
75
|
return
|
|
46
76
|
}
|
|
47
77
|
|
|
48
|
-
logger.success(
|
|
49
|
-
logger.
|
|
78
|
+
logger.success(`已提交 ${ui.hash(result.commitId)} ${ui.subject(result.commitMessage.split('\n')[0])}`)
|
|
79
|
+
logger.hint('推送: icode push')
|
|
50
80
|
}
|
|
51
81
|
|
|
52
82
|
/**
|
|
@@ -63,6 +93,8 @@ export async function runSaveCommand(rawArgs) {
|
|
|
63
93
|
'ai-commit': { type: 'boolean', default: false },
|
|
64
94
|
profile: { type: 'string', default: '' },
|
|
65
95
|
lang: { type: 'string', default: 'zh' },
|
|
96
|
+
'staged-only': { type: 'boolean', default: false },
|
|
97
|
+
'allow-risky': { type: 'boolean', default: false },
|
|
66
98
|
'dry-run': { type: 'boolean', default: false },
|
|
67
99
|
json: { type: 'boolean', default: false },
|
|
68
100
|
'repo-mode': { type: 'string', default: 'auto' },
|
|
@@ -83,6 +115,10 @@ export async function runSaveCommand(rawArgs) {
|
|
|
83
115
|
profile: parsed.values.profile,
|
|
84
116
|
lang: parsed.values.lang,
|
|
85
117
|
dryRun: parsed.values['dry-run'],
|
|
118
|
+
interactive: !parsed.values.json && isInteractiveTerminal(),
|
|
119
|
+
paths: parsed.positionals,
|
|
120
|
+
stagedOnly: parsed.values['staged-only'],
|
|
121
|
+
allowRisky: parsed.values['allow-risky'],
|
|
86
122
|
repoMode: parsed.values['repo-mode'],
|
|
87
123
|
noVerify: parsed.values['no-verify'],
|
|
88
124
|
yes: parsed.values.yes
|
|
@@ -93,6 +129,11 @@ export async function runSaveCommand(rawArgs) {
|
|
|
93
129
|
return result
|
|
94
130
|
}
|
|
95
131
|
|
|
132
|
+
if (result.canceled) {
|
|
133
|
+
logger.warn('已取消,未提交。')
|
|
134
|
+
return result
|
|
135
|
+
}
|
|
136
|
+
|
|
96
137
|
printSaveResult(result)
|
|
97
138
|
return result
|
|
98
139
|
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { IcodeError } from '../core/errors.js'
|
|
2
|
+
import { parseCommandArgs, printJson } from '../core/cli/output.js'
|
|
3
|
+
import { c, print, table, 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, confirm, isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
7
|
+
import { logger } from '../core/tools/logger.js'
|
|
8
|
+
|
|
9
|
+
function printHelp() {
|
|
10
|
+
process.stdout.write(`
|
|
11
|
+
Usage:
|
|
12
|
+
icode stash 交互式选择 stash 并操作(应用/弹出/删除/查看)
|
|
13
|
+
icode stash list 列出 stash
|
|
14
|
+
icode stash save [-m <msg>] [path...] 暂存当前改动(默认包含未跟踪文件)
|
|
15
|
+
icode stash apply [ref] 应用 stash(保留记录),默认最新
|
|
16
|
+
icode stash pop [ref] 应用并删除 stash,默认最新
|
|
17
|
+
icode stash drop [ref] 删除 stash(会确认)
|
|
18
|
+
icode stash show [ref] 查看 stash 包含的文件
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-m, --message <msg> save 时的说明
|
|
22
|
+
--staged save 时只暂存已 add 的改动
|
|
23
|
+
--no-untracked save 时不包含未跟踪文件
|
|
24
|
+
--index apply/pop 时恢复暂存区状态
|
|
25
|
+
--json 输出机器可读 JSON
|
|
26
|
+
-y, --yes 自动确认
|
|
27
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
28
|
+
-h, --help 查看帮助
|
|
29
|
+
|
|
30
|
+
Notes:
|
|
31
|
+
ref 可以写 stash@{1},也可以只写数字 1。
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
icode stash save -m "wip: login"
|
|
35
|
+
icode stash pop
|
|
36
|
+
icode stash apply 2
|
|
37
|
+
`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 支持只写数字的 stash 引用。
|
|
41
|
+
function normalizeStashRef(ref) {
|
|
42
|
+
const value = String(ref || '').trim()
|
|
43
|
+
if (!value) {
|
|
44
|
+
return 'stash@{0}'
|
|
45
|
+
}
|
|
46
|
+
return /^\d+$/.test(value) ? `stash@{${value}}` : value
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printStashList(list) {
|
|
50
|
+
if (!list.length) {
|
|
51
|
+
print(c.gray(' 没有 stash。用 icode stash save -m "说明" 暂存当前改动。'))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
print(table(list, [
|
|
55
|
+
{ key: 'ref', label: 'stash', format: (value) => c.yellow(value) },
|
|
56
|
+
{ key: 'relativeDate', label: '时间', format: (value) => ui.time(value) },
|
|
57
|
+
{ key: 'subject', label: '说明', maxWidth: 80 }
|
|
58
|
+
]))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function ensureStashExists(git, ref) {
|
|
62
|
+
const list = await git.stashList()
|
|
63
|
+
const entry = list.find((item) => item.ref === ref)
|
|
64
|
+
if (!entry) {
|
|
65
|
+
throw new IcodeError(`stash 不存在: ${ref}`, { code: 'STASH_NOT_FOUND', exitCode: 2 })
|
|
66
|
+
}
|
|
67
|
+
return entry
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function applyStash(git, ref, options) {
|
|
71
|
+
try {
|
|
72
|
+
await git.stashApply(ref, { pop: options.pop, index: options.index })
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const text = `${error?.meta?.stdout || ''}\n${error?.meta?.stderr || ''}`
|
|
75
|
+
if (/CONFLICT|conflict/i.test(text)) {
|
|
76
|
+
throw new IcodeError(
|
|
77
|
+
`应用 ${ref} 时发生冲突。请解决冲突文件后 git add;stash 记录${options.pop ? '已保留' : '仍在'},确认无误后可执行 icode stash drop ${ref}。`,
|
|
78
|
+
{ code: 'STASH_APPLY_CONFLICT', exitCode: 2, cause: error }
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
throw error
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 执行 stash 子命令。
|
|
87
|
+
* 输入为原始参数,输出各子命令结果;无子命令时进入交互选择。
|
|
88
|
+
*/
|
|
89
|
+
export async function runStashCommand(rawArgs) {
|
|
90
|
+
const parsed = parseCommandArgs(rawArgs, {
|
|
91
|
+
message: { type: 'string', short: 'm' },
|
|
92
|
+
staged: { type: 'boolean', default: false },
|
|
93
|
+
'no-untracked': { type: 'boolean', default: false },
|
|
94
|
+
index: { type: 'boolean', default: false }
|
|
95
|
+
})
|
|
96
|
+
const { values, positionals } = parsed
|
|
97
|
+
|
|
98
|
+
if (values.help) {
|
|
99
|
+
printHelp()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const context = await resolveGitContext({ repoMode: values['repo-mode'] })
|
|
104
|
+
const git = new GitService(context)
|
|
105
|
+
let [subcommand = '', ...rest] = positionals
|
|
106
|
+
|
|
107
|
+
if (!subcommand) {
|
|
108
|
+
if (values.json || !isInteractiveTerminal()) {
|
|
109
|
+
subcommand = 'list'
|
|
110
|
+
} else {
|
|
111
|
+
const list = await git.stashList()
|
|
112
|
+
if (!list.length) {
|
|
113
|
+
logger.info('没有 stash。可以用 icode stash save -m "<说明>" 暂存当前改动。')
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
const ref = await chooseOne('选择 stash', list.map((item) => ({
|
|
117
|
+
value: item.ref,
|
|
118
|
+
label: `${item.ref} ${item.subject}`,
|
|
119
|
+
hint: item.relativeDate
|
|
120
|
+
})), 0)
|
|
121
|
+
const action = await chooseOne(`对 ${ref} 执行`, [
|
|
122
|
+
{ value: 'pop', label: '应用并删除(pop)' },
|
|
123
|
+
{ value: 'apply', label: '应用但保留(apply)' },
|
|
124
|
+
{ value: 'show', label: '查看包含的文件' },
|
|
125
|
+
{ value: 'drop', label: '删除' },
|
|
126
|
+
{ value: '', label: '取消' }
|
|
127
|
+
], 0)
|
|
128
|
+
if (!action) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
subcommand = action
|
|
132
|
+
rest = [ref]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (subcommand === 'list') {
|
|
137
|
+
const list = await git.stashList()
|
|
138
|
+
if (values.json) {
|
|
139
|
+
printJson({ stashes: list })
|
|
140
|
+
return list
|
|
141
|
+
}
|
|
142
|
+
printStashList(list)
|
|
143
|
+
return list
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (subcommand === 'save' || subcommand === 'push') {
|
|
147
|
+
const changes = await git.getStatusSummary()
|
|
148
|
+
if (changes.clean) {
|
|
149
|
+
throw new IcodeError('没有需要暂存的改动。', { code: 'STASH_NO_CHANGES', exitCode: 2 })
|
|
150
|
+
}
|
|
151
|
+
await git.stashPush({
|
|
152
|
+
message: values.message,
|
|
153
|
+
staged: values.staged,
|
|
154
|
+
includeUntracked: !values['no-untracked'] && !values.staged,
|
|
155
|
+
paths: rest
|
|
156
|
+
})
|
|
157
|
+
const [latest] = await git.stashList()
|
|
158
|
+
if (values.json) {
|
|
159
|
+
printJson({ action: 'save', stash: latest })
|
|
160
|
+
return latest
|
|
161
|
+
}
|
|
162
|
+
logger.success(`已暂存: ${latest?.ref || 'stash@{0}'} ${latest?.subject || ''}`)
|
|
163
|
+
return latest
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const ref = normalizeStashRef(rest[0])
|
|
167
|
+
|
|
168
|
+
if (subcommand === 'apply' || subcommand === 'pop') {
|
|
169
|
+
const entry = await ensureStashExists(git, ref)
|
|
170
|
+
await applyStash(git, ref, { pop: subcommand === 'pop', index: values.index })
|
|
171
|
+
if (values.json) {
|
|
172
|
+
printJson({ action: subcommand, stash: entry })
|
|
173
|
+
return entry
|
|
174
|
+
}
|
|
175
|
+
logger.success(`已${subcommand === 'pop' ? '应用并删除' : '应用'}: ${ref} ${entry.subject}`)
|
|
176
|
+
return entry
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (subcommand === 'drop') {
|
|
180
|
+
const entry = await ensureStashExists(git, ref)
|
|
181
|
+
if (!values.yes) {
|
|
182
|
+
const accepted = await confirm(`确认删除 ${ref}(${entry.subject})?`, false)
|
|
183
|
+
if (!accepted) {
|
|
184
|
+
logger.warn('已取消。')
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
await git.stashDrop(ref)
|
|
189
|
+
if (values.json) {
|
|
190
|
+
printJson({ action: 'drop', stash: entry, recoverHint: `git stash apply ${entry.hash}` })
|
|
191
|
+
return entry
|
|
192
|
+
}
|
|
193
|
+
logger.success(`已删除 ${ref}。如需找回: git stash apply ${entry.hash.slice(0, 12)}`)
|
|
194
|
+
return entry
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (subcommand === 'show') {
|
|
198
|
+
const entry = await ensureStashExists(git, ref)
|
|
199
|
+
const stat = await git.stashShow(ref)
|
|
200
|
+
if (values.json) {
|
|
201
|
+
printJson({ stash: entry, stat })
|
|
202
|
+
return entry
|
|
203
|
+
}
|
|
204
|
+
logger.info(`${ref} ${entry.subject}`)
|
|
205
|
+
process.stdout.write(stat)
|
|
206
|
+
return entry
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
throw new IcodeError(`未知 stash 子命令: ${subcommand}`, { code: 'STASH_SUBCOMMAND_UNKNOWN', exitCode: 2 })
|
|
210
|
+
}
|