neuro-cli 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - Git Worktree Integration (GAP-40)
|
|
3
|
+
// Parallel development using git worktrees with agent binding,
|
|
4
|
+
// conflict detection, and auto-merge. Uses only Node.js built-ins.
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import { existsSync, rmSync } from 'fs';
|
|
9
|
+
import { join, resolve, basename } from 'path';
|
|
10
|
+
// ---- Constants ----
|
|
11
|
+
const DEFAULT_BRANCH = 'main';
|
|
12
|
+
// ============================================================
|
|
13
|
+
// GitWorktreeManager Class
|
|
14
|
+
// ============================================================
|
|
15
|
+
export class GitWorktreeManager extends EventEmitter {
|
|
16
|
+
repoRoot;
|
|
17
|
+
agentBindings = new Map();
|
|
18
|
+
constructor(repoRoot) {
|
|
19
|
+
super();
|
|
20
|
+
this.repoRoot = repoRoot ?? process.cwd();
|
|
21
|
+
}
|
|
22
|
+
// ============================================================
|
|
23
|
+
// Worktree Management
|
|
24
|
+
// ============================================================
|
|
25
|
+
/**
|
|
26
|
+
* Create a new git worktree for parallel development.
|
|
27
|
+
* If no branch is specified, creates a new branch named after the worktree.
|
|
28
|
+
*/
|
|
29
|
+
async createWorktree(name, branch) {
|
|
30
|
+
// Validate: no worktree with this name already exists
|
|
31
|
+
const existing = await this.listWorktrees();
|
|
32
|
+
const alreadyExists = existing.some((wt) => wt.name === name || wt.path.endsWith(name));
|
|
33
|
+
if (alreadyExists) {
|
|
34
|
+
throw new Error(`Worktree "${name}" already exists`);
|
|
35
|
+
}
|
|
36
|
+
const worktreeBranch = branch ?? name;
|
|
37
|
+
const worktreePath = join(this.repoRoot, '..', basename(this.repoRoot) + '-' + name);
|
|
38
|
+
// Determine whether the branch already exists
|
|
39
|
+
const branchExists = await this.branchExists(worktreeBranch);
|
|
40
|
+
const args = branchExists
|
|
41
|
+
? ['worktree', 'add', worktreePath, worktreeBranch]
|
|
42
|
+
: ['worktree', 'add', '-b', worktreeBranch, worktreePath];
|
|
43
|
+
const result = await this.execGit(args, this.repoRoot);
|
|
44
|
+
if (result.exitCode !== 0) {
|
|
45
|
+
throw new Error(`Failed to create worktree "${name}": ${result.stderr}`);
|
|
46
|
+
}
|
|
47
|
+
// Get the HEAD of the new worktree
|
|
48
|
+
const headResult = await this.execGit(['rev-parse', '--short', 'HEAD'], worktreePath);
|
|
49
|
+
const head = headResult.exitCode === 0 ? headResult.stdout.trim() : 'unknown';
|
|
50
|
+
const info = {
|
|
51
|
+
name,
|
|
52
|
+
path: worktreePath,
|
|
53
|
+
branch: worktreeBranch,
|
|
54
|
+
isCurrent: false,
|
|
55
|
+
isBare: false,
|
|
56
|
+
head,
|
|
57
|
+
};
|
|
58
|
+
this.emit('worktree:created', name, info);
|
|
59
|
+
return info;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Remove a git worktree. If merge is true, attempt to merge the branch first.
|
|
63
|
+
*/
|
|
64
|
+
async removeWorktree(name, options) {
|
|
65
|
+
const existing = await this.listWorktrees();
|
|
66
|
+
const worktree = existing.find((wt) => wt.name === name || wt.path.endsWith(name));
|
|
67
|
+
if (!worktree) {
|
|
68
|
+
throw new Error(`Worktree "${name}" not found`);
|
|
69
|
+
}
|
|
70
|
+
// If merge requested, attempt auto-merge first
|
|
71
|
+
if (options?.merge) {
|
|
72
|
+
try {
|
|
73
|
+
await this.autoMerge(name);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (!options.force) {
|
|
77
|
+
throw new Error(`Merge failed for worktree "${name}": ${err instanceof Error ? err.message : String(err)}. ` +
|
|
78
|
+
`Use force: true to remove anyway.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Unbind any agents from this worktree
|
|
83
|
+
const entries = Array.from(this.agentBindings.entries());
|
|
84
|
+
for (const [agentId, binding] of entries) {
|
|
85
|
+
if (binding.worktreeName === name) {
|
|
86
|
+
this.agentBindings.delete(agentId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Remove the worktree
|
|
90
|
+
const forceFlag = options?.force ? '--force' : '';
|
|
91
|
+
const args = ['worktree', 'remove', worktree.path];
|
|
92
|
+
if (options?.force) {
|
|
93
|
+
args.splice(2, 0, '--force');
|
|
94
|
+
}
|
|
95
|
+
const result = await this.execGit(args, this.repoRoot);
|
|
96
|
+
if (result.exitCode !== 0) {
|
|
97
|
+
// Try pruning as fallback
|
|
98
|
+
await this.execGit(['worktree', 'prune'], this.repoRoot);
|
|
99
|
+
// If the directory still exists, try to remove it manually
|
|
100
|
+
if (existsSync(worktree.path)) {
|
|
101
|
+
try {
|
|
102
|
+
rmSync(worktree.path, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
if (!options?.force) {
|
|
106
|
+
throw new Error(`Failed to remove worktree directory: ${worktree.path}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Optionally delete the branch if it's not the default branch
|
|
112
|
+
if (worktree.branch !== DEFAULT_BRANCH && worktree.branch !== 'master') {
|
|
113
|
+
try {
|
|
114
|
+
await this.execGit(['branch', '-d', worktree.branch], this.repoRoot);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Branch deletion is best-effort
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.emit('worktree:removed', name);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* List all git worktrees in the repository.
|
|
124
|
+
*/
|
|
125
|
+
async listWorktrees() {
|
|
126
|
+
const result = await this.execGit(['worktree', 'list', '--porcelain'], this.repoRoot);
|
|
127
|
+
if (result.exitCode !== 0) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
return this.parseWorktreeList(result.stdout);
|
|
131
|
+
}
|
|
132
|
+
// ============================================================
|
|
133
|
+
// Agent-Worktree Binding
|
|
134
|
+
// ============================================================
|
|
135
|
+
/**
|
|
136
|
+
* Bind an agent to a specific worktree. The agent will operate
|
|
137
|
+
* within that worktree's directory.
|
|
138
|
+
*/
|
|
139
|
+
bindAgentToWorktree(agentId, worktreeName) {
|
|
140
|
+
// Check if agent is already bound
|
|
141
|
+
const existingBinding = this.agentBindings.get(agentId);
|
|
142
|
+
if (existingBinding) {
|
|
143
|
+
throw new Error(`Agent "${agentId}" is already bound to worktree "${existingBinding.worktreeName}". ` +
|
|
144
|
+
`Unbind first before reassigning.`);
|
|
145
|
+
}
|
|
146
|
+
this.agentBindings.set(agentId, {
|
|
147
|
+
agentId,
|
|
148
|
+
worktreeName,
|
|
149
|
+
boundAt: Date.now(),
|
|
150
|
+
});
|
|
151
|
+
this.emit('agent:bound', agentId, worktreeName);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Unbind an agent from its worktree.
|
|
155
|
+
*/
|
|
156
|
+
unbindAgent(agentId) {
|
|
157
|
+
const binding = this.agentBindings.get(agentId);
|
|
158
|
+
if (!binding) {
|
|
159
|
+
return; // Already unbound — no-op
|
|
160
|
+
}
|
|
161
|
+
this.agentBindings.delete(agentId);
|
|
162
|
+
this.emit('agent:unbound', agentId, binding.worktreeName);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get the worktree name bound to an agent, or null if unbound.
|
|
166
|
+
*/
|
|
167
|
+
getWorktreeForAgent(agentId) {
|
|
168
|
+
const binding = this.agentBindings.get(agentId);
|
|
169
|
+
return binding?.worktreeName ?? null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get all agent bindings.
|
|
173
|
+
*/
|
|
174
|
+
getAgentBindings() {
|
|
175
|
+
const result = [];
|
|
176
|
+
const bindings = Array.from(this.agentBindings.values());
|
|
177
|
+
for (const binding of bindings) {
|
|
178
|
+
result.push({ agentId: binding.agentId, worktreeName: binding.worktreeName });
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
// ============================================================
|
|
183
|
+
// Conflict Detection
|
|
184
|
+
// ============================================================
|
|
185
|
+
/**
|
|
186
|
+
* Detect potential conflicts between two worktrees by comparing
|
|
187
|
+
* their modified files and checking for overlaps.
|
|
188
|
+
*/
|
|
189
|
+
async detectConflicts(worktreeA, worktreeB) {
|
|
190
|
+
const worktrees = await this.listWorktrees();
|
|
191
|
+
const wtA = worktrees.find((wt) => wt.name === worktreeA || wt.path.endsWith(worktreeA));
|
|
192
|
+
const wtB = worktrees.find((wt) => wt.name === worktreeB || wt.path.endsWith(worktreeB));
|
|
193
|
+
if (!wtA) {
|
|
194
|
+
throw new Error(`Worktree "${worktreeA}" not found`);
|
|
195
|
+
}
|
|
196
|
+
if (!wtB) {
|
|
197
|
+
throw new Error(`WorktreeB "${worktreeB}" not found`);
|
|
198
|
+
}
|
|
199
|
+
// Get modified files in each worktree
|
|
200
|
+
const filesA = await this.getModifiedFiles(wtA.path);
|
|
201
|
+
const filesB = await this.getModifiedFiles(wtB.path);
|
|
202
|
+
const conflicts = [];
|
|
203
|
+
// Find overlapping file paths
|
|
204
|
+
const filesASet = new Set(filesA.map((f) => f.path));
|
|
205
|
+
const filesBMap = new Map(filesB.map((f) => [f.path, f]));
|
|
206
|
+
for (const fileA of filesA) {
|
|
207
|
+
const fileB = filesBMap.get(fileA.path);
|
|
208
|
+
if (!fileB)
|
|
209
|
+
continue;
|
|
210
|
+
// Both worktrees modified the same file — potential conflict
|
|
211
|
+
const conflictType = this.determineConflictType(fileA, fileB);
|
|
212
|
+
// Get the actual diff content to assess conflict severity
|
|
213
|
+
const diffAnalysis = await this.analyzeFileDiff(wtA.path, wtB.path, fileA.path);
|
|
214
|
+
const conflict = {
|
|
215
|
+
filePath: fileA.path,
|
|
216
|
+
type: conflictType,
|
|
217
|
+
worktreeA: worktreeA,
|
|
218
|
+
worktreeB: worktreeB,
|
|
219
|
+
conflictingLines: diffAnalysis.conflictingLines,
|
|
220
|
+
autoResolvable: diffAnalysis.autoResolvable,
|
|
221
|
+
};
|
|
222
|
+
conflicts.push(conflict);
|
|
223
|
+
}
|
|
224
|
+
// Check for delete/modify conflicts
|
|
225
|
+
const deletedInA = new Set(filesA.filter((f) => f.status === 'deleted').map((f) => f.path));
|
|
226
|
+
const modifiedInB = new Set(filesB.filter((f) => f.status === 'modified' || f.status === 'added').map((f) => f.path));
|
|
227
|
+
for (const path of deletedInA) {
|
|
228
|
+
if (modifiedInB.has(path) && !conflicts.some((c) => c.filePath === path)) {
|
|
229
|
+
conflicts.push({
|
|
230
|
+
filePath: path,
|
|
231
|
+
type: 'delete_modify',
|
|
232
|
+
worktreeA: worktreeA,
|
|
233
|
+
worktreeB: worktreeB,
|
|
234
|
+
conflictingLines: 0,
|
|
235
|
+
autoResolvable: false,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const deletedInB = new Set(filesB.filter((f) => f.status === 'deleted').map((f) => f.path));
|
|
240
|
+
const modifiedInA = new Set(filesA.filter((f) => f.status === 'modified' || f.status === 'added').map((f) => f.path));
|
|
241
|
+
for (const path of deletedInB) {
|
|
242
|
+
if (modifiedInA.has(path) && !conflicts.some((c) => c.filePath === path)) {
|
|
243
|
+
conflicts.push({
|
|
244
|
+
filePath: path,
|
|
245
|
+
type: 'delete_modify',
|
|
246
|
+
worktreeA: worktreeA,
|
|
247
|
+
worktreeB: worktreeB,
|
|
248
|
+
conflictingLines: 0,
|
|
249
|
+
autoResolvable: false,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return conflicts;
|
|
254
|
+
}
|
|
255
|
+
// ============================================================
|
|
256
|
+
// Auto-Merge
|
|
257
|
+
// ============================================================
|
|
258
|
+
/**
|
|
259
|
+
* Attempt to automatically merge a worktree's branch into the target branch.
|
|
260
|
+
* Defaults to merging into the main branch.
|
|
261
|
+
*/
|
|
262
|
+
async autoMerge(worktreeName, targetBranch) {
|
|
263
|
+
const worktrees = await this.listWorktrees();
|
|
264
|
+
const worktree = worktrees.find((wt) => wt.name === worktreeName || wt.path.endsWith(worktreeName));
|
|
265
|
+
if (!worktree) {
|
|
266
|
+
throw new Error(`Worktree "${worktreeName}" not found`);
|
|
267
|
+
}
|
|
268
|
+
const target = targetBranch ?? DEFAULT_BRANCH;
|
|
269
|
+
// First, ensure the worktree's changes are committed
|
|
270
|
+
const status = await this.getWorktreeStatus(worktreeName);
|
|
271
|
+
if (!status.isClean) {
|
|
272
|
+
// Stage and commit all changes in the worktree
|
|
273
|
+
await this.execGit(['add', '-A'], worktree.path);
|
|
274
|
+
await this.execGit(['commit', '-m', `auto-commit: worktree "${worktreeName}" before merge`], worktree.path);
|
|
275
|
+
}
|
|
276
|
+
// Switch to the target branch in the main repo and merge
|
|
277
|
+
// Use --no-edit to accept default merge message
|
|
278
|
+
const mergeResult = await this.execGit(['merge', worktree.branch, '--no-edit'], this.repoRoot);
|
|
279
|
+
if (mergeResult.exitCode === 0) {
|
|
280
|
+
// Merge succeeded
|
|
281
|
+
const mergedFiles = await this.getChangedFilesInMerge();
|
|
282
|
+
this.emit('worktree:merged', worktreeName, target, true);
|
|
283
|
+
return {
|
|
284
|
+
success: true,
|
|
285
|
+
targetBranch: target,
|
|
286
|
+
sourceWorktree: worktreeName,
|
|
287
|
+
mergedFiles,
|
|
288
|
+
conflictFiles: [],
|
|
289
|
+
message: `Successfully merged "${worktree.branch}" into "${target}"`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// Merge had conflicts — check what files are conflicted
|
|
293
|
+
const conflictCheck = await this.execGit(['diff', '--name-only', '--diff-filter=U'], this.repoRoot);
|
|
294
|
+
const conflictFiles = conflictCheck.exitCode === 0
|
|
295
|
+
? conflictCheck.stdout.trim().split('\n').filter(Boolean)
|
|
296
|
+
: [];
|
|
297
|
+
// Abort the merge to leave the repo in a clean state
|
|
298
|
+
await this.execGit(['merge', '--abort'], this.repoRoot);
|
|
299
|
+
this.emit('worktree:merge-conflict', worktreeName, target, conflictFiles);
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
targetBranch: target,
|
|
303
|
+
sourceWorktree: worktreeName,
|
|
304
|
+
mergedFiles: [],
|
|
305
|
+
conflictFiles,
|
|
306
|
+
message: `Merge conflict in ${conflictFiles.length} file(s): ${conflictFiles.join(', ')}`,
|
|
307
|
+
conflictOutput: mergeResult.stderr || mergeResult.stdout,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
// ============================================================
|
|
311
|
+
// Worktree Status
|
|
312
|
+
// ============================================================
|
|
313
|
+
/**
|
|
314
|
+
* Get the status of a worktree (modified, added, deleted files, etc.).
|
|
315
|
+
*/
|
|
316
|
+
async getWorktreeStatus(name) {
|
|
317
|
+
const worktrees = await this.listWorktrees();
|
|
318
|
+
const worktree = worktrees.find((wt) => wt.name === name || wt.path.endsWith(name));
|
|
319
|
+
if (!worktree) {
|
|
320
|
+
throw new Error(`Worktree "${name}" not found`);
|
|
321
|
+
}
|
|
322
|
+
const statusResult = await this.execGit(['status', '--porcelain=v1'], worktree.path);
|
|
323
|
+
let modified = 0;
|
|
324
|
+
let added = 0;
|
|
325
|
+
let deleted = 0;
|
|
326
|
+
let untracked = 0;
|
|
327
|
+
let staged = 0;
|
|
328
|
+
const modifiedFiles = [];
|
|
329
|
+
if (statusResult.exitCode === 0) {
|
|
330
|
+
const lines = statusResult.stdout.trim().split('\n').filter(Boolean);
|
|
331
|
+
for (const line of lines) {
|
|
332
|
+
const x = line[0];
|
|
333
|
+
const y = line[1];
|
|
334
|
+
const filePath = line.substring(3);
|
|
335
|
+
// Index status (x)
|
|
336
|
+
if (x === 'M' || x === 'A' || x === 'D' || x === 'R') {
|
|
337
|
+
staged++;
|
|
338
|
+
}
|
|
339
|
+
// Working tree status (y) or combined
|
|
340
|
+
if (y === 'M' || (x === 'M' && y === 'M')) {
|
|
341
|
+
modified++;
|
|
342
|
+
modifiedFiles.push(filePath);
|
|
343
|
+
}
|
|
344
|
+
else if (y === 'A' || x === 'A') {
|
|
345
|
+
added++;
|
|
346
|
+
modifiedFiles.push(filePath);
|
|
347
|
+
}
|
|
348
|
+
else if (y === 'D' || x === 'D') {
|
|
349
|
+
deleted++;
|
|
350
|
+
modifiedFiles.push(filePath);
|
|
351
|
+
}
|
|
352
|
+
else if (x === '?' && y === '?') {
|
|
353
|
+
untracked++;
|
|
354
|
+
}
|
|
355
|
+
else if (x === 'M') {
|
|
356
|
+
modified++;
|
|
357
|
+
modifiedFiles.push(filePath);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
name,
|
|
363
|
+
branch: worktree.branch,
|
|
364
|
+
modified,
|
|
365
|
+
added,
|
|
366
|
+
deleted,
|
|
367
|
+
untracked,
|
|
368
|
+
staged,
|
|
369
|
+
isClean: modified === 0 && added === 0 && deleted === 0 && untracked === 0,
|
|
370
|
+
modifiedFiles,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// ============================================================
|
|
374
|
+
// Cleanup
|
|
375
|
+
// ============================================================
|
|
376
|
+
/**
|
|
377
|
+
* Remove all worktrees (except the main one) and optionally delete branches.
|
|
378
|
+
*/
|
|
379
|
+
async cleanup() {
|
|
380
|
+
const worktrees = await this.listWorktrees();
|
|
381
|
+
for (const worktree of worktrees) {
|
|
382
|
+
// Skip the main worktree (the repo root itself)
|
|
383
|
+
if (worktree.isCurrent)
|
|
384
|
+
continue;
|
|
385
|
+
try {
|
|
386
|
+
await this.removeWorktree(worktree.name, { force: true });
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// Best-effort removal
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Clear all agent bindings
|
|
393
|
+
this.agentBindings.clear();
|
|
394
|
+
// Prune any stale worktree references
|
|
395
|
+
await this.execGit(['worktree', 'prune'], this.repoRoot);
|
|
396
|
+
this.emit('worktree:cleanup');
|
|
397
|
+
}
|
|
398
|
+
// ============================================================
|
|
399
|
+
// Private Helpers — Git Execution
|
|
400
|
+
// ============================================================
|
|
401
|
+
/**
|
|
402
|
+
* Execute a git command and return its output.
|
|
403
|
+
*/
|
|
404
|
+
execGit(args, cwd) {
|
|
405
|
+
return new Promise((resolve) => {
|
|
406
|
+
const child = spawn('git', args, {
|
|
407
|
+
cwd,
|
|
408
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
409
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
410
|
+
});
|
|
411
|
+
const stdoutChunks = [];
|
|
412
|
+
const stderrChunks = [];
|
|
413
|
+
child.stdout.on('data', (chunk) => {
|
|
414
|
+
stdoutChunks.push(chunk);
|
|
415
|
+
});
|
|
416
|
+
child.stderr.on('data', (chunk) => {
|
|
417
|
+
stderrChunks.push(chunk);
|
|
418
|
+
});
|
|
419
|
+
child.on('close', (code) => {
|
|
420
|
+
resolve({
|
|
421
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf-8'),
|
|
422
|
+
stderr: Buffer.concat(stderrChunks).toString('utf-8'),
|
|
423
|
+
exitCode: code ?? 0,
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
child.on('error', (err) => {
|
|
427
|
+
resolve({
|
|
428
|
+
stdout: '',
|
|
429
|
+
stderr: err.message,
|
|
430
|
+
exitCode: 1,
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
// ============================================================
|
|
436
|
+
// Private Helpers — Parsing & Analysis
|
|
437
|
+
// ============================================================
|
|
438
|
+
/**
|
|
439
|
+
* Parse the porcelain output of `git worktree list`.
|
|
440
|
+
*/
|
|
441
|
+
parseWorktreeList(output) {
|
|
442
|
+
const worktrees = [];
|
|
443
|
+
const blocks = output.trim().split('\n\n');
|
|
444
|
+
for (const block of blocks) {
|
|
445
|
+
if (!block.trim())
|
|
446
|
+
continue;
|
|
447
|
+
let path = '';
|
|
448
|
+
let head = '';
|
|
449
|
+
let branch = '';
|
|
450
|
+
let isBare = false;
|
|
451
|
+
const lines = block.split('\n');
|
|
452
|
+
for (const line of lines) {
|
|
453
|
+
if (line.startsWith('worktree ')) {
|
|
454
|
+
path = line.substring('worktree '.length);
|
|
455
|
+
}
|
|
456
|
+
else if (line.startsWith('HEAD ')) {
|
|
457
|
+
head = line.substring('HEAD '.length);
|
|
458
|
+
}
|
|
459
|
+
else if (line.startsWith('branch ')) {
|
|
460
|
+
branch = line.substring('branch '.length);
|
|
461
|
+
// Remove refs/heads/ prefix
|
|
462
|
+
if (branch.startsWith('refs/heads/')) {
|
|
463
|
+
branch = branch.substring('refs/heads/'.length);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else if (line === 'bare') {
|
|
467
|
+
isBare = true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const isCurrent = resolve(path) === resolve(this.repoRoot);
|
|
471
|
+
const name = this.worktreeNameFromPath(path, branch);
|
|
472
|
+
worktrees.push({
|
|
473
|
+
name,
|
|
474
|
+
path,
|
|
475
|
+
branch,
|
|
476
|
+
isCurrent,
|
|
477
|
+
isBare,
|
|
478
|
+
head: head.substring(0, 7), // short hash
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return worktrees;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Derive a human-readable name from the worktree path or branch.
|
|
485
|
+
*/
|
|
486
|
+
worktreeNameFromPath(path, branch) {
|
|
487
|
+
// If the branch name is available and not HEAD, use it
|
|
488
|
+
if (branch && branch !== 'detached HEAD') {
|
|
489
|
+
return branch;
|
|
490
|
+
}
|
|
491
|
+
// Otherwise, use the directory name
|
|
492
|
+
return basename(path);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Check whether a branch exists in the repository.
|
|
496
|
+
*/
|
|
497
|
+
async branchExists(branch) {
|
|
498
|
+
const result = await this.execGit(['rev-parse', '--verify', branch], this.repoRoot);
|
|
499
|
+
return result.exitCode === 0;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Get the list of modified files in a worktree.
|
|
503
|
+
*/
|
|
504
|
+
async getModifiedFiles(worktreePath) {
|
|
505
|
+
const result = await this.execGit(['status', '--porcelain=v1'], worktreePath);
|
|
506
|
+
if (result.exitCode !== 0)
|
|
507
|
+
return [];
|
|
508
|
+
const files = [];
|
|
509
|
+
const lines = result.stdout.trim().split('\n').filter(Boolean);
|
|
510
|
+
for (const line of lines) {
|
|
511
|
+
const x = line[0];
|
|
512
|
+
const y = line[1];
|
|
513
|
+
// Extract file path (after the two status chars and a space)
|
|
514
|
+
let filePath = line.substring(3);
|
|
515
|
+
// Handle rename (path includes "old -> new")
|
|
516
|
+
if (line.includes(' -> ')) {
|
|
517
|
+
const parts = filePath.split(' -> ');
|
|
518
|
+
filePath = parts[parts.length - 1];
|
|
519
|
+
}
|
|
520
|
+
let status;
|
|
521
|
+
if (x === 'R' || y === 'R') {
|
|
522
|
+
status = 'renamed';
|
|
523
|
+
}
|
|
524
|
+
else if (x === 'A' || y === 'A' || x === '?') {
|
|
525
|
+
status = 'added';
|
|
526
|
+
}
|
|
527
|
+
else if (x === 'D' || y === 'D') {
|
|
528
|
+
status = 'deleted';
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
status = 'modified';
|
|
532
|
+
}
|
|
533
|
+
files.push({ path: filePath, status });
|
|
534
|
+
}
|
|
535
|
+
return files;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Determine the type of conflict between two file modifications.
|
|
539
|
+
*/
|
|
540
|
+
determineConflictType(fileA, fileB) {
|
|
541
|
+
if (fileA.status === 'added' && fileB.status === 'added') {
|
|
542
|
+
return 'add_add';
|
|
543
|
+
}
|
|
544
|
+
if ((fileA.status === 'deleted' && fileB.status !== 'deleted') ||
|
|
545
|
+
(fileB.status === 'deleted' && fileA.status !== 'deleted')) {
|
|
546
|
+
return 'delete_modify';
|
|
547
|
+
}
|
|
548
|
+
if (fileA.status === 'renamed' || fileB.status === 'renamed') {
|
|
549
|
+
return 'rename';
|
|
550
|
+
}
|
|
551
|
+
return 'content';
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Analyze the diff between the same file in two worktrees to estimate
|
|
555
|
+
* conflict severity.
|
|
556
|
+
*/
|
|
557
|
+
async analyzeFileDiff(worktreeAPath, worktreeBPath, filePath) {
|
|
558
|
+
// Get the diff of each worktree's version against the merge base
|
|
559
|
+
const diffA = await this.execGit(['diff', 'HEAD', '--', filePath], worktreeAPath);
|
|
560
|
+
const diffB = await this.execGit(['diff', 'HEAD', '--', filePath], worktreeBPath);
|
|
561
|
+
if (diffA.exitCode !== 0 || diffB.exitCode !== 0) {
|
|
562
|
+
return { conflictingLines: 0, autoResolvable: false };
|
|
563
|
+
}
|
|
564
|
+
// Extract changed line ranges from each diff
|
|
565
|
+
const rangesA = this.extractChangedLineRanges(diffA.stdout);
|
|
566
|
+
const rangesB = this.extractChangedLineRanges(diffB.stdout);
|
|
567
|
+
// Count overlapping lines
|
|
568
|
+
let overlappingLines = 0;
|
|
569
|
+
for (const rangeA of rangesA) {
|
|
570
|
+
for (const rangeB of rangesB) {
|
|
571
|
+
const overlapStart = Math.max(rangeA.start, rangeB.start);
|
|
572
|
+
const overlapEnd = Math.min(rangeA.end, rangeB.end);
|
|
573
|
+
if (overlapStart <= overlapEnd) {
|
|
574
|
+
overlappingLines += overlapEnd - overlapStart + 1;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// If no overlapping changed lines, the merge should be auto-resolvable
|
|
579
|
+
const autoResolvable = overlappingLines === 0;
|
|
580
|
+
return {
|
|
581
|
+
conflictingLines: overlappingLines,
|
|
582
|
+
autoResolvable,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Extract changed line ranges from a unified diff output.
|
|
587
|
+
*/
|
|
588
|
+
extractChangedLineRanges(diffOutput) {
|
|
589
|
+
const ranges = [];
|
|
590
|
+
if (!diffOutput.trim())
|
|
591
|
+
return ranges;
|
|
592
|
+
const lines = diffOutput.split('\n');
|
|
593
|
+
for (const line of lines) {
|
|
594
|
+
// Match hunk headers like @@ -10,5 +10,7 @@
|
|
595
|
+
const match = line.match(/^@@@? -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@@?/);
|
|
596
|
+
if (match) {
|
|
597
|
+
const start = parseInt(match[1], 10);
|
|
598
|
+
const count = match[2] ? parseInt(match[2], 10) : 1;
|
|
599
|
+
ranges.push({ start, end: start + count - 1 });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return ranges;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Get the list of files changed in the last merge.
|
|
606
|
+
*/
|
|
607
|
+
async getChangedFilesInMerge() {
|
|
608
|
+
const result = await this.execGit(['diff', '--name-only', 'HEAD~1', 'HEAD'], this.repoRoot);
|
|
609
|
+
if (result.exitCode !== 0)
|
|
610
|
+
return [];
|
|
611
|
+
return result.stdout.trim().split('\n').filter(Boolean);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
//# sourceMappingURL=git-worktree.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface HeadlessOptions {
|
|
2
|
+
prompt: string;
|
|
3
|
+
model?: string;
|
|
4
|
+
agent?: string;
|
|
5
|
+
maxTurns?: number;
|
|
6
|
+
allowedTools?: string[];
|
|
7
|
+
deniedTools?: string[];
|
|
8
|
+
outputFormat?: 'text' | 'json' | 'stream-json';
|
|
9
|
+
autoApprove?: boolean;
|
|
10
|
+
workingDirectory?: string;
|
|
11
|
+
continueSession?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface HeadlessResult {
|
|
14
|
+
content: string;
|
|
15
|
+
model: string;
|
|
16
|
+
inputTokens: number;
|
|
17
|
+
outputTokens: number;
|
|
18
|
+
cost: number;
|
|
19
|
+
exitCode: number;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
duration: number;
|
|
22
|
+
toolCallsCount: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class HeadlessMode {
|
|
25
|
+
/**
|
|
26
|
+
* Run a single headless task and return structured result
|
|
27
|
+
*/
|
|
28
|
+
static run(options: HeadlessOptions): Promise<HeadlessResult>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=headless.d.ts.map
|