@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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/LICENSE +674 -674
- package/README.md +474 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +228 -220
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +89 -89
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/commands/update.mjs +32 -0
- package/src/config/defaults.mjs +289 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +604 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +87 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +4 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3371 -2981
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +298 -298
- package/src/session/engine.mjs +417 -232
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1097
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -900
- package/src/session/loop.mjs +1005 -930
- package/src/session/prompt/agent.txt +25 -25
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +31 -31
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +196 -195
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -519
- package/src/session/system-prompt.mjs +308 -273
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +17 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +99 -93
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/update/checker.mjs +184 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
- package/src/version.mjs +3 -0
package/src/util/git.mjs
CHANGED
|
@@ -1,519 +1,562 @@
|
|
|
1
|
-
import { spawn } from "node:child_process"
|
|
2
|
-
import { mkdtemp, writeFile, unlink,
|
|
3
|
-
import { tmpdir } from "node:os"
|
|
4
|
-
import path from "node:path"
|
|
5
|
-
|
|
6
|
-
const GIT_TIMEOUT_MS = 30000
|
|
7
|
-
|
|
8
|
-
function run(args, cwd = process.cwd(), timeoutMs = GIT_TIMEOUT_MS, env = {}) {
|
|
9
|
-
return new Promise((resolve) => {
|
|
10
|
-
let stdout = ""
|
|
11
|
-
let stderr = ""
|
|
12
|
-
let done = false
|
|
13
|
-
|
|
14
|
-
const child = spawn("git", args, {
|
|
15
|
-
cwd,
|
|
16
|
-
windowsHide: true,
|
|
17
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
-
env: { ...process.env, ...env }
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const timer = setTimeout(() => {
|
|
22
|
-
done = true
|
|
23
|
-
child.kill()
|
|
24
|
-
resolve({ ok: false, stdout, stderr: "git command timed out", code: null })
|
|
25
|
-
}, timeoutMs)
|
|
26
|
-
|
|
27
|
-
child.stdout.on("data", (buf) => { stdout += String(buf) })
|
|
28
|
-
child.stderr.on("data", (buf) => { stderr += String(buf) })
|
|
29
|
-
|
|
30
|
-
child.on("error", (error) => {
|
|
31
|
-
if (done) return
|
|
32
|
-
done = true
|
|
33
|
-
clearTimeout(timer)
|
|
34
|
-
resolve({ ok: false, stdout, stderr: error.message, code: null })
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
child.on("close", (code) => {
|
|
38
|
-
if (done) return
|
|
39
|
-
done = true
|
|
40
|
-
clearTimeout(timer)
|
|
41
|
-
resolve({ ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), code })
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Check if cwd is inside a git repo */
|
|
47
|
-
export async function isGitRepo(cwd = process.cwd()) {
|
|
48
|
-
const result = await run(["rev-parse", "--is-inside-work-tree"], cwd)
|
|
49
|
-
return result.ok && result.stdout.trim() === "true"
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Get current branch name */
|
|
53
|
-
export async function currentBranch(cwd = process.cwd()) {
|
|
54
|
-
const result = await run(["rev-parse", "--abbrev-ref", "HEAD"], cwd)
|
|
55
|
-
return result.ok ? result.stdout.trim() : null
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Check if working tree is clean */
|
|
59
|
-
export async function isClean(cwd = process.cwd()) {
|
|
60
|
-
const result = await run(["status", "--porcelain"], cwd)
|
|
61
|
-
return result.ok && !result.stdout.trim()
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Create and checkout a new branch */
|
|
65
|
-
export async function createBranch(name, cwd = process.cwd()) {
|
|
66
|
-
const result = await run(["checkout", "-b", name], cwd)
|
|
67
|
-
return { ok: result.ok, message: result.ok ? `created branch: ${name}` : result.stderr }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Checkout an existing branch */
|
|
71
|
-
export async function checkoutBranch(name, cwd = process.cwd()) {
|
|
72
|
-
const result = await run(["checkout", name], cwd)
|
|
73
|
-
return { ok: result.ok, message: result.ok ? `switched to: ${name}` : result.stderr }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Stage all changes and commit */
|
|
77
|
-
export async function commitAll(message, cwd = process.cwd()) {
|
|
78
|
-
const add = await run(["add", "-A"], cwd)
|
|
79
|
-
if (!add.ok) return { ok: false, message: `git add failed: ${add.stderr}` }
|
|
80
|
-
const commit = await run(["commit", "-m", message, "--allow-empty"], cwd)
|
|
81
|
-
if (!commit.ok) {
|
|
82
|
-
// Nothing to commit is not an error
|
|
83
|
-
if (commit.stderr.includes("nothing to commit")) {
|
|
84
|
-
return { ok: true, message: "nothing to commit", empty: true }
|
|
85
|
-
}
|
|
86
|
-
return { ok: false, message: `git commit failed: ${commit.stderr}` }
|
|
87
|
-
}
|
|
88
|
-
return { ok: true, message: commit.stdout.split("\n")[0] || "committed" }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Merge a branch into current branch */
|
|
92
|
-
export async function mergeBranch(source, cwd = process.cwd()) {
|
|
93
|
-
const result = await run(["merge", source, "--no-ff", "-m", `Merge branch '${source}'`], cwd)
|
|
94
|
-
return { ok: result.ok, message: result.ok ? `merged ${source}` : result.stderr }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Delete a branch */
|
|
98
|
-
export async function deleteBranch(name, cwd = process.cwd()) {
|
|
99
|
-
const result = await run(["branch", "-d", name], cwd)
|
|
100
|
-
return { ok: result.ok, message: result.ok ? `deleted branch: ${name}` : result.stderr }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Get short log of recent commits */
|
|
104
|
-
export async function recentCommits(count = 5, cwd = process.cwd()) {
|
|
105
|
-
const result = await run(["log", `--oneline`, `-${count}`], cwd)
|
|
106
|
-
return result.ok ? result.stdout.trim().split("\n").filter(Boolean) : []
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Get diff stat summary */
|
|
110
|
-
export async function diffStat(cwd = process.cwd()) {
|
|
111
|
-
const result = await run(["diff", "--stat", "HEAD"], cwd)
|
|
112
|
-
return result.ok ? result.stdout.trim() : ""
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
export async function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
const
|
|
212
|
-
["
|
|
213
|
-
repoPath,
|
|
214
|
-
GIT_TIMEOUT_MS,
|
|
215
|
-
{ GIT_INDEX_FILE: indexPath }
|
|
216
|
-
)
|
|
217
|
-
if (!
|
|
218
|
-
return { ok: false, error: `
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
["
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
error:
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
//
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
// ============================================================================
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { mkdtemp, writeFile, unlink, rm } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
|
|
6
|
+
const GIT_TIMEOUT_MS = 30000
|
|
7
|
+
|
|
8
|
+
function run(args, cwd = process.cwd(), timeoutMs = GIT_TIMEOUT_MS, env = {}) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
let stdout = ""
|
|
11
|
+
let stderr = ""
|
|
12
|
+
let done = false
|
|
13
|
+
|
|
14
|
+
const child = spawn("git", args, {
|
|
15
|
+
cwd,
|
|
16
|
+
windowsHide: true,
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
env: { ...process.env, ...env }
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
done = true
|
|
23
|
+
child.kill()
|
|
24
|
+
resolve({ ok: false, stdout, stderr: "git command timed out", code: null })
|
|
25
|
+
}, timeoutMs)
|
|
26
|
+
|
|
27
|
+
child.stdout.on("data", (buf) => { stdout += String(buf) })
|
|
28
|
+
child.stderr.on("data", (buf) => { stderr += String(buf) })
|
|
29
|
+
|
|
30
|
+
child.on("error", (error) => {
|
|
31
|
+
if (done) return
|
|
32
|
+
done = true
|
|
33
|
+
clearTimeout(timer)
|
|
34
|
+
resolve({ ok: false, stdout, stderr: error.message, code: null })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
child.on("close", (code) => {
|
|
38
|
+
if (done) return
|
|
39
|
+
done = true
|
|
40
|
+
clearTimeout(timer)
|
|
41
|
+
resolve({ ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), code })
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Check if cwd is inside a git repo */
|
|
47
|
+
export async function isGitRepo(cwd = process.cwd()) {
|
|
48
|
+
const result = await run(["rev-parse", "--is-inside-work-tree"], cwd)
|
|
49
|
+
return result.ok && result.stdout.trim() === "true"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Get current branch name */
|
|
53
|
+
export async function currentBranch(cwd = process.cwd()) {
|
|
54
|
+
const result = await run(["rev-parse", "--abbrev-ref", "HEAD"], cwd)
|
|
55
|
+
return result.ok ? result.stdout.trim() : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Check if working tree is clean */
|
|
59
|
+
export async function isClean(cwd = process.cwd()) {
|
|
60
|
+
const result = await run(["status", "--porcelain"], cwd)
|
|
61
|
+
return result.ok && !result.stdout.trim()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Create and checkout a new branch */
|
|
65
|
+
export async function createBranch(name, cwd = process.cwd()) {
|
|
66
|
+
const result = await run(["checkout", "-b", name], cwd)
|
|
67
|
+
return { ok: result.ok, message: result.ok ? `created branch: ${name}` : result.stderr }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Checkout an existing branch */
|
|
71
|
+
export async function checkoutBranch(name, cwd = process.cwd()) {
|
|
72
|
+
const result = await run(["checkout", name], cwd)
|
|
73
|
+
return { ok: result.ok, message: result.ok ? `switched to: ${name}` : result.stderr }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Stage all changes and commit */
|
|
77
|
+
export async function commitAll(message, cwd = process.cwd()) {
|
|
78
|
+
const add = await run(["add", "-A"], cwd)
|
|
79
|
+
if (!add.ok) return { ok: false, message: `git add failed: ${add.stderr}` }
|
|
80
|
+
const commit = await run(["commit", "-m", message, "--allow-empty"], cwd)
|
|
81
|
+
if (!commit.ok) {
|
|
82
|
+
// Nothing to commit is not an error
|
|
83
|
+
if (commit.stderr.includes("nothing to commit")) {
|
|
84
|
+
return { ok: true, message: "nothing to commit", empty: true }
|
|
85
|
+
}
|
|
86
|
+
return { ok: false, message: `git commit failed: ${commit.stderr}` }
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, message: commit.stdout.split("\n")[0] || "committed" }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Merge a branch into current branch */
|
|
92
|
+
export async function mergeBranch(source, cwd = process.cwd()) {
|
|
93
|
+
const result = await run(["merge", source, "--no-ff", "-m", `Merge branch '${source}'`], cwd)
|
|
94
|
+
return { ok: result.ok, message: result.ok ? `merged ${source}` : result.stderr }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Delete a branch */
|
|
98
|
+
export async function deleteBranch(name, cwd = process.cwd()) {
|
|
99
|
+
const result = await run(["branch", "-d", name], cwd)
|
|
100
|
+
return { ok: result.ok, message: result.ok ? `deleted branch: ${name}` : result.stderr }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Get short log of recent commits */
|
|
104
|
+
export async function recentCommits(count = 5, cwd = process.cwd()) {
|
|
105
|
+
const result = await run(["log", `--oneline`, `-${count}`], cwd)
|
|
106
|
+
return result.ok ? result.stdout.trim().split("\n").filter(Boolean) : []
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Get diff stat summary */
|
|
110
|
+
export async function diffStat(cwd = process.cwd()) {
|
|
111
|
+
const result = await run(["diff", "--stat", "HEAD"], cwd)
|
|
112
|
+
return result.ok ? result.stdout.trim() : ""
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Create a detached git worktree rooted at HEAD for isolated local execution */
|
|
116
|
+
export async function createDetachedWorktree(cwd = process.cwd(), label = "task") {
|
|
117
|
+
if (!(await isGitRepo(cwd))) {
|
|
118
|
+
return { ok: false, error: "not a git repository" }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const prefix = `kkcode-worktree-${String(label || "task").replace(/[^a-zA-Z0-9_-]+/g, "-").slice(0, 24)}-`
|
|
122
|
+
const worktreePath = await mkdtemp(path.join(tmpdir(), prefix))
|
|
123
|
+
const addResult = await run(["worktree", "add", "--detach", worktreePath, "HEAD"], cwd, GIT_TIMEOUT_MS)
|
|
124
|
+
if (!addResult.ok) {
|
|
125
|
+
await rm(worktreePath, { recursive: true, force: true }).catch(() => {})
|
|
126
|
+
return { ok: false, error: addResult.stderr || "git worktree add failed" }
|
|
127
|
+
}
|
|
128
|
+
return { ok: true, path: worktreePath }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Remove an existing git worktree */
|
|
132
|
+
export async function removeWorktree(worktreePath, cwd = process.cwd()) {
|
|
133
|
+
const result = await run(["worktree", "remove", "--force", worktreePath], cwd, GIT_TIMEOUT_MS)
|
|
134
|
+
if (!result.ok) {
|
|
135
|
+
await rm(worktreePath, { recursive: true, force: true }).catch(() => {})
|
|
136
|
+
}
|
|
137
|
+
return { ok: result.ok, message: result.ok ? `removed worktree: ${worktreePath}` : result.stderr || "git worktree remove failed" }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Stash current changes */
|
|
141
|
+
export async function stash(message = "auto-stash", cwd = process.cwd()) {
|
|
142
|
+
const result = await run(["stash", "push", "-m", message], cwd)
|
|
143
|
+
return { ok: result.ok, message: result.ok ? result.stdout.trim() : result.stderr }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Pop stash */
|
|
147
|
+
export async function stashPop(cwd = process.cwd()) {
|
|
148
|
+
const result = await run(["stash", "pop"], cwd)
|
|
149
|
+
return { ok: result.ok, message: result.ok ? result.stdout.trim() : result.stderr }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Generate a branch name from session/objective */
|
|
153
|
+
export function generateBranchName(sessionId, objective = "") {
|
|
154
|
+
const prefix = "kkcode"
|
|
155
|
+
const shortId = String(sessionId || "").slice(0, 8)
|
|
156
|
+
const slug = String(objective || "")
|
|
157
|
+
.toLowerCase()
|
|
158
|
+
.replace(/[^a-z0-9\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff]+/g, "-")
|
|
159
|
+
.replace(/^-|-$/g, "")
|
|
160
|
+
.slice(0, 40)
|
|
161
|
+
return `${prefix}/${shortId}${slug ? "-" + slug : ""}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Ghost Commit (幽灵提交) - AI Agent Git 自动化核心功能
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Ghost Commit 元数据结构
|
|
170
|
+
* @typedef {Object} GhostCommitInfo
|
|
171
|
+
* @property {string} id - 幽灵提交ID (UUID)
|
|
172
|
+
* @property {string} commitHash - Git 提交对象哈希
|
|
173
|
+
* @property {string} repoPath - 仓库绝对路径
|
|
174
|
+
* @property {string} parentHash - 父提交哈希
|
|
175
|
+
* @property {string} message - 提交信息
|
|
176
|
+
* @property {number} createdAt - 创建时间戳
|
|
177
|
+
* @property {string[]} files - 包含的文件列表
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 创建幽灵提交 (Ghost Commit)
|
|
182
|
+
* 使用临时索引创建不引用在任何分支上的提交对象
|
|
183
|
+
*
|
|
184
|
+
* @param {string} repoPath - 仓库路径
|
|
185
|
+
* @param {string} message - 提交信息
|
|
186
|
+
* @param {string[]} [paths=[]] - 要包含的文件路径(相对于repoPath),空数组表示所有更改
|
|
187
|
+
* @returns {Promise<{ok: boolean, ghostCommit?: GhostCommitInfo, error?: string}>}
|
|
188
|
+
*/
|
|
189
|
+
export async function createGhostCommit(repoPath, message = "kkcode snapshot", paths = []) {
|
|
190
|
+
// 检查是否是 Git 仓库
|
|
191
|
+
if (!(await isGitRepo(repoPath))) {
|
|
192
|
+
return { ok: false, error: "not a git repository" }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 获取当前 HEAD
|
|
196
|
+
const headResult = await run(["rev-parse", "HEAD"], repoPath)
|
|
197
|
+
if (!headResult.ok) {
|
|
198
|
+
return { ok: false, error: `failed to get HEAD: ${headResult.stderr}` }
|
|
199
|
+
}
|
|
200
|
+
const parentHash = headResult.stdout.trim()
|
|
201
|
+
|
|
202
|
+
// 创建临时目录和临时索引文件
|
|
203
|
+
let tmpDir = null
|
|
204
|
+
let indexPath = null
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
tmpDir = await mkdtemp(path.join(tmpdir(), "kkcode-git-"))
|
|
208
|
+
indexPath = path.join(tmpDir, "index")
|
|
209
|
+
|
|
210
|
+
// 1. 读取当前 HEAD 到临时索引
|
|
211
|
+
const readTreeResult = await run(
|
|
212
|
+
["read-tree", "HEAD"],
|
|
213
|
+
repoPath,
|
|
214
|
+
GIT_TIMEOUT_MS,
|
|
215
|
+
{ GIT_INDEX_FILE: indexPath }
|
|
216
|
+
)
|
|
217
|
+
if (!readTreeResult.ok) {
|
|
218
|
+
return { ok: false, error: `read-tree failed: ${readTreeResult.stderr}` }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 2. 添加更改到临时索引
|
|
222
|
+
const addArgs = paths.length > 0
|
|
223
|
+
? ["add", "--", ...paths]
|
|
224
|
+
: ["add", "-A"]
|
|
225
|
+
const addResult = await run(
|
|
226
|
+
addArgs,
|
|
227
|
+
repoPath,
|
|
228
|
+
GIT_TIMEOUT_MS,
|
|
229
|
+
{ GIT_INDEX_FILE: indexPath }
|
|
230
|
+
)
|
|
231
|
+
if (!addResult.ok) {
|
|
232
|
+
return { ok: false, error: `git add failed: ${addResult.stderr}` }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 3. 写入树对象
|
|
236
|
+
const writeTreeResult = await run(
|
|
237
|
+
["write-tree"],
|
|
238
|
+
repoPath,
|
|
239
|
+
GIT_TIMEOUT_MS,
|
|
240
|
+
{ GIT_INDEX_FILE: indexPath }
|
|
241
|
+
)
|
|
242
|
+
if (!writeTreeResult.ok) {
|
|
243
|
+
return { ok: false, error: `write-tree failed: ${writeTreeResult.stderr}` }
|
|
244
|
+
}
|
|
245
|
+
const treeHash = writeTreeResult.stdout.trim()
|
|
246
|
+
|
|
247
|
+
// 4. 创建提交对象 (幽灵提交)
|
|
248
|
+
const commitTreeResult = await run(
|
|
249
|
+
["commit-tree", treeHash, "-p", parentHash, "-m", message],
|
|
250
|
+
repoPath
|
|
251
|
+
)
|
|
252
|
+
if (!commitTreeResult.ok) {
|
|
253
|
+
return { ok: false, error: `commit-tree failed: ${commitTreeResult.stderr}` }
|
|
254
|
+
}
|
|
255
|
+
const commitHash = commitTreeResult.stdout.trim()
|
|
256
|
+
|
|
257
|
+
// 5. 获取包含的文件列表
|
|
258
|
+
const diffResult = await run(
|
|
259
|
+
["diff-tree", "--no-commit-id", "--name-only", "-r", commitHash],
|
|
260
|
+
repoPath
|
|
261
|
+
)
|
|
262
|
+
const files = diffResult.ok
|
|
263
|
+
? diffResult.stdout.trim().split("\n").filter(Boolean)
|
|
264
|
+
: []
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
ghostCommit: {
|
|
269
|
+
id: generateGhostCommitId(),
|
|
270
|
+
commitHash,
|
|
271
|
+
repoPath: path.resolve(repoPath),
|
|
272
|
+
parentHash,
|
|
273
|
+
message,
|
|
274
|
+
createdAt: Date.now(),
|
|
275
|
+
files
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return { ok: false, error: error.message }
|
|
280
|
+
} finally {
|
|
281
|
+
// 清理临时目录
|
|
282
|
+
if (tmpDir) {
|
|
283
|
+
try {
|
|
284
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
285
|
+
} catch { /* ignore cleanup errors */ }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 恢复到幽灵提交状态
|
|
292
|
+
* 使用 git restore 将工作区恢复到幽灵提交的状态
|
|
293
|
+
*
|
|
294
|
+
* @param {string} repoPath - 仓库路径
|
|
295
|
+
* @param {string} commitHash - 幽灵提交的 commit hash
|
|
296
|
+
* @param {boolean} [restoreIndex=false] - 是否也恢复暂存区
|
|
297
|
+
* @returns {Promise<{ok: boolean, message?: string, error?: string}>}
|
|
298
|
+
*/
|
|
299
|
+
export async function restoreGhostCommit(repoPath, commitHash, restoreIndex = false) {
|
|
300
|
+
// 验证提交对象存在
|
|
301
|
+
const catFileResult = await run(["cat-file", "-t", commitHash], repoPath)
|
|
302
|
+
if (!catFileResult.ok || catFileResult.stdout.trim() !== "commit") {
|
|
303
|
+
return { ok: false, error: `invalid commit hash: ${commitHash}` }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 恢复工作区
|
|
307
|
+
const restoreArgs = ["restore", "--source", commitHash, "."]
|
|
308
|
+
const restoreResult = await run(restoreArgs, repoPath)
|
|
309
|
+
if (!restoreResult.ok) {
|
|
310
|
+
return { ok: false, error: `restore failed: ${restoreResult.stderr}` }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 如果需要,也恢复暂存区
|
|
314
|
+
if (restoreIndex) {
|
|
315
|
+
const readTreeResult = await run(["read-tree", commitHash], repoPath)
|
|
316
|
+
if (!readTreeResult.ok) {
|
|
317
|
+
return { ok: false, error: `restore index failed: ${readTreeResult.stderr}` }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { ok: true, message: `restored to ${commitHash.slice(0, 8)}` }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 应用 Patch (AI 生成的 diff)
|
|
326
|
+
* 支持 git apply --3way 进行三方合并
|
|
327
|
+
*
|
|
328
|
+
* @param {string} repoPath - 仓库路径
|
|
329
|
+
* @param {string} diff - 统一格式的 diff 文本
|
|
330
|
+
* @param {Object} options - 选项
|
|
331
|
+
* @param {boolean} [options.threeway=true] - 使用三方合并
|
|
332
|
+
* @param {boolean} [options.check=false] - 仅检查,不实际应用
|
|
333
|
+
* @param {boolean} [options.whitespace="nowarn"] - 空白字符处理
|
|
334
|
+
* @returns {Promise<{ok: boolean, applied?: string[], skipped?: string[], conflicts?: string[], error?: string}>}
|
|
335
|
+
*/
|
|
336
|
+
export async function applyPatch(repoPath, diff, options = {}) {
|
|
337
|
+
const {
|
|
338
|
+
threeway = true,
|
|
339
|
+
check = false,
|
|
340
|
+
whitespace = "nowarn"
|
|
341
|
+
} = options
|
|
342
|
+
|
|
343
|
+
// 创建临时 patch 文件
|
|
344
|
+
let patchPath = null
|
|
345
|
+
try {
|
|
346
|
+
const tmpDir = await mkdtemp(path.join(tmpdir(), "kkcode-patch-"))
|
|
347
|
+
patchPath = path.join(tmpDir, "changes.patch")
|
|
348
|
+
await writeFile(patchPath, diff, "utf8")
|
|
349
|
+
|
|
350
|
+
// 构建 git apply 参数
|
|
351
|
+
const applyArgs = ["apply"]
|
|
352
|
+
if (threeway) applyArgs.push("--3way")
|
|
353
|
+
if (check) applyArgs.push("--check")
|
|
354
|
+
if (whitespace) applyArgs.push(`--whitespace=${whitespace}`)
|
|
355
|
+
if (!check) applyArgs.push("-v") // verbose for parsing results
|
|
356
|
+
applyArgs.push(patchPath)
|
|
357
|
+
|
|
358
|
+
const result = await run(applyArgs, repoPath)
|
|
359
|
+
|
|
360
|
+
// 解析结果
|
|
361
|
+
if (!result.ok) {
|
|
362
|
+
// 解析错误信息,提取冲突文件
|
|
363
|
+
const conflictMatch = result.stderr.match(/error: patch failed: (.+)/g)
|
|
364
|
+
const conflicts = conflictMatch
|
|
365
|
+
? conflictMatch.map(m => m.replace(/error: patch failed: /, "").split(":")[0])
|
|
366
|
+
: []
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
ok: false,
|
|
370
|
+
error: result.stderr,
|
|
371
|
+
conflicts
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 解析成功应用的文件
|
|
376
|
+
const appliedMatch = result.stdout.match(/Applied patch to (.+)/g)
|
|
377
|
+
const applied = appliedMatch
|
|
378
|
+
? appliedMatch.map(m => m.replace(/Applied patch to /, "").trim())
|
|
379
|
+
: []
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
ok: true,
|
|
383
|
+
applied,
|
|
384
|
+
skipped: [],
|
|
385
|
+
conflicts: []
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return { ok: false, error: error.message }
|
|
389
|
+
} finally {
|
|
390
|
+
// 清理临时文件
|
|
391
|
+
if (patchPath) {
|
|
392
|
+
try {
|
|
393
|
+
const tmpDir = path.dirname(patchPath)
|
|
394
|
+
await unlink(patchPath)
|
|
395
|
+
await rmdir(tmpDir)
|
|
396
|
+
} catch { /* ignore cleanup errors */ }
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 预检 Patch - 检查 patch 是否可以应用,不实际修改文件
|
|
403
|
+
*
|
|
404
|
+
* @param {string} repoPath - 仓库路径
|
|
405
|
+
* @param {string} diff - 统一格式的 diff 文本
|
|
406
|
+
* @returns {Promise<{applicable: boolean, conflicts?: string[], error?: string}>}
|
|
407
|
+
*/
|
|
408
|
+
export async function preflightPatch(repoPath, diff) {
|
|
409
|
+
const result = await applyPatch(repoPath, diff, { check: true })
|
|
410
|
+
return {
|
|
411
|
+
applicable: result.ok,
|
|
412
|
+
conflicts: result.conflicts,
|
|
413
|
+
error: result.error
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 获取 Git 仓库信息
|
|
419
|
+
* 收集当前仓库的上下文信息供 AI 使用
|
|
420
|
+
*
|
|
421
|
+
* @param {string} repoPath - 仓库路径
|
|
422
|
+
* @returns {Promise<{ok: boolean, info?: Object, error?: string}>}
|
|
423
|
+
*/
|
|
424
|
+
export async function getGitInfo(repoPath) {
|
|
425
|
+
if (!(await isGitRepo(repoPath))) {
|
|
426
|
+
return { ok: false, error: "not a git repository" }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
// 并行获取各种信息
|
|
431
|
+
const [
|
|
432
|
+
branchResult,
|
|
433
|
+
commitResult,
|
|
434
|
+
remoteResult,
|
|
435
|
+
statusResult,
|
|
436
|
+
statusPorcelain
|
|
437
|
+
] = await Promise.all([
|
|
438
|
+
run(["rev-parse", "--abbrev-ref", "HEAD"], repoPath),
|
|
439
|
+
run(["rev-parse", "HEAD"], repoPath),
|
|
440
|
+
run(["remote", "-v"], repoPath),
|
|
441
|
+
run(["status", "--short"], repoPath),
|
|
442
|
+
run(["status", "--porcelain"], repoPath)
|
|
443
|
+
])
|
|
444
|
+
|
|
445
|
+
// 解析远程仓库信息
|
|
446
|
+
const remotes = remoteResult.ok
|
|
447
|
+
? remoteResult.stdout.split("\n")
|
|
448
|
+
.filter(line => line.includes("(fetch)"))
|
|
449
|
+
.map(line => {
|
|
450
|
+
const parts = line.split(/\s+/)
|
|
451
|
+
return { name: parts[0], url: parts[1] }
|
|
452
|
+
})
|
|
453
|
+
: []
|
|
454
|
+
|
|
455
|
+
// 解析状态
|
|
456
|
+
const hasUncommittedChanges = statusPorcelain.ok && statusPorcelain.stdout.trim() !== ""
|
|
457
|
+
const changedFiles = statusPorcelain.ok
|
|
458
|
+
? statusPorcelain.stdout.split("\n").filter(Boolean).map(line => ({
|
|
459
|
+
status: line.slice(0, 2),
|
|
460
|
+
path: line.slice(3)
|
|
461
|
+
}))
|
|
462
|
+
: []
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
ok: true,
|
|
466
|
+
info: {
|
|
467
|
+
isGitRepo: true,
|
|
468
|
+
currentBranch: branchResult.ok ? branchResult.stdout.trim() : null,
|
|
469
|
+
currentCommit: commitResult.ok ? commitResult.stdout.trim() : null,
|
|
470
|
+
remotes,
|
|
471
|
+
hasUncommittedChanges,
|
|
472
|
+
changedFiles,
|
|
473
|
+
statusSummary: statusResult.ok ? statusResult.stdout : ""
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
return { ok: false, error: error.message }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* 获取当前工作目录与指定提交的 diff
|
|
483
|
+
*
|
|
484
|
+
* @param {string} repoPath - 仓库路径
|
|
485
|
+
* @param {string} [target="HEAD"] - 目标提交
|
|
486
|
+
* @returns {Promise<{ok: boolean, diff?: string, error?: string}>}
|
|
487
|
+
*/
|
|
488
|
+
export async function getDiff(repoPath, target = "HEAD") {
|
|
489
|
+
const result = await run(["diff", target], repoPath)
|
|
490
|
+
return {
|
|
491
|
+
ok: result.ok,
|
|
492
|
+
diff: result.ok ? result.stdout : undefined,
|
|
493
|
+
error: result.ok ? undefined : result.stderr
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 获取暂存区的 diff
|
|
499
|
+
*
|
|
500
|
+
* @param {string} repoPath - 仓库路径
|
|
501
|
+
* @returns {Promise<{ok: boolean, diff?: string, error?: string}>}
|
|
502
|
+
*/
|
|
503
|
+
export async function getStagedDiff(repoPath) {
|
|
504
|
+
const result = await run(["diff", "--staged"], repoPath)
|
|
505
|
+
return {
|
|
506
|
+
ok: result.ok,
|
|
507
|
+
diff: result.ok ? result.stdout : undefined,
|
|
508
|
+
error: result.ok ? undefined : result.stderr
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ============================================================================
|
|
513
|
+
// Conflict Detection Helpers
|
|
514
|
+
// ============================================================================
|
|
515
|
+
|
|
516
|
+
/** Check if an error is a merge conflict */
|
|
517
|
+
export function isConflictError(error) {
|
|
518
|
+
const msg = String(error?.message || error || "")
|
|
519
|
+
return msg.includes("CONFLICT") || msg.includes("Merge conflict") || msg.includes("merge conflict")
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Get list of files with merge conflicts */
|
|
523
|
+
export async function getConflictFiles(cwd = process.cwd()) {
|
|
524
|
+
const result = await run(["diff", "--name-only", "--diff-filter=U"], cwd)
|
|
525
|
+
if (!result.ok) return []
|
|
526
|
+
return result.stdout.trim().split("\n").filter(Boolean)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Abort an in-progress merge */
|
|
530
|
+
export async function mergeAbort(cwd = process.cwd()) {
|
|
531
|
+
const result = await run(["merge", "--abort"], cwd)
|
|
532
|
+
return { ok: result.ok, message: result.ok ? "merge aborted" : result.stderr }
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Get current HEAD commit hash (for rollback savepoints) */
|
|
536
|
+
export async function getHeadHash(cwd = process.cwd()) {
|
|
537
|
+
const result = await run(["rev-parse", "HEAD"], cwd)
|
|
538
|
+
return result.ok ? result.stdout.trim() : null
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Hard reset to a specific commit (rollback) */
|
|
542
|
+
export async function resetTo(ref, cwd = process.cwd()) {
|
|
543
|
+
const result = await run(["reset", "--hard", ref], cwd)
|
|
544
|
+
return { ok: result.ok, message: result.ok ? `reset to ${ref}` : result.stderr }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Check if conflict markers remain in working tree */
|
|
548
|
+
export async function hasConflictMarkers(cwd = process.cwd()) {
|
|
549
|
+
const result = await run(["diff", "--check"], cwd)
|
|
550
|
+
return !result.ok
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ============================================================================
|
|
554
|
+
// 内部工具函数
|
|
555
|
+
// ============================================================================
|
|
556
|
+
|
|
557
|
+
/** 生成幽灵提交ID */
|
|
558
|
+
function generateGhostCommitId() {
|
|
559
|
+
const timestamp = Date.now().toString(36)
|
|
560
|
+
const random = Math.random().toString(36).substring(2, 8)
|
|
561
|
+
return `gc_${timestamp}_${random}`
|
|
562
|
+
}
|