@northflare/runner 0.0.30 → 0.0.32

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 (119) hide show
  1. package/bin/northflare-runner +1 -1
  2. package/dist/chunk-3QTLJ4CG.js +33622 -0
  3. package/dist/chunk-3QTLJ4CG.js.map +1 -0
  4. package/dist/chunk-7D4SUZUM.js +38 -0
  5. package/dist/chunk-7D4SUZUM.js.map +1 -0
  6. package/dist/dist-W7DZRE4U.js +365 -0
  7. package/dist/dist-W7DZRE4U.js.map +1 -0
  8. package/dist/index.d.ts +764 -5
  9. package/dist/index.js +9872 -202
  10. package/dist/index.js.map +1 -1
  11. package/dist/sdk-query-TRMSGGID-EIENWDKW.js +14 -0
  12. package/dist/sdk-query-TRMSGGID-EIENWDKW.js.map +1 -0
  13. package/package.json +17 -17
  14. package/tsup.config.ts +5 -2
  15. package/dist/components/claude-sdk-manager.d.ts +0 -60
  16. package/dist/components/claude-sdk-manager.d.ts.map +0 -1
  17. package/dist/components/claude-sdk-manager.js +0 -1378
  18. package/dist/components/claude-sdk-manager.js.map +0 -1
  19. package/dist/components/codex-sdk-manager.d.ts +0 -94
  20. package/dist/components/codex-sdk-manager.d.ts.map +0 -1
  21. package/dist/components/codex-sdk-manager.js +0 -1450
  22. package/dist/components/codex-sdk-manager.js.map +0 -1
  23. package/dist/components/enhanced-repository-manager.d.ts +0 -173
  24. package/dist/components/enhanced-repository-manager.d.ts.map +0 -1
  25. package/dist/components/enhanced-repository-manager.js +0 -1097
  26. package/dist/components/enhanced-repository-manager.js.map +0 -1
  27. package/dist/components/message-handler-sse.d.ts +0 -77
  28. package/dist/components/message-handler-sse.d.ts.map +0 -1
  29. package/dist/components/message-handler-sse.js +0 -1224
  30. package/dist/components/message-handler-sse.js.map +0 -1
  31. package/dist/components/northflare-agent-sdk-manager.d.ts +0 -58
  32. package/dist/components/northflare-agent-sdk-manager.d.ts.map +0 -1
  33. package/dist/components/northflare-agent-sdk-manager.js +0 -2032
  34. package/dist/components/northflare-agent-sdk-manager.js.map +0 -1
  35. package/dist/components/repository-manager.d.ts +0 -51
  36. package/dist/components/repository-manager.d.ts.map +0 -1
  37. package/dist/components/repository-manager.js +0 -256
  38. package/dist/components/repository-manager.js.map +0 -1
  39. package/dist/index.d.ts.map +0 -1
  40. package/dist/runner-sse.d.ts +0 -102
  41. package/dist/runner-sse.d.ts.map +0 -1
  42. package/dist/runner-sse.js +0 -877
  43. package/dist/runner-sse.js.map +0 -1
  44. package/dist/services/RunnerAPIClient.d.ts +0 -61
  45. package/dist/services/RunnerAPIClient.d.ts.map +0 -1
  46. package/dist/services/RunnerAPIClient.js +0 -187
  47. package/dist/services/RunnerAPIClient.js.map +0 -1
  48. package/dist/services/SSEClient.d.ts +0 -62
  49. package/dist/services/SSEClient.d.ts.map +0 -1
  50. package/dist/services/SSEClient.js +0 -225
  51. package/dist/services/SSEClient.js.map +0 -1
  52. package/dist/types/claude.d.ts +0 -80
  53. package/dist/types/claude.d.ts.map +0 -1
  54. package/dist/types/claude.js +0 -5
  55. package/dist/types/claude.js.map +0 -1
  56. package/dist/types/index.d.ts +0 -52
  57. package/dist/types/index.d.ts.map +0 -1
  58. package/dist/types/index.js +0 -7
  59. package/dist/types/index.js.map +0 -1
  60. package/dist/types/messages.d.ts +0 -33
  61. package/dist/types/messages.d.ts.map +0 -1
  62. package/dist/types/messages.js +0 -5
  63. package/dist/types/messages.js.map +0 -1
  64. package/dist/types/runner-interface.d.ts +0 -38
  65. package/dist/types/runner-interface.d.ts.map +0 -1
  66. package/dist/types/runner-interface.js +0 -5
  67. package/dist/types/runner-interface.js.map +0 -1
  68. package/dist/utils/StateManager.d.ts +0 -61
  69. package/dist/utils/StateManager.d.ts.map +0 -1
  70. package/dist/utils/StateManager.js +0 -170
  71. package/dist/utils/StateManager.js.map +0 -1
  72. package/dist/utils/config.d.ts +0 -48
  73. package/dist/utils/config.d.ts.map +0 -1
  74. package/dist/utils/config.js +0 -378
  75. package/dist/utils/config.js.map +0 -1
  76. package/dist/utils/console.d.ts +0 -8
  77. package/dist/utils/console.d.ts.map +0 -1
  78. package/dist/utils/console.js +0 -31
  79. package/dist/utils/console.js.map +0 -1
  80. package/dist/utils/debug.d.ts +0 -12
  81. package/dist/utils/debug.d.ts.map +0 -1
  82. package/dist/utils/debug.js +0 -94
  83. package/dist/utils/debug.js.map +0 -1
  84. package/dist/utils/expand-env.d.ts +0 -2
  85. package/dist/utils/expand-env.d.ts.map +0 -1
  86. package/dist/utils/expand-env.js +0 -17
  87. package/dist/utils/expand-env.js.map +0 -1
  88. package/dist/utils/inactivity-timeout.d.ts +0 -19
  89. package/dist/utils/inactivity-timeout.d.ts.map +0 -1
  90. package/dist/utils/inactivity-timeout.js +0 -72
  91. package/dist/utils/inactivity-timeout.js.map +0 -1
  92. package/dist/utils/logger.d.ts +0 -10
  93. package/dist/utils/logger.d.ts.map +0 -1
  94. package/dist/utils/logger.js +0 -129
  95. package/dist/utils/logger.js.map +0 -1
  96. package/dist/utils/message-log.d.ts +0 -23
  97. package/dist/utils/message-log.d.ts.map +0 -1
  98. package/dist/utils/message-log.js +0 -69
  99. package/dist/utils/message-log.js.map +0 -1
  100. package/dist/utils/model.d.ts +0 -8
  101. package/dist/utils/model.d.ts.map +0 -1
  102. package/dist/utils/model.js +0 -37
  103. package/dist/utils/model.js.map +0 -1
  104. package/dist/utils/status-line.d.ts +0 -34
  105. package/dist/utils/status-line.d.ts.map +0 -1
  106. package/dist/utils/status-line.js +0 -131
  107. package/dist/utils/status-line.js.map +0 -1
  108. package/dist/utils/tool-response-sanitizer.d.ts +0 -9
  109. package/dist/utils/tool-response-sanitizer.d.ts.map +0 -1
  110. package/dist/utils/tool-response-sanitizer.js +0 -118
  111. package/dist/utils/tool-response-sanitizer.js.map +0 -1
  112. package/dist/utils/update-coordinator.d.ts +0 -53
  113. package/dist/utils/update-coordinator.d.ts.map +0 -1
  114. package/dist/utils/update-coordinator.js +0 -159
  115. package/dist/utils/update-coordinator.js.map +0 -1
  116. package/dist/utils/version.d.ts +0 -10
  117. package/dist/utils/version.d.ts.map +0 -1
  118. package/dist/utils/version.js +0 -33
  119. package/dist/utils/version.js.map +0 -1
@@ -1,1097 +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
- import { RepositoryManager } from './repository-manager.js';
20
- import path from "path";
21
- import fs from "fs/promises";
22
- import crypto from "crypto";
23
- import { simpleGit } from "simple-git";
24
- import { createScopedConsole } from '../utils/console.js';
25
- import { createLogger } from '../utils/logger.js';
26
- const console = createScopedConsole("repo");
27
- const logger = createLogger("EnhancedRepositoryManager", "repo");
28
- export class EnhancedRepositoryManager extends RepositoryManager {
29
- repoStates;
30
- stateFilePath;
31
- legacyStateFilePath;
32
- repoLocks;
33
- taskWorktreesBasePath;
34
- constructor(runner) {
35
- super(runner);
36
- this.repoStates = new Map();
37
- this.repoLocks = new Map();
38
- const dataDir = runner.config_.dataDir;
39
- const runnerId = runner.getRunnerId();
40
- const stateFileName = runnerId
41
- ? `repository-state-${runnerId}.json`
42
- : "repository-state.json";
43
- this.stateFilePath = path.join(dataDir, "git", stateFileName);
44
- this.legacyStateFilePath = path.join(this.repoBasePath, "repository-state.json");
45
- this.taskWorktreesBasePath = path.join(dataDir, "git", "worktrees");
46
- // Load persisted state on initialization
47
- this.restoreState().catch((err) => {
48
- console.error("Failed to restore repository state:", err);
49
- });
50
- }
51
- /**
52
- * Get repository key from owner and repo name
53
- */
54
- getRepoKey(repoUrl) {
55
- // Handle local repository URLs
56
- if (repoUrl.startsWith("file://")) {
57
- const localPath = repoUrl.replace("file://", "");
58
- const repoName = path.basename(localPath) || "local";
59
- const digest = crypto
60
- .createHash("sha1")
61
- .update(localPath)
62
- .digest("hex")
63
- .slice(0, 12);
64
- return { key: `local__${repoName}__${digest}`, owner: "local", repo: repoName };
65
- }
66
- // Extract owner and repo from URL
67
- const match = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)(\.git)?$/);
68
- if (!match) {
69
- throw new Error(`Invalid GitHub repository URL: ${repoUrl}`);
70
- }
71
- const owner = match[1] || "";
72
- const repo = match[2] || "";
73
- const key = `${owner}__${repo}`;
74
- return { key, owner, repo };
75
- }
76
- parseRepoKey(repoKey) {
77
- const parts = repoKey.split("__");
78
- const owner = parts[0] || repoKey;
79
- const repo = parts[1] || repoKey;
80
- return { owner, repo };
81
- }
82
- findTaskStateEntry(taskId) {
83
- for (const [repoKey, state] of this.repoStates) {
84
- const repo = state.repositories[repoKey];
85
- const taskState = repo?.worktrees?.[taskId];
86
- if (repo && taskState) {
87
- return { repoKey, repo, taskState };
88
- }
89
- }
90
- return null;
91
- }
92
- async withRepoLock(repoKey, fn) {
93
- const previous = this.repoLocks.get(repoKey) ?? Promise.resolve();
94
- let release;
95
- const current = new Promise((resolve) => {
96
- release = resolve;
97
- });
98
- const chained = previous.then(() => current);
99
- this.repoLocks.set(repoKey, chained);
100
- await previous;
101
- try {
102
- return await fn();
103
- }
104
- finally {
105
- release();
106
- if (this.repoLocks.get(repoKey) === chained) {
107
- this.repoLocks.delete(repoKey);
108
- }
109
- }
110
- }
111
- /**
112
- * Ensure control repository exists
113
- */
114
- async ensureControlRepository(repoUrl, githubToken) {
115
- const { key, owner, repo } = this.getRepoKey(repoUrl);
116
- const controlPath = path.join(this.repoBasePath, "repos", key, "control");
117
- // Check if control repository already exists
118
- try {
119
- // Support both bare repos (config/objects at root) and legacy non-bare repos (.git/)
120
- const hasGitDir = await fs
121
- .access(path.join(controlPath, ".git"))
122
- .then(() => true)
123
- .catch(() => false);
124
- const hasBareLayout = await fs
125
- .access(path.join(controlPath, "config"))
126
- .then(() => fs.access(path.join(controlPath, "objects")).then(() => true))
127
- .catch(() => false);
128
- if (!hasGitDir && !hasBareLayout) {
129
- throw new Error("missing");
130
- }
131
- console.log(`Control repository already exists for ${key}`);
132
- // Update remote URL if token changed
133
- if (githubToken) {
134
- const git = simpleGit(controlPath);
135
- const authUrl = this.getAuthenticatedUrl(repoUrl, githubToken);
136
- await git.remote(["set-url", "origin", authUrl]);
137
- }
138
- // Fetch latest changes
139
- const git = simpleGit(controlPath);
140
- await git.fetch("origin");
141
- return controlPath;
142
- }
143
- catch {
144
- // Control repository doesn't exist, create it
145
- console.log(`Creating control repository for ${key}...`);
146
- await this.ensureDirectory(path.dirname(controlPath));
147
- const authUrl = this.getAuthenticatedUrl(repoUrl, githubToken);
148
- await this.executeGit(["clone", "--bare", authUrl, controlPath]);
149
- console.log(`Successfully created control repository for ${key}`);
150
- return controlPath;
151
- }
152
- }
153
- /**
154
- * Create a task-specific worktree for local workspaces
155
- */
156
- async createLocalTaskHandle(taskId, localPath) {
157
- // For local workspaces, we don't create worktrees or branches
158
- // Just return a handle pointing to the local path
159
- const handle = {
160
- taskId,
161
- worktreePath: localPath,
162
- branch: "local", // Placeholder branch name for local workspaces
163
- baseBranch: "local",
164
- };
165
- // Create and persist state for tracking
166
- const state = {
167
- taskId,
168
- branch: "local",
169
- baseBranch: "local",
170
- worktreePath: localPath,
171
- status: "active",
172
- createdAt: new Date(),
173
- updatedAt: new Date(),
174
- };
175
- // Store state using a special key for local workspaces
176
- await this.updateTaskState("__local__", state);
177
- console.log(`Created local task handle for task ${taskId} at ${localPath}`);
178
- return handle;
179
- }
180
- /**
181
- * Create a task-specific worktree
182
- */
183
- async createTaskWorktree(taskId, workspaceId, repoUrl, baseBranch = "main", githubToken) {
184
- const { key, owner, repo } = this.getRepoKey(repoUrl);
185
- // Reuse an existing worktree/branch for this task when possible
186
- const existing = this.findTaskStateEntry(taskId);
187
- if (existing && existing.repoKey !== "__local__" && existing.taskState.branch !== "local") {
188
- const expectedWorktreePath = path.join(this.taskWorktreesBasePath, existing.repoKey, `task_${taskId}`);
189
- const worktreePath = existing.taskState.worktreePath || expectedWorktreePath;
190
- const shouldPersistMetadata = existing.taskState.workspaceId !== workspaceId ||
191
- existing.taskState.baseBranch !== baseBranch;
192
- if (shouldPersistMetadata) {
193
- existing.taskState.workspaceId = workspaceId;
194
- existing.taskState.baseBranch = baseBranch;
195
- existing.taskState.updatedAt = new Date();
196
- await this.updateTaskStateByTaskId(taskId, existing.taskState);
197
- }
198
- try {
199
- await fs.access(worktreePath);
200
- return {
201
- taskId,
202
- worktreePath,
203
- branch: existing.taskState.branch,
204
- baseBranch: existing.taskState.baseBranch,
205
- };
206
- }
207
- catch {
208
- // Recreate missing worktree from the existing branch
209
- const git = simpleGit(existing.repo.controlPath);
210
- await this.ensureDirectory(path.dirname(worktreePath));
211
- try {
212
- await git.raw(["worktree", "add", worktreePath, existing.taskState.branch]);
213
- }
214
- catch (error) {
215
- console.error(`Failed to recreate worktree for task ${taskId}:`, error);
216
- await this.cleanupDirectory(worktreePath);
217
- throw error;
218
- }
219
- const worktreeGit = simpleGit(worktreePath);
220
- await worktreeGit.addConfig("user.name", "Northflare");
221
- await worktreeGit.addConfig("user.email", "runner@northflare.ai");
222
- existing.taskState.worktreePath = worktreePath;
223
- existing.taskState.workspaceId = workspaceId;
224
- existing.taskState.baseBranch = baseBranch;
225
- existing.taskState.status = "active";
226
- existing.taskState.updatedAt = new Date();
227
- await this.updateTaskState(existing.repoKey, existing.taskState);
228
- return {
229
- taskId,
230
- worktreePath,
231
- branch: existing.taskState.branch,
232
- baseBranch: existing.taskState.baseBranch,
233
- };
234
- }
235
- }
236
- const isLocalRepo = repoUrl.startsWith("file://");
237
- // Ensure control repository exists
238
- let controlPath;
239
- if (isLocalRepo) {
240
- controlPath = repoUrl.replace("file://", "");
241
- // Verify local path exists and has a Git directory
242
- const stats = await fs.stat(controlPath);
243
- if (!stats.isDirectory()) {
244
- throw new Error(`Local repository path is not a directory: ${controlPath}`);
245
- }
246
- await fs.access(path.join(controlPath, ".git"));
247
- }
248
- else {
249
- controlPath = await this.ensureControlRepository(repoUrl, githubToken);
250
- }
251
- const worktreesPath = path.join(this.taskWorktreesBasePath, key);
252
- const taskWorktreePath = path.join(worktreesPath, `task_${taskId}`);
253
- // Create unique branch name for the task
254
- const branchName = `task/${taskId}`;
255
- try {
256
- console.log(`Creating worktree for task ${taskId} on branch ${branchName}...`);
257
- // Create worktree directory
258
- await this.ensureDirectory(worktreesPath);
259
- // Add worktree with new branch based on baseBranch
260
- const git = simpleGit(controlPath);
261
- const baseRef = isLocalRepo ? baseBranch : `origin/${baseBranch}`;
262
- let baseRefIsValid = await git
263
- .raw(["rev-parse", "--verify", `${baseRef}^{commit}`])
264
- .then(() => true)
265
- .catch(() => false);
266
- if (!baseRefIsValid) {
267
- const hasAnyCommit = await git
268
- .raw(["rev-parse", "--verify", "HEAD^{commit}"])
269
- .then(() => true)
270
- .catch(() => false);
271
- if (!hasAnyCommit) {
272
- await this.ensureInitialCommitOnBranch(controlPath, baseBranch);
273
- baseRefIsValid = await git
274
- .raw(["rev-parse", "--verify", `${baseRef}^{commit}`])
275
- .then(() => true)
276
- .catch(() => false);
277
- }
278
- if (!baseRefIsValid) {
279
- throw new Error(`Cannot create a task worktree for ${taskId}: base branch '${baseBranch}' is not a valid Git reference in ${controlPath}. Set Workspace → Settings → Git branch to an existing branch name and retry.`);
280
- }
281
- }
282
- const branchExists = await git
283
- .raw(["show-ref", "--verify", `refs/heads/${branchName}`])
284
- .then(() => true)
285
- .catch(() => false);
286
- if (branchExists) {
287
- await git.raw(["worktree", "add", taskWorktreePath, branchName]);
288
- }
289
- else {
290
- await git.raw([
291
- "worktree",
292
- "add",
293
- "-b",
294
- branchName,
295
- taskWorktreePath,
296
- baseRef,
297
- ]);
298
- }
299
- // Configure the worktree
300
- const worktreeGit = simpleGit(taskWorktreePath);
301
- await worktreeGit.addConfig("user.name", "Northflare");
302
- await worktreeGit.addConfig("user.email", "runner@northflare.ai");
303
- // Create and persist state
304
- const state = {
305
- taskId,
306
- workspaceId,
307
- branch: branchName,
308
- baseBranch,
309
- worktreePath: taskWorktreePath,
310
- status: "active",
311
- createdAt: new Date(),
312
- updatedAt: new Date(),
313
- };
314
- await this.updateTaskState(key, state, { owner, repo, controlPath });
315
- console.log(`Successfully created worktree for task ${taskId}`);
316
- return {
317
- taskId,
318
- worktreePath: taskWorktreePath,
319
- branch: branchName,
320
- baseBranch,
321
- };
322
- }
323
- catch (error) {
324
- console.error(`Failed to create worktree for task ${taskId}:`, error);
325
- // Cleanup on failure
326
- await this.cleanupDirectory(taskWorktreePath);
327
- throw error;
328
- }
329
- }
330
- async ensureInitialCommitOnBranch(repoPath, branchName) {
331
- const git = simpleGit(repoPath);
332
- await git.addConfig("user.name", "Northflare");
333
- await git.addConfig("user.email", "runner@northflare.ai");
334
- const hasAnyCommit = await git
335
- .raw(["rev-parse", "--verify", "HEAD^{commit}"])
336
- .then(() => true)
337
- .catch(() => false);
338
- if (hasAnyCommit)
339
- return;
340
- console.log(`Repository at ${repoPath} has no commits; creating an empty initial commit on '${branchName}'...`);
341
- const isBareRepo = await git
342
- .raw(["rev-parse", "--is-bare-repository"])
343
- .then((output) => output.trim() === "true")
344
- .catch(() => false);
345
- if (isBareRepo) {
346
- const emptyTreeHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
347
- const commitHash = (await git.raw([
348
- "commit-tree",
349
- emptyTreeHash,
350
- "-m",
351
- "Initial commit",
352
- ])).trim();
353
- await git.raw(["update-ref", `refs/heads/${branchName}`, commitHash]);
354
- await git.raw(["symbolic-ref", "HEAD", `refs/heads/${branchName}`]);
355
- const remotes = await git.getRemotes(true);
356
- const hasOrigin = remotes.some((remote) => remote.name === "origin");
357
- void hasOrigin;
358
- }
359
- else {
360
- await git.raw(["checkout", "--orphan", branchName]);
361
- await git.raw(["commit", "--allow-empty", "-m", "Initial commit"]);
362
- }
363
- }
364
- /**
365
- * Remove a task worktree
366
- */
367
- async removeTaskWorktree(taskId, options = {}) {
368
- const entry = this.findTaskStateEntry(taskId);
369
- if (!entry) {
370
- console.log(`No worktree found for task ${taskId}`);
371
- return;
372
- }
373
- const { repoKey, repo, taskState } = entry;
374
- // Handle local workspaces differently
375
- if (repoKey === "__local__") {
376
- // Just remove from state, don't touch the filesystem
377
- const repoState = this.repoStates.get(repoKey);
378
- const repo = repoState?.repositories?.[repoKey];
379
- if (repo?.worktrees?.[taskId]) {
380
- delete repo.worktrees[taskId];
381
- }
382
- await this.persistState();
383
- console.log(`Removed local task handle for task ${taskId}`);
384
- return;
385
- }
386
- const controlPath = repo.controlPath;
387
- try {
388
- console.log(`Removing worktree for task ${taskId}...`);
389
- const git = simpleGit(controlPath);
390
- // Remove the worktree (when present)
391
- if (taskState.worktreePath) {
392
- const worktreePath = taskState.worktreePath;
393
- try {
394
- await fs.access(worktreePath);
395
- await git.raw(["worktree", "remove", worktreePath, "--force"]);
396
- }
397
- catch {
398
- // Worktree path already gone; continue so we can still delete the branch if needed
399
- }
400
- finally {
401
- await this.cleanupDirectory(worktreePath);
402
- }
403
- }
404
- // Delete the branch if not preserving
405
- if (!options.preserveBranch) {
406
- try {
407
- await git.raw(["branch", "-D", taskState.branch]);
408
- }
409
- catch (error) {
410
- console.warn(`Failed to delete branch ${taskState.branch}:`, error);
411
- }
412
- }
413
- // Update state
414
- const repoState = this.repoStates.get(repoKey);
415
- if (repoState?.repositories[repoKey]?.worktrees) {
416
- if (options.preserveBranch) {
417
- repoState.repositories[repoKey].worktrees[taskId] = {
418
- ...taskState,
419
- worktreePath: "",
420
- updatedAt: new Date(),
421
- };
422
- }
423
- else {
424
- delete repoState.repositories[repoKey].worktrees[taskId];
425
- }
426
- }
427
- await this.persistState();
428
- console.log(`Successfully removed worktree for task ${taskId}`);
429
- }
430
- catch (error) {
431
- console.error(`Failed to remove worktree for task ${taskId}:`, error);
432
- if (options.force) {
433
- // Force cleanup
434
- await this.cleanupDirectory(taskState.worktreePath);
435
- }
436
- else {
437
- throw error;
438
- }
439
- }
440
- }
441
- /**
442
- * Stage all changes in a task worktree
443
- */
444
- async stageAll(taskId) {
445
- const taskState = await this.getTaskState(taskId);
446
- if (!taskState) {
447
- throw new Error(`No worktree found for task ${taskId}`);
448
- }
449
- // Skip Git operations for local workspaces
450
- if (taskState.branch === "local") {
451
- console.log(`Skipping stage operation for local workspace task ${taskId}`);
452
- return;
453
- }
454
- const git = simpleGit(taskState.worktreePath);
455
- await git.add(".");
456
- console.log(`Staged all changes for task ${taskId}`);
457
- }
458
- /**
459
- * Commit changes in a task worktree
460
- */
461
- async commit(taskId, message, author) {
462
- const taskState = await this.getTaskState(taskId);
463
- if (!taskState) {
464
- throw new Error(`No worktree found for task ${taskId}`);
465
- }
466
- // Skip Git operations for local workspaces
467
- if (taskState.branch === "local") {
468
- console.log(`Skipping commit operation for local workspace task ${taskId}`);
469
- return "local-commit"; // Return a placeholder commit hash
470
- }
471
- const git = simpleGit(taskState.worktreePath);
472
- // Set author if provided
473
- if (author) {
474
- await git.addConfig("user.name", author.name);
475
- await git.addConfig("user.email", author.email);
476
- }
477
- // Commit changes
478
- const result = await git.commit(message);
479
- const commitHash = result.commit;
480
- // Update task state
481
- taskState.lastCommit = commitHash;
482
- taskState.commitCount = (taskState.commitCount ?? 0) + 1;
483
- taskState.updatedAt = new Date();
484
- await this.updateTaskStateByTaskId(taskId, taskState);
485
- console.log(`Created commit ${commitHash} for task ${taskId}`);
486
- logger.info(`Committed task ${taskId} (${taskState.branch}) -> ${commitHash}`, {
487
- taskId,
488
- branch: taskState.branch,
489
- baseBranch: taskState.baseBranch,
490
- commit: commitHash,
491
- worktreePath: taskState.worktreePath,
492
- });
493
- return commitHash;
494
- }
495
- /**
496
- * Create a new branch for a task
497
- */
498
- async createBranch(taskId, branchName) {
499
- const taskState = await this.getTaskState(taskId);
500
- if (!taskState) {
501
- throw new Error(`No worktree found for task ${taskId}`);
502
- }
503
- // Skip Git operations for local workspaces
504
- if (taskState.branch === "local") {
505
- console.log(`Skipping branch creation for local workspace task ${taskId}`);
506
- return;
507
- }
508
- const git = simpleGit(taskState.worktreePath);
509
- await git.checkoutBranch(branchName, taskState.branch);
510
- // Update task state
511
- taskState.branch = branchName;
512
- taskState.updatedAt = new Date();
513
- await this.updateTaskStateByTaskId(taskId, taskState);
514
- console.log(`Created branch ${branchName} for task ${taskId}`);
515
- }
516
- /**
517
- * Rebase a task branch onto target branch
518
- */
519
- async rebaseTask(taskId, targetBranch) {
520
- const taskState = await this.getTaskState(taskId);
521
- if (!taskState) {
522
- throw new Error(`No worktree found for task ${taskId}`);
523
- }
524
- // Skip Git operations for local workspaces
525
- if (taskState.branch === "local") {
526
- console.log(`Skipping rebase operation for local workspace task ${taskId}`);
527
- return { success: true }; // Return success for local workspaces
528
- }
529
- const git = simpleGit(taskState.worktreePath);
530
- try {
531
- const remotes = await git.getRemotes(true);
532
- const hasOrigin = remotes.some((r) => r.name === "origin");
533
- logger.info(`Rebasing task ${taskId} (${taskState.branch}) onto ${targetBranch}`, {
534
- taskId,
535
- branch: taskState.branch,
536
- targetBranch,
537
- hasOrigin,
538
- worktreePath: taskState.worktreePath,
539
- });
540
- if (hasOrigin) {
541
- // Fetch latest changes
542
- await git.fetch("origin", targetBranch);
543
- // Perform rebase
544
- await git.rebase([`origin/${targetBranch}`]);
545
- }
546
- else {
547
- await git.rebase([targetBranch]);
548
- }
549
- // Update base branch in state
550
- taskState.baseBranch = targetBranch;
551
- taskState.updatedAt = new Date();
552
- await this.updateTaskStateByTaskId(taskId, taskState);
553
- console.log(`Successfully rebased task ${taskId} onto ${targetBranch}`);
554
- logger.info(`Rebased task ${taskId} (${taskState.branch}) onto ${targetBranch}`, {
555
- taskId,
556
- branch: taskState.branch,
557
- targetBranch,
558
- });
559
- return { success: true };
560
- }
561
- catch (error) {
562
- console.error(`Rebase failed for task ${taskId}:`, error);
563
- logger.error(`Rebase failed for task ${taskId} (${taskState.branch})`, {
564
- taskId,
565
- branch: taskState.branch,
566
- targetBranch,
567
- error: error instanceof Error ? error.message : String(error),
568
- });
569
- // Check for conflicts
570
- const status = await git.status();
571
- if (status.conflicted.length > 0) {
572
- taskState.status = "conflicted";
573
- await this.updateTaskStateByTaskId(taskId, taskState);
574
- return {
575
- success: false,
576
- conflicts: status.conflicted,
577
- error: "Rebase resulted in conflicts",
578
- };
579
- }
580
- // Abort rebase on error
581
- try {
582
- await git.rebase(["--abort"]);
583
- }
584
- catch {
585
- // Ignore abort errors
586
- }
587
- return {
588
- success: false,
589
- error: error instanceof Error ? error.message : String(error),
590
- };
591
- }
592
- }
593
- /**
594
- * Merge a task branch into target branch
595
- */
596
- async mergeTask(taskId, targetBranch, mode = "no-ff") {
597
- const entry = this.findTaskStateEntry(taskId);
598
- if (!entry) {
599
- throw new Error(`No worktree found for task ${taskId}`);
600
- }
601
- const { repoKey, taskState } = entry;
602
- // Skip Git operations for local workspaces
603
- if (taskState.branch === "local") {
604
- console.log(`Skipping merge operation for local workspace task ${taskId}`);
605
- return { success: true, mergedCommit: "local-merge" }; // Return success for local workspaces
606
- }
607
- return await this.withRepoLock(repoKey, async () => {
608
- return this.mergeTaskUnlocked(entry, targetBranch, mode);
609
- });
610
- }
611
- async mergeTaskUnlocked(entry, targetBranch, mode) {
612
- const { repoKey, repo, taskState } = entry;
613
- const isLocalRepo = repoKey.startsWith("local__");
614
- const mergePath = await (async () => {
615
- if (isLocalRepo)
616
- return repo.controlPath;
617
- if (!taskState.workspaceId) {
618
- throw new Error(`Cannot merge task ${taskState.taskId}: missing workspaceId in task Git state`);
619
- }
620
- const worktreesPath = path.join(this.repoBasePath, "repos", repoKey, "worktrees");
621
- const workspaceWorktreePath = path.join(worktreesPath, `workspace_${taskState.workspaceId}`);
622
- const exists = await fs
623
- .access(workspaceWorktreePath)
624
- .then(() => true)
625
- .catch(() => false);
626
- if (exists)
627
- return workspaceWorktreePath;
628
- await this.ensureDirectory(worktreesPath);
629
- const controlGit = simpleGit(repo.controlPath);
630
- await controlGit.fetch("origin", targetBranch);
631
- await controlGit.raw([
632
- "worktree",
633
- "add",
634
- workspaceWorktreePath,
635
- `origin/${targetBranch}`,
636
- ]);
637
- console.log(`Created merge worktree for workspace ${taskState.workspaceId} at ${workspaceWorktreePath}`);
638
- return workspaceWorktreePath;
639
- })();
640
- const git = simpleGit(mergePath);
641
- try {
642
- const remotes = await git.getRemotes(true);
643
- const hasOrigin = remotes.some((r) => r.name === "origin");
644
- logger.info(`Merging task ${taskState.taskId} (${taskState.branch}) into ${targetBranch}`, {
645
- taskId: taskState.taskId,
646
- taskBranch: taskState.branch,
647
- targetBranch,
648
- mergePath,
649
- hasOrigin,
650
- isLocalRepo,
651
- mode,
652
- });
653
- if (hasOrigin) {
654
- // Fetch latest changes and ensure a local targetBranch exists + is up-to-date
655
- await git.fetch("origin", targetBranch);
656
- const localBranches = await git.branchLocal();
657
- const hasLocalTarget = localBranches.all.includes(targetBranch);
658
- if (!hasLocalTarget) {
659
- await git.raw(["checkout", "-B", targetBranch, `origin/${targetBranch}`]);
660
- }
661
- else {
662
- await git.checkout(targetBranch);
663
- try {
664
- await git.merge(["--ff-only", `origin/${targetBranch}`]);
665
- }
666
- catch (error) {
667
- throw new Error(`Base branch ${targetBranch} has diverged from origin/${targetBranch}; cannot fast-forward`);
668
- }
669
- }
670
- }
671
- else {
672
- await git.checkout(targetBranch);
673
- }
674
- // Perform merge based on mode
675
- let mergeArgs = [];
676
- switch (mode) {
677
- case "ff-only":
678
- mergeArgs = ["--ff-only"];
679
- break;
680
- case "no-ff":
681
- mergeArgs = ["--no-ff"];
682
- break;
683
- case "squash":
684
- mergeArgs = ["--squash"];
685
- break;
686
- }
687
- const result = await git.merge([...mergeArgs, taskState.branch]);
688
- // For squash merge, we need to commit
689
- if (mode === "squash") {
690
- const commitResult = await git.commit(`Merge task ${taskState.taskId} (${taskState.branch})`);
691
- result.result = commitResult.commit;
692
- }
693
- // Update task state
694
- taskState.status = "merged";
695
- taskState.updatedAt = new Date();
696
- await this.updateTaskStateByTaskId(taskState.taskId, taskState);
697
- console.log(`Successfully merged task ${taskState.taskId} into ${targetBranch}`);
698
- logger.info(`Merged task ${taskState.taskId} (${taskState.branch}) into ${targetBranch}`, {
699
- taskId: taskState.taskId,
700
- taskBranch: taskState.branch,
701
- targetBranch,
702
- mergedCommit: result.result,
703
- });
704
- return {
705
- success: true,
706
- mergedCommit: result.result,
707
- };
708
- }
709
- catch (error) {
710
- console.error(`Merge failed for task ${taskState.taskId}:`, error);
711
- logger.error(`Merge failed for task ${taskState.taskId} (${taskState.branch})`, {
712
- taskId: taskState.taskId,
713
- taskBranch: taskState.branch,
714
- targetBranch,
715
- mergePath,
716
- error: error instanceof Error ? error.message : String(error),
717
- });
718
- // Check for conflicts
719
- try {
720
- const status = await git.status();
721
- if (status.conflicted.length > 0) {
722
- taskState.status = "conflicted";
723
- await this.updateTaskStateByTaskId(taskState.taskId, taskState);
724
- return {
725
- success: false,
726
- conflicts: status.conflicted,
727
- error: "Merge resulted in conflicts",
728
- };
729
- }
730
- }
731
- catch {
732
- // Ignore status errors (e.g. bare repositories)
733
- }
734
- return {
735
- success: false,
736
- error: error instanceof Error ? error.message : String(error),
737
- };
738
- }
739
- }
740
- async getTaskRepoInfo(taskId) {
741
- const entry = this.findTaskStateEntry(taskId);
742
- if (!entry)
743
- return null;
744
- return {
745
- repoKey: entry.repoKey,
746
- controlPath: entry.repo.controlPath,
747
- worktreePath: entry.taskState.worktreePath,
748
- branch: entry.taskState.branch,
749
- baseBranch: entry.taskState.baseBranch,
750
- };
751
- }
752
- /**
753
- * Fast-forward (or hard reset) the shared workspace worktree to a branch tip after a task merge.
754
- * This keeps the "workspace" checkout in sync with concurrent task integrations.
755
- *
756
- * We only update existing workspace worktrees and only when they're clean to avoid
757
- * clobbering any active non-concurrent task that might be using the workspace checkout.
758
- */
759
- async syncWorkspaceWorktree(workspaceId, repoUrl, branch) {
760
- if (!workspaceId || !repoUrl)
761
- return;
762
- // Local repos merge directly into the primary working tree (controlPath), nothing to sync.
763
- if (repoUrl.startsWith("file://"))
764
- return;
765
- const { key } = this.getRepoKey(repoUrl);
766
- const workspaceWorktreePath = path.join(this.repoBasePath, "repos", key, "worktrees", `workspace_${workspaceId}`);
767
- const exists = await fs
768
- .access(workspaceWorktreePath)
769
- .then(() => true)
770
- .catch(() => false);
771
- await this.withRepoLock(key, async () => {
772
- if (!exists) {
773
- const controlPath = path.join(this.repoBasePath, "repos", key, "control");
774
- const controlExists = await fs
775
- .access(controlPath)
776
- .then(() => true)
777
- .catch(() => false);
778
- if (!controlExists) {
779
- console.warn(`Skipping workspace worktree sync for ${workspaceId}: control repo missing`, { controlPath, repoUrl });
780
- return;
781
- }
782
- await this.ensureDirectory(path.dirname(workspaceWorktreePath));
783
- const controlGit = simpleGit(controlPath);
784
- const remotes = await controlGit.getRemotes(true);
785
- const hasOrigin = remotes.some((remote) => remote.name === "origin");
786
- const checkoutRef = hasOrigin ? `origin/${branch}` : branch;
787
- await controlGit.raw(["worktree", "add", workspaceWorktreePath, checkoutRef]);
788
- console.log(`Created workspace worktree for ${workspaceId} at ${workspaceWorktreePath}`);
789
- }
790
- const git = simpleGit(workspaceWorktreePath);
791
- const status = await git.status();
792
- if (status.conflicted.length > 0 || !status.isClean()) {
793
- console.warn(`Skipping workspace worktree sync for ${workspaceId}: worktree is not clean`, {
794
- path: workspaceWorktreePath,
795
- branch,
796
- conflicted: status.conflicted,
797
- fileCount: status.files.length,
798
- });
799
- return;
800
- }
801
- const remotes = await git.getRemotes(true);
802
- const hasOrigin = remotes.some((remote) => remote.name === "origin");
803
- if (hasOrigin) {
804
- await git.fetch("origin", branch);
805
- await git.reset(["--hard", `origin/${branch}`]);
806
- }
807
- else {
808
- await git.reset(["--hard", branch]);
809
- }
810
- await git.clean("f", ["-d"]);
811
- console.log(`Synced workspace worktree for ${workspaceId} to ${branch} at ${workspaceWorktreePath}`);
812
- });
813
- }
814
- async isTaskBranchMerged(taskId, targetBranch) {
815
- const entry = this.findTaskStateEntry(taskId);
816
- if (!entry)
817
- return false;
818
- const { repo, taskState } = entry;
819
- const git = simpleGit(repo.controlPath);
820
- try {
821
- // Ensure targetBranch exists locally (avoid erroring for repos that only have origin/<branch>)
822
- const showRef = await git.raw([
823
- "show-ref",
824
- "--verify",
825
- `refs/heads/${targetBranch}`,
826
- ]);
827
- if (!showRef?.trim())
828
- return false;
829
- }
830
- catch {
831
- return false;
832
- }
833
- try {
834
- await git.raw([
835
- "merge-base",
836
- "--is-ancestor",
837
- taskState.branch,
838
- targetBranch,
839
- ]);
840
- return true;
841
- }
842
- catch {
843
- return false;
844
- }
845
- }
846
- async pushBranch(taskId, branch) {
847
- const entry = this.findTaskStateEntry(taskId);
848
- if (!entry)
849
- return;
850
- const git = simpleGit(entry.repo.controlPath);
851
- const remotes = await git.getRemotes(true);
852
- const hasOrigin = remotes.some((r) => r.name === "origin");
853
- if (!hasOrigin)
854
- return;
855
- await git.push("origin", branch);
856
- }
857
- async integrateTask(taskId, targetBranch, mode = "no-ff") {
858
- const entry = this.findTaskStateEntry(taskId);
859
- if (!entry) {
860
- return {
861
- success: false,
862
- phase: "merge",
863
- error: `No worktree found for task ${taskId}`,
864
- conflictWorkdir: "",
865
- };
866
- }
867
- const { repoKey, repo, taskState } = entry;
868
- const isLocalRepo = repoKey.startsWith("local__");
869
- const mergeWorkdir = isLocalRepo
870
- ? repo.controlPath
871
- : taskState.workspaceId
872
- ? path.join(this.repoBasePath, "repos", repoKey, "worktrees", `workspace_${taskState.workspaceId}`)
873
- : taskState.worktreePath;
874
- logger.info(`Integrating task ${taskId} (${taskState.branch}) into ${targetBranch}`, {
875
- taskId,
876
- taskBranch: taskState.branch,
877
- targetBranch,
878
- mode,
879
- isLocalRepo,
880
- controlPath: repo.controlPath,
881
- worktreePath: taskState.worktreePath,
882
- });
883
- return await this.withRepoLock(repoKey, async () => {
884
- const rebaseResult = await this.rebaseTask(taskId, targetBranch);
885
- if (!rebaseResult.success) {
886
- logger.warn(`Task ${taskId} rebase failed`, {
887
- taskId,
888
- phase: "rebase",
889
- targetBranch,
890
- conflicts: rebaseResult.conflicts ?? [],
891
- error: rebaseResult.error,
892
- });
893
- return {
894
- success: false,
895
- phase: "rebase",
896
- conflicts: rebaseResult.conflicts ?? [],
897
- error: rebaseResult.error,
898
- conflictWorkdir: taskState.worktreePath,
899
- };
900
- }
901
- const mergeResult = await this.mergeTaskUnlocked(entry, targetBranch, mode);
902
- if (!mergeResult.success) {
903
- logger.warn(`Task ${taskId} merge failed`, {
904
- taskId,
905
- phase: "merge",
906
- targetBranch,
907
- conflicts: mergeResult.conflicts ?? [],
908
- error: mergeResult.error,
909
- });
910
- return {
911
- success: false,
912
- phase: "merge",
913
- conflicts: mergeResult.conflicts ?? [],
914
- error: mergeResult.error,
915
- conflictWorkdir: mergeWorkdir,
916
- };
917
- }
918
- logger.info(`Integrated task ${taskId} into ${targetBranch}`, {
919
- taskId,
920
- targetBranch,
921
- mergedCommit: mergeResult.mergedCommit,
922
- });
923
- return {
924
- success: true,
925
- mergedCommit: mergeResult.mergedCommit,
926
- };
927
- });
928
- }
929
- /**
930
- * Get task Git state
931
- */
932
- async getTaskState(taskId) {
933
- return this.findTaskStateEntry(taskId)?.taskState ?? null;
934
- }
935
- /**
936
- * Update task state
937
- */
938
- async updateTaskState(repoKey, state, repoInfo) {
939
- if (!this.repoStates.has(repoKey)) {
940
- this.repoStates.set(repoKey, {
941
- repositories: {},
942
- });
943
- }
944
- const repoState = this.repoStates.get(repoKey);
945
- if (!repoState.repositories[repoKey]) {
946
- // Handle special case for local workspaces
947
- if (repoKey === "__local__") {
948
- repoState.repositories[repoKey] = {
949
- owner: "local",
950
- repo: "local",
951
- controlPath: "local",
952
- worktrees: {},
953
- };
954
- }
955
- else if (repoInfo) {
956
- repoState.repositories[repoKey] = {
957
- owner: repoInfo.owner,
958
- repo: repoInfo.repo,
959
- controlPath: repoInfo.controlPath,
960
- worktrees: {},
961
- };
962
- }
963
- else {
964
- const { owner, repo } = this.parseRepoKey(repoKey);
965
- repoState.repositories[repoKey] = {
966
- owner,
967
- repo,
968
- controlPath: path.join(this.repoBasePath, "repos", repoKey, "control"),
969
- worktrees: {},
970
- };
971
- }
972
- }
973
- else if (repoInfo?.controlPath) {
974
- // Ensure we persist the latest controlPath (important for local repos)
975
- repoState.repositories[repoKey].controlPath = repoInfo.controlPath;
976
- }
977
- repoState.repositories[repoKey].worktrees[state.taskId] = state;
978
- await this.persistState();
979
- }
980
- /**
981
- * Update task state by task ID
982
- */
983
- async updateTaskStateByTaskId(taskId, state) {
984
- for (const [repoKey, repoState] of this.repoStates) {
985
- for (const repo of Object.values(repoState.repositories)) {
986
- if (repo.worktrees[taskId]) {
987
- repo.worktrees[taskId] = state;
988
- await this.persistState();
989
- return;
990
- }
991
- }
992
- }
993
- }
994
- /**
995
- * Persist state to disk
996
- */
997
- async persistState() {
998
- try {
999
- const stateData = {};
1000
- for (const [key, value] of this.repoStates) {
1001
- stateData[key] = value;
1002
- }
1003
- await this.ensureDirectory(path.dirname(this.stateFilePath));
1004
- await fs.writeFile(this.stateFilePath, JSON.stringify(stateData, null, 2));
1005
- console.log("Persisted repository state");
1006
- }
1007
- catch (error) {
1008
- console.error("Failed to persist repository state:", error);
1009
- }
1010
- }
1011
- /**
1012
- * Restore state from disk
1013
- */
1014
- async restoreState() {
1015
- await this.migrateLegacyStateFileIfNeeded();
1016
- try {
1017
- const data = await fs.readFile(this.stateFilePath, "utf-8");
1018
- const stateData = JSON.parse(data);
1019
- this.repoStates.clear();
1020
- for (const [key, value] of Object.entries(stateData)) {
1021
- // Convert date strings back to Date objects
1022
- for (const repo of Object.values(value.repositories)) {
1023
- for (const worktree of Object.values(repo.worktrees)) {
1024
- worktree.createdAt = new Date(worktree.createdAt);
1025
- worktree.updatedAt = new Date(worktree.updatedAt);
1026
- }
1027
- }
1028
- this.repoStates.set(key, value);
1029
- }
1030
- console.log("Restored repository state");
1031
- }
1032
- catch (error) {
1033
- if (error.code !== "ENOENT") {
1034
- console.error("Failed to restore repository state:", error);
1035
- }
1036
- // If file doesn't exist, that's okay - we'll start fresh
1037
- }
1038
- }
1039
- async migrateLegacyStateFileIfNeeded() {
1040
- const hasNewStateFile = await fs
1041
- .access(this.stateFilePath)
1042
- .then(() => true)
1043
- .catch(() => false);
1044
- if (hasNewStateFile)
1045
- return;
1046
- const hasLegacyStateFile = await fs
1047
- .access(this.legacyStateFilePath)
1048
- .then(() => true)
1049
- .catch(() => false);
1050
- if (!hasLegacyStateFile)
1051
- return;
1052
- await this.ensureDirectory(path.dirname(this.stateFilePath));
1053
- await fs.rename(this.legacyStateFilePath, this.stateFilePath);
1054
- console.log(`Moved legacy repository state file to runner data directory: ${this.stateFilePath}`);
1055
- }
1056
- /**
1057
- * Override parent's checkoutRepository to use worktrees for backward compatibility
1058
- */
1059
- async checkoutRepository(workspaceId, repoUrl, branch, githubToken) {
1060
- // If it's a local repository URL, delegate to parent's checkoutLocalRepository
1061
- if (repoUrl.startsWith("file://")) {
1062
- const localPath = repoUrl.replace("file://", "");
1063
- return super.checkoutLocalRepository(workspaceId, localPath);
1064
- }
1065
- const { key } = this.getRepoKey(repoUrl);
1066
- // Ensure control repository exists
1067
- await this.ensureControlRepository(repoUrl, githubToken);
1068
- // Create or update workspace worktree
1069
- const worktreesPath = path.join(this.repoBasePath, "repos", key, "worktrees");
1070
- const workspaceWorktreePath = path.join(worktreesPath, `workspace_${workspaceId}`);
1071
- try {
1072
- // Check if workspace worktree already exists
1073
- await fs.access(workspaceWorktreePath);
1074
- // Update existing worktree
1075
- const git = simpleGit(workspaceWorktreePath);
1076
- await git.fetch("origin");
1077
- await git.reset(["--hard", `origin/${branch}`]);
1078
- await git.clean("f", ["-d"]);
1079
- console.log(`Updated workspace worktree for ${workspaceId}`);
1080
- }
1081
- catch {
1082
- // Create new workspace worktree
1083
- const controlPath = path.join(this.repoBasePath, "repos", key, "control");
1084
- const git = simpleGit(controlPath);
1085
- await this.ensureDirectory(worktreesPath);
1086
- await git.raw([
1087
- "worktree",
1088
- "add",
1089
- workspaceWorktreePath,
1090
- `origin/${branch}`,
1091
- ]);
1092
- console.log(`Created workspace worktree for ${workspaceId}`);
1093
- }
1094
- return workspaceWorktreePath;
1095
- }
1096
- }
1097
- //# sourceMappingURL=enhanced-repository-manager.js.map