@northflare/runner 0.0.11 → 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.
Files changed (81) hide show
  1. package/dist/utils/config.d.ts +1 -0
  2. package/dist/utils/config.d.ts.map +1 -1
  3. package/dist/utils/config.js +13 -2
  4. package/dist/utils/config.js.map +1 -1
  5. package/package.json +1 -2
  6. package/coverage/base.css +0 -224
  7. package/coverage/block-navigation.js +0 -87
  8. package/coverage/coverage-final.json +0 -12
  9. package/coverage/favicon.png +0 -0
  10. package/coverage/index.html +0 -176
  11. package/coverage/lib/index.html +0 -116
  12. package/coverage/lib/preload-script.js.html +0 -964
  13. package/coverage/prettify.css +0 -1
  14. package/coverage/prettify.js +0 -2
  15. package/coverage/sort-arrow-sprite.png +0 -0
  16. package/coverage/sorter.js +0 -196
  17. package/coverage/src/collections/index.html +0 -116
  18. package/coverage/src/collections/runner-messages.ts.html +0 -312
  19. package/coverage/src/components/claude-manager.ts.html +0 -1290
  20. package/coverage/src/components/index.html +0 -146
  21. package/coverage/src/components/message-handler.ts.html +0 -730
  22. package/coverage/src/components/repository-manager.ts.html +0 -841
  23. package/coverage/src/index.html +0 -131
  24. package/coverage/src/index.ts.html +0 -448
  25. package/coverage/src/runner.ts.html +0 -1239
  26. package/coverage/src/utils/config.ts.html +0 -780
  27. package/coverage/src/utils/console.ts.html +0 -121
  28. package/coverage/src/utils/index.html +0 -161
  29. package/coverage/src/utils/logger.ts.html +0 -475
  30. package/coverage/src/utils/status-line.ts.html +0 -445
  31. package/exceptions.log +0 -24
  32. package/lib/codex-sdk/src/codex.ts +0 -38
  33. package/lib/codex-sdk/src/codexOptions.ts +0 -10
  34. package/lib/codex-sdk/src/events.ts +0 -80
  35. package/lib/codex-sdk/src/exec.ts +0 -336
  36. package/lib/codex-sdk/src/index.ts +0 -39
  37. package/lib/codex-sdk/src/items.ts +0 -127
  38. package/lib/codex-sdk/src/outputSchemaFile.ts +0 -40
  39. package/lib/codex-sdk/src/thread.ts +0 -155
  40. package/lib/codex-sdk/src/threadOptions.ts +0 -18
  41. package/lib/codex-sdk/src/turnOptions.ts +0 -6
  42. package/lib/codex-sdk/tests/abort.test.ts +0 -165
  43. package/lib/codex-sdk/tests/codexExecSpy.ts +0 -37
  44. package/lib/codex-sdk/tests/responsesProxy.ts +0 -225
  45. package/lib/codex-sdk/tests/run.test.ts +0 -687
  46. package/lib/codex-sdk/tests/runStreamed.test.ts +0 -211
  47. package/lib/codex-sdk/tsconfig.json +0 -24
  48. package/rejections.log +0 -68
  49. package/runner.log +0 -488
  50. package/src/components/claude-sdk-manager.ts +0 -1425
  51. package/src/components/codex-sdk-manager.ts +0 -1358
  52. package/src/components/enhanced-repository-manager.ts +0 -823
  53. package/src/components/message-handler-sse.ts +0 -1097
  54. package/src/components/repository-manager.ts +0 -337
  55. package/src/index.ts +0 -168
  56. package/src/runner-sse.ts +0 -917
  57. package/src/services/RunnerAPIClient.ts +0 -175
  58. package/src/services/SSEClient.ts +0 -258
  59. package/src/types/claude.ts +0 -66
  60. package/src/types/computer-name.d.ts +0 -4
  61. package/src/types/index.ts +0 -64
  62. package/src/types/messages.ts +0 -39
  63. package/src/types/runner-interface.ts +0 -36
  64. package/src/utils/StateManager.ts +0 -187
  65. package/src/utils/config.ts +0 -316
  66. package/src/utils/console.ts +0 -15
  67. package/src/utils/debug.ts +0 -18
  68. package/src/utils/expand-env.ts +0 -22
  69. package/src/utils/logger.ts +0 -134
  70. package/src/utils/model.ts +0 -29
  71. package/src/utils/status-line.ts +0 -122
  72. package/src/utils/tool-response-sanitizer.ts +0 -160
  73. package/test-debug.sh +0 -26
  74. package/tests/retry-strategies.test.ts +0 -410
  75. package/tests/sdk-integration.test.ts +0 -329
  76. package/tests/sdk-streaming.test.ts +0 -1180
  77. package/tests/setup.ts +0 -5
  78. package/tests/test-claude-manager.ts +0 -120
  79. package/tests/tool-response-sanitizer.test.ts +0 -63
  80. package/tsconfig.json +0 -36
  81. 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
- }