@ezmodo/mcp-server 0.13.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/README.md +305 -0
- package/config/development.js +20 -0
- package/config/endpoint-map.js +351 -0
- package/config/index.js +34 -0
- package/config/production.js +18 -0
- package/config/staging.js +18 -0
- package/handlers/access.js +141 -0
- package/handlers/activity.js +112 -0
- package/handlers/agents.js +95 -0
- package/handlers/ai-intelligence.js +55 -0
- package/handlers/attachments.js +30 -0
- package/handlers/catalogs.js +169 -0
- package/handlers/components.js +282 -0
- package/handlers/context-manifest.js +1150 -0
- package/handlers/decisions.js +114 -0
- package/handlers/designs.js +118 -0
- package/handlers/documents.js +227 -0
- package/handlers/entities.js +95 -0
- package/handlers/epics.js +190 -0
- package/handlers/facts.js +62 -0
- package/handlers/feature-flags.js +142 -0
- package/handlers/features.js +137 -0
- package/handlers/folders.js +127 -0
- package/handlers/git-context.js +917 -0
- package/handlers/github.js +72 -0
- package/handlers/graph.js +23 -0
- package/handlers/index.js +205 -0
- package/handlers/links.js +156 -0
- package/handlers/milestones.js +131 -0
- package/handlers/organizations.js +14 -0
- package/handlers/projects.js +122 -0
- package/handlers/recurring-tasks.js +33 -0
- package/handlers/tags.js +124 -0
- package/handlers/tasks.js +561 -0
- package/handlers/testing.js +116 -0
- package/handlers/todos.js +43 -0
- package/handlers/watchers.js +54 -0
- package/handlers/work-templates.js +32 -0
- package/index.js +175 -0
- package/lib/active-session.js +86 -0
- package/lib/auto-assign.js +93 -0
- package/lib/autolink.js +176 -0
- package/lib/changed-files.js +22 -0
- package/lib/env.js +45 -0
- package/lib/git-helpers.js +553 -0
- package/lib/git-utils.js +73 -0
- package/lib/http-client.js +164 -0
- package/lib/links-at-create.js +94 -0
- package/lib/local-cache.js +140 -0
- package/lib/logger.js +109 -0
- package/lib/manifest-loader.js +182 -0
- package/lib/manifest-query.js +686 -0
- package/lib/repo-config-dir.js +118 -0
- package/lib/version.js +10 -0
- package/lib/web-url.js +69 -0
- package/lib/worktree-tools.js +950 -0
- package/package.json +62 -0
- package/prompts/ai-workflow-automation.js +96 -0
- package/prompts/index.js +39 -0
- package/prompts/zephly-usage-guide-content.txt +631 -0
- package/prompts/zephly-usage-guide.js +119 -0
- package/tools/access-entity-types.js +28 -0
- package/tools/access.js +152 -0
- package/tools/activity.js +38 -0
- package/tools/agents.js +208 -0
- package/tools/ai-intelligence.js +111 -0
- package/tools/attachments.js +92 -0
- package/tools/catalogs.js +341 -0
- package/tools/components.js +249 -0
- package/tools/context-manifest.js +236 -0
- package/tools/decisions.js +168 -0
- package/tools/designs.js +222 -0
- package/tools/documents.js +287 -0
- package/tools/entities.js +223 -0
- package/tools/epics.js +267 -0
- package/tools/facts.js +70 -0
- package/tools/feature-flags.js +300 -0
- package/tools/features.js +246 -0
- package/tools/folders.js +122 -0
- package/tools/git-context.js +109 -0
- package/tools/github.js +172 -0
- package/tools/graph.js +70 -0
- package/tools/index.js +77 -0
- package/tools/link-params.js +93 -0
- package/tools/linkable-types.js +36 -0
- package/tools/links.js +199 -0
- package/tools/milestones.js +176 -0
- package/tools/organizations.js +23 -0
- package/tools/projects.js +172 -0
- package/tools/recurring-tasks.js +115 -0
- package/tools/tags.js +219 -0
- package/tools/task-item-schema.js +57 -0
- package/tools/task-type.js +33 -0
- package/tools/tasks.js +680 -0
- package/tools/testing.js +344 -0
- package/tools/todos.js +69 -0
- package/tools/watchers.js +81 -0
- package/tools/work-templates.js +96 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Worktree Tools for MCP Server
|
|
3
|
+
* Implements worktree management operations for AI agents
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { callZephlyAPI } from './http-client.js';
|
|
8
|
+
import { getLogger } from './logger.js';
|
|
9
|
+
import {
|
|
10
|
+
isGitRepository,
|
|
11
|
+
getRepositoryRoot,
|
|
12
|
+
branchExists,
|
|
13
|
+
createBranch,
|
|
14
|
+
createWorktree,
|
|
15
|
+
listWorktrees,
|
|
16
|
+
removeWorktree,
|
|
17
|
+
getWorktreeStatus,
|
|
18
|
+
getAheadBehindCounts,
|
|
19
|
+
checkForConflicts,
|
|
20
|
+
deleteBranch,
|
|
21
|
+
isBranchMerged,
|
|
22
|
+
getLastCommit,
|
|
23
|
+
generateBranchName,
|
|
24
|
+
generateEpicBranchName,
|
|
25
|
+
getCurrentBranch,
|
|
26
|
+
getCommitsSince,
|
|
27
|
+
getCommitUrlBase,
|
|
28
|
+
getMergeBase,
|
|
29
|
+
getDefaultBranch,
|
|
30
|
+
} from './git-helpers.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Tool Definitions for MCP Server
|
|
34
|
+
*/
|
|
35
|
+
export const WORKTREE_TOOLS = [
|
|
36
|
+
{
|
|
37
|
+
name: 'manage_worktree',
|
|
38
|
+
description: 'Create, sync, or clean up git worktrees for tasks and epics. ' +
|
|
39
|
+
'Worktrees enable parallel development by creating isolated workspaces.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
action: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
enum: ['create_epic', 'create_task', 'sync_status', 'link_branch_commits', 'cleanup'],
|
|
46
|
+
description: 'Action to perform',
|
|
47
|
+
},
|
|
48
|
+
// --- Identifiers ---
|
|
49
|
+
epicId: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'Epic ID (required for create_epic)',
|
|
52
|
+
},
|
|
53
|
+
taskId: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'Task ID (required for create_task, sync_status, link_branch_commits)',
|
|
56
|
+
},
|
|
57
|
+
projectId: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Project ID (required for cleanup)',
|
|
60
|
+
},
|
|
61
|
+
// --- Shared creation fields ---
|
|
62
|
+
repositoryPath: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'Path to the git repository root (create_epic, create_task)',
|
|
65
|
+
},
|
|
66
|
+
worktreePath: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'Path where the worktree should be created (create_epic, create_task) or synced from (sync_status)',
|
|
69
|
+
},
|
|
70
|
+
baseBranch: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'Base branch to branch from (create_epic, create_task, link_branch_commits)',
|
|
73
|
+
},
|
|
74
|
+
branchName: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'Custom branch name. Auto-generated if not provided (create_epic, create_task)',
|
|
77
|
+
},
|
|
78
|
+
// --- create_epic specific ---
|
|
79
|
+
updateTasks: {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
description: 'Update all tasks in the epic to reference the epic branch (create_epic only). Default: true',
|
|
82
|
+
default: true,
|
|
83
|
+
},
|
|
84
|
+
// --- sync_status specific ---
|
|
85
|
+
autoLinkCommits: {
|
|
86
|
+
type: 'boolean',
|
|
87
|
+
description: 'Automatically detect and link new commits (sync_status only). Default: true',
|
|
88
|
+
default: true,
|
|
89
|
+
},
|
|
90
|
+
// --- link_branch_commits specific ---
|
|
91
|
+
repoPath: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
description: 'Path to the git repository (link_branch_commits only). Defaults to cwd.',
|
|
94
|
+
},
|
|
95
|
+
branch: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: 'Branch to get commits from (link_branch_commits only). Defaults to current.',
|
|
98
|
+
},
|
|
99
|
+
sinceCommit: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Only link commits after this SHA (link_branch_commits only)',
|
|
102
|
+
},
|
|
103
|
+
maxCommits: {
|
|
104
|
+
type: 'number',
|
|
105
|
+
description: 'Max commits to link (link_branch_commits only). Default: 100',
|
|
106
|
+
default: 100,
|
|
107
|
+
},
|
|
108
|
+
// --- cleanup specific ---
|
|
109
|
+
taskIds: {
|
|
110
|
+
type: 'array',
|
|
111
|
+
items: { type: 'string' },
|
|
112
|
+
description: 'Specific task IDs to clean up (cleanup only). Omit for all completed/cancelled.',
|
|
113
|
+
},
|
|
114
|
+
dryRun: {
|
|
115
|
+
type: 'boolean',
|
|
116
|
+
description: 'Preview cleanup without making changes (cleanup only)',
|
|
117
|
+
default: false,
|
|
118
|
+
},
|
|
119
|
+
deleteBranches: {
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
description: 'Also delete merged branches (cleanup only)',
|
|
122
|
+
default: false,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
required: ['action'],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'list_project_worktrees',
|
|
130
|
+
description: 'List all worktrees for a project with their associated tasks and status.',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
projectId: {
|
|
135
|
+
type: 'string',
|
|
136
|
+
description: 'Project ID to list worktrees for',
|
|
137
|
+
},
|
|
138
|
+
repositoryPath: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Path to the git repository. If not provided, attempts to detect from project metadata.',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
required: ['projectId'],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Dispatch manage_worktree actions to the appropriate handler
|
|
150
|
+
*/
|
|
151
|
+
export async function manageWorktree(args) {
|
|
152
|
+
const { action, ...params } = args;
|
|
153
|
+
switch (action) {
|
|
154
|
+
case 'create_epic': return createEpicWorktree(params);
|
|
155
|
+
case 'create_task': return createTaskWorktree(params);
|
|
156
|
+
case 'sync_status': return syncTaskWorktreeStatus(params);
|
|
157
|
+
case 'link_branch_commits': return linkBranchCommitsToTask(params);
|
|
158
|
+
case 'cleanup': return cleanupTaskWorktrees(params);
|
|
159
|
+
default: throw new Error(`Unknown action: ${action}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a worktree for an epic
|
|
165
|
+
* @param {object} args - Arguments from MCP tool call
|
|
166
|
+
* @returns {object} Result with worktree info
|
|
167
|
+
*/
|
|
168
|
+
async function createEpicWorktree(args) {
|
|
169
|
+
const { epicId, repositoryPath, worktreePath, baseBranch, branchName, updateTasks = true } = args;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Get epic details
|
|
173
|
+
// Note: callZephlyAPI unwraps Go API responses, so we get {epic} directly
|
|
174
|
+
const epicData = await callZephlyAPI('mcpGetEpic', { epicId });
|
|
175
|
+
if (!epicData || !epicData.epic) {
|
|
176
|
+
throw new Error('Epic not found');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const epic = epicData.epic;
|
|
180
|
+
|
|
181
|
+
// Get project context to determine repository path and base branch
|
|
182
|
+
// Project-First: epics belong to projects (required)
|
|
183
|
+
let projectResult = null;
|
|
184
|
+
if (epic.projectId) {
|
|
185
|
+
try {
|
|
186
|
+
projectResult = await callZephlyAPI('mcpGetProjectContext', {
|
|
187
|
+
projectId: epic.projectId,
|
|
188
|
+
});
|
|
189
|
+
} catch (err) {
|
|
190
|
+
// Project context is optional, continue with defaults
|
|
191
|
+
getLogger().warn('Could not get project context for worktree', { error: err.message });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Determine repository path
|
|
196
|
+
let repoPath = repositoryPath;
|
|
197
|
+
if (!repoPath && projectResult?.project?.gitContext?.repositoryPath) {
|
|
198
|
+
repoPath = projectResult.project.gitContext.repositoryPath;
|
|
199
|
+
}
|
|
200
|
+
if (!repoPath) {
|
|
201
|
+
repoPath = process.cwd();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Verify it's a git repository
|
|
205
|
+
if (!isGitRepository(repoPath)) {
|
|
206
|
+
throw new Error(`Not a git repository: ${repoPath}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Get repository root
|
|
210
|
+
const repoRoot = getRepositoryRoot(repoPath);
|
|
211
|
+
if (!repoRoot) {
|
|
212
|
+
throw new Error('Could not determine repository root');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Determine base branch
|
|
216
|
+
let baseRef = baseBranch;
|
|
217
|
+
if (!baseRef && projectResult?.project?.gitContext?.defaultBranch) {
|
|
218
|
+
baseRef = projectResult.project.gitContext.defaultBranch;
|
|
219
|
+
}
|
|
220
|
+
if (!baseRef) {
|
|
221
|
+
baseRef = getCurrentBranch(repoRoot) || 'main';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Generate or use provided branch name
|
|
225
|
+
const newBranchName = branchName || generateEpicBranchName(epic);
|
|
226
|
+
|
|
227
|
+
// Check if branch already exists
|
|
228
|
+
const exists = branchExists(repoRoot, newBranchName);
|
|
229
|
+
if (exists.local) {
|
|
230
|
+
throw new Error(`Branch ${newBranchName} already exists locally`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Determine worktree path
|
|
234
|
+
let wtPath = worktreePath;
|
|
235
|
+
if (!wtPath && projectResult?.project?.gitContext?.defaultWorktreePath) {
|
|
236
|
+
wtPath = path.join(repoRoot, projectResult.project.gitContext.defaultWorktreePath, `epic-${epicId}`);
|
|
237
|
+
}
|
|
238
|
+
if (!wtPath) {
|
|
239
|
+
wtPath = path.join(path.dirname(repoRoot), 'worktrees', `epic-${epicId}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Create branch
|
|
243
|
+
const baseCommit = createBranch(repoRoot, newBranchName, baseRef);
|
|
244
|
+
|
|
245
|
+
// Create worktree
|
|
246
|
+
createWorktree(repoRoot, wtPath, newBranchName, false);
|
|
247
|
+
|
|
248
|
+
// Update epic with git context
|
|
249
|
+
const epicGitContext = {
|
|
250
|
+
branchName: newBranchName,
|
|
251
|
+
baseBranch: baseRef,
|
|
252
|
+
baseCommit,
|
|
253
|
+
lastSyncedAt: new Date(),
|
|
254
|
+
isDirty: false,
|
|
255
|
+
aheadBy: 0,
|
|
256
|
+
behindBy: 0,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
await callZephlyAPI('mcpUpdateEpic', {
|
|
260
|
+
epicId,
|
|
261
|
+
gitContext: epicGitContext,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Optionally update all tasks in the epic to reference the epic branch
|
|
265
|
+
let updatedTaskCount = 0;
|
|
266
|
+
if (updateTasks) {
|
|
267
|
+
// Get all tasks in the epic
|
|
268
|
+
// Project-First: tasks belong to projects with optional epic grouping
|
|
269
|
+
const searchParams = { epicId };
|
|
270
|
+
if (epic.projectId) {
|
|
271
|
+
searchParams.projectId = epic.projectId;
|
|
272
|
+
}
|
|
273
|
+
const tasksData = await callZephlyAPI('mcpSearchTasks', searchParams);
|
|
274
|
+
|
|
275
|
+
if (tasksData?.tasks) {
|
|
276
|
+
for (const task of tasksData.tasks) {
|
|
277
|
+
await callZephlyAPI('mcpUpdateTask', {
|
|
278
|
+
taskId: task.id,
|
|
279
|
+
gitContext: {
|
|
280
|
+
branchName: newBranchName,
|
|
281
|
+
baseBranch: baseRef,
|
|
282
|
+
baseCommit,
|
|
283
|
+
lastSyncedAt: new Date(),
|
|
284
|
+
isDirty: false,
|
|
285
|
+
aheadBy: 0,
|
|
286
|
+
behindBy: 0,
|
|
287
|
+
isEpicBranch: true, // Flag to indicate this is shared via epic
|
|
288
|
+
epicId, // Reference back to epic
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
updatedTaskCount++;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
success: true,
|
|
298
|
+
worktreePath: wtPath,
|
|
299
|
+
branchName: newBranchName,
|
|
300
|
+
baseBranch: baseRef,
|
|
301
|
+
baseCommit,
|
|
302
|
+
updatedTaskCount,
|
|
303
|
+
message: `Created epic worktree at ${wtPath} on branch ` +
|
|
304
|
+
`${newBranchName}` +
|
|
305
|
+
`${updatedTaskCount > 0
|
|
306
|
+
? ` (updated ${updatedTaskCount} tasks)` : ''}`,
|
|
307
|
+
};
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
error: error.message,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Create a worktree for a task
|
|
318
|
+
* @param {object} args - Arguments from MCP tool call
|
|
319
|
+
* @returns {object} Result with worktree info
|
|
320
|
+
*/
|
|
321
|
+
async function createTaskWorktree(args) {
|
|
322
|
+
const { taskId, repositoryPath, worktreePath, baseBranch, branchName } = args;
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
// Get task details
|
|
326
|
+
// Note: callZephlyAPI unwraps Go API responses, so we get {task} directly
|
|
327
|
+
const taskData = await callZephlyAPI('mcpGetTask', { taskId });
|
|
328
|
+
if (!taskData || !taskData.task) {
|
|
329
|
+
throw new Error('Task not found');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const task = taskData.task;
|
|
333
|
+
|
|
334
|
+
// Get project context to determine repository path and base branch
|
|
335
|
+
const projectResult = await callZephlyAPI('mcpGetProjectContext', {
|
|
336
|
+
projectId: task.projectId,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Determine repository path
|
|
340
|
+
let repoPath = repositoryPath;
|
|
341
|
+
if (!repoPath && projectResult?.project?.gitContext?.repositoryPath) {
|
|
342
|
+
repoPath = projectResult.project.gitContext.repositoryPath;
|
|
343
|
+
}
|
|
344
|
+
if (!repoPath) {
|
|
345
|
+
repoPath = process.cwd();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Verify it's a git repository
|
|
349
|
+
if (!isGitRepository(repoPath)) {
|
|
350
|
+
throw new Error(`Not a git repository: ${repoPath}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Get repository root
|
|
354
|
+
const repoRoot = getRepositoryRoot(repoPath);
|
|
355
|
+
if (!repoRoot) {
|
|
356
|
+
throw new Error('Could not determine repository root');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Determine base branch
|
|
360
|
+
let baseRef = baseBranch;
|
|
361
|
+
if (!baseRef && projectResult?.project?.gitContext?.defaultBranch) {
|
|
362
|
+
baseRef = projectResult.project.gitContext.defaultBranch;
|
|
363
|
+
}
|
|
364
|
+
if (!baseRef) {
|
|
365
|
+
baseRef = getCurrentBranch(repoRoot) || 'main';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Generate or use provided branch name
|
|
369
|
+
const newBranchName = branchName || generateBranchName(task);
|
|
370
|
+
|
|
371
|
+
// Check if branch already exists
|
|
372
|
+
const exists = branchExists(repoRoot, newBranchName);
|
|
373
|
+
if (exists.local) {
|
|
374
|
+
throw new Error(`Branch ${newBranchName} already exists locally`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Determine worktree path
|
|
378
|
+
let wtPath = worktreePath;
|
|
379
|
+
if (!wtPath && projectResult?.project?.gitContext?.defaultWorktreePath) {
|
|
380
|
+
wtPath = path.join(repoRoot, projectResult.project.gitContext.defaultWorktreePath, taskId);
|
|
381
|
+
}
|
|
382
|
+
if (!wtPath) {
|
|
383
|
+
wtPath = path.join(path.dirname(repoRoot), 'worktrees', taskId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Create branch
|
|
387
|
+
const baseCommit = createBranch(repoRoot, newBranchName, baseRef);
|
|
388
|
+
|
|
389
|
+
// Create worktree
|
|
390
|
+
createWorktree(repoRoot, wtPath, newBranchName, false);
|
|
391
|
+
|
|
392
|
+
// Update task with git context
|
|
393
|
+
const gitContext = {
|
|
394
|
+
branchName: newBranchName,
|
|
395
|
+
baseBranch: baseRef,
|
|
396
|
+
baseCommit,
|
|
397
|
+
lastSyncedAt: new Date(),
|
|
398
|
+
isDirty: false,
|
|
399
|
+
aheadBy: 0,
|
|
400
|
+
behindBy: 0,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
await callZephlyAPI('mcpUpdateTask', {
|
|
404
|
+
taskId,
|
|
405
|
+
gitContext,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
worktreePath: wtPath,
|
|
411
|
+
branchName: newBranchName,
|
|
412
|
+
baseBranch: baseRef,
|
|
413
|
+
baseCommit,
|
|
414
|
+
message: `Created worktree at ${wtPath} on branch ${newBranchName}`,
|
|
415
|
+
};
|
|
416
|
+
} catch (error) {
|
|
417
|
+
return {
|
|
418
|
+
success: false,
|
|
419
|
+
error: error.message,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Sync worktree status to task
|
|
426
|
+
* @param {object} args - Arguments from MCP tool call
|
|
427
|
+
* @param {string} args.taskId - Task ID
|
|
428
|
+
* @param {string} [args.worktreePath] - Path to worktree (optional if stored in task)
|
|
429
|
+
* @param {boolean} [args.autoLinkCommits=true] - Automatically link new commits
|
|
430
|
+
* @returns {object} Result with status info
|
|
431
|
+
*/
|
|
432
|
+
async function syncTaskWorktreeStatus(args) {
|
|
433
|
+
const { taskId, worktreePath, autoLinkCommits = true } = args;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
// Get task details
|
|
437
|
+
// Note: callZephlyAPI unwraps Go API responses, so we get {task} directly
|
|
438
|
+
const taskData = await callZephlyAPI('mcpGetTask', { taskId });
|
|
439
|
+
if (!taskData || !taskData.task) {
|
|
440
|
+
throw new Error('Task not found');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const task = taskData.task;
|
|
444
|
+
|
|
445
|
+
if (!task.gitContext || !task.gitContext.branchName) {
|
|
446
|
+
throw new Error('Task has no git context');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Determine worktree path
|
|
450
|
+
let wtPath = worktreePath;
|
|
451
|
+
if (!wtPath && task.gitContext.worktreePath) {
|
|
452
|
+
wtPath = task.gitContext.worktreePath;
|
|
453
|
+
}
|
|
454
|
+
if (!wtPath) {
|
|
455
|
+
throw new Error('Worktree path not specified and not found in task');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Verify worktree exists
|
|
459
|
+
if (!isGitRepository(wtPath)) {
|
|
460
|
+
throw new Error(`Worktree not found or not a git repository: ${wtPath}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Get status
|
|
464
|
+
const status = getWorktreeStatus(wtPath);
|
|
465
|
+
|
|
466
|
+
// Get ahead/behind counts
|
|
467
|
+
const { ahead, behind } = getAheadBehindCounts(
|
|
468
|
+
wtPath,
|
|
469
|
+
task.gitContext.branchName,
|
|
470
|
+
`origin/${task.gitContext.baseBranch}`
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Check for conflicts
|
|
474
|
+
const conflicts = checkForConflicts(wtPath);
|
|
475
|
+
|
|
476
|
+
// Get last commit info
|
|
477
|
+
const lastCommit = getLastCommit(wtPath, task.gitContext.branchName);
|
|
478
|
+
|
|
479
|
+
// Auto-link new commits if enabled
|
|
480
|
+
let newlyLinkedCommits = [];
|
|
481
|
+
if (autoLinkCommits) {
|
|
482
|
+
// Determine the starting point for detecting new commits
|
|
483
|
+
// Use lastLinkedCommitSha if available, otherwise fall back to baseCommit
|
|
484
|
+
const sinceCommit = task.gitContext.lastLinkedCommitSha || task.gitContext.baseCommit;
|
|
485
|
+
|
|
486
|
+
// Get all commits since the last linked commit
|
|
487
|
+
const commits = getCommitsSince(wtPath, sinceCommit, task.gitContext.branchName);
|
|
488
|
+
|
|
489
|
+
// Get existing linked commit SHAs to avoid duplicates
|
|
490
|
+
const existingCommitShas = new Set(
|
|
491
|
+
(task.linkedCommits || []).map(c => c.sha)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// Filter to only new commits
|
|
495
|
+
const newCommits = commits.filter(c => !existingCommitShas.has(c.sha));
|
|
496
|
+
|
|
497
|
+
// Get commit URL base for generating links
|
|
498
|
+
const commitUrlBase = getCommitUrlBase(wtPath);
|
|
499
|
+
|
|
500
|
+
// Link each new commit
|
|
501
|
+
for (const commit of newCommits) {
|
|
502
|
+
try {
|
|
503
|
+
const commitUrl = commitUrlBase ? `${commitUrlBase}/commit/${commit.sha}` : undefined;
|
|
504
|
+
|
|
505
|
+
await callZephlyAPI('mcpLinkCommitToTask', {
|
|
506
|
+
taskId,
|
|
507
|
+
sha: commit.sha,
|
|
508
|
+
message: commit.message,
|
|
509
|
+
author: commit.author,
|
|
510
|
+
email: commit.email,
|
|
511
|
+
timestamp: commit.timestamp.toISOString(),
|
|
512
|
+
url: commitUrl,
|
|
513
|
+
branch: task.gitContext.branchName,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
newlyLinkedCommits.push(commit);
|
|
517
|
+
} catch (linkError) {
|
|
518
|
+
// Log but don't fail the sync if a single commit link fails
|
|
519
|
+
getLogger().warn('Failed to link commit', { sha: commit.shortSha, error: linkError.message });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Update task git context
|
|
525
|
+
const updatedGitContext = {
|
|
526
|
+
...task.gitContext,
|
|
527
|
+
isDirty: status.isDirty,
|
|
528
|
+
aheadBy: ahead,
|
|
529
|
+
behindBy: behind,
|
|
530
|
+
hasConflicts: conflicts.hasConflicts,
|
|
531
|
+
conflictFiles: conflicts.files,
|
|
532
|
+
lastSyncedAt: new Date(),
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
if (lastCommit) {
|
|
536
|
+
updatedGitContext.lastPushedBy = lastCommit.author;
|
|
537
|
+
updatedGitContext.lastPushedAt = lastCommit.date;
|
|
538
|
+
// Track the last commit we've linked to avoid re-linking on next sync
|
|
539
|
+
if (autoLinkCommits && newlyLinkedCommits.length > 0) {
|
|
540
|
+
updatedGitContext.lastLinkedCommitSha = lastCommit.hash;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
await callZephlyAPI('mcpUpdateTask', {
|
|
545
|
+
taskId,
|
|
546
|
+
gitContext: updatedGitContext,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
success: true,
|
|
551
|
+
status: {
|
|
552
|
+
isDirty: status.isDirty,
|
|
553
|
+
aheadBy: ahead,
|
|
554
|
+
behindBy: behind,
|
|
555
|
+
hasConflicts: conflicts.hasConflicts,
|
|
556
|
+
conflictFiles: conflicts.files,
|
|
557
|
+
files: status.files,
|
|
558
|
+
lastCommit,
|
|
559
|
+
},
|
|
560
|
+
linkedCommits: newlyLinkedCommits.length > 0 ? {
|
|
561
|
+
count: newlyLinkedCommits.length,
|
|
562
|
+
commits: newlyLinkedCommits.map(c => ({ sha: c.shortSha, message: c.message })),
|
|
563
|
+
} : undefined,
|
|
564
|
+
};
|
|
565
|
+
} catch (error) {
|
|
566
|
+
return {
|
|
567
|
+
success: false,
|
|
568
|
+
error: error.message,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Link commits from a branch to a task (works without worktree)
|
|
575
|
+
* @param {object} args - Arguments from MCP tool call
|
|
576
|
+
* @param {string} args.taskId - Task ID to link commits to
|
|
577
|
+
* @param {string} [args.repoPath] - Path to git repository (defaults to cwd)
|
|
578
|
+
* @param {string} [args.branch] - Branch to get commits from (defaults to current branch)
|
|
579
|
+
* @param {string} [args.sinceCommit] - Only link commits after this SHA
|
|
580
|
+
* @param {string} [args.baseBranch] - Base branch to compare against (defaults to main/master)
|
|
581
|
+
* @param {number} [args.maxCommits=100] - Maximum number of commits to link
|
|
582
|
+
* @returns {object} Result with linked commits info
|
|
583
|
+
*/
|
|
584
|
+
async function linkBranchCommitsToTask(args) {
|
|
585
|
+
const { taskId, repoPath, branch, sinceCommit, baseBranch, maxCommits = 100 } = args;
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
// Determine repo path
|
|
589
|
+
const gitRepoPath = repoPath || process.cwd();
|
|
590
|
+
|
|
591
|
+
// Verify it's a git repository
|
|
592
|
+
if (!isGitRepository(gitRepoPath)) {
|
|
593
|
+
throw new Error(`Not a git repository: ${gitRepoPath}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Get current branch if not specified
|
|
597
|
+
const targetBranch = branch || getCurrentBranch(gitRepoPath);
|
|
598
|
+
if (!targetBranch) {
|
|
599
|
+
throw new Error('Could not determine current branch');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Get task to check for existing linked commits
|
|
603
|
+
const taskData = await callZephlyAPI('mcpGetTask', { taskId });
|
|
604
|
+
if (!taskData || !taskData.task) {
|
|
605
|
+
throw new Error('Task not found');
|
|
606
|
+
}
|
|
607
|
+
const task = taskData.task;
|
|
608
|
+
|
|
609
|
+
// Determine starting point for commits
|
|
610
|
+
let startCommit = sinceCommit;
|
|
611
|
+
if (!startCommit) {
|
|
612
|
+
// Check if task has gitContext with tracking info
|
|
613
|
+
if (task.gitContext?.lastLinkedCommitSha) {
|
|
614
|
+
startCommit = task.gitContext.lastLinkedCommitSha;
|
|
615
|
+
} else if (task.gitContext?.baseCommit) {
|
|
616
|
+
startCommit = task.gitContext.baseCommit;
|
|
617
|
+
} else {
|
|
618
|
+
// Find merge base with main/master
|
|
619
|
+
const defaultBase = baseBranch || getDefaultBranch(gitRepoPath);
|
|
620
|
+
startCommit = getMergeBase(gitRepoPath, targetBranch, defaultBase);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Get commits since start point
|
|
625
|
+
let commits = getCommitsSince(gitRepoPath, startCommit, targetBranch);
|
|
626
|
+
|
|
627
|
+
// Limit commits
|
|
628
|
+
if (commits.length > maxCommits) {
|
|
629
|
+
commits = commits.slice(0, maxCommits);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Get existing linked commit SHAs to avoid duplicates
|
|
633
|
+
const existingCommitShas = new Set(
|
|
634
|
+
(task.linkedCommits || []).map(c => c.sha)
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
// Filter to only new commits
|
|
638
|
+
const newCommits = commits.filter(c => !existingCommitShas.has(c.sha));
|
|
639
|
+
|
|
640
|
+
if (newCommits.length === 0) {
|
|
641
|
+
return {
|
|
642
|
+
success: true,
|
|
643
|
+
message: 'No new commits to link',
|
|
644
|
+
branch: targetBranch,
|
|
645
|
+
existingCommitCount: existingCommitShas.size,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Get commit URL base for generating links
|
|
650
|
+
const commitUrlBase = getCommitUrlBase(gitRepoPath);
|
|
651
|
+
|
|
652
|
+
// Link each new commit
|
|
653
|
+
const linkedCommits = [];
|
|
654
|
+
const errors = [];
|
|
655
|
+
|
|
656
|
+
for (const commit of newCommits) {
|
|
657
|
+
try {
|
|
658
|
+
const commitUrl = commitUrlBase ? `${commitUrlBase}/commit/${commit.sha}` : undefined;
|
|
659
|
+
|
|
660
|
+
await callZephlyAPI('mcpLinkCommitToTask', {
|
|
661
|
+
taskId,
|
|
662
|
+
sha: commit.sha,
|
|
663
|
+
message: commit.message,
|
|
664
|
+
author: commit.author,
|
|
665
|
+
email: commit.email,
|
|
666
|
+
timestamp: commit.timestamp.toISOString(),
|
|
667
|
+
url: commitUrl,
|
|
668
|
+
branch: targetBranch,
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
linkedCommits.push(commit);
|
|
672
|
+
} catch (linkError) {
|
|
673
|
+
errors.push({ sha: commit.shortSha, error: linkError.message });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Update task gitContext with tracking info if not already set
|
|
678
|
+
if (!task.gitContext || !task.gitContext.branchName) {
|
|
679
|
+
const lastCommit = getLastCommit(gitRepoPath, targetBranch);
|
|
680
|
+
const defaultBase = baseBranch || getDefaultBranch(gitRepoPath);
|
|
681
|
+
|
|
682
|
+
await callZephlyAPI('mcpUpdateTask', {
|
|
683
|
+
taskId,
|
|
684
|
+
gitContext: {
|
|
685
|
+
...task.gitContext,
|
|
686
|
+
branchName: targetBranch,
|
|
687
|
+
baseBranch: defaultBase,
|
|
688
|
+
baseCommit: startCommit || getMergeBase(gitRepoPath, targetBranch, defaultBase),
|
|
689
|
+
lastLinkedCommitSha: lastCommit?.hash,
|
|
690
|
+
lastSyncedAt: new Date(),
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
} else if (linkedCommits.length > 0) {
|
|
694
|
+
// Just update the last linked commit SHA
|
|
695
|
+
const lastCommit = getLastCommit(gitRepoPath, targetBranch);
|
|
696
|
+
await callZephlyAPI('mcpUpdateTask', {
|
|
697
|
+
taskId,
|
|
698
|
+
gitContext: {
|
|
699
|
+
...task.gitContext,
|
|
700
|
+
lastLinkedCommitSha: lastCommit?.hash,
|
|
701
|
+
lastSyncedAt: new Date(),
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
success: true,
|
|
708
|
+
branch: targetBranch,
|
|
709
|
+
linkedCommits: {
|
|
710
|
+
count: linkedCommits.length,
|
|
711
|
+
commits: linkedCommits.map(c => ({ sha: c.shortSha, message: c.message })),
|
|
712
|
+
},
|
|
713
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
714
|
+
skipped: commits.length - newCommits.length,
|
|
715
|
+
};
|
|
716
|
+
} catch (error) {
|
|
717
|
+
return {
|
|
718
|
+
success: false,
|
|
719
|
+
error: error.message,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Clean up worktrees for completed/cancelled tasks
|
|
726
|
+
* @param {object} args - Arguments from MCP tool call
|
|
727
|
+
* @returns {object} Result with cleanup info
|
|
728
|
+
*/
|
|
729
|
+
async function cleanupTaskWorktrees(args) {
|
|
730
|
+
const { projectId, taskIds, dryRun = false, deleteBranches = false } = args;
|
|
731
|
+
|
|
732
|
+
try {
|
|
733
|
+
// Get project context
|
|
734
|
+
const projectData = await callZephlyAPI('mcpGetProjectContext', { projectId });
|
|
735
|
+
if (!projectData?.project) {
|
|
736
|
+
throw new Error('Failed to get project context');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Determine repository path
|
|
740
|
+
const repoPath = projectData.project?.gitContext?.repositoryPath || process.cwd();
|
|
741
|
+
|
|
742
|
+
if (!isGitRepository(repoPath)) {
|
|
743
|
+
throw new Error(`Not a git repository: ${repoPath}`);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const repoRoot = getRepositoryRoot(repoPath);
|
|
747
|
+
|
|
748
|
+
// Get tasks to clean up
|
|
749
|
+
let tasksToCleanup = [];
|
|
750
|
+
if (taskIds && taskIds.length > 0) {
|
|
751
|
+
// Specific tasks
|
|
752
|
+
for (const taskId of taskIds) {
|
|
753
|
+
const taskData = await callZephlyAPI('mcpGetTask', { taskId });
|
|
754
|
+
if (taskData?.task) {
|
|
755
|
+
tasksToCleanup.push(taskData.task);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
// Find all completed/cancelled tasks with worktrees
|
|
760
|
+
const completedData = await callZephlyAPI('mcpSearchTasks', {
|
|
761
|
+
projectId,
|
|
762
|
+
status: 'completed',
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const cancelledData = await callZephlyAPI('mcpSearchTasks', {
|
|
766
|
+
projectId,
|
|
767
|
+
status: 'cancelled',
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
if (completedData?.tasks) {
|
|
771
|
+
tasksToCleanup.push(...completedData.tasks);
|
|
772
|
+
}
|
|
773
|
+
if (cancelledData?.tasks) {
|
|
774
|
+
tasksToCleanup.push(...cancelledData.tasks);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Filter to only tasks with git context
|
|
778
|
+
tasksToCleanup = tasksToCleanup.filter(t => t.gitContext && t.gitContext.branchName);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Clean up each worktree
|
|
782
|
+
const results = [];
|
|
783
|
+
const existingWorktrees = listWorktrees(repoRoot);
|
|
784
|
+
|
|
785
|
+
for (const task of tasksToCleanup) {
|
|
786
|
+
const result = {
|
|
787
|
+
taskId: task.id,
|
|
788
|
+
taskTitle: task.title,
|
|
789
|
+
branchName: task.gitContext.branchName,
|
|
790
|
+
action: 'skipped',
|
|
791
|
+
reason: '',
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// Find matching worktree
|
|
795
|
+
const worktree = existingWorktrees.find(
|
|
796
|
+
wt => wt.branch === task.gitContext.branchName
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
if (!worktree) {
|
|
800
|
+
result.reason = 'Worktree not found';
|
|
801
|
+
results.push(result);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (dryRun) {
|
|
806
|
+
result.action = 'would-remove';
|
|
807
|
+
result.worktreePath = worktree.path;
|
|
808
|
+
|
|
809
|
+
if (deleteBranches) {
|
|
810
|
+
const merged = isBranchMerged(
|
|
811
|
+
repoRoot,
|
|
812
|
+
task.gitContext.branchName,
|
|
813
|
+
task.gitContext.baseBranch
|
|
814
|
+
);
|
|
815
|
+
if (merged) {
|
|
816
|
+
result.branchAction = 'would-delete';
|
|
817
|
+
result.reason = 'Branch is merged';
|
|
818
|
+
} else {
|
|
819
|
+
result.branchAction = 'would-keep';
|
|
820
|
+
result.reason = 'Branch not merged';
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
} else {
|
|
824
|
+
try {
|
|
825
|
+
// Remove worktree
|
|
826
|
+
removeWorktree(repoRoot, worktree.path, true);
|
|
827
|
+
result.action = 'removed';
|
|
828
|
+
result.worktreePath = worktree.path;
|
|
829
|
+
|
|
830
|
+
// Delete branch if requested and merged
|
|
831
|
+
if (deleteBranches) {
|
|
832
|
+
const merged = isBranchMerged(
|
|
833
|
+
repoRoot,
|
|
834
|
+
task.gitContext.branchName,
|
|
835
|
+
task.gitContext.baseBranch
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
if (merged) {
|
|
839
|
+
deleteBranch(repoRoot, task.gitContext.branchName, false);
|
|
840
|
+
result.branchAction = 'deleted';
|
|
841
|
+
result.reason = 'Branch was merged';
|
|
842
|
+
} else {
|
|
843
|
+
result.branchAction = 'kept';
|
|
844
|
+
result.reason = 'Branch not merged (kept for safety)';
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Clear git context from task
|
|
849
|
+
await callZephlyAPI('mcpUpdateTask', {
|
|
850
|
+
taskId: task.id,
|
|
851
|
+
gitContext: null,
|
|
852
|
+
});
|
|
853
|
+
} catch (error) {
|
|
854
|
+
result.action = 'failed';
|
|
855
|
+
result.reason = error.message;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
results.push(result);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return {
|
|
863
|
+
success: true,
|
|
864
|
+
dryRun,
|
|
865
|
+
cleaned: results.filter(r => r.action === 'removed').length,
|
|
866
|
+
wouldClean: results.filter(r => r.action === 'would-remove').length,
|
|
867
|
+
results,
|
|
868
|
+
};
|
|
869
|
+
} catch (error) {
|
|
870
|
+
return {
|
|
871
|
+
success: false,
|
|
872
|
+
error: error.message,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* List all worktrees for a project
|
|
879
|
+
* @param {object} args - Arguments from MCP tool call
|
|
880
|
+
* @returns {object} Result with worktree list
|
|
881
|
+
*/
|
|
882
|
+
export async function listProjectWorktrees(args) {
|
|
883
|
+
const { projectId, repositoryPath } = args;
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
// Get project context
|
|
887
|
+
const projectData = await callZephlyAPI('mcpGetProjectContext', { projectId });
|
|
888
|
+
if (!projectData?.project) {
|
|
889
|
+
throw new Error('Failed to get project context');
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Determine repository path
|
|
893
|
+
const repoPath = repositoryPath || projectData.project?.gitContext?.repositoryPath || process.cwd();
|
|
894
|
+
|
|
895
|
+
if (!isGitRepository(repoPath)) {
|
|
896
|
+
throw new Error(`Not a git repository: ${repoPath}`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const repoRoot = getRepositoryRoot(repoPath);
|
|
900
|
+
|
|
901
|
+
// List all worktrees
|
|
902
|
+
const worktrees = listWorktrees(repoRoot);
|
|
903
|
+
|
|
904
|
+
// Get all tasks with git context
|
|
905
|
+
const tasksData = await callZephlyAPI('mcpSearchTasks', { projectId });
|
|
906
|
+
const tasks = tasksData?.tasks || [];
|
|
907
|
+
const tasksWithGit = tasks.filter(t => t.gitContext && t.gitContext.branchName);
|
|
908
|
+
|
|
909
|
+
// Match worktrees with tasks
|
|
910
|
+
const worktreeList = worktrees.map(wt => {
|
|
911
|
+
const task = tasksWithGit.find(t => t.gitContext.branchName === wt.branch);
|
|
912
|
+
|
|
913
|
+
const info = {
|
|
914
|
+
path: wt.path,
|
|
915
|
+
branch: wt.branch,
|
|
916
|
+
commit: wt.commit,
|
|
917
|
+
taskId: task?.id || null,
|
|
918
|
+
taskTitle: task?.title || null,
|
|
919
|
+
taskStatus: task?.status || null,
|
|
920
|
+
assignee: task?.assignee || null,
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
if (task && task.gitContext) {
|
|
924
|
+
info.isDirty = task.gitContext.isDirty;
|
|
925
|
+
info.aheadBy = task.gitContext.aheadBy;
|
|
926
|
+
info.behindBy = task.gitContext.behindBy;
|
|
927
|
+
info.hasConflicts = task.gitContext.hasConflicts;
|
|
928
|
+
info.lastSyncedAt = task.gitContext.lastSyncedAt;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return info;
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// Find orphaned worktrees (no matching task)
|
|
935
|
+
const orphaned = worktreeList.filter(wt => !wt.taskId);
|
|
936
|
+
|
|
937
|
+
return {
|
|
938
|
+
success: true,
|
|
939
|
+
worktrees: worktreeList,
|
|
940
|
+
totalCount: worktreeList.length,
|
|
941
|
+
orphanedCount: orphaned.length,
|
|
942
|
+
orphaned,
|
|
943
|
+
};
|
|
944
|
+
} catch (error) {
|
|
945
|
+
return {
|
|
946
|
+
success: false,
|
|
947
|
+
error: error.message,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|