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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. 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