@thincoder/core 0.9.1
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/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
|
@@ -0,0 +1,173 @@
|
|
|
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/tools/git.mjs
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DESC,
|
|
3
|
+
truncate,
|
|
4
|
+
runGit,
|
|
5
|
+
gitFailureMessage
|
|
6
|
+
} from "./shared.mjs";
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { filterLines, runGitStrict, validateRef, gitConfigArgs, snapshotBefore, executeExtAction } from "./git-ext.mjs";
|
|
10
|
+
import { executeCheckpointAction } from "./git-checkpoint.mjs";
|
|
11
|
+
import { discoverRepos, MANIFEST_REL } from "../manifest.mjs";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/** Run git PRESERVING per-line leading whitespace. runGit trims the WHOLE output, which
|
|
15
|
+
* strips a porcelain line's leading " " (the unstaged marker) and misclassifies an
|
|
16
|
+
* unstaged-only first line as staged. status uses this so the staged/unstaged column survives.
|
|
17
|
+
* #55 fail-closed:失败不再吞成 ""(曾把非仓 / 任意失败渲染成 `(clean — no changes)`)——
|
|
18
|
+
* 溢出保留部分输出;其余 ⇒ throw(消息契约 = `shared.mjs` `gitFailureMessage`)。 */
|
|
19
|
+
function runGitRaw(cwd, cmdArgs, config = []) {
|
|
20
|
+
try {
|
|
21
|
+
return execFileSync("git", [...config, ...cmdArgs], { cwd, encoding: "utf8", maxBuffer: 10 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }).replace(/\r/g, "").replace(/\n$/, "")
|
|
22
|
+
} catch (e) {
|
|
23
|
+
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" && e.stdout) return String(e.stdout).replace(/\r/g, "")
|
|
24
|
+
throw new Error(gitFailureMessage(e, cmdArgs, cwd))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/** Resolve workdir relative to cwd — no boundary assertion
|
|
32
|
+
* (§10.1 2026-09-02: workspace confinement removed; git itself is not
|
|
33
|
+
* directory-limited — same boundary as bash). */
|
|
34
|
+
function resolveBaseDir(cwd, workdir) {
|
|
35
|
+
if (!workdir || typeof workdir !== "string") return cwd
|
|
36
|
+
return resolve(cwd, workdir)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── 仓发现(§6.13 · #62——单源 = `manifest.mjs` `discoverRepos`,禁第二份实现)───────────────
|
|
40
|
+
/** 动作面例外:`init` / `clone` 以 **cwd 为落点**——不做发现(`init` 在无仓处天然合法;`clone` 的
|
|
41
|
+
* 落点 = cwd 相对路径——判据同 `init`),否则「在此处建仓 / 克隆」被静默搬进别仓;
|
|
42
|
+
* 二者仍可经显式 `workdir` 指落点。 */
|
|
43
|
+
const NO_REPO_DISCOVERY = new Set(["init", "clone"])
|
|
44
|
+
|
|
45
|
+
/** 歧义(多态)消息:候选全列(绝对路径、按名排序)+ 指引显式 `workdir`——不猜(§6.13 解析序行 5)。 */
|
|
46
|
+
function ambiguousRepoMessage(candidates, cwd) {
|
|
47
|
+
return `Ambiguous git repo at ${cwd}: ${candidates.length} subdirectories carry .git and ${MANIFEST_REL} — ` +
|
|
48
|
+
`pass workdir to run git in the intended one (no silent pick):\n` + candidates.map((c) => `- ${c}`).join("\n")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── 审批门注入缝(#59——「只读判定 / 审批门按端注入」,形态参 §2.13.5 注入缝)────────
|
|
52
|
+
/**
|
|
53
|
+
* 审批门注入位(**缺省不覆盖** = 无审批面:全部动作照常执行——CLI 语义,零行为变;端装配层
|
|
54
|
+
* 可覆盖为本端审批 / 只读判定。核内零端名分支(契约 5——本档只认 `gate` 函数名)。
|
|
55
|
+
* 契约:`gate(args, ctx) → string | null | Promise<同>`——非空字符串 = 拒执行(该串原样作为
|
|
56
|
+
* 工具结果返回);null / undefined / 其他值 = 放行。每个 execute 恰问一次(workdir 归一后)。
|
|
57
|
+
*/
|
|
58
|
+
let injectedApproval = null
|
|
59
|
+
export function configureGitApproval(impl) {
|
|
60
|
+
injectedApproval = impl && typeof impl.gate === "function" ? impl.gate : null
|
|
61
|
+
}
|
|
62
|
+
/** 撤销注入(测试与端装配生命周期用——缺省态 = 无审批门)。 */
|
|
63
|
+
export function resetGitApproval() { injectedApproval = null }
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export const gitTool = {
|
|
68
|
+
name: "git",
|
|
69
|
+
description: DESC("git"),
|
|
70
|
+
parameters: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
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 恢复" },
|
|
74
|
+
// diff/log params
|
|
75
|
+
staged: { type: "boolean", description: "(diff) Show staged changes instead of working tree" },
|
|
76
|
+
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 处理)" },
|
|
77
|
+
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)" },
|
|
78
|
+
count: { type: "number", description: "(log) Number of commits (default 10)" },
|
|
79
|
+
oneline: { type: "boolean", description: "(log) One-line-per-commit format" },
|
|
80
|
+
message: { type: "string", description: "(commit) Commit message — required for commit; (stash:push) stash message" },
|
|
81
|
+
filter: { type: "string", description: "Optional: keep only status/diff/log output lines matching this regex (case-insensitive)" },
|
|
82
|
+
// write-op params
|
|
83
|
+
name: { type: "string", description: "(branch/tag) The branch or tag name (create/delete/switch)" },
|
|
84
|
+
remote: { type: "string", description: "(push/fetch/pull) Remote name (e.g. origin). Default: current upstream" },
|
|
85
|
+
workdir: { type: "string", description: "Run git in this subdirectory (monorepo / multi-repo). Path relative to cwd — no directory restriction. Default: the discovered project repo root(缺省 = 发现的项目仓根;显式 workdir 优先)" },
|
|
86
|
+
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" },
|
|
87
|
+
tags: { type: "boolean", description: "(push) Also push all tags (--tags)" },
|
|
88
|
+
mode: { type: "string", enum: ["soft", "mixed", "hard"], description: "(reset) reset mode — hard snapshots the tree first + needs confirmation(操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
89
|
+
tagAction: { type: "string", enum: ["list", "create", "delete"], description: "(tag) list tags / create one / delete one(delete 操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
90
|
+
branchAction: { type: "string", enum: ["list", "create", "delete", "switch"], description: "(branch) list branches / create / delete / switch to one(delete 操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
91
|
+
stashAction: { type: "string", enum: ["push", "pop", "list"], description: "(stash) push (stash now) / pop (apply+drop) / list(pop 操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
92
|
+
// checkpoint params
|
|
93
|
+
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 可恢复操作前状态,恢复前自动快照可逆)" },
|
|
94
|
+
checkpointId: { type: "string", description: "(checkpoint) Snapshot id — required for rewind and cat; optional for list (shows file tree)" },
|
|
95
|
+
// F7 new-action params
|
|
96
|
+
remoteAction: { type: "string", enum: ["list", "add", "remove", "set-url"], description: "(remote) list remotes / add / remove / set-url" },
|
|
97
|
+
remoteUrl: { type: "string", description: "(remote add/set-url) Remote URL (https/git/ssh or local path)" },
|
|
98
|
+
rebaseAction: { type: "string", enum: ["start", "abort", "continue"], description: "(rebase) start (ref required) / abort / continue(操作前自动快照,checkpointAction=rewind 恢复)" },
|
|
99
|
+
dryRun: { type: "boolean", description: "(clean) preview only (-n) — no deletion, no snapshot; real clean 操作前自动快照,checkpointAction=rewind 恢复" },
|
|
100
|
+
create: { type: "boolean", description: "(switch) create the branch then switch (-c)" },
|
|
101
|
+
dest: { type: "string", description: "(mv) destination path (file or directory)" },
|
|
102
|
+
worktreeAction: { type: "string", enum: ["list", "add", "remove"], description: "(worktree) list / add (path, ref) / remove (path)" },
|
|
103
|
+
},
|
|
104
|
+
required: ["action"],
|
|
105
|
+
},
|
|
106
|
+
readonly: false,
|
|
107
|
+
async execute(args, ctx) {
|
|
108
|
+
// workdir: run git in a subdirectory (monorepo / multi-repo). Shadow ctx.cwd so
|
|
109
|
+
// every action + snapshotBefore + checkpoint resolves against the workdir.
|
|
110
|
+
if (args.workdir) ctx = { ...ctx, cwd: resolveBaseDir(ctx.cwd, args.workdir) }
|
|
111
|
+
// 仓发现(§6.13 · #62):缺省路径(无 workdir)⇒ 单源 = `discoverRepos`——工作区根(非仓)⇒
|
|
112
|
+
// 唯一带 manifest 子仓:重定向 `ctx.cwd` + 结果首行注记;零 ⇒ 原值落 §6.12 fail-closed(零
|
|
113
|
+
// 行为变);多 ⇒ throw(列候选 + 指 workdir,不猜)。判据 = workdir 真值在场(与上行同源——
|
|
114
|
+
// 空串与缺省同判);`init` / `clone` 例外不做发现。self / none 态 ctx 对象引用透传(不 clone)。
|
|
115
|
+
let repoNote = ""
|
|
116
|
+
if (!args.workdir && !NO_REPO_DISCOVERY.has(args.action)) {
|
|
117
|
+
const discovered = discoverRepos(ctx.cwd)
|
|
118
|
+
if (discovered.kind === "ambiguous") throw new Error(ambiguousRepoMessage(discovered.candidates, ctx.cwd))
|
|
119
|
+
if (discovered.kind === "unique") {
|
|
120
|
+
ctx = { ...ctx, cwd: discovered.root }
|
|
121
|
+
repoNote = `(repo: ${discovered.root})`
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// #59:审批门(端注入面)——缺省无门(CLI 语义);非空字符串返回 ⇒ 拒执行(拒串原样、不加注记)。
|
|
125
|
+
if (injectedApproval) {
|
|
126
|
+
const refusal = await injectedApproval(args, ctx)
|
|
127
|
+
if (typeof refusal === "string" && refusal.length > 0) return refusal
|
|
128
|
+
}
|
|
129
|
+
const out = await gitActionCore.dispatch(args, ctx)
|
|
130
|
+
return repoNote ? `${repoNote}\n${out}` : out
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** git 动作执行体(本批自 `execute()` 抽出——§6.13 落位表行 2:两函数均 <300 行;switch 体原样搬、
|
|
135
|
+
* **不用 `this`**(端装配面 `{...coreGitTool}` 展开装饰,`this` 绑定不可依赖))。ctx 已经过 workdir
|
|
136
|
+
* 归一与仓发现(`execute()` 头部)——本函数只做动作派发,零发现语义。 */
|
|
137
|
+
const gitActionCore = {
|
|
138
|
+
async dispatch(args, ctx) {
|
|
139
|
+
// git -c overrides (proxy etc.) — only network actions need them; passing to every
|
|
140
|
+
// action would be harmless but noisy. cfgArgs stays [] for local ops.
|
|
141
|
+
const cfgArgs = gitConfigArgs(args.config)
|
|
142
|
+
switch (args.action) {
|
|
143
|
+
case "diff": {
|
|
144
|
+
const ref = args.ref ?? "HEAD"
|
|
145
|
+
if (!/^[A-Za-z0-9._/~^@][A-Za-z0-9._/~^@{}-]*$/.test(ref)) throw new Error(`Invalid git ref: ${ref}`)
|
|
146
|
+
const flags = args.staged ? ["--staged"] : []
|
|
147
|
+
const paths = args.path ? [args.path] : []
|
|
148
|
+
const out = runGit(ctx.cwd, ["diff", ...flags, ref, "--", ...paths])
|
|
149
|
+
return truncate(filterLines(out || "(no changes)", args.filter))
|
|
150
|
+
}
|
|
151
|
+
case "status": {
|
|
152
|
+
// Preserve per-line leading whitespace — porcelain " M"/"M " staged/unstaged markers are
|
|
153
|
+
// significant (runGit trims the whole output's leading space, corrupting an unstaged-first-line).
|
|
154
|
+
const porcelain = runGitRaw(ctx.cwd, ["status", "--porcelain"])
|
|
155
|
+
if (!porcelain) return "(clean — no changes)"
|
|
156
|
+
|
|
157
|
+
const staged = []
|
|
158
|
+
const unstaged = []
|
|
159
|
+
const untracked = []
|
|
160
|
+
const conflicts = []
|
|
161
|
+
for (const line of porcelain.split("\n")) {
|
|
162
|
+
if (!line) continue
|
|
163
|
+
const clean = line.replace(/\r/g, "")
|
|
164
|
+
const m = clean.match(/^(..?)\s+(.+)$/)
|
|
165
|
+
if (!m) continue
|
|
166
|
+
const [, status, rawFile] = m
|
|
167
|
+
const file = status.includes("R") && rawFile.includes(" -> ") ? rawFile.replace(" -> ", " → ") : rawFile
|
|
168
|
+
const idx = status[0] ?? " "
|
|
169
|
+
const wt = status[1] ?? " "
|
|
170
|
+
if (idx === "U" || wt === "U" || (idx === "A" && wt === "A")) {
|
|
171
|
+
conflicts.push(file)
|
|
172
|
+
} else if (idx === "?" && wt === "?") {
|
|
173
|
+
untracked.push(file)
|
|
174
|
+
} else {
|
|
175
|
+
if (idx !== " " && idx !== "?") staged.push(idx + " " + file)
|
|
176
|
+
if (wt !== " " && wt !== "?") unstaged.push(wt + " " + file)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const parts = []
|
|
180
|
+
if (staged.length) parts.push("Staged (" + staged.length + "):\n" + staged.join("\n"))
|
|
181
|
+
if (unstaged.length) parts.push("Unstaged (" + unstaged.length + "):\n" + unstaged.join("\n"))
|
|
182
|
+
if (untracked.length) parts.push("Untracked (" + untracked.length + "):\n" + untracked.join("\n"))
|
|
183
|
+
if (conflicts.length) parts.push("Conflicts (" + conflicts.length + "):\n" + conflicts.join("\n"))
|
|
184
|
+
return truncate(filterLines(parts.join("\n\n"), args.filter))
|
|
185
|
+
}
|
|
186
|
+
case "log": {
|
|
187
|
+
const parsed = Number.parseInt(args.count, 10)
|
|
188
|
+
const n = Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 200) : 10
|
|
189
|
+
const isOneline = args.oneline
|
|
190
|
+
const cmdArgs = isOneline
|
|
191
|
+
? ["log", "-" + n, "--oneline"]
|
|
192
|
+
: ["log", "-" + n, "--format=%h %ad %an %s", "--date=short"]
|
|
193
|
+
if (args.path) cmdArgs.push("--", args.path)
|
|
194
|
+
const out = runGit(ctx.cwd, cmdArgs)
|
|
195
|
+
return truncate(filterLines(out || "(no commits)", args.filter))
|
|
196
|
+
}
|
|
197
|
+
case "show": {
|
|
198
|
+
const ref = args.ref ?? "HEAD"
|
|
199
|
+
if (!/^[A-Za-z0-9._/~^@][A-Za-z0-9._/~^@{}-]*$/.test(ref)) throw new Error(`Invalid git ref: ${ref}`)
|
|
200
|
+
const out = runGit(ctx.cwd, ["show", "--stat", ref])
|
|
201
|
+
return truncate(out || "(no such commit)")
|
|
202
|
+
}
|
|
203
|
+
case "rm": {
|
|
204
|
+
if (!args.path) return "Error: rm requires path (the file/directory to untrack, relative to repo root)"
|
|
205
|
+
const paths = args.path.split(/\s+/).filter(Boolean)
|
|
206
|
+
const r = runGitStrict(ctx.cwd, ["rm", "--cached", "-r", "--", ...paths])
|
|
207
|
+
return r.ok ? truncate(r.out || `Untracked ${paths.join(" ")} (kept on disk)`) : truncate(`git rm failed: ${r.err || r.out}`)
|
|
208
|
+
}
|
|
209
|
+
case "commit": {
|
|
210
|
+
if (!args.message) return "Error: commit requires message"
|
|
211
|
+
// F-3 (QUICKFIX-BATCH-2):path 给定(含空串——空/空白 trim 后空一律明确错误,绝不
|
|
212
|
+
// 回落 add -A)→ git commit --only <paths>——从工作树取列文件提交(忽略索引他批——
|
|
213
|
+
// 原子——不再先 add)。空格分隔唯一形态(含空格文件名不支持)。无 path(缺省)→ add
|
|
214
|
+
// -A 全量(单代理语义保留)。
|
|
215
|
+
const parts = []
|
|
216
|
+
let commit
|
|
217
|
+
if (args.path !== undefined && args.path !== null) {
|
|
218
|
+
const trimmed = args.path.trim()
|
|
219
|
+
if (!trimmed) return "Error: commit path is empty/whitespace — give at least one file path (space-separated)"
|
|
220
|
+
commit = runGitStrict(ctx.cwd, ["commit", "--only", "-m", args.message, "--", ...trimmed.split(/\s+/)])
|
|
221
|
+
} else {
|
|
222
|
+
const add = runGitStrict(ctx.cwd, ["add", "-A"])
|
|
223
|
+
if (!add.ok) return truncate(`git add failed: ${add.err || add.out || "(no output)"}`)
|
|
224
|
+
if (add.out) parts.push(add.out)
|
|
225
|
+
commit = runGitStrict(ctx.cwd, ["commit", "-m", args.message])
|
|
226
|
+
}
|
|
227
|
+
if (commit.ok) {
|
|
228
|
+
if (commit.out) parts.push(commit.out)
|
|
229
|
+
// F6: commit = new safety baseline — clear this project's checkpoints
|
|
230
|
+
// (best-effort per NF7: a failed cleanup never blocks the commit result).
|
|
231
|
+
try {
|
|
232
|
+
const { deleteCheckpointsForCwd } = await import("../git/checkpoint.mjs")
|
|
233
|
+
await deleteCheckpointsForCwd(ctx.cwd)
|
|
234
|
+
parts.push("(checkpoints cleared — commit is a new safety baseline)")
|
|
235
|
+
} catch (e) {
|
|
236
|
+
parts.push(`(checkpoint cleanup skipped: ${e.message})`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else parts.push(`git commit failed: ${commit.err || "(no output)"}`)
|
|
240
|
+
return truncate(parts.join("\n") || "(commit produced no output)")
|
|
241
|
+
}
|
|
242
|
+
case "push": {
|
|
243
|
+
const cmdArgs = ["push"]
|
|
244
|
+
if (args.remote) cmdArgs.push(validateRef(args.remote, "remote"))
|
|
245
|
+
if (args.ref) for (const r of args.ref.split(/\s+/).filter(Boolean)) cmdArgs.push(validateRef(r, "ref"))
|
|
246
|
+
if (args.tags) cmdArgs.push("--tags")
|
|
247
|
+
const r = runGitStrict(ctx.cwd, cmdArgs, cfgArgs)
|
|
248
|
+
return r.ok ? truncate(r.out || "(push complete — no output)") : truncate(`git push failed: ${r.err || r.out || "(no output)"}`)
|
|
249
|
+
}
|
|
250
|
+
case "ls-remote": {
|
|
251
|
+
// Lightweight remote-ref check (which refs a remote has) — network action,
|
|
252
|
+
// read-only, no snapshot. Config plumbing for blocked/gated remotes.
|
|
253
|
+
const cmdArgs = ["ls-remote"]
|
|
254
|
+
if (args.remote) cmdArgs.push(validateRef(args.remote, "remote"))
|
|
255
|
+
if (args.ref) for (const r of args.ref.split(/\s+/).filter(Boolean)) cmdArgs.push(validateRef(r, "ref"))
|
|
256
|
+
const out = runGit(ctx.cwd, cmdArgs, cfgArgs)
|
|
257
|
+
if (!out) return "(no refs / remote unreachable)"
|
|
258
|
+
return truncate(filterLines(out, args.filter))
|
|
259
|
+
}
|
|
260
|
+
case "add": {
|
|
261
|
+
// Granular staging: stage `path` when given, else all changes (add -A).
|
|
262
|
+
// 多路径:空格分隔(ref 先例 L175——2026-09-05 发版痛点——git add 单路径被迫 N 次调用)
|
|
263
|
+
const paths = args.path ? args.path.split(/\s+/).filter(Boolean) : null
|
|
264
|
+
const cmdArgs = paths?.length ? ["add", "--", ...paths] : ["add", "-A"]
|
|
265
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
266
|
+
return r.ok ? truncate(r.out || `Staged ${paths?.join(" ") || "all changes"}`) : truncate(`git add failed: ${r.err || r.out}`)
|
|
267
|
+
}
|
|
268
|
+
case "tag": {
|
|
269
|
+
const sub = args.tagAction
|
|
270
|
+
if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["tag", "-l"]) || "(no tags)", args.filter))
|
|
271
|
+
if (sub === "create") {
|
|
272
|
+
if (!args.name) return "Error: tag create requires name"
|
|
273
|
+
validateRef(args.name, "tag")
|
|
274
|
+
const cmdArgs = ["tag", args.name]
|
|
275
|
+
if (args.ref) cmdArgs.push(validateRef(args.ref))
|
|
276
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
277
|
+
return r.ok ? `Tag ${args.name} created` : truncate(`git tag failed: ${r.err || r.out}`)
|
|
278
|
+
}
|
|
279
|
+
if (sub === "delete") {
|
|
280
|
+
if (!args.name) return "Error: tag delete requires name"
|
|
281
|
+
validateRef(args.name, "tag")
|
|
282
|
+
const snap = await snapshotBefore(ctx, `tag delete ${args.name}`)
|
|
283
|
+
const r = runGitStrict(ctx.cwd, ["tag", "-d", args.name])
|
|
284
|
+
return r.ok ? truncate(snap + `Tag ${args.name} deleted`) : truncate(`git tag -d failed: ${r.err || r.out}`)
|
|
285
|
+
}
|
|
286
|
+
return "Error: tag requires tagAction — use: list | create | delete"
|
|
287
|
+
}
|
|
288
|
+
case "branch": {
|
|
289
|
+
const sub = args.branchAction
|
|
290
|
+
if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["branch", "--all", "-vv"]) || "(no branches)", args.filter))
|
|
291
|
+
if (sub === "create") {
|
|
292
|
+
if (!args.name) return "Error: branch create requires name"
|
|
293
|
+
validateRef(args.name, "branch")
|
|
294
|
+
const cmdArgs = ["branch", args.name]
|
|
295
|
+
if (args.ref) cmdArgs.push(validateRef(args.ref))
|
|
296
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
297
|
+
return r.ok ? `Branch ${args.name} created` : truncate(`git branch failed: ${r.err || r.out}`)
|
|
298
|
+
}
|
|
299
|
+
if (sub === "switch") {
|
|
300
|
+
if (!args.name) return "Error: branch switch requires name"
|
|
301
|
+
validateRef(args.name, "branch")
|
|
302
|
+
const r = runGitStrict(ctx.cwd, ["checkout", args.name])
|
|
303
|
+
return r.ok ? `Switched to branch ${args.name}` : truncate(`git checkout ${args.name} failed: ${r.err || r.out}`)
|
|
304
|
+
}
|
|
305
|
+
if (sub === "delete") {
|
|
306
|
+
if (!args.name) return "Error: branch delete requires name"
|
|
307
|
+
validateRef(args.name, "branch")
|
|
308
|
+
const snap = await snapshotBefore(ctx, `branch delete ${args.name}`)
|
|
309
|
+
const r = runGitStrict(ctx.cwd, ["branch", "-d", args.name])
|
|
310
|
+
return r.ok ? truncate(snap + `Branch ${args.name} deleted`) : truncate(`git branch -d failed: ${r.err || r.out}`)
|
|
311
|
+
}
|
|
312
|
+
return "Error: branch requires branchAction — use: list | create | delete | switch"
|
|
313
|
+
}
|
|
314
|
+
case "checkout": {
|
|
315
|
+
if (args.path) {
|
|
316
|
+
// Restore file from index (discards working-tree changes to it) — destructive: snapshot first.
|
|
317
|
+
const snap = await snapshotBefore(ctx, `checkout -- ${args.path}`)
|
|
318
|
+
const r = runGitStrict(ctx.cwd, ["checkout", "--", args.path])
|
|
319
|
+
return r.ok ? truncate(snap + `Restored ${args.path}`) : truncate(`git checkout -- ${args.path} failed: ${r.err || r.out}`)
|
|
320
|
+
}
|
|
321
|
+
if (args.ref) {
|
|
322
|
+
validateRef(args.ref, "ref")
|
|
323
|
+
const r = runGitStrict(ctx.cwd, ["checkout", args.ref])
|
|
324
|
+
return r.ok ? truncate(r.out || `Checked out ${args.ref}`) : truncate(`git checkout ${args.ref} failed: ${r.err || r.out}`)
|
|
325
|
+
}
|
|
326
|
+
return "Error: checkout requires ref (branch/commit) or path (file to restore)"
|
|
327
|
+
}
|
|
328
|
+
case "restore": {
|
|
329
|
+
if (!args.path) return "Error: restore requires path"
|
|
330
|
+
const snap = await snapshotBefore(ctx, `restore ${args.path}`)
|
|
331
|
+
const cmdArgs = ["restore"]
|
|
332
|
+
if (args.staged) cmdArgs.push("--staged")
|
|
333
|
+
cmdArgs.push("--", args.path)
|
|
334
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
335
|
+
return r.ok ? truncate(snap + `Restored ${args.path}`) : truncate(`git restore failed: ${r.err || r.out}`)
|
|
336
|
+
}
|
|
337
|
+
case "stash": {
|
|
338
|
+
const sub = args.stashAction
|
|
339
|
+
if (sub === "list") return truncate(filterLines(runGit(ctx.cwd, ["stash", "list"]) || "(no stashes)", args.filter))
|
|
340
|
+
if (sub === "push") {
|
|
341
|
+
const cmdArgs = ["stash", "push"]
|
|
342
|
+
if (args.message) cmdArgs.push("-m", args.message)
|
|
343
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
344
|
+
return r.ok ? truncate(r.out || "Stashed") : truncate(`git stash push failed: ${r.err || r.out}`)
|
|
345
|
+
}
|
|
346
|
+
if (sub === "pop") {
|
|
347
|
+
const snap = await snapshotBefore(ctx, "stash pop")
|
|
348
|
+
const r = runGitStrict(ctx.cwd, ["stash", "pop"])
|
|
349
|
+
return r.ok ? truncate(snap + (r.out || "Popped")) : truncate(`git stash pop failed: ${r.err || r.out}`)
|
|
350
|
+
}
|
|
351
|
+
return "Error: stash requires stashAction — use: push | pop | list"
|
|
352
|
+
}
|
|
353
|
+
case "fetch": {
|
|
354
|
+
const cmdArgs = ["fetch"]
|
|
355
|
+
if (args.remote) cmdArgs.push(validateRef(args.remote, "remote"))
|
|
356
|
+
if (args.ref) cmdArgs.push(validateRef(args.ref, "ref"))
|
|
357
|
+
const r = runGitStrict(ctx.cwd, cmdArgs, cfgArgs)
|
|
358
|
+
return r.ok ? truncate(r.out || "(fetch complete — no output)") : truncate(`git fetch failed: ${r.err || r.out}`)
|
|
359
|
+
}
|
|
360
|
+
case "pull": {
|
|
361
|
+
const cmdArgs = ["pull"]
|
|
362
|
+
if (args.remote) cmdArgs.push(validateRef(args.remote, "remote"))
|
|
363
|
+
if (args.ref) cmdArgs.push(validateRef(args.ref, "ref"))
|
|
364
|
+
const r = runGitStrict(ctx.cwd, cmdArgs, cfgArgs)
|
|
365
|
+
return r.ok ? truncate(r.out || "(pull complete — no output)") : truncate(`git pull failed: ${r.err || r.out}`)
|
|
366
|
+
}
|
|
367
|
+
case "reset": {
|
|
368
|
+
const mode = args.mode ?? "mixed"
|
|
369
|
+
if (!["soft", "mixed", "hard"].includes(mode)) return "Error: reset mode must be soft | mixed | hard"
|
|
370
|
+
let snap = ""
|
|
371
|
+
if (mode === "hard") snap = await snapshotBefore(ctx, "reset --hard") // destructive: drops working-tree changes
|
|
372
|
+
const cmdArgs = ["reset", `--${mode}`]
|
|
373
|
+
if (args.ref) cmdArgs.push(validateRef(args.ref))
|
|
374
|
+
const r = runGitStrict(ctx.cwd, cmdArgs)
|
|
375
|
+
return r.ok ? truncate(snap + (r.out || `Reset (${mode}) complete`)) : truncate(`git reset failed: ${r.err || r.out}`)
|
|
376
|
+
}
|
|
377
|
+
case "revert": {
|
|
378
|
+
const ref = validateRef(args.ref ?? "HEAD")
|
|
379
|
+
const r = runGitStrict(ctx.cwd, ["revert", "--no-edit", ref])
|
|
380
|
+
return r.ok ? truncate(r.out || `Reverted ${ref}`) : truncate(`git revert failed: ${r.err || r.out}`)
|
|
381
|
+
}
|
|
382
|
+
case "merge": {
|
|
383
|
+
if (!args.ref) return "Error: merge requires ref (branch/commit to merge)"
|
|
384
|
+
validateRef(args.ref, "ref")
|
|
385
|
+
const r = runGitStrict(ctx.cwd, ["merge", "--no-edit", args.ref])
|
|
386
|
+
return r.ok ? truncate(r.out || `Merged ${args.ref}`) : truncate(`git merge failed: ${r.err || r.out} — resolve conflicts, then commit`)
|
|
387
|
+
}
|
|
388
|
+
case "cherry-pick": {
|
|
389
|
+
if (!args.ref) return "Error: cherry-pick requires ref (commit)"
|
|
390
|
+
validateRef(args.ref, "ref")
|
|
391
|
+
const r = runGitStrict(ctx.cwd, ["cherry-pick", args.ref])
|
|
392
|
+
return r.ok ? truncate(r.out || `Cherry-picked ${args.ref}`) : truncate(`git cherry-pick failed: ${r.err || r.out}`)
|
|
393
|
+
}
|
|
394
|
+
// F7 扩展 action + checkpoint:实现拆在 git-ext.mjs / git-checkpoint.mjs(500 行硬限)
|
|
395
|
+
case "clone":
|
|
396
|
+
case "init":
|
|
397
|
+
case "rebase":
|
|
398
|
+
case "remote":
|
|
399
|
+
case "clean":
|
|
400
|
+
case "switch":
|
|
401
|
+
case "apply":
|
|
402
|
+
case "worktree":
|
|
403
|
+
case "archive":
|
|
404
|
+
case "blame":
|
|
405
|
+
case "mv":
|
|
406
|
+
return executeExtAction(args, ctx)
|
|
407
|
+
case "checkpoint":
|
|
408
|
+
return executeCheckpointAction(args, ctx)
|
|
409
|
+
|
|
410
|
+
default:
|
|
411
|
+
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`
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
}
|
|
415
|
+
|