thincoder 0.12.62 → 0.12.64

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 (231) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +46 -49
  3. package/bin/thincoder.mjs +68 -34
  4. package/package.json +7 -6
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +257 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +60 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +35 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-plan.mjs +9 -0
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +24 -1
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +33 -11
  38. package/src/tui/index.mjs +18 -10
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-modes.mjs +9 -4
  41. package/src/tui/ledger-surface.mjs +22 -59
  42. package/src/tui/model-catalog.mjs +4 -4
  43. package/src/tui/model-picker.mjs +8 -7
  44. package/src/tui/mouse.mjs +11 -6
  45. package/src/tui/pickers.mjs +15 -2
  46. package/src/tui/render-conversation.mjs +1 -1
  47. package/src/tui/render-frame.mjs +15 -6
  48. package/src/tui/render-loop.mjs +1 -1
  49. package/src/tui/render-segments.mjs +3 -1
  50. package/src/tui/slash-commands.mjs +1 -1
  51. package/src/tui/startup.mjs +14 -14
  52. package/src/tui/subagent-blocks.mjs +20 -3
  53. package/src/tui/subagent-freeze.mjs +73 -2
  54. package/src/tui/suspension-drive.mjs +57 -23
  55. package/src/tui/tool-events.mjs +11 -8
  56. package/src/tui/tui-lifecycle.mjs +9 -0
  57. package/src/tui/wizard.mjs +3 -3
  58. package/src/tui/wrapped-spawn.mjs +6 -2
  59. package/src/abort-provenance.mjs +0 -116
  60. package/src/advisor/citations.mjs +0 -139
  61. package/src/advisor/compaction.mjs +0 -174
  62. package/src/advisor/convergence.mjs +0 -80
  63. package/src/advisor/history.mjs +0 -77
  64. package/src/advisor/loop.mjs +0 -293
  65. package/src/advisor/messages.mjs +0 -299
  66. package/src/advisor/project-context.mjs +0 -194
  67. package/src/advisor/repos.mjs +0 -150
  68. package/src/advisor/run.mjs +0 -293
  69. package/src/advisor/truncate.mjs +0 -57
  70. package/src/advisor.mjs +0 -290
  71. package/src/agent/completion.mjs +0 -146
  72. package/src/agent/dispatch.mjs +0 -489
  73. package/src/agent/helpers.mjs +0 -384
  74. package/src/agent/post-turn.mjs +0 -70
  75. package/src/agent/record-results.mjs +0 -174
  76. package/src/agent/relay-prefix.mjs +0 -39
  77. package/src/agent/run-stages.mjs +0 -244
  78. package/src/agent/setup-reminders.mjs +0 -69
  79. package/src/agent/setup.mjs +0 -354
  80. package/src/agent/spawn-child.mjs +0 -243
  81. package/src/agent-tools/advisor-async.mjs +0 -346
  82. package/src/agent-tools/advisor-settle.mjs +0 -231
  83. package/src/agent-tools/advisor.mjs +0 -260
  84. package/src/agent-tools/async-settle.mjs +0 -204
  85. package/src/agent-tools/batch-segment.mjs +0 -195
  86. package/src/agent-tools/consult.mjs +0 -473
  87. package/src/agent-tools/design-token.mjs +0 -117
  88. package/src/agent-tools/digest-budget.mjs +0 -76
  89. package/src/agent-tools/eng.mjs +0 -67
  90. package/src/agent-tools/escalate-async.mjs +0 -295
  91. package/src/agent-tools/goal.mjs +0 -119
  92. package/src/agent-tools/plan.mjs +0 -81
  93. package/src/agent-tools/read-history.mjs +0 -309
  94. package/src/agent-tools/recent-changes.mjs +0 -24
  95. package/src/agent-tools/review-streak.mjs +0 -93
  96. package/src/agent-tools/settings.mjs +0 -265
  97. package/src/agent-tools/skill.mjs +0 -47
  98. package/src/agent-tools/subagent-actions.mjs +0 -482
  99. package/src/agent-tools/subagent-async.mjs +0 -434
  100. package/src/agent-tools/subagent-panel.mjs +0 -160
  101. package/src/agent-tools/subagent-run.mjs +0 -205
  102. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  103. package/src/agent-tools/subagent-spawn.mjs +0 -459
  104. package/src/agent-tools/subagent.mjs +0 -404
  105. package/src/agent-tools/task.mjs +0 -87
  106. package/src/agent-tools/timer.mjs +0 -46
  107. package/src/agent-tools/verify.mjs +0 -271
  108. package/src/agent-tools.mjs +0 -17
  109. package/src/agent.mjs +0 -417
  110. package/src/auto-think.mjs +0 -115
  111. package/src/config-migrate.mjs +0 -70
  112. package/src/config.mjs +0 -496
  113. package/src/context.mjs +0 -392
  114. package/src/conventions.mjs +0 -223
  115. package/src/embedding.mjs +0 -120
  116. package/src/escape.mjs +0 -152
  117. package/src/expand-home.mjs +0 -16
  118. package/src/explore-distill.mjs +0 -155
  119. package/src/generate-title.mjs +0 -88
  120. package/src/git/checkpoint.mjs +0 -448
  121. package/src/git/gitmem.mjs +0 -100
  122. package/src/hooks.mjs +0 -97
  123. package/src/ledger.mjs +0 -227
  124. package/src/log.mjs +0 -195
  125. package/src/markdown.mjs +0 -106
  126. package/src/mcp/helpers.mjs +0 -51
  127. package/src/mcp/transport-http.mjs +0 -248
  128. package/src/mcp/transport-stdio.mjs +0 -140
  129. package/src/mcp/transport-ws.mjs +0 -122
  130. package/src/mcp.mjs +0 -295
  131. package/src/memory/code-index.mjs +0 -219
  132. package/src/memory/code-sync.mjs +0 -415
  133. package/src/memory/core.mjs +0 -299
  134. package/src/memory/delete.mjs +0 -236
  135. package/src/memory/docs.mjs +0 -419
  136. package/src/memory/file-walk.mjs +0 -109
  137. package/src/memory/scan.mjs +0 -95
  138. package/src/memory/schema.mjs +0 -452
  139. package/src/memory.mjs +0 -21
  140. package/src/model-ref.mjs +0 -66
  141. package/src/model-specs.mjs +0 -179
  142. package/src/peer-domains.mjs +0 -265
  143. package/src/peer-instances.mjs +0 -231
  144. package/src/prompt-overlays.mjs +0 -82
  145. package/src/prompts/advisor-design.md +0 -41
  146. package/src/prompts/advisor-round1.md +0 -41
  147. package/src/prompts/advisor-round2.md +0 -46
  148. package/src/prompts/advisor-round3.md +0 -42
  149. package/src/prompts/common.md +0 -115
  150. package/src/prompts/consult-base.md +0 -19
  151. package/src/prompts/discipline-engineering.md +0 -258
  152. package/src/prompts/discipline-normal.md +0 -185
  153. package/src/prompts/persona-coder.md +0 -21
  154. package/src/prompts/persona-eng-coder.md +0 -37
  155. package/src/prompts/persona-eng-designer.md +0 -60
  156. package/src/prompts/persona-engineering.md +0 -55
  157. package/src/prompts/persona-explore.md +0 -15
  158. package/src/prompts/persona-normal.md +0 -27
  159. package/src/prompts/persona-plan.md +0 -26
  160. package/src/provider/anthropic.mjs +0 -225
  161. package/src/provider/core.mjs +0 -476
  162. package/src/provider/errors.mjs +0 -101
  163. package/src/provider/google.mjs +0 -257
  164. package/src/provider/index.mjs +0 -7
  165. package/src/provider/list-models.mjs +0 -93
  166. package/src/provider/normalize.mjs +0 -81
  167. package/src/provider/rate.mjs +0 -108
  168. package/src/provider/responses.mjs +0 -495
  169. package/src/provider/retry.mjs +0 -88
  170. package/src/provider/sse.mjs +0 -264
  171. package/src/proxy.mjs +0 -261
  172. package/src/rules.mjs +0 -53
  173. package/src/session-gc.mjs +0 -221
  174. package/src/session-guard.mjs +0 -59
  175. package/src/session-migrate.mjs +0 -48
  176. package/src/session-rename.mjs +0 -38
  177. package/src/session-segments.mjs +0 -100
  178. package/src/session-slots.mjs +0 -492
  179. package/src/session-store.mjs +0 -441
  180. package/src/session.mjs +0 -492
  181. package/src/skills.mjs +0 -153
  182. package/src/text-budget.mjs +0 -46
  183. package/src/token-ttl.mjs +0 -274
  184. package/src/tools/apply_patch.md +0 -15
  185. package/src/tools/bash.md +0 -37
  186. package/src/tools/bash.mjs +0 -268
  187. package/src/tools/checklist-sync.mjs +0 -181
  188. package/src/tools/checklist.md +0 -13
  189. package/src/tools/checklist.mjs +0 -299
  190. package/src/tools/delete.md +0 -13
  191. package/src/tools/edit-batch.mjs +0 -191
  192. package/src/tools/edit-diff.mjs +0 -348
  193. package/src/tools/edit.md +0 -30
  194. package/src/tools/execute.md +0 -21
  195. package/src/tools/execute.mjs +0 -228
  196. package/src/tools/fetch.md +0 -12
  197. package/src/tools/file.mjs +0 -469
  198. package/src/tools/file_ops.md +0 -17
  199. package/src/tools/get_current_time.md +0 -8
  200. package/src/tools/git-checkpoint.mjs +0 -143
  201. package/src/tools/git-ext.mjs +0 -173
  202. package/src/tools/git.md +0 -54
  203. package/src/tools/git.mjs +0 -356
  204. package/src/tools/glob-dialect.mjs +0 -130
  205. package/src/tools/glob.md +0 -11
  206. package/src/tools/grep.md +0 -19
  207. package/src/tools/hashline_edit.md +0 -14
  208. package/src/tools/index.mjs +0 -36
  209. package/src/tools/insert_after.md +0 -15
  210. package/src/tools/lint.md +0 -10
  211. package/src/tools/linter.mjs +0 -128
  212. package/src/tools/ls.md +0 -12
  213. package/src/tools/lsp.md +0 -10
  214. package/src/tools/lsp.mjs +0 -316
  215. package/src/tools/ops.mjs +0 -299
  216. package/src/tools/patch.mjs +0 -282
  217. package/src/tools/process.md +0 -10
  218. package/src/tools/question.md +0 -16
  219. package/src/tools/question.mjs +0 -26
  220. package/src/tools/read.md +0 -20
  221. package/src/tools/read_image.md +0 -8
  222. package/src/tools/repomap.mjs +0 -314
  223. package/src/tools/search.mjs +0 -236
  224. package/src/tools/shared.mjs +0 -446
  225. package/src/tools/tree.md +0 -14
  226. package/src/tools/tree.mjs +0 -66
  227. package/src/tools/wait_for.md +0 -22
  228. package/src/tools/web.mjs +0 -224
  229. package/src/tools/websearch.md +0 -16
  230. package/src/tools/write.md +0 -11
  231. package/src/traces/trace-store.mjs +0 -355
@@ -1,173 +0,0 @@
1
- /**
2
- * git-ext.mjs — git 工具 F7 扩展 action(clone/init/rebase/remote/clean/switch/apply/worktree/
3
- * archive/blame/mv)+ 共享 git 辅助函数(validateRef/runGitStrict/filterLines/snapshotBefore,
4
- * 供 git.mjs 核心 action 复用——500 行硬限拆分)。CLI 与 VS Code 两端同构(镜像,修改须两端同批)。
5
- */
6
- import { runGit, truncate } from "./shared.mjs"
7
- import { execFileSync } from "node:child_process"
8
-
9
- /** Keep only output lines matching a regex (git filter, case-insensitive). */
10
- export function filterLines(output, filter) {
11
- if (!filter) return output
12
- try {
13
- const re = new RegExp(filter, "i")
14
- const lines = output.split("\n").filter((l) => re.test(l))
15
- return lines.length ? lines.join("\n") : `(no lines matched filter "${filter}")`
16
- } catch (e) {
17
- return `Error: filter regex invalid: ${e.message}`
18
- }
19
- }
20
-
21
- /** Run git and report failure (stderr + exit code) instead of swallowing it.
22
- * Used by write ops (commit/push/rm) where a silent "" would masquerade as success. */
23
- export function runGitStrict(cwd, cmdArgs, config = []) {
24
- try {
25
- const out = execFileSync("git", [...config, ...cmdArgs], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim().replace(/\r/g, "")
26
- return { ok: true, out }
27
- } catch (e) {
28
- return { ok: false, out: String(e.stdout || "").trim(), err: String(e.stderr || e.message || "").trim() }
29
- }
30
- }
31
-
32
- /** Validate a git ref / branch / tag / remote name (no option injection, no whitespace). */
33
- export function validateRef(ref, what = "git ref") {
34
- if (!/^[A-Za-z0-9._/~^@][A-Za-z0-9._/~^@{}-]*$/.test(ref)) throw new Error(`Invalid ${what}: ${ref}`)
35
- return ref
36
- }
37
-
38
- /** Normalize args.config into `-c key=value` pairs (git -c overrides, e.g. a proxy).
39
- * Values are execFileSync array args (no shell injection) — still reject newlines/empty. */
40
- export function gitConfigArgs(config) {
41
- if (config == null) return []
42
- if (!Array.isArray(config)) throw new Error("config must be an array of \"key=value\" strings")
43
- const out = []
44
- for (const c of config) {
45
- if (typeof c !== "string" || !c.trim() || c.includes("\n")) throw new Error(`invalid git -c config entry: ${String(c).slice(0, 60)}`)
46
- out.push("-c", c)
47
- }
48
- return out
49
- }
50
-
51
- /** Snapshot the working tree before a destructive op (reset --hard / checkout file / restore /
52
- * stash pop / branch|tag delete / clean / rebase). Best-effort — a snapshot failure must not
53
- * block the op (the approval/permission layer is the real gate). Returns a note line or "". */
54
- export async function snapshotBefore(ctx, label) {
55
- try {
56
- const { createCheckpoint, isGitRepo } = await import("../git/checkpoint.mjs")
57
- if (!isGitRepo(ctx.cwd)) return ""
58
- const cp = await createCheckpoint(ctx.cwd)
59
- return `[snapshot ${cp.id} created before ${label}]\n`
60
- } catch {
61
- return ""
62
- }
63
- }
64
-
65
- /** F7 扩展 action 主入口(git 工具 switch 的 fall-through 组委托到这里)。 */
66
- export async function executeExtAction(args, ctx) {
67
- switch (args.action) {
68
- case "clone": {
69
- // Clone a repo into a NEW directory — non-destructive (never touches existing work).
70
- if (!args.remote) return "Error: clone requires remote (URL or local path)"
71
- const cmdArgs = ["clone", args.remote]
72
- if (args.path) cmdArgs.push(args.path)
73
- const r = runGitStrict(ctx.cwd, cmdArgs, gitConfigArgs(args.config))
74
- return r.ok ? truncate(r.out || `Cloned ${args.remote}`) : truncate(`git clone failed: ${r.err || r.out}`)
75
- }
76
- case "init": {
77
- const r = runGitStrict(ctx.cwd, ["init"])
78
- return r.ok ? truncate(r.out || "Initialized empty git repository") : truncate(`git init failed: ${r.err || r.out}`)
79
- }
80
- case "rebase": {
81
- // Belt-and-braces snapshot: bare rebase refuses uncommitted changes (unless
82
- // --autostash), but --autostash restore-failure / interrupted-rebase scenarios
83
- // can leave the working tree damaged — the snapshot makes that recoverable.
84
- // The snapshot line is included on FAILURE too: a rejected rebase (unstaged
85
- // changes) is exactly when the model must know its work is protected (F1 loop).
86
- const snap = await snapshotBefore(ctx, "rebase")
87
- const sub = args.rebaseAction ?? "start"
88
- const cmdArgs = ["rebase"]
89
- if (sub === "abort") cmdArgs.push("--abort")
90
- else if (sub === "continue") cmdArgs.push("--continue")
91
- else { if (!args.ref) return "Error: rebase requires ref (branch/commit to rebase onto)"; cmdArgs.push(validateRef(args.ref)) }
92
- const r = runGitStrict(ctx.cwd, cmdArgs)
93
- return r.ok ? truncate(snap + (r.out || `Rebase ${sub} complete`)) : truncate(snap + `git rebase failed: ${r.err || r.out} — use rebaseAction=abort to abort`)
94
- }
95
- case "remote": {
96
- const sub = args.remoteAction ?? "list"
97
- if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["remote", "-v"]) || "(no remotes)", args.filter))
98
- if (!args.remote) return `Error: remote ${sub} requires remote (name)`
99
- validateRef(args.remote, "remote name")
100
- if (sub === "add" || sub === "set-url") {
101
- if (!args.remoteUrl) return `Error: remote ${sub} requires remoteUrl`
102
- const r = runGitStrict(ctx.cwd, ["remote", sub === "add" ? "add" : "set-url", args.remote, args.remoteUrl])
103
- return r.ok ? `Remote ${args.remote} ${sub === "add" ? "added" : "URL set"}` : truncate(`git remote ${sub} failed: ${r.err || r.out}`)
104
- }
105
- if (sub === "remove") {
106
- const r = runGitStrict(ctx.cwd, ["remote", "remove", args.remote])
107
- return r.ok ? `Remote ${args.remote} removed` : truncate(`git remote remove failed: ${r.err || r.out}`)
108
- }
109
- return "Error: remote requires remoteAction — use: list | add | remove | set-url"
110
- }
111
- case "clean": {
112
- // Destructive: removes untracked files/dirs — snapshot first (guard parity).
113
- // dryRun (-n) is a preview: no deletion, no snapshot.
114
- const snap = args.dryRun ? "" : await snapshotBefore(ctx, "clean")
115
- const cmdArgs = ["clean", args.dryRun ? "-n" : "-f", "-d"]
116
- const r = runGitStrict(ctx.cwd, cmdArgs)
117
- return r.ok ? truncate(snap + (r.out || (args.dryRun ? "Nothing to clean (dry run)" : "Clean complete"))) : truncate(snap + `git clean failed: ${r.err || r.out}`)
118
- }
119
- case "switch": {
120
- if (!args.name) return "Error: switch requires name (branch)"
121
- validateRef(args.name, "branch")
122
- const cmdArgs = ["switch"]
123
- if (args.create) cmdArgs.push("-c")
124
- cmdArgs.push(args.name)
125
- const r = runGitStrict(ctx.cwd, cmdArgs)
126
- return r.ok ? truncate(r.out || `Switched to branch ${args.name}`) : truncate(`git switch failed: ${r.err || r.out}`)
127
- }
128
- case "apply": {
129
- // Apply a patch — non-destructive (fails cleanly on conflict, applies nothing).
130
- if (!args.path) return "Error: apply requires path (patch file)"
131
- const r = runGitStrict(ctx.cwd, ["apply", "--", args.path])
132
- return r.ok ? truncate(r.out || `Applied ${args.path}`) : truncate(`git apply failed: ${r.err || r.out}`)
133
- }
134
- case "worktree": {
135
- const sub = args.worktreeAction ?? "list"
136
- if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["worktree", "list"]) || "(no worktrees)", args.filter))
137
- if (sub === "add") {
138
- if (!args.path) return "Error: worktree add requires path (new worktree directory)"
139
- const cmdArgs = ["worktree", "add", args.path]
140
- if (args.ref) cmdArgs.push(validateRef(args.ref))
141
- const r = runGitStrict(ctx.cwd, cmdArgs)
142
- return r.ok ? truncate(r.out || `Worktree added at ${args.path}`) : truncate(`git worktree add failed: ${r.err || r.out}`)
143
- }
144
- if (sub === "remove") {
145
- if (!args.path) return "Error: worktree remove requires path"
146
- const r = runGitStrict(ctx.cwd, ["worktree", "remove", args.path])
147
- return r.ok ? truncate(r.out || `Worktree removed: ${args.path}`) : truncate(`git worktree remove failed: ${r.err || r.out}`)
148
- }
149
- return "Error: worktree requires worktreeAction — use: list | add | remove"
150
- }
151
- case "archive": {
152
- // Write a tar of a commit/branch — non-destructive (output file only).
153
- if (!args.path) return "Error: archive requires path (output file)"
154
- const cmdArgs = ["archive", "--format=tar", "-o", args.path]
155
- if (args.ref) cmdArgs.push(validateRef(args.ref))
156
- else cmdArgs.push("HEAD")
157
- const r = runGitStrict(ctx.cwd, cmdArgs)
158
- return r.ok ? truncate(r.out || `Archived ${args.ref ?? "HEAD"} to ${args.path}`) : truncate(`git archive failed: ${r.err || r.out}`)
159
- }
160
- case "blame": {
161
- if (!args.path) return "Error: blame requires path (file)"
162
- const out = runGit(ctx.cwd, ["blame", "--", args.path])
163
- return truncate(out || `(no blame output for ${args.path})`)
164
- }
165
- case "mv": {
166
- if (!args.path || !args.dest) return "Error: mv requires path (source) and dest (destination)"
167
- const r = runGitStrict(ctx.cwd, ["mv", "--", args.path, args.dest])
168
- return r.ok ? truncate(r.out || `Moved ${args.path} → ${args.dest}`) : truncate(`git mv failed: ${r.err || r.out}`)
169
- }
170
- default:
171
- throw new Error(`Unknown ext action: ${args.action}`)
172
- }
173
- }
package/src/tools/git.md DELETED
@@ -1,54 +0,0 @@
1
- Run a git command. Only works inside a git repository.
2
-
3
- **Route to git instead of bash:** `git status`→status, `git log`→log, `git diff`→diff, `git show`→show, `git add`→add, `git rm`→rm, `git commit -m`→commit, `git push <remote> <branch> <tag>`→push, `git tag`→tag, `git branch`→branch, `git checkout`→checkout, `git restore`→restore, `git stash`→stash, `git fetch/pull`→fetch/pull, `git reset`→reset, `git revert`→revert, `git merge`→merge, `git cherry-pick`→cherry-pick, `git ls-remote`→ls-remote, `git clone`→clone, `git init`→init, `git rebase`→rebase, `git remote`→remote, `git clean`→clean, `git switch`→switch, `git apply`→apply, `git worktree`→worktree, `git archive`→archive, `git blame`→blame, `git mv`→mv.
4
-
5
- - action='diff': unified diff — what changed since last commit. staged=true for staged-only; ref=<ref> to compare a commit/branch; path=<dir> to scope.
6
- - action='status': working tree state — staged / unstaged / untracked / conflicts, categorized.
7
- - action='log': recent commits. count (default 10), oneline=true compact, path=<file> for one file's history.
8
- - action='show': a commit's details (--stat). ref=<ref> (default HEAD).
9
- - action='add': stage files — path=<file> (granular) or all changes when path omitted.
10
- - action='commit': commit. message required; path=<file> → `git commit --only <paths>` — commits those files' working-tree content only (other staged batches are NOT mixed in — atomic); without path → add -A + full commit. New (untracked) files aren't committable by path alone — `git add` them first (staging extra is safe: --only still commits only the listed files).
11
- - action='rm': untrack a file/dir (git rm --cached, kept on disk). path required.
12
- - action='push'/'fetch'/'pull': sync with remote. remote=<origin>, ref=<branch or tag> (space-separated for multiple), tags=true for --tags.
13
- - action='tag': manage tags. tagAction=list (optional filter) / create (name, optional ref) / delete (name; snapshots first).
14
- - action='branch': manage branches. branchAction=list / create (name, optional ref) / switch (name) / delete (name; snapshots first).
15
- - action='checkout': switch to ref=<branch/commit>, or restore a file path=<file> (discards its working-tree changes; snapshots first).
16
- - action='restore': restore a file from index/HEAD. path required; staged=true restores the staged copy; snapshots first.
17
- - action='stash': manage the stash. stashAction=list / push (message) / pop (snapshots first).
18
- - action='reset': reset to ref (default HEAD). mode=soft/mixed/hard; hard snapshots the tree first (drops working-tree changes).
19
- - action='revert': revert a commit (safe). ref=<commit> (default HEAD).
20
- - action='merge': merge ref=<branch/commit>; conflicts reported for you to resolve.
21
- - action='cherry-pick': cherry-pick ref=<commit>.
22
- - action='ls-remote': light remote-ref check — which refs a remote has (read-only, network). remote=<origin>, ref=<branch/tag> optional, config for proxy.
23
- - action='clone': clone a repo. remote required (URL or local path); path optional (target dir).
24
- - action='init': init a repo in the current (work)dir.
25
- - action='rebase': rebase onto ref. rebaseAction=start (ref required) / abort / continue(操作前自动快照,checkpointAction=rewind 恢复).
26
- - action='remote': manage remotes. remoteAction=list / add / remove / set-url; remoteUrl for add/set-url.
27
- - action='clean': remove untracked files/dirs. dryRun for -n preview(真删除操作前自动快照,checkpointAction=rewind 恢复).
28
- - action='switch': switch branch. name required; create for -c (new branch).
29
- - action='apply': apply a patch. path required (patch file).
30
- - action='worktree': manage worktrees. worktreeAction=list / add (path, ref) / remove (path).
31
- - action='archive': write a tar of ref (default HEAD). path required (output file).
32
- - action='blame': file blame. path required.
33
- - action='mv': rename/move. path (source) + dest required.
34
- - action='checkpoint': git snapshots. checkpointAction=list/create/rewind/cat/versions; checkpointId required for rewind/cat.
35
- - Destructive ops (checkout -- path / restore / reset --hard / stash pop / branch|tag delete / clean / rebase) auto-snapshot first — restore via checkpointAction=rewind.
36
-
37
- Parameters:
38
- - action (required): diff / status / log / show / checkpoint / add / rm / commit / push / tag / branch / checkout / restore / stash / fetch / pull / reset / revert / merge / cherry-pick / ls-remote / clone / init / rebase / remote / clean / switch / apply / worktree / archive / blame / mv
39
- - workdir: run git in this subdirectory (monorepo / multi-repo). Path relative to cwd — no directory restriction. Default: cwd
40
- - config: (network actions push/fetch/pull/ls-remote/clone) git -c overrides, e.g. ["http.proxy=http://10.2.2.112:3128"] for blocked remotes
41
- - path: (diff/log/add/commit/checkout/restore/rm/apply/archive/blame/mv/worktree) file or directory to scope / stage / restore
42
- - ref: (show/diff/checkout/reset/revert/merge/cherry-pick/tag:create/branch:create/rebase/worktree:add/archive) commit/branch/ref; (push/pull/fetch) the branch or tag (space-separated for multiple)
43
- - name: (branch/tag/switch) the branch or tag name
44
- - remote: (push/fetch/pull/remote/clone) remote name (e.g. origin) or URL; default: current upstream
45
- - tags: (push) also push all tags (--tags)
46
- - staged: (diff) staged changes; (restore) the staged copy
47
- - count: (log) number of commits (default 10)
48
- - oneline: (log) one-line-per-commit
49
- - message: (commit) commit message — required; (stash:push) stash message
50
- - mode: (reset) soft / mixed / hard — hard snapshots the tree first + needs confirmation
51
- - tagAction: (tag) list / create / delete — branchAction: (branch) list / create / delete / switch — stashAction: (stash) push / pop / list
52
- - filter: (read-only actions) keep only output lines matching this regex (case-insensitive)
53
- - checkpointAction: (checkpoint) list / create / rewind / cat / versions — checkpointId: snapshot id (rewind/cat)
54
- - remoteAction: (remote) list / add / remove / set-url — remoteUrl: (remote add/set-url) URL — rebaseAction: (rebase) start / abort / continue — dryRun: (clean) -n preview — create: (switch) -c — dest: (mv) destination — worktreeAction: (worktree) list / add / remove
package/src/tools/git.mjs DELETED
@@ -1,356 +0,0 @@
1
- import {
2
- DESC,
3
- truncate,
4
- runGit
5
- } from "./shared.mjs";
6
- import { execFileSync } from "node:child_process";
7
- import { resolve } from "node:path";
8
- import { filterLines, runGitStrict, validateRef, gitConfigArgs, snapshotBefore, executeExtAction } from "./git-ext.mjs";
9
- import { executeCheckpointAction } from "./git-checkpoint.mjs";
10
-
11
-
12
- /** Run git PRESERVING per-line leading whitespace. runGit trims the WHOLE output, which
13
- * strips a porcelain line's leading " " (the unstaged marker) and misclassifies an
14
- * unstaged-only first line as staged. status uses this so the staged/unstaged column survives. */
15
- function runGitRaw(cwd, cmdArgs, config = []) {
16
- try {
17
- return execFileSync("git", [...config, ...cmdArgs], { cwd, encoding: "utf8", maxBuffer: 10 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }).replace(/\r/g, "").replace(/\n$/, "")
18
- } catch (e) {
19
- return String(e.stdout || "").replace(/\r/g, "")
20
- }
21
- }
22
-
23
-
24
-
25
-
26
- /** Resolve workdir relative to cwd — no boundary assertion
27
- * (§10.1 2026-09-02: workspace confinement removed; git itself is not
28
- * directory-limited — same boundary as bash). */
29
- function resolveBaseDir(cwd, workdir) {
30
- if (!workdir || typeof workdir !== "string") return cwd
31
- return resolve(cwd, workdir)
32
- }
33
-
34
-
35
-
36
- export const gitTool = {
37
- name: "git",
38
- description: DESC("git"),
39
- parameters: {
40
- type: "object",
41
- properties: {
42
- action: { type: "string", enum: ["diff", "status", "log", "show", "checkpoint", "add", "rm", "commit", "push", "tag", "branch", "checkout", "restore", "stash", "fetch", "pull", "reset", "revert", "merge", "cherry-pick", "ls-remote", "clone", "init", "rebase", "remote", "clean", "switch", "apply", "worktree", "archive", "blame", "mv"], description: "diff / status / log / show / checkpoint / add / rm / commit / push / tag / branch / checkout / restore / stash / fetch / pull / reset / revert / merge / cherry-pick / ls-remote / clone / init / rebase / remote / clean / switch / apply / worktree / archive / blame / mv — clean/rebase 操作前自动快照,checkpointAction=rewind 恢复" },
43
- // diff/log params
44
- staged: { type: "boolean", description: "(diff) Show staged changes instead of working tree" },
45
- path: { type: "string", description: "(diff/log/add/commit/checkout/restore/checkpoint:cat/versions/rewind/rm/apply/archive/blame/mv/worktree) File or directory to scope to / stage / restore(checkout/restore 操作前自动快照,checkpointAction=rewind 恢复)— add/rm/commit: space-separated for multiple(git add a b c——2026-09-05 发版痛点;含空格的文件名会被按多路径拆分——请用 bash git 处理)" },
46
- ref: { type: "string", description: "(diff/show/checkout/reset/revert/merge/cherry-pick/tag:create/branch:create/rebase/worktree:add/archive) Commit/branch/ref; (push/pull/fetch) the branch or tag (space-separated for multiple)" },
47
- count: { type: "number", description: "(log) Number of commits (default 10)" },
48
- oneline: { type: "boolean", description: "(log) One-line-per-commit format" },
49
- message: { type: "string", description: "(commit) Commit message — required for commit; (stash:push) stash message" },
50
- filter: { type: "string", description: "Optional: keep only status/diff/log output lines matching this regex (case-insensitive)" },
51
- // write-op params
52
- name: { type: "string", description: "(branch/tag) The branch or tag name (create/delete/switch)" },
53
- remote: { type: "string", description: "(push/fetch/pull) Remote name (e.g. origin). Default: current upstream" },
54
- workdir: { type: "string", description: "Run git in this subdirectory (monorepo / multi-repo). Path relative to cwd — no directory restriction. Default: cwd" },
55
- config: { type: "array", items: { type: "string" }, description: "(network actions: push/fetch/pull/ls-remote) git -c overrides, e.g. [\"http.proxy=http://10.2.2.112:3128\"] for blocked remotes" },
56
- tags: { type: "boolean", description: "(push) Also push all tags (--tags)" },
57
- mode: { type: "string", enum: ["soft", "mixed", "hard"], description: "(reset) reset mode — hard snapshots the tree first + needs confirmation(操作前自动快照,checkpointAction=rewind 恢复)" },
58
- tagAction: { type: "string", enum: ["list", "create", "delete"], description: "(tag) list tags / create one / delete one(delete 操作前自动快照,checkpointAction=rewind 恢复)" },
59
- branchAction: { type: "string", enum: ["list", "create", "delete", "switch"], description: "(branch) list branches / create / delete / switch to one(delete 操作前自动快照,checkpointAction=rewind 恢复)" },
60
- stashAction: { type: "string", enum: ["push", "pop", "list"], description: "(stash) push (stash now) / pop (apply+drop) / list(pop 操作前自动快照,checkpointAction=rewind 恢复)" },
61
- // checkpoint params
62
- checkpointAction: { type: "string", enum: ["list", "create", "rewind", "cat", "versions"], description: "(checkpoint) list snapshots / create one / restore by id / read file from snapshot / list a file's historical versions(rewind 可恢复操作前状态,恢复前自动快照可逆)" },
63
- checkpointId: { type: "string", description: "(checkpoint) Snapshot id — required for rewind and cat; optional for list (shows file tree)" },
64
- // F7 new-action params
65
- remoteAction: { type: "string", enum: ["list", "add", "remove", "set-url"], description: "(remote) list remotes / add / remove / set-url" },
66
- remoteUrl: { type: "string", description: "(remote add/set-url) Remote URL (https/git/ssh or local path)" },
67
- rebaseAction: { type: "string", enum: ["start", "abort", "continue"], description: "(rebase) start (ref required) / abort / continue(操作前自动快照,checkpointAction=rewind 恢复)" },
68
- dryRun: { type: "boolean", description: "(clean) preview only (-n) — no deletion, no snapshot; real clean 操作前自动快照,checkpointAction=rewind 恢复" },
69
- create: { type: "boolean", description: "(switch) create the branch then switch (-c)" },
70
- dest: { type: "string", description: "(mv) destination path (file or directory)" },
71
- worktreeAction: { type: "string", enum: ["list", "add", "remove"], description: "(worktree) list / add (path, ref) / remove (path)" },
72
- },
73
- required: ["action"],
74
- },
75
- readonly: false,
76
- async execute(args, ctx) {
77
- // workdir: run git in a subdirectory (monorepo / multi-repo). Shadow ctx.cwd so
78
- // every action + snapshotBefore + checkpoint resolves against the workdir.
79
- if (args.workdir) ctx = { ...ctx, cwd: resolveBaseDir(ctx.cwd, args.workdir) }
80
- // git -c overrides (proxy etc.) — only network actions need them; passing to every
81
- // action would be harmless but noisy. cfgArgs stays [] for local ops.
82
- const cfgArgs = gitConfigArgs(args.config)
83
- switch (args.action) {
84
- case "diff": {
85
- const ref = args.ref ?? "HEAD"
86
- if (!/^[A-Za-z0-9._/~^@][A-Za-z0-9._/~^@{}-]*$/.test(ref)) throw new Error(`Invalid git ref: ${ref}`)
87
- const flags = args.staged ? ["--staged"] : []
88
- const paths = args.path ? [args.path] : []
89
- const out = runGit(ctx.cwd, ["diff", ...flags, ref, "--", ...paths])
90
- return truncate(filterLines(out || "(no changes)", args.filter))
91
- }
92
- case "status": {
93
- // Preserve per-line leading whitespace — porcelain " M"/"M " staged/unstaged markers are
94
- // significant (runGit trims the whole output's leading space, corrupting an unstaged-first-line).
95
- const porcelain = runGitRaw(ctx.cwd, ["status", "--porcelain"])
96
- if (!porcelain) return "(clean — no changes)"
97
-
98
- const staged = []
99
- const unstaged = []
100
- const untracked = []
101
- const conflicts = []
102
- for (const line of porcelain.split("\n")) {
103
- if (!line) continue
104
- const clean = line.replace(/\r/g, "")
105
- const m = clean.match(/^(..?)\s+(.+)$/)
106
- if (!m) continue
107
- const [, status, rawFile] = m
108
- const file = status.includes("R") && rawFile.includes(" -> ") ? rawFile.replace(" -> ", " → ") : rawFile
109
- const idx = status[0] ?? " "
110
- const wt = status[1] ?? " "
111
- if (idx === "U" || wt === "U" || (idx === "A" && wt === "A")) {
112
- conflicts.push(file)
113
- } else if (idx === "?" && wt === "?") {
114
- untracked.push(file)
115
- } else {
116
- if (idx !== " " && idx !== "?") staged.push(idx + " " + file)
117
- if (wt !== " " && wt !== "?") unstaged.push(wt + " " + file)
118
- }
119
- }
120
- const parts = []
121
- if (staged.length) parts.push("Staged (" + staged.length + "):\n" + staged.join("\n"))
122
- if (unstaged.length) parts.push("Unstaged (" + unstaged.length + "):\n" + unstaged.join("\n"))
123
- if (untracked.length) parts.push("Untracked (" + untracked.length + "):\n" + untracked.join("\n"))
124
- if (conflicts.length) parts.push("Conflicts (" + conflicts.length + "):\n" + conflicts.join("\n"))
125
- return truncate(filterLines(parts.join("\n\n"), args.filter))
126
- }
127
- case "log": {
128
- const parsed = Number.parseInt(args.count, 10)
129
- const n = Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 200) : 10
130
- const isOneline = args.oneline
131
- const cmdArgs = isOneline
132
- ? ["log", "-" + n, "--oneline"]
133
- : ["log", "-" + n, "--format=%h %ad %an %s", "--date=short"]
134
- if (args.path) cmdArgs.push("--", args.path)
135
- const out = runGit(ctx.cwd, cmdArgs)
136
- return truncate(filterLines(out || "(no commits)", args.filter))
137
- }
138
- case "show": {
139
- const ref = args.ref ?? "HEAD"
140
- if (!/^[A-Za-z0-9._/~^@][A-Za-z0-9._/~^@{}-]*$/.test(ref)) throw new Error(`Invalid git ref: ${ref}`)
141
- const out = runGit(ctx.cwd, ["show", "--stat", ref])
142
- return truncate(out || "(no such commit)")
143
- }
144
- case "rm": {
145
- if (!args.path) return "Error: rm requires path (the file/directory to untrack, relative to repo root)"
146
- const paths = args.path.split(/\s+/).filter(Boolean)
147
- const r = runGitStrict(ctx.cwd, ["rm", "--cached", "-r", "--", ...paths])
148
- return r.ok ? truncate(r.out || `Untracked ${paths.join(" ")} (kept on disk)`) : truncate(`git rm failed: ${r.err || r.out}`)
149
- }
150
- case "commit": {
151
- if (!args.message) return "Error: commit requires message"
152
- // F-3 (QUICKFIX-BATCH-2):path 给定(含空串——空/空白 trim 后空一律明确错误,绝不
153
- // 回落 add -A)→ git commit --only <paths>——从工作树取列文件提交(忽略索引他批——
154
- // 原子——不再先 add)。空格分隔唯一形态(含空格文件名不支持)。无 path(缺省)→ add
155
- // -A 全量(单代理语义保留)。
156
- const parts = []
157
- let commit
158
- if (args.path !== undefined && args.path !== null) {
159
- const trimmed = args.path.trim()
160
- if (!trimmed) return "Error: commit path is empty/whitespace — give at least one file path (space-separated)"
161
- commit = runGitStrict(ctx.cwd, ["commit", "--only", "-m", args.message, "--", ...trimmed.split(/\s+/)])
162
- } else {
163
- const add = runGitStrict(ctx.cwd, ["add", "-A"])
164
- if (!add.ok) return truncate(`git add failed: ${add.err || add.out || "(no output)"}`)
165
- if (add.out) parts.push(add.out)
166
- commit = runGitStrict(ctx.cwd, ["commit", "-m", args.message])
167
- }
168
- if (commit.ok) {
169
- if (commit.out) parts.push(commit.out)
170
- // F6: commit = new safety baseline — clear this project's checkpoints
171
- // (best-effort per NF7: a failed cleanup never blocks the commit result).
172
- try {
173
- const { deleteCheckpointsForCwd } = await import("../git/checkpoint.mjs")
174
- await deleteCheckpointsForCwd(ctx.cwd)
175
- parts.push("(checkpoints cleared — commit is a new safety baseline)")
176
- } catch (e) {
177
- parts.push(`(checkpoint cleanup skipped: ${e.message})`)
178
- }
179
- }
180
- else parts.push(`git commit failed: ${commit.err || "(no output)"}`)
181
- return truncate(parts.join("\n") || "(commit produced no output)")
182
- }
183
- case "push": {
184
- const cmdArgs = ["push"]
185
- if (args.remote) cmdArgs.push(validateRef(args.remote, "remote"))
186
- if (args.ref) for (const r of args.ref.split(/\s+/).filter(Boolean)) cmdArgs.push(validateRef(r, "ref"))
187
- if (args.tags) cmdArgs.push("--tags")
188
- const r = runGitStrict(ctx.cwd, cmdArgs, cfgArgs)
189
- return r.ok ? truncate(r.out || "(push complete — no output)") : truncate(`git push failed: ${r.err || r.out || "(no output)"}`)
190
- }
191
- case "ls-remote": {
192
- // Lightweight remote-ref check (which refs a remote has) — network action,
193
- // read-only, no snapshot. Config plumbing for blocked/gated remotes.
194
- const cmdArgs = ["ls-remote"]
195
- if (args.remote) cmdArgs.push(validateRef(args.remote, "remote"))
196
- if (args.ref) for (const r of args.ref.split(/\s+/).filter(Boolean)) cmdArgs.push(validateRef(r, "ref"))
197
- const out = runGit(ctx.cwd, cmdArgs, cfgArgs)
198
- if (!out) return "(no refs / remote unreachable)"
199
- return truncate(filterLines(out, args.filter))
200
- }
201
- case "add": {
202
- // Granular staging: stage `path` when given, else all changes (add -A).
203
- // 多路径:空格分隔(ref 先例 L175——2026-09-05 发版痛点——git add 单路径被迫 N 次调用)
204
- const paths = args.path ? args.path.split(/\s+/).filter(Boolean) : null
205
- const cmdArgs = paths?.length ? ["add", "--", ...paths] : ["add", "-A"]
206
- const r = runGitStrict(ctx.cwd, cmdArgs)
207
- return r.ok ? truncate(r.out || `Staged ${paths?.join(" ") || "all changes"}`) : truncate(`git add failed: ${r.err || r.out}`)
208
- }
209
- case "tag": {
210
- const sub = args.tagAction
211
- if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["tag", "-l"]) || "(no tags)", args.filter))
212
- if (sub === "create") {
213
- if (!args.name) return "Error: tag create requires name"
214
- validateRef(args.name, "tag")
215
- const cmdArgs = ["tag", args.name]
216
- if (args.ref) cmdArgs.push(validateRef(args.ref))
217
- const r = runGitStrict(ctx.cwd, cmdArgs)
218
- return r.ok ? `Tag ${args.name} created` : truncate(`git tag failed: ${r.err || r.out}`)
219
- }
220
- if (sub === "delete") {
221
- if (!args.name) return "Error: tag delete requires name"
222
- validateRef(args.name, "tag")
223
- const snap = await snapshotBefore(ctx, `tag delete ${args.name}`)
224
- const r = runGitStrict(ctx.cwd, ["tag", "-d", args.name])
225
- return r.ok ? truncate(snap + `Tag ${args.name} deleted`) : truncate(`git tag -d failed: ${r.err || r.out}`)
226
- }
227
- return "Error: tag requires tagAction — use: list | create | delete"
228
- }
229
- case "branch": {
230
- const sub = args.branchAction
231
- if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["branch", "--all", "-vv"]) || "(no branches)", args.filter))
232
- if (sub === "create") {
233
- if (!args.name) return "Error: branch create requires name"
234
- validateRef(args.name, "branch")
235
- const cmdArgs = ["branch", args.name]
236
- if (args.ref) cmdArgs.push(validateRef(args.ref))
237
- const r = runGitStrict(ctx.cwd, cmdArgs)
238
- return r.ok ? `Branch ${args.name} created` : truncate(`git branch failed: ${r.err || r.out}`)
239
- }
240
- if (sub === "switch") {
241
- if (!args.name) return "Error: branch switch requires name"
242
- validateRef(args.name, "branch")
243
- const r = runGitStrict(ctx.cwd, ["checkout", args.name])
244
- return r.ok ? `Switched to branch ${args.name}` : truncate(`git checkout ${args.name} failed: ${r.err || r.out}`)
245
- }
246
- if (sub === "delete") {
247
- if (!args.name) return "Error: branch delete requires name"
248
- validateRef(args.name, "branch")
249
- const snap = await snapshotBefore(ctx, `branch delete ${args.name}`)
250
- const r = runGitStrict(ctx.cwd, ["branch", "-d", args.name])
251
- return r.ok ? truncate(snap + `Branch ${args.name} deleted`) : truncate(`git branch -d failed: ${r.err || r.out}`)
252
- }
253
- return "Error: branch requires branchAction — use: list | create | delete | switch"
254
- }
255
- case "checkout": {
256
- if (args.path) {
257
- // Restore file from index (discards working-tree changes to it) — destructive: snapshot first.
258
- const snap = await snapshotBefore(ctx, `checkout -- ${args.path}`)
259
- const r = runGitStrict(ctx.cwd, ["checkout", "--", args.path])
260
- return r.ok ? truncate(snap + `Restored ${args.path}`) : truncate(`git checkout -- ${args.path} failed: ${r.err || r.out}`)
261
- }
262
- if (args.ref) {
263
- validateRef(args.ref, "ref")
264
- const r = runGitStrict(ctx.cwd, ["checkout", args.ref])
265
- return r.ok ? truncate(r.out || `Checked out ${args.ref}`) : truncate(`git checkout ${args.ref} failed: ${r.err || r.out}`)
266
- }
267
- return "Error: checkout requires ref (branch/commit) or path (file to restore)"
268
- }
269
- case "restore": {
270
- if (!args.path) return "Error: restore requires path"
271
- const snap = await snapshotBefore(ctx, `restore ${args.path}`)
272
- const cmdArgs = ["restore"]
273
- if (args.staged) cmdArgs.push("--staged")
274
- cmdArgs.push("--", args.path)
275
- const r = runGitStrict(ctx.cwd, cmdArgs)
276
- return r.ok ? truncate(snap + `Restored ${args.path}`) : truncate(`git restore failed: ${r.err || r.out}`)
277
- }
278
- case "stash": {
279
- const sub = args.stashAction
280
- if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["stash", "list"]) || "(no stashes)", args.filter))
281
- if (sub === "push") {
282
- const cmdArgs = ["stash", "push"]
283
- if (args.message) cmdArgs.push("-m", args.message)
284
- const r = runGitStrict(ctx.cwd, cmdArgs)
285
- return r.ok ? truncate(r.out || "Stashed") : truncate(`git stash push failed: ${r.err || r.out}`)
286
- }
287
- if (sub === "pop") {
288
- const snap = await snapshotBefore(ctx, "stash pop")
289
- const r = runGitStrict(ctx.cwd, ["stash", "pop"])
290
- return r.ok ? truncate(snap + (r.out || "Popped")) : truncate(`git stash pop failed: ${r.err || r.out}`)
291
- }
292
- return "Error: stash requires stashAction — use: push | pop | list"
293
- }
294
- case "fetch": {
295
- const cmdArgs = ["fetch"]
296
- if (args.remote) cmdArgs.push(validateRef(args.remote, "remote"))
297
- if (args.ref) cmdArgs.push(validateRef(args.ref, "ref"))
298
- const r = runGitStrict(ctx.cwd, cmdArgs, cfgArgs)
299
- return r.ok ? truncate(r.out || "(fetch complete — no output)") : truncate(`git fetch failed: ${r.err || r.out}`)
300
- }
301
- case "pull": {
302
- const cmdArgs = ["pull"]
303
- if (args.remote) cmdArgs.push(validateRef(args.remote, "remote"))
304
- if (args.ref) cmdArgs.push(validateRef(args.ref, "ref"))
305
- const r = runGitStrict(ctx.cwd, cmdArgs, cfgArgs)
306
- return r.ok ? truncate(r.out || "(pull complete — no output)") : truncate(`git pull failed: ${r.err || r.out}`)
307
- }
308
- case "reset": {
309
- const mode = args.mode ?? "mixed"
310
- if (!["soft", "mixed", "hard"].includes(mode)) return "Error: reset mode must be soft | mixed | hard"
311
- let snap = ""
312
- if (mode === "hard") snap = await snapshotBefore(ctx, "reset --hard") // destructive: drops working-tree changes
313
- const cmdArgs = ["reset", `--${mode}`]
314
- if (args.ref) cmdArgs.push(validateRef(args.ref))
315
- const r = runGitStrict(ctx.cwd, cmdArgs)
316
- return r.ok ? truncate(snap + (r.out || `Reset (${mode}) complete`)) : truncate(`git reset failed: ${r.err || r.out}`)
317
- }
318
- case "revert": {
319
- const ref = validateRef(args.ref ?? "HEAD")
320
- const r = runGitStrict(ctx.cwd, ["revert", "--no-edit", ref])
321
- return r.ok ? truncate(r.out || `Reverted ${ref}`) : truncate(`git revert failed: ${r.err || r.out}`)
322
- }
323
- case "merge": {
324
- if (!args.ref) return "Error: merge requires ref (branch/commit to merge)"
325
- validateRef(args.ref, "ref")
326
- const r = runGitStrict(ctx.cwd, ["merge", "--no-edit", args.ref])
327
- return r.ok ? truncate(r.out || `Merged ${args.ref}`) : truncate(`git merge failed: ${r.err || r.out} — resolve conflicts, then commit`)
328
- }
329
- case "cherry-pick": {
330
- if (!args.ref) return "Error: cherry-pick requires ref (commit)"
331
- validateRef(args.ref, "ref")
332
- const r = runGitStrict(ctx.cwd, ["cherry-pick", args.ref])
333
- return r.ok ? truncate(r.out || `Cherry-picked ${args.ref}`) : truncate(`git cherry-pick failed: ${r.err || r.out}`)
334
- }
335
- // F7 扩展 action + checkpoint:实现拆在 git-ext.mjs / git-checkpoint.mjs(500 行硬限)
336
- case "clone":
337
- case "init":
338
- case "rebase":
339
- case "remote":
340
- case "clean":
341
- case "switch":
342
- case "apply":
343
- case "worktree":
344
- case "archive":
345
- case "blame":
346
- case "mv":
347
- return executeExtAction(args, ctx)
348
- case "checkpoint":
349
- return executeCheckpointAction(args, ctx)
350
-
351
- default:
352
- return `Unknown action '${args.action}'. Use: diff | status | log | show | checkpoint | add | rm | commit | push | tag | branch | checkout | restore | stash | fetch | pull | reset | revert | merge | cherry-pick | ls-remote | clone | init | rebase | remote | clean | switch | apply | worktree | archive | blame | mv`
353
- }
354
- },
355
- }
356
-