@northflare/runner 0.0.12 → 0.0.13
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/package.json +2 -3
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -12
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -176
- package/coverage/lib/index.html +0 -116
- package/coverage/lib/preload-script.js.html +0 -964
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -196
- package/coverage/src/collections/index.html +0 -116
- package/coverage/src/collections/runner-messages.ts.html +0 -312
- package/coverage/src/components/claude-manager.ts.html +0 -1290
- package/coverage/src/components/index.html +0 -146
- package/coverage/src/components/message-handler.ts.html +0 -730
- package/coverage/src/components/repository-manager.ts.html +0 -841
- package/coverage/src/index.html +0 -131
- package/coverage/src/index.ts.html +0 -448
- package/coverage/src/runner.ts.html +0 -1239
- package/coverage/src/utils/config.ts.html +0 -780
- package/coverage/src/utils/console.ts.html +0 -121
- package/coverage/src/utils/index.html +0 -161
- package/coverage/src/utils/logger.ts.html +0 -475
- package/coverage/src/utils/status-line.ts.html +0 -445
- package/exceptions.log +0 -24
- package/lib/codex-sdk/src/codex.ts +0 -38
- package/lib/codex-sdk/src/codexOptions.ts +0 -10
- package/lib/codex-sdk/src/events.ts +0 -80
- package/lib/codex-sdk/src/exec.ts +0 -336
- package/lib/codex-sdk/src/index.ts +0 -39
- package/lib/codex-sdk/src/items.ts +0 -127
- package/lib/codex-sdk/src/outputSchemaFile.ts +0 -40
- package/lib/codex-sdk/src/thread.ts +0 -155
- package/lib/codex-sdk/src/threadOptions.ts +0 -18
- package/lib/codex-sdk/src/turnOptions.ts +0 -6
- package/lib/codex-sdk/tests/abort.test.ts +0 -165
- package/lib/codex-sdk/tests/codexExecSpy.ts +0 -37
- package/lib/codex-sdk/tests/responsesProxy.ts +0 -225
- package/lib/codex-sdk/tests/run.test.ts +0 -687
- package/lib/codex-sdk/tests/runStreamed.test.ts +0 -211
- package/lib/codex-sdk/tsconfig.json +0 -24
- package/rejections.log +0 -68
- package/runner.log +0 -488
- package/src/components/claude-sdk-manager.ts +0 -1425
- package/src/components/codex-sdk-manager.ts +0 -1358
- package/src/components/enhanced-repository-manager.ts +0 -823
- package/src/components/message-handler-sse.ts +0 -1097
- package/src/components/repository-manager.ts +0 -337
- package/src/index.ts +0 -168
- package/src/runner-sse.ts +0 -917
- package/src/services/RunnerAPIClient.ts +0 -175
- package/src/services/SSEClient.ts +0 -258
- package/src/types/claude.ts +0 -66
- package/src/types/computer-name.d.ts +0 -4
- package/src/types/index.ts +0 -64
- package/src/types/messages.ts +0 -39
- package/src/types/runner-interface.ts +0 -36
- package/src/utils/StateManager.ts +0 -187
- package/src/utils/config.ts +0 -327
- package/src/utils/console.ts +0 -15
- package/src/utils/debug.ts +0 -18
- package/src/utils/expand-env.ts +0 -22
- package/src/utils/logger.ts +0 -134
- package/src/utils/model.ts +0 -29
- package/src/utils/status-line.ts +0 -122
- package/src/utils/tool-response-sanitizer.ts +0 -160
- package/test-debug.sh +0 -26
- package/tests/retry-strategies.test.ts +0 -410
- package/tests/sdk-integration.test.ts +0 -329
- package/tests/sdk-streaming.test.ts +0 -1180
- package/tests/setup.ts +0 -5
- package/tests/test-claude-manager.ts +0 -120
- package/tests/tool-response-sanitizer.test.ts +0 -63
- package/tsconfig.json +0 -36
- package/vitest.config.ts +0 -27
|
@@ -1,823 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EnhancedRepositoryManager - Advanced Git repository management with worktree support
|
|
3
|
-
*
|
|
4
|
-
* This enhanced version builds upon the original RepositoryManager to add:
|
|
5
|
-
* - Task-level Git isolation through worktrees
|
|
6
|
-
* - Git operations (stage, commit, push, merge, rebase)
|
|
7
|
-
* - State persistence and recovery
|
|
8
|
-
* - Backward compatibility with workspace-based operations
|
|
9
|
-
*
|
|
10
|
-
* Directory structure:
|
|
11
|
-
* /workspace/repos/<owner>__<repo>/
|
|
12
|
-
* control/ # Primary clone (.git directory owner)
|
|
13
|
-
* .git/
|
|
14
|
-
* worktrees/
|
|
15
|
-
* workspace_<workspaceId>/ # Backward compatibility - workspace default branch
|
|
16
|
-
* task_<taskId>/ # New - isolated task worktrees
|
|
17
|
-
* state.json # Persistent state mapping
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { RepositoryManager } from "./repository-manager";
|
|
21
|
-
import { IRunnerApp } from "../types/runner-interface";
|
|
22
|
-
import path from "path";
|
|
23
|
-
import fs from "fs/promises";
|
|
24
|
-
import simpleGit from "simple-git";
|
|
25
|
-
import { console } from "../utils/console";
|
|
26
|
-
|
|
27
|
-
// Types for enhanced functionality
|
|
28
|
-
export interface TaskHandle {
|
|
29
|
-
taskId: string;
|
|
30
|
-
worktreePath: string;
|
|
31
|
-
branch: string;
|
|
32
|
-
baseBranch: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface RemoveOptions {
|
|
36
|
-
preserveBranch?: boolean;
|
|
37
|
-
force?: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface GitAuthor {
|
|
41
|
-
name: string;
|
|
42
|
-
email: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface RebaseResult {
|
|
46
|
-
success: boolean;
|
|
47
|
-
conflicts?: string[];
|
|
48
|
-
error?: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface MergeResult {
|
|
52
|
-
success: boolean;
|
|
53
|
-
conflicts?: string[];
|
|
54
|
-
mergedCommit?: string;
|
|
55
|
-
error?: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type MergeMode = "ff-only" | "no-ff" | "squash";
|
|
59
|
-
|
|
60
|
-
export interface TaskGitState {
|
|
61
|
-
taskId: string;
|
|
62
|
-
branch: string;
|
|
63
|
-
baseBranch: string;
|
|
64
|
-
lastCommit?: string;
|
|
65
|
-
worktreePath: string;
|
|
66
|
-
status: "active" | "conflicted" | "merged" | "abandoned";
|
|
67
|
-
createdAt: Date;
|
|
68
|
-
updatedAt: Date;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface RepositoryState {
|
|
72
|
-
repositories: Record<
|
|
73
|
-
string,
|
|
74
|
-
{
|
|
75
|
-
owner: string;
|
|
76
|
-
repo: string;
|
|
77
|
-
controlPath: string;
|
|
78
|
-
worktrees: Record<string, TaskGitState>;
|
|
79
|
-
}
|
|
80
|
-
>;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export class EnhancedRepositoryManager extends RepositoryManager {
|
|
84
|
-
private repoStates: Map<string, RepositoryState>;
|
|
85
|
-
private stateFilePath: string;
|
|
86
|
-
|
|
87
|
-
constructor(runner: IRunnerApp) {
|
|
88
|
-
super(runner);
|
|
89
|
-
this.repoStates = new Map();
|
|
90
|
-
// this.repoBasePath is set by parent constructor, so we can use it now
|
|
91
|
-
this.stateFilePath = path.join(this.repoBasePath, "repository-state.json");
|
|
92
|
-
|
|
93
|
-
// Load persisted state on initialization
|
|
94
|
-
this.restoreState().catch((err) => {
|
|
95
|
-
console.error("Failed to restore repository state:", err);
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Get repository key from owner and repo name
|
|
101
|
-
*/
|
|
102
|
-
private getRepoKey(repoUrl: string): {
|
|
103
|
-
key: string;
|
|
104
|
-
owner: string;
|
|
105
|
-
repo: string;
|
|
106
|
-
} {
|
|
107
|
-
// Handle local repository URLs
|
|
108
|
-
if (repoUrl.startsWith("file://")) {
|
|
109
|
-
const localPath = repoUrl.replace("file://", "");
|
|
110
|
-
const pathParts = localPath.split("/");
|
|
111
|
-
const repoName = pathParts[pathParts.length - 1] || "local";
|
|
112
|
-
return { key: `local__${repoName}`, owner: "local", repo: repoName };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Extract owner and repo from URL
|
|
116
|
-
const match = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)(\.git)?$/);
|
|
117
|
-
if (!match) {
|
|
118
|
-
throw new Error(`Invalid GitHub repository URL: ${repoUrl}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const owner = match[1] || "";
|
|
122
|
-
const repo = match[2] || "";
|
|
123
|
-
const key = `${owner}__${repo}`;
|
|
124
|
-
|
|
125
|
-
return { key, owner, repo };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Ensure control repository exists
|
|
130
|
-
*/
|
|
131
|
-
private async ensureControlRepository(
|
|
132
|
-
repoUrl: string,
|
|
133
|
-
githubToken?: string
|
|
134
|
-
): Promise<string> {
|
|
135
|
-
const { key, owner, repo } = this.getRepoKey(repoUrl);
|
|
136
|
-
const controlPath = path.join(this.repoBasePath, "repos", key, "control");
|
|
137
|
-
|
|
138
|
-
// Check if control repository already exists
|
|
139
|
-
try {
|
|
140
|
-
await fs.access(path.join(controlPath, ".git"));
|
|
141
|
-
console.log(`Control repository already exists for ${key}`);
|
|
142
|
-
|
|
143
|
-
// Update remote URL if token changed
|
|
144
|
-
if (githubToken) {
|
|
145
|
-
const git = simpleGit(controlPath);
|
|
146
|
-
const authUrl = this.getAuthenticatedUrl(repoUrl, githubToken);
|
|
147
|
-
await git.remote(["set-url", "origin", authUrl]);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Fetch latest changes
|
|
151
|
-
const git = simpleGit(controlPath);
|
|
152
|
-
await git.fetch("origin");
|
|
153
|
-
|
|
154
|
-
return controlPath;
|
|
155
|
-
} catch {
|
|
156
|
-
// Control repository doesn't exist, create it
|
|
157
|
-
console.log(`Creating control repository for ${key}...`);
|
|
158
|
-
|
|
159
|
-
await this.ensureDirectory(path.dirname(controlPath));
|
|
160
|
-
|
|
161
|
-
const authUrl = this.getAuthenticatedUrl(repoUrl, githubToken);
|
|
162
|
-
await this.executeGit(["clone", "--bare", authUrl, controlPath]);
|
|
163
|
-
|
|
164
|
-
// Convert to regular repository for worktree support
|
|
165
|
-
const git = simpleGit(controlPath);
|
|
166
|
-
await git.raw(["config", "--bool", "core.bare", "false"]);
|
|
167
|
-
|
|
168
|
-
console.log(`Successfully created control repository for ${key}`);
|
|
169
|
-
return controlPath;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Create a task-specific worktree for local workspaces
|
|
175
|
-
*/
|
|
176
|
-
async createLocalTaskHandle(
|
|
177
|
-
taskId: string,
|
|
178
|
-
localPath: string
|
|
179
|
-
): Promise<TaskHandle> {
|
|
180
|
-
// For local workspaces, we don't create worktrees or branches
|
|
181
|
-
// Just return a handle pointing to the local path
|
|
182
|
-
const handle: TaskHandle = {
|
|
183
|
-
taskId,
|
|
184
|
-
worktreePath: localPath,
|
|
185
|
-
branch: "local", // Placeholder branch name for local workspaces
|
|
186
|
-
baseBranch: "local",
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
// Create and persist state for tracking
|
|
190
|
-
const state: TaskGitState = {
|
|
191
|
-
taskId,
|
|
192
|
-
branch: "local",
|
|
193
|
-
baseBranch: "local",
|
|
194
|
-
worktreePath: localPath,
|
|
195
|
-
status: "active",
|
|
196
|
-
createdAt: new Date(),
|
|
197
|
-
updatedAt: new Date(),
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
// Store state using a special key for local workspaces
|
|
201
|
-
await this.updateTaskState("__local__", state);
|
|
202
|
-
|
|
203
|
-
console.log(`Created local task handle for task ${taskId} at ${localPath}`);
|
|
204
|
-
return handle;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Create a task-specific worktree
|
|
209
|
-
*/
|
|
210
|
-
async createTaskWorktree(
|
|
211
|
-
taskId: string,
|
|
212
|
-
workspaceId: string,
|
|
213
|
-
repoUrl: string,
|
|
214
|
-
baseBranch: string = "main",
|
|
215
|
-
githubToken?: string
|
|
216
|
-
): Promise<TaskHandle> {
|
|
217
|
-
// For local repositories, just return the path without creating worktrees
|
|
218
|
-
if (repoUrl.startsWith("file://")) {
|
|
219
|
-
const localPath = repoUrl.replace("file://", "");
|
|
220
|
-
console.log(`Using local repository for task ${taskId}: ${localPath}`);
|
|
221
|
-
|
|
222
|
-
// Verify the path exists
|
|
223
|
-
try {
|
|
224
|
-
const stats = await fs.stat(localPath);
|
|
225
|
-
if (!stats.isDirectory()) {
|
|
226
|
-
throw new Error(`Path is not a directory: ${localPath}`);
|
|
227
|
-
}
|
|
228
|
-
} catch (error) {
|
|
229
|
-
console.error(`Local repository path not found: ${localPath}`);
|
|
230
|
-
throw new Error(`Local repository path not found: ${localPath}`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
taskId,
|
|
235
|
-
worktreePath: localPath,
|
|
236
|
-
branch: "local",
|
|
237
|
-
baseBranch: "local",
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const { key } = this.getRepoKey(repoUrl);
|
|
242
|
-
|
|
243
|
-
// Ensure control repository exists
|
|
244
|
-
const controlPath = await this.ensureControlRepository(
|
|
245
|
-
repoUrl,
|
|
246
|
-
githubToken
|
|
247
|
-
);
|
|
248
|
-
const worktreesPath = path.join(
|
|
249
|
-
this.repoBasePath,
|
|
250
|
-
"repos",
|
|
251
|
-
key,
|
|
252
|
-
"worktrees"
|
|
253
|
-
);
|
|
254
|
-
const taskWorktreePath = path.join(worktreesPath, `task_${taskId}`);
|
|
255
|
-
|
|
256
|
-
// Create unique branch name for the task
|
|
257
|
-
const timestamp = Date.now();
|
|
258
|
-
const shortTaskId = taskId.substring(0, 8);
|
|
259
|
-
const branchName = `task/${shortTaskId}-${timestamp}`;
|
|
260
|
-
|
|
261
|
-
try {
|
|
262
|
-
console.log(
|
|
263
|
-
`Creating worktree for task ${taskId} on branch ${branchName}...`
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
// Create worktree directory
|
|
267
|
-
await this.ensureDirectory(worktreesPath);
|
|
268
|
-
|
|
269
|
-
// Add worktree with new branch based on baseBranch
|
|
270
|
-
const git = simpleGit(controlPath);
|
|
271
|
-
await git.raw([
|
|
272
|
-
"worktree",
|
|
273
|
-
"add",
|
|
274
|
-
"-b",
|
|
275
|
-
branchName,
|
|
276
|
-
taskWorktreePath,
|
|
277
|
-
`origin/${baseBranch}`,
|
|
278
|
-
]);
|
|
279
|
-
|
|
280
|
-
// Configure the worktree
|
|
281
|
-
const worktreeGit = simpleGit(taskWorktreePath);
|
|
282
|
-
await worktreeGit.addConfig("user.name", "Northflare");
|
|
283
|
-
await worktreeGit.addConfig("user.email", "runner@northflare.ai");
|
|
284
|
-
|
|
285
|
-
// Create and persist state
|
|
286
|
-
const state: TaskGitState = {
|
|
287
|
-
taskId,
|
|
288
|
-
branch: branchName,
|
|
289
|
-
baseBranch,
|
|
290
|
-
worktreePath: taskWorktreePath,
|
|
291
|
-
status: "active",
|
|
292
|
-
createdAt: new Date(),
|
|
293
|
-
updatedAt: new Date(),
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
await this.updateTaskState(key, state);
|
|
297
|
-
|
|
298
|
-
console.log(`Successfully created worktree for task ${taskId}`);
|
|
299
|
-
|
|
300
|
-
return {
|
|
301
|
-
taskId,
|
|
302
|
-
worktreePath: taskWorktreePath,
|
|
303
|
-
branch: branchName,
|
|
304
|
-
baseBranch,
|
|
305
|
-
};
|
|
306
|
-
} catch (error) {
|
|
307
|
-
console.error(`Failed to create worktree for task ${taskId}:`, error);
|
|
308
|
-
// Cleanup on failure
|
|
309
|
-
await this.cleanupDirectory(taskWorktreePath);
|
|
310
|
-
throw error;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Remove a task worktree
|
|
316
|
-
*/
|
|
317
|
-
async removeTaskWorktree(
|
|
318
|
-
taskId: string,
|
|
319
|
-
options: RemoveOptions = {}
|
|
320
|
-
): Promise<void> {
|
|
321
|
-
// Find the task state across all repositories
|
|
322
|
-
let repoKey: string | null = null;
|
|
323
|
-
let taskState: TaskGitState | null = null;
|
|
324
|
-
|
|
325
|
-
for (const [key, state] of this.repoStates) {
|
|
326
|
-
const repo = state.repositories[key];
|
|
327
|
-
if (repo?.worktrees[taskId]) {
|
|
328
|
-
repoKey = key;
|
|
329
|
-
taskState = repo.worktrees[taskId];
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (!repoKey || !taskState) {
|
|
335
|
-
console.log(`No worktree found for task ${taskId}`);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Handle local workspaces differently
|
|
340
|
-
if (repoKey === "__local__") {
|
|
341
|
-
// Just remove from state, don't touch the filesystem
|
|
342
|
-
const repoState = this.repoStates.get(repoKey);
|
|
343
|
-
const repo = repoState?.repositories?.[repoKey];
|
|
344
|
-
if (repo?.worktrees?.[taskId]) {
|
|
345
|
-
delete repo.worktrees[taskId];
|
|
346
|
-
}
|
|
347
|
-
await this.persistState();
|
|
348
|
-
console.log(`Removed local task handle for task ${taskId}`);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const { key } = this.getRepoKey(repoKey);
|
|
353
|
-
const controlPath = path.join(this.repoBasePath, "repos", key, "control");
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
console.log(`Removing worktree for task ${taskId}...`);
|
|
357
|
-
|
|
358
|
-
// Remove the worktree
|
|
359
|
-
const git = simpleGit(controlPath);
|
|
360
|
-
await git.raw(["worktree", "remove", taskState.worktreePath, "--force"]);
|
|
361
|
-
|
|
362
|
-
// Delete the branch if not preserving
|
|
363
|
-
if (!options.preserveBranch) {
|
|
364
|
-
try {
|
|
365
|
-
await git.raw(["branch", "-D", taskState.branch]);
|
|
366
|
-
} catch (error) {
|
|
367
|
-
console.warn(`Failed to delete branch ${taskState.branch}:`, error);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Remove from state
|
|
372
|
-
const repoState = this.repoStates.get(repoKey);
|
|
373
|
-
if (repoState?.repositories[key]?.worktrees) {
|
|
374
|
-
delete repoState.repositories[key].worktrees[taskId];
|
|
375
|
-
}
|
|
376
|
-
await this.persistState();
|
|
377
|
-
|
|
378
|
-
console.log(`Successfully removed worktree for task ${taskId}`);
|
|
379
|
-
} catch (error) {
|
|
380
|
-
console.error(`Failed to remove worktree for task ${taskId}:`, error);
|
|
381
|
-
if (options.force) {
|
|
382
|
-
// Force cleanup
|
|
383
|
-
await this.cleanupDirectory(taskState.worktreePath);
|
|
384
|
-
} else {
|
|
385
|
-
throw error;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Stage all changes in a task worktree
|
|
392
|
-
*/
|
|
393
|
-
async stageAll(taskId: string): Promise<void> {
|
|
394
|
-
const taskState = await this.getTaskState(taskId);
|
|
395
|
-
if (!taskState) {
|
|
396
|
-
throw new Error(`No worktree found for task ${taskId}`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Skip Git operations for local workspaces
|
|
400
|
-
if (taskState.branch === "local") {
|
|
401
|
-
console.log(
|
|
402
|
-
`Skipping stage operation for local workspace task ${taskId}`
|
|
403
|
-
);
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const git = simpleGit(taskState.worktreePath);
|
|
408
|
-
await git.add(".");
|
|
409
|
-
|
|
410
|
-
console.log(`Staged all changes for task ${taskId}`);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Commit changes in a task worktree
|
|
415
|
-
*/
|
|
416
|
-
async commit(
|
|
417
|
-
taskId: string,
|
|
418
|
-
message: string,
|
|
419
|
-
author?: GitAuthor
|
|
420
|
-
): Promise<string> {
|
|
421
|
-
const taskState = await this.getTaskState(taskId);
|
|
422
|
-
if (!taskState) {
|
|
423
|
-
throw new Error(`No worktree found for task ${taskId}`);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Skip Git operations for local workspaces
|
|
427
|
-
if (taskState.branch === "local") {
|
|
428
|
-
console.log(
|
|
429
|
-
`Skipping commit operation for local workspace task ${taskId}`
|
|
430
|
-
);
|
|
431
|
-
return "local-commit"; // Return a placeholder commit hash
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const git = simpleGit(taskState.worktreePath);
|
|
435
|
-
|
|
436
|
-
// Set author if provided
|
|
437
|
-
if (author) {
|
|
438
|
-
await git.addConfig("user.name", author.name);
|
|
439
|
-
await git.addConfig("user.email", author.email);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Commit changes
|
|
443
|
-
const result = await git.commit(message);
|
|
444
|
-
const commitHash = result.commit;
|
|
445
|
-
|
|
446
|
-
// Update task state
|
|
447
|
-
taskState.lastCommit = commitHash;
|
|
448
|
-
taskState.updatedAt = new Date();
|
|
449
|
-
await this.updateTaskStateByTaskId(taskId, taskState);
|
|
450
|
-
|
|
451
|
-
console.log(`Created commit ${commitHash} for task ${taskId}`);
|
|
452
|
-
return commitHash;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Create a new branch for a task
|
|
457
|
-
*/
|
|
458
|
-
async createBranch(taskId: string, branchName: string): Promise<void> {
|
|
459
|
-
const taskState = await this.getTaskState(taskId);
|
|
460
|
-
if (!taskState) {
|
|
461
|
-
throw new Error(`No worktree found for task ${taskId}`);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Skip Git operations for local workspaces
|
|
465
|
-
if (taskState.branch === "local") {
|
|
466
|
-
console.log(
|
|
467
|
-
`Skipping branch creation for local workspace task ${taskId}`
|
|
468
|
-
);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const git = simpleGit(taskState.worktreePath);
|
|
473
|
-
await git.checkoutBranch(branchName, taskState.branch);
|
|
474
|
-
|
|
475
|
-
// Update task state
|
|
476
|
-
taskState.branch = branchName;
|
|
477
|
-
taskState.updatedAt = new Date();
|
|
478
|
-
await this.updateTaskStateByTaskId(taskId, taskState);
|
|
479
|
-
|
|
480
|
-
console.log(`Created branch ${branchName} for task ${taskId}`);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Rebase a task branch onto target branch
|
|
485
|
-
*/
|
|
486
|
-
async rebaseTask(
|
|
487
|
-
taskId: string,
|
|
488
|
-
targetBranch: string
|
|
489
|
-
): Promise<RebaseResult> {
|
|
490
|
-
const taskState = await this.getTaskState(taskId);
|
|
491
|
-
if (!taskState) {
|
|
492
|
-
throw new Error(`No worktree found for task ${taskId}`);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Skip Git operations for local workspaces
|
|
496
|
-
if (taskState.branch === "local") {
|
|
497
|
-
console.log(
|
|
498
|
-
`Skipping rebase operation for local workspace task ${taskId}`
|
|
499
|
-
);
|
|
500
|
-
return { success: true }; // Return success for local workspaces
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const git = simpleGit(taskState.worktreePath);
|
|
504
|
-
|
|
505
|
-
try {
|
|
506
|
-
// Fetch latest changes
|
|
507
|
-
await git.fetch("origin", targetBranch);
|
|
508
|
-
|
|
509
|
-
// Perform rebase
|
|
510
|
-
await git.rebase([`origin/${targetBranch}`]);
|
|
511
|
-
|
|
512
|
-
// Update base branch in state
|
|
513
|
-
taskState.baseBranch = targetBranch;
|
|
514
|
-
taskState.updatedAt = new Date();
|
|
515
|
-
await this.updateTaskStateByTaskId(taskId, taskState);
|
|
516
|
-
|
|
517
|
-
console.log(`Successfully rebased task ${taskId} onto ${targetBranch}`);
|
|
518
|
-
return { success: true };
|
|
519
|
-
} catch (error) {
|
|
520
|
-
console.error(`Rebase failed for task ${taskId}:`, error);
|
|
521
|
-
|
|
522
|
-
// Check for conflicts
|
|
523
|
-
const status = await git.status();
|
|
524
|
-
if (status.conflicted.length > 0) {
|
|
525
|
-
taskState.status = "conflicted";
|
|
526
|
-
await this.updateTaskStateByTaskId(taskId, taskState);
|
|
527
|
-
|
|
528
|
-
return {
|
|
529
|
-
success: false,
|
|
530
|
-
conflicts: status.conflicted,
|
|
531
|
-
error: "Rebase resulted in conflicts",
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Abort rebase on error
|
|
536
|
-
try {
|
|
537
|
-
await git.rebase(["--abort"]);
|
|
538
|
-
} catch {
|
|
539
|
-
// Ignore abort errors
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return {
|
|
543
|
-
success: false,
|
|
544
|
-
error: error instanceof Error ? error.message : String(error),
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Merge a task branch into target branch
|
|
551
|
-
*/
|
|
552
|
-
async mergeTask(
|
|
553
|
-
taskId: string,
|
|
554
|
-
targetBranch: string,
|
|
555
|
-
mode: MergeMode = "no-ff"
|
|
556
|
-
): Promise<MergeResult> {
|
|
557
|
-
const taskState = await this.getTaskState(taskId);
|
|
558
|
-
if (!taskState) {
|
|
559
|
-
throw new Error(`No worktree found for task ${taskId}`);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Skip Git operations for local workspaces
|
|
563
|
-
if (taskState.branch === "local") {
|
|
564
|
-
console.log(
|
|
565
|
-
`Skipping merge operation for local workspace task ${taskId}`
|
|
566
|
-
);
|
|
567
|
-
return { success: true, mergedCommit: "local-merge" }; // Return success for local workspaces
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const git = simpleGit(taskState.worktreePath);
|
|
571
|
-
|
|
572
|
-
try {
|
|
573
|
-
// Fetch latest changes
|
|
574
|
-
await git.fetch("origin", targetBranch);
|
|
575
|
-
|
|
576
|
-
// Checkout target branch
|
|
577
|
-
await git.checkout(targetBranch);
|
|
578
|
-
await git.pull("origin", targetBranch);
|
|
579
|
-
|
|
580
|
-
// Perform merge based on mode
|
|
581
|
-
let mergeArgs: string[] = [];
|
|
582
|
-
switch (mode) {
|
|
583
|
-
case "ff-only":
|
|
584
|
-
mergeArgs = ["--ff-only"];
|
|
585
|
-
break;
|
|
586
|
-
case "no-ff":
|
|
587
|
-
mergeArgs = ["--no-ff"];
|
|
588
|
-
break;
|
|
589
|
-
case "squash":
|
|
590
|
-
mergeArgs = ["--squash"];
|
|
591
|
-
break;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const result = await git.merge([taskState.branch, ...mergeArgs]);
|
|
595
|
-
|
|
596
|
-
// For squash merge, we need to commit
|
|
597
|
-
if (mode === "squash") {
|
|
598
|
-
const commitResult = await git.commit(
|
|
599
|
-
`Merge task ${taskId} (${taskState.branch})`
|
|
600
|
-
);
|
|
601
|
-
result.result = commitResult.commit;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Update task state
|
|
605
|
-
taskState.status = "merged";
|
|
606
|
-
taskState.updatedAt = new Date();
|
|
607
|
-
await this.updateTaskStateByTaskId(taskId, taskState);
|
|
608
|
-
|
|
609
|
-
console.log(`Successfully merged task ${taskId} into ${targetBranch}`);
|
|
610
|
-
return {
|
|
611
|
-
success: true,
|
|
612
|
-
mergedCommit: result.result,
|
|
613
|
-
};
|
|
614
|
-
} catch (error) {
|
|
615
|
-
console.error(`Merge failed for task ${taskId}:`, error);
|
|
616
|
-
|
|
617
|
-
// Check for conflicts
|
|
618
|
-
const status = await git.status();
|
|
619
|
-
if (status.conflicted.length > 0) {
|
|
620
|
-
taskState.status = "conflicted";
|
|
621
|
-
await this.updateTaskStateByTaskId(taskId, taskState);
|
|
622
|
-
|
|
623
|
-
return {
|
|
624
|
-
success: false,
|
|
625
|
-
conflicts: status.conflicted,
|
|
626
|
-
error: "Merge resulted in conflicts",
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
return {
|
|
631
|
-
success: false,
|
|
632
|
-
error: error instanceof Error ? error.message : String(error),
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Get task Git state
|
|
639
|
-
*/
|
|
640
|
-
async getTaskState(taskId: string): Promise<TaskGitState | null> {
|
|
641
|
-
for (const state of this.repoStates.values()) {
|
|
642
|
-
for (const repo of Object.values(state.repositories)) {
|
|
643
|
-
if (repo.worktrees[taskId]) {
|
|
644
|
-
return repo.worktrees[taskId];
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return null;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Update task state
|
|
653
|
-
*/
|
|
654
|
-
private async updateTaskState(
|
|
655
|
-
repoKey: string,
|
|
656
|
-
state: TaskGitState
|
|
657
|
-
): Promise<void> {
|
|
658
|
-
if (!this.repoStates.has(repoKey)) {
|
|
659
|
-
this.repoStates.set(repoKey, {
|
|
660
|
-
repositories: {},
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const repoState = this.repoStates.get(repoKey)!;
|
|
665
|
-
if (!repoState.repositories[repoKey]) {
|
|
666
|
-
// Handle special case for local workspaces
|
|
667
|
-
if (repoKey === "__local__") {
|
|
668
|
-
repoState.repositories[repoKey] = {
|
|
669
|
-
owner: "local",
|
|
670
|
-
repo: "local",
|
|
671
|
-
controlPath: "local",
|
|
672
|
-
worktrees: {},
|
|
673
|
-
};
|
|
674
|
-
} else {
|
|
675
|
-
const { owner, repo } = this.getRepoKey(repoKey);
|
|
676
|
-
repoState.repositories[repoKey] = {
|
|
677
|
-
owner,
|
|
678
|
-
repo,
|
|
679
|
-
controlPath: path.join(
|
|
680
|
-
this.repoBasePath,
|
|
681
|
-
"repos",
|
|
682
|
-
repoKey,
|
|
683
|
-
"control"
|
|
684
|
-
),
|
|
685
|
-
worktrees: {},
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
repoState.repositories[repoKey].worktrees[state.taskId] = state;
|
|
691
|
-
await this.persistState();
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* Update task state by task ID
|
|
696
|
-
*/
|
|
697
|
-
private async updateTaskStateByTaskId(
|
|
698
|
-
taskId: string,
|
|
699
|
-
state: TaskGitState
|
|
700
|
-
): Promise<void> {
|
|
701
|
-
for (const [repoKey, repoState] of this.repoStates) {
|
|
702
|
-
for (const repo of Object.values(repoState.repositories)) {
|
|
703
|
-
if (repo.worktrees[taskId]) {
|
|
704
|
-
repo.worktrees[taskId] = state;
|
|
705
|
-
await this.persistState();
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
/**
|
|
713
|
-
* Persist state to disk
|
|
714
|
-
*/
|
|
715
|
-
async persistState(): Promise<void> {
|
|
716
|
-
try {
|
|
717
|
-
const stateData: Record<string, RepositoryState> = {};
|
|
718
|
-
for (const [key, value] of this.repoStates) {
|
|
719
|
-
stateData[key] = value;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
await fs.writeFile(
|
|
723
|
-
this.stateFilePath,
|
|
724
|
-
JSON.stringify(stateData, null, 2)
|
|
725
|
-
);
|
|
726
|
-
|
|
727
|
-
console.log("Persisted repository state");
|
|
728
|
-
} catch (error) {
|
|
729
|
-
console.error("Failed to persist repository state:", error);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Restore state from disk
|
|
735
|
-
*/
|
|
736
|
-
async restoreState(): Promise<void> {
|
|
737
|
-
try {
|
|
738
|
-
const data = await fs.readFile(this.stateFilePath, "utf-8");
|
|
739
|
-
const stateData = JSON.parse(data) as Record<string, RepositoryState>;
|
|
740
|
-
|
|
741
|
-
this.repoStates.clear();
|
|
742
|
-
for (const [key, value] of Object.entries(stateData)) {
|
|
743
|
-
// Convert date strings back to Date objects
|
|
744
|
-
for (const repo of Object.values(value.repositories)) {
|
|
745
|
-
for (const worktree of Object.values(repo.worktrees)) {
|
|
746
|
-
worktree.createdAt = new Date(worktree.createdAt);
|
|
747
|
-
worktree.updatedAt = new Date(worktree.updatedAt);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
this.repoStates.set(key, value);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
console.log("Restored repository state");
|
|
754
|
-
} catch (error) {
|
|
755
|
-
if ((error as any).code !== "ENOENT") {
|
|
756
|
-
console.error("Failed to restore repository state:", error);
|
|
757
|
-
}
|
|
758
|
-
// If file doesn't exist, that's okay - we'll start fresh
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* Override parent's checkoutRepository to use worktrees for backward compatibility
|
|
764
|
-
*/
|
|
765
|
-
override async checkoutRepository(
|
|
766
|
-
workspaceId: string,
|
|
767
|
-
repoUrl: string,
|
|
768
|
-
branch: string,
|
|
769
|
-
githubToken?: string
|
|
770
|
-
): Promise<string> {
|
|
771
|
-
// If it's a local repository URL, delegate to parent's checkoutLocalRepository
|
|
772
|
-
if (repoUrl.startsWith("file://")) {
|
|
773
|
-
const localPath = repoUrl.replace("file://", "");
|
|
774
|
-
return super.checkoutLocalRepository(workspaceId, localPath);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const { key } = this.getRepoKey(repoUrl);
|
|
778
|
-
|
|
779
|
-
// Ensure control repository exists
|
|
780
|
-
await this.ensureControlRepository(repoUrl, githubToken);
|
|
781
|
-
|
|
782
|
-
// Create or update workspace worktree
|
|
783
|
-
const worktreesPath = path.join(
|
|
784
|
-
this.repoBasePath,
|
|
785
|
-
"repos",
|
|
786
|
-
key,
|
|
787
|
-
"worktrees"
|
|
788
|
-
);
|
|
789
|
-
const workspaceWorktreePath = path.join(
|
|
790
|
-
worktreesPath,
|
|
791
|
-
`workspace_${workspaceId}`
|
|
792
|
-
);
|
|
793
|
-
|
|
794
|
-
try {
|
|
795
|
-
// Check if workspace worktree already exists
|
|
796
|
-
await fs.access(workspaceWorktreePath);
|
|
797
|
-
|
|
798
|
-
// Update existing worktree
|
|
799
|
-
const git = simpleGit(workspaceWorktreePath);
|
|
800
|
-
await git.fetch("origin");
|
|
801
|
-
await git.reset(["--hard", `origin/${branch}`]);
|
|
802
|
-
await git.clean("f", ["-d"]);
|
|
803
|
-
|
|
804
|
-
console.log(`Updated workspace worktree for ${workspaceId}`);
|
|
805
|
-
} catch {
|
|
806
|
-
// Create new workspace worktree
|
|
807
|
-
const controlPath = path.join(this.repoBasePath, "repos", key, "control");
|
|
808
|
-
const git = simpleGit(controlPath);
|
|
809
|
-
|
|
810
|
-
await this.ensureDirectory(worktreesPath);
|
|
811
|
-
await git.raw([
|
|
812
|
-
"worktree",
|
|
813
|
-
"add",
|
|
814
|
-
workspaceWorktreePath,
|
|
815
|
-
`origin/${branch}`,
|
|
816
|
-
]);
|
|
817
|
-
|
|
818
|
-
console.log(`Created workspace worktree for ${workspaceId}`);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
return workspaceWorktreePath;
|
|
822
|
-
}
|
|
823
|
-
}
|