@sudocode-ai/local-server 0.1.0 → 0.1.1
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/dist/cli.js +6 -104
- package/dist/cli.js.map +1 -7
- package/dist/execution/engine/engine.js +10 -0
- package/dist/execution/engine/engine.js.map +1 -0
- package/dist/execution/engine/simple-engine.js +611 -0
- package/dist/execution/engine/simple-engine.js.map +1 -0
- package/dist/execution/engine/types.js +10 -0
- package/dist/execution/engine/types.js.map +1 -0
- package/dist/execution/output/ag-ui-adapter.js +438 -0
- package/dist/execution/output/ag-ui-adapter.js.map +1 -0
- package/dist/execution/output/ag-ui-integration.js +96 -0
- package/dist/execution/output/ag-ui-integration.js.map +1 -0
- package/dist/execution/output/claude-code-output-processor.js +769 -0
- package/dist/execution/output/claude-code-output-processor.js.map +1 -0
- package/dist/execution/output/index.js +15 -0
- package/dist/execution/output/index.js.map +1 -0
- package/dist/execution/output/types.js +22 -0
- package/dist/execution/output/types.js.map +1 -0
- package/dist/execution/process/builders/claude.js +59 -0
- package/dist/execution/process/builders/claude.js.map +1 -0
- package/dist/execution/process/index.js +15 -0
- package/dist/execution/process/index.js.map +1 -0
- package/dist/execution/process/manager.js +10 -0
- package/dist/execution/process/manager.js.map +1 -0
- package/dist/execution/process/simple-manager.js +336 -0
- package/dist/execution/process/simple-manager.js.map +1 -0
- package/dist/execution/process/types.js +10 -0
- package/dist/execution/process/types.js.map +1 -0
- package/dist/execution/process/utils.js +97 -0
- package/dist/execution/process/utils.js.map +1 -0
- package/dist/execution/resilience/circuit-breaker.js +291 -0
- package/dist/execution/resilience/circuit-breaker.js.map +1 -0
- package/dist/execution/resilience/executor.js +10 -0
- package/dist/execution/resilience/executor.js.map +1 -0
- package/dist/execution/resilience/index.js +15 -0
- package/dist/execution/resilience/index.js.map +1 -0
- package/dist/execution/resilience/resilient-executor.js +261 -0
- package/dist/execution/resilience/resilient-executor.js.map +1 -0
- package/dist/execution/resilience/retry.js +234 -0
- package/dist/execution/resilience/retry.js.map +1 -0
- package/dist/execution/resilience/types.js +30 -0
- package/dist/execution/resilience/types.js.map +1 -0
- package/dist/execution/transport/event-buffer.js +208 -0
- package/dist/execution/transport/event-buffer.js.map +1 -0
- package/dist/execution/transport/index.js +10 -0
- package/dist/execution/transport/index.js.map +1 -0
- package/dist/execution/transport/sse-transport.js +282 -0
- package/dist/execution/transport/sse-transport.js.map +1 -0
- package/dist/execution/transport/transport-manager.js +231 -0
- package/dist/execution/transport/transport-manager.js.map +1 -0
- package/dist/execution/workflow/index.js +13 -0
- package/dist/execution/workflow/index.js.map +1 -0
- package/dist/execution/workflow/linear-orchestrator.js +683 -0
- package/dist/execution/workflow/linear-orchestrator.js.map +1 -0
- package/dist/execution/workflow/memory-storage.js +68 -0
- package/dist/execution/workflow/memory-storage.js.map +1 -0
- package/dist/execution/workflow/orchestrator.js +9 -0
- package/dist/execution/workflow/orchestrator.js.map +1 -0
- package/dist/execution/workflow/types.js +9 -0
- package/dist/execution/workflow/types.js.map +1 -0
- package/dist/execution/workflow/utils.js +152 -0
- package/dist/execution/workflow/utils.js.map +1 -0
- package/dist/execution/worktree/config.js +280 -0
- package/dist/execution/worktree/config.js.map +1 -0
- package/dist/execution/worktree/git-cli.js +189 -0
- package/dist/execution/worktree/git-cli.js.map +1 -0
- package/dist/execution/worktree/index.js +15 -0
- package/dist/execution/worktree/index.js.map +1 -0
- package/dist/execution/worktree/manager.js +452 -0
- package/dist/execution/worktree/manager.js.map +1 -0
- package/dist/execution/worktree/types.js +42 -0
- package/dist/execution/worktree/types.js.map +1 -0
- package/dist/index.js +356 -104
- package/dist/index.js.map +1 -7
- package/dist/routes/executions-stream.js +55 -0
- package/dist/routes/executions-stream.js.map +1 -0
- package/dist/routes/executions.js +267 -0
- package/dist/routes/executions.js.map +1 -0
- package/dist/routes/feedback.js +329 -0
- package/dist/routes/feedback.js.map +1 -0
- package/dist/routes/issues.js +280 -0
- package/dist/routes/issues.js.map +1 -0
- package/dist/routes/relationships.js +308 -0
- package/dist/routes/relationships.js.map +1 -0
- package/dist/routes/specs.js +270 -0
- package/dist/routes/specs.js.map +1 -0
- package/dist/services/db.js +85 -0
- package/dist/services/db.js.map +1 -0
- package/dist/services/execution-lifecycle.js +286 -0
- package/dist/services/execution-lifecycle.js.map +1 -0
- package/dist/services/execution-service.js +676 -0
- package/dist/services/execution-service.js.map +1 -0
- package/dist/services/executions.js +164 -0
- package/dist/services/executions.js.map +1 -0
- package/dist/services/export.js +106 -0
- package/dist/services/export.js.map +1 -0
- package/dist/services/feedback.js +54 -0
- package/dist/services/feedback.js.map +1 -0
- package/dist/services/issues.js +35 -0
- package/dist/services/issues.js.map +1 -0
- package/dist/services/prompt-template-engine.js +212 -0
- package/dist/services/prompt-template-engine.js.map +1 -0
- package/dist/services/prompt-templates.js +236 -0
- package/dist/services/prompt-templates.js.map +1 -0
- package/dist/services/relationships.js +42 -0
- package/dist/services/relationships.js.map +1 -0
- package/dist/services/specs.js +35 -0
- package/dist/services/specs.js.map +1 -0
- package/dist/services/watcher.js +69 -0
- package/dist/services/watcher.js.map +1 -0
- package/dist/services/websocket.js +389 -0
- package/dist/services/websocket.js.map +1 -0
- package/dist/utils/sudocode-dir.js +9 -0
- package/dist/utils/sudocode-dir.js.map +1 -0
- package/package.json +4 -6
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git CLI Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides a wrapper around git CLI commands for worktree operations.
|
|
5
|
+
* Uses git CLI directly for reliability (recommended over libgit2/nodegit).
|
|
6
|
+
*
|
|
7
|
+
* @module execution/worktree/git-cli
|
|
8
|
+
*/
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { WorktreeError, WorktreeErrorCode } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* GitCli - Implementation of IGitCli using child_process
|
|
13
|
+
*/
|
|
14
|
+
export class GitCli {
|
|
15
|
+
/**
|
|
16
|
+
* Execute a git command
|
|
17
|
+
*
|
|
18
|
+
* @param command - Git command to execute
|
|
19
|
+
* @param cwd - Working directory
|
|
20
|
+
* @returns Command output
|
|
21
|
+
* @throws WorktreeError on failure
|
|
22
|
+
*/
|
|
23
|
+
execGit(command, cwd) {
|
|
24
|
+
try {
|
|
25
|
+
return execSync(command, {
|
|
26
|
+
cwd,
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const stderr = error.stderr?.toString() || '';
|
|
33
|
+
const stdout = error.stdout?.toString() || '';
|
|
34
|
+
const message = stderr || stdout || error.message || 'Unknown git error';
|
|
35
|
+
throw new WorktreeError(`Git command failed: ${command}\n${message}`, WorktreeErrorCode.GIT_ERROR, error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Escape shell argument
|
|
40
|
+
*
|
|
41
|
+
* @param arg - Argument to escape
|
|
42
|
+
* @returns Escaped argument
|
|
43
|
+
*/
|
|
44
|
+
escapeShellArg(arg) {
|
|
45
|
+
// Escape single quotes and wrap in single quotes
|
|
46
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
47
|
+
}
|
|
48
|
+
async worktreeAdd(repoPath, worktreePath, branch, force = false) {
|
|
49
|
+
const escapedPath = this.escapeShellArg(worktreePath);
|
|
50
|
+
const escapedBranch = this.escapeShellArg(branch);
|
|
51
|
+
const forceFlag = force ? '--force' : '';
|
|
52
|
+
const command = `git worktree add ${forceFlag} ${escapedPath} ${escapedBranch}`.trim();
|
|
53
|
+
this.execGit(command, repoPath);
|
|
54
|
+
}
|
|
55
|
+
async worktreeRemove(repoPath, worktreePath, force = false) {
|
|
56
|
+
const escapedPath = this.escapeShellArg(worktreePath);
|
|
57
|
+
const forceFlag = force ? '--force' : '';
|
|
58
|
+
const command = `git worktree remove ${forceFlag} ${escapedPath}`.trim();
|
|
59
|
+
this.execGit(command, repoPath);
|
|
60
|
+
}
|
|
61
|
+
async worktreePrune(repoPath) {
|
|
62
|
+
this.execGit('git worktree prune', repoPath);
|
|
63
|
+
}
|
|
64
|
+
async worktreeList(repoPath) {
|
|
65
|
+
const output = this.execGit('git worktree list --porcelain', repoPath);
|
|
66
|
+
return this.parseWorktreeList(output);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse output from git worktree list --porcelain
|
|
70
|
+
*
|
|
71
|
+
* Format:
|
|
72
|
+
* worktree /path/to/worktree
|
|
73
|
+
* HEAD abc123...
|
|
74
|
+
* branch refs/heads/branch-name
|
|
75
|
+
* locked reason (optional)
|
|
76
|
+
* prunable reason (optional)
|
|
77
|
+
*
|
|
78
|
+
* @param output - Output from git worktree list --porcelain
|
|
79
|
+
* @returns Array of WorktreeInfo
|
|
80
|
+
*/
|
|
81
|
+
parseWorktreeList(output) {
|
|
82
|
+
const worktrees = [];
|
|
83
|
+
const lines = output.split('\n').filter((line) => line.trim());
|
|
84
|
+
let currentWorktree = null;
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (line.startsWith('worktree ')) {
|
|
87
|
+
// Start a new worktree entry
|
|
88
|
+
if (currentWorktree && currentWorktree.path) {
|
|
89
|
+
worktrees.push(this.finalizeWorktreeInfo(currentWorktree));
|
|
90
|
+
}
|
|
91
|
+
currentWorktree = {
|
|
92
|
+
path: line.substring('worktree '.length).trim(),
|
|
93
|
+
isMain: false,
|
|
94
|
+
isLocked: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
else if (line.startsWith('HEAD ')) {
|
|
98
|
+
if (currentWorktree) {
|
|
99
|
+
currentWorktree.commit = line.substring('HEAD '.length).trim();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (line.startsWith('branch ')) {
|
|
103
|
+
if (currentWorktree) {
|
|
104
|
+
const branchRef = line.substring('branch '.length).trim();
|
|
105
|
+
// Extract branch name from refs/heads/branch-name
|
|
106
|
+
currentWorktree.branch = branchRef.replace('refs/heads/', '');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (line.startsWith('bare')) {
|
|
110
|
+
if (currentWorktree) {
|
|
111
|
+
currentWorktree.isMain = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (line.startsWith('locked ')) {
|
|
115
|
+
if (currentWorktree) {
|
|
116
|
+
currentWorktree.isLocked = true;
|
|
117
|
+
currentWorktree.lockReason = line.substring('locked '.length).trim();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Add the last worktree
|
|
122
|
+
if (currentWorktree && currentWorktree.path) {
|
|
123
|
+
worktrees.push(this.finalizeWorktreeInfo(currentWorktree));
|
|
124
|
+
}
|
|
125
|
+
return worktrees;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Finalize worktree info with defaults
|
|
129
|
+
*
|
|
130
|
+
* @param partial - Partial worktree info
|
|
131
|
+
* @returns Complete WorktreeInfo
|
|
132
|
+
*/
|
|
133
|
+
finalizeWorktreeInfo(partial) {
|
|
134
|
+
return {
|
|
135
|
+
path: partial.path || '',
|
|
136
|
+
branch: partial.branch || '(detached)',
|
|
137
|
+
commit: partial.commit || '',
|
|
138
|
+
isMain: partial.isMain || false,
|
|
139
|
+
isLocked: partial.isLocked || false,
|
|
140
|
+
lockReason: partial.lockReason,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async createBranch(repoPath, branchName, baseBranchOrCommit) {
|
|
144
|
+
const escapedBranch = this.escapeShellArg(branchName);
|
|
145
|
+
const escapedBase = this.escapeShellArg(baseBranchOrCommit);
|
|
146
|
+
const command = `git branch ${escapedBranch} ${escapedBase}`;
|
|
147
|
+
this.execGit(command, repoPath);
|
|
148
|
+
}
|
|
149
|
+
async deleteBranch(repoPath, branchName, force = false) {
|
|
150
|
+
const escapedBranch = this.escapeShellArg(branchName);
|
|
151
|
+
const flag = force ? '-D' : '-d';
|
|
152
|
+
const command = `git branch ${flag} ${escapedBranch}`;
|
|
153
|
+
this.execGit(command, repoPath);
|
|
154
|
+
}
|
|
155
|
+
async configureSparseCheckout(worktreePath, patterns) {
|
|
156
|
+
// Enable sparse-checkout
|
|
157
|
+
this.execGit('git sparse-checkout init --cone', worktreePath);
|
|
158
|
+
// Set patterns
|
|
159
|
+
const escapedPatterns = patterns.map((p) => this.escapeShellArg(p)).join(' ');
|
|
160
|
+
const command = `git sparse-checkout set ${escapedPatterns}`;
|
|
161
|
+
this.execGit(command, worktreePath);
|
|
162
|
+
}
|
|
163
|
+
async isValidRepo(repoPath) {
|
|
164
|
+
try {
|
|
165
|
+
this.execGit('git rev-parse --git-dir', repoPath);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async listBranches(repoPath) {
|
|
173
|
+
const output = this.execGit(`git branch --list --all --format='%(refname:short)'`, repoPath);
|
|
174
|
+
return output
|
|
175
|
+
.split('\n')
|
|
176
|
+
.map((line) => line.trim())
|
|
177
|
+
.filter((line) => line.length > 0)
|
|
178
|
+
.map((branch) => {
|
|
179
|
+
// Remove 'origin/' prefix if present for remote branches
|
|
180
|
+
// This gives us both local and remote branch names in a consistent format
|
|
181
|
+
return branch.replace(/^remotes\/origin\//, '');
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async getCurrentCommit(repoPath) {
|
|
185
|
+
const output = this.execGit('git rev-parse HEAD', repoPath);
|
|
186
|
+
return output.trim();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=git-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-cli.js","sourceRoot":"","sources":["../../../src/execution/worktree/git-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree Layer - Public API
|
|
3
|
+
*
|
|
4
|
+
* Barrel export for the Worktree Layer of the execution system.
|
|
5
|
+
* Exports all public types, interfaces, and implementations.
|
|
6
|
+
*
|
|
7
|
+
* @module execution/worktree
|
|
8
|
+
*/
|
|
9
|
+
export { WorktreeError, WorktreeErrorCode } from './types.js';
|
|
10
|
+
// Implementation
|
|
11
|
+
export { WorktreeManager } from './manager.js';
|
|
12
|
+
export { GitCli } from './git-cli.js';
|
|
13
|
+
// Configuration
|
|
14
|
+
export { DEFAULT_WORKTREE_CONFIG, getWorktreeConfig, loadWorktreeConfig, validateWorktreeConfig, updateWorktreeConfig, setWorktreeConfigProperty, resetWorktreeConfig, clearWorktreeConfigCache, } from './config.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/execution/worktree/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAK9D,iBAAiB;AACjB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAI/C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,gBAAgB;AAChB,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,kBAAkB,EAClB,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worktree Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages git worktrees for session isolation.
|
|
5
|
+
*
|
|
6
|
+
* WORKTREE ISOLATION:
|
|
7
|
+
* ===================
|
|
8
|
+
* Each worktree created by this manager is completely isolated from the main
|
|
9
|
+
* repository. This prevents race conditions and unexpected modifications during
|
|
10
|
+
* concurrent executions.
|
|
11
|
+
*
|
|
12
|
+
* Isolation is achieved through:
|
|
13
|
+
* 1. Local database (.sudocode/cache.db) in each worktree
|
|
14
|
+
* 2. Synced JSONL files with latest state (including uncommitted changes)
|
|
15
|
+
* 3. Claude config (.claude/config.json) that forces MCP to use local database
|
|
16
|
+
*
|
|
17
|
+
* Key Benefit: Multiple executions can run concurrently without interfering
|
|
18
|
+
* with each other or the main repository. All MCP/CLI operations in a worktree
|
|
19
|
+
* stay contained within that worktree.
|
|
20
|
+
*
|
|
21
|
+
* See setupWorktreeEnvironment() method for detailed implementation.
|
|
22
|
+
*
|
|
23
|
+
* @module execution/worktree/manager
|
|
24
|
+
*/
|
|
25
|
+
import { Mutex } from "async-mutex";
|
|
26
|
+
import fs from "fs";
|
|
27
|
+
import path from "path";
|
|
28
|
+
import { WorktreeError, WorktreeErrorCode } from "./types.js";
|
|
29
|
+
import { GitCli } from "./git-cli.js";
|
|
30
|
+
import { initDatabase } from "@sudocode-ai/cli/dist/db.js";
|
|
31
|
+
import { importFromJSONL } from "@sudocode-ai/cli/dist/import.js";
|
|
32
|
+
import { execSync } from "child_process";
|
|
33
|
+
/**
|
|
34
|
+
* WorktreeManager - Implementation of IWorktreeManager
|
|
35
|
+
*
|
|
36
|
+
* Manages git worktrees with proper locking, cleanup, and error recovery.
|
|
37
|
+
* Uses git CLI commands for reliability.
|
|
38
|
+
*/
|
|
39
|
+
export class WorktreeManager {
|
|
40
|
+
/** Per-path locks to prevent concurrent operations */
|
|
41
|
+
locks = new Map();
|
|
42
|
+
/** Configuration loaded from .sudocode/config.json */
|
|
43
|
+
config;
|
|
44
|
+
/** Git CLI wrapper */
|
|
45
|
+
git;
|
|
46
|
+
/**
|
|
47
|
+
* Create a new WorktreeManager
|
|
48
|
+
*
|
|
49
|
+
* @param config - Worktree configuration
|
|
50
|
+
* @param git - Optional git CLI implementation (defaults to GitCli)
|
|
51
|
+
*/
|
|
52
|
+
constructor(config, git) {
|
|
53
|
+
this.config = config;
|
|
54
|
+
this.git = git || new GitCli();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get or create a lock for a specific path
|
|
58
|
+
*
|
|
59
|
+
* @param path - Path to lock
|
|
60
|
+
* @returns Mutex for the path
|
|
61
|
+
*/
|
|
62
|
+
getLock(path) {
|
|
63
|
+
let lock = this.locks.get(path);
|
|
64
|
+
if (!lock) {
|
|
65
|
+
lock = new Mutex();
|
|
66
|
+
this.locks.set(path, lock);
|
|
67
|
+
}
|
|
68
|
+
return lock;
|
|
69
|
+
}
|
|
70
|
+
async createWorktree(params) {
|
|
71
|
+
const { repoPath, branchName, worktreePath, baseBranch: _baseBranch, createBranch, commitSha, } = params;
|
|
72
|
+
try {
|
|
73
|
+
// 1. Create branch if requested
|
|
74
|
+
if (createBranch) {
|
|
75
|
+
// Use the specified commit SHA or the current HEAD commit SHA to branch from
|
|
76
|
+
const targetCommit = commitSha || (await this.git.getCurrentCommit(repoPath));
|
|
77
|
+
await this.git.createBranch(repoPath, branchName, targetCommit);
|
|
78
|
+
}
|
|
79
|
+
// 2. Create parent directory if needed
|
|
80
|
+
const parentDir = path.dirname(worktreePath);
|
|
81
|
+
if (!fs.existsSync(parentDir)) {
|
|
82
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
// 3. Call git worktree add
|
|
85
|
+
await this.git.worktreeAdd(repoPath, worktreePath, branchName);
|
|
86
|
+
// 4. Apply sparse-checkout if configured
|
|
87
|
+
if (this.config.enableSparseCheckout &&
|
|
88
|
+
this.config.sparseCheckoutPatterns) {
|
|
89
|
+
await this.git.configureSparseCheckout(worktreePath, this.config.sparseCheckoutPatterns);
|
|
90
|
+
}
|
|
91
|
+
// 5. Validate creation
|
|
92
|
+
if (!fs.existsSync(worktreePath)) {
|
|
93
|
+
throw new WorktreeError(`Worktree creation succeeded but path does not exist: ${worktreePath}`, WorktreeErrorCode.REPOSITORY_ERROR);
|
|
94
|
+
}
|
|
95
|
+
// 6. Setup isolated worktree environment
|
|
96
|
+
// This is critical for preventing the worktree from modifying the main repository.
|
|
97
|
+
// It creates a local database, syncs JSONL files, and configures Claude to use
|
|
98
|
+
// the local environment. See setupWorktreeEnvironment() for detailed explanation.
|
|
99
|
+
await this.setupWorktreeEnvironment(repoPath, worktreePath);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof WorktreeError) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
throw new WorktreeError(`Failed to create worktree: ${error}`, WorktreeErrorCode.REPOSITORY_ERROR, error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Setup isolated environment for worktree
|
|
110
|
+
*
|
|
111
|
+
* WORKTREE ISOLATION ARCHITECTURE:
|
|
112
|
+
* ================================
|
|
113
|
+
* Problem: Previously, MCP/CLI tools running in worktrees would search upward
|
|
114
|
+
* and find the main repository's database, causing race conditions and
|
|
115
|
+
* unexpected modifications to the main repo during execution.
|
|
116
|
+
*
|
|
117
|
+
* Solution: Each worktree gets its own isolated environment:
|
|
118
|
+
* - Local database (.sudocode/cache.db in worktree)
|
|
119
|
+
* - Synced JSONL files with latest state (including uncommitted changes)
|
|
120
|
+
* - Claude config that forces MCP to use the local database
|
|
121
|
+
*
|
|
122
|
+
* Benefits:
|
|
123
|
+
* - Worktree operations never affect main repository
|
|
124
|
+
* - Multiple executions can run concurrently without conflicts
|
|
125
|
+
* - Worktree gets consistent state (newly created issues are available)
|
|
126
|
+
* - Easy to inspect/debug worktree state after execution
|
|
127
|
+
*
|
|
128
|
+
* Flow:
|
|
129
|
+
* 1. Git creates worktree → checks out files from committed git tree
|
|
130
|
+
* 2. This method runs → copies latest JSONL (including uncommitted changes)
|
|
131
|
+
* 3. Initializes local DB from JSONL → worktree has complete state
|
|
132
|
+
* 4. Creates .claude/config.json → MCP uses local DB via env vars
|
|
133
|
+
* 5. Claude runs → all MCP operations stay in worktree
|
|
134
|
+
* 6. (Future) Merge worktree changes back to main after execution
|
|
135
|
+
*
|
|
136
|
+
* @param repoPath - Path to the main git repository
|
|
137
|
+
* @param worktreePath - Path to the worktree directory
|
|
138
|
+
*/
|
|
139
|
+
async setupWorktreeEnvironment(repoPath, worktreePath) {
|
|
140
|
+
console.debug("[WorktreeManager] Setting up isolated worktree environment", {
|
|
141
|
+
repoPath,
|
|
142
|
+
worktreePath,
|
|
143
|
+
});
|
|
144
|
+
const mainSudocodeDir = path.join(repoPath, ".sudocode");
|
|
145
|
+
const worktreeSudocodeDir = path.join(worktreePath, ".sudocode");
|
|
146
|
+
console.debug("[WorktreeManager] Directory paths", {
|
|
147
|
+
mainSudocodeDir,
|
|
148
|
+
worktreeSudocodeDir,
|
|
149
|
+
});
|
|
150
|
+
// Ensure .sudocode directory exists in worktree
|
|
151
|
+
if (!fs.existsSync(worktreeSudocodeDir)) {
|
|
152
|
+
fs.mkdirSync(worktreeSudocodeDir, { recursive: true });
|
|
153
|
+
console.debug(`[WorktreeManager] Created .sudocode directory in worktree: ${worktreeSudocodeDir}`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.debug(`[WorktreeManager] .sudocode directory already exists in worktree`);
|
|
157
|
+
}
|
|
158
|
+
// STEP 1: Copy uncommitted JSONL files from main repo to worktree
|
|
159
|
+
// ================================================================
|
|
160
|
+
// Why: Git worktree checkout only gets committed files from git history.
|
|
161
|
+
// If the user created new issues/specs before starting the execution,
|
|
162
|
+
// those changes are in the main repo's JSONL files but not committed.
|
|
163
|
+
// We need to copy them so the worktree has the complete, up-to-date state.
|
|
164
|
+
//
|
|
165
|
+
// Example: User creates ISSUE-144, starts execution immediately.
|
|
166
|
+
// - Git tree: has 138 issues (old state)
|
|
167
|
+
// - Main JSONL: has 140 issues (includes ISSUE-144, uncommitted)
|
|
168
|
+
// - Without this copy: worktree would only have 138 issues, ISSUE-144 missing!
|
|
169
|
+
// - With this copy: worktree gets all 140 issues, execution can reference ISSUE-144
|
|
170
|
+
const jsonlFiles = ["issues.jsonl", "specs.jsonl"];
|
|
171
|
+
for (const file of jsonlFiles) {
|
|
172
|
+
const mainFile = path.join(mainSudocodeDir, file);
|
|
173
|
+
const worktreeFile = path.join(worktreeSudocodeDir, file);
|
|
174
|
+
console.debug(`[WorktreeManager] Processing ${file}`, {
|
|
175
|
+
mainFile,
|
|
176
|
+
worktreeFile,
|
|
177
|
+
mainFileExists: fs.existsSync(mainFile),
|
|
178
|
+
});
|
|
179
|
+
if (fs.existsSync(mainFile)) {
|
|
180
|
+
const mainStats = fs.statSync(mainFile);
|
|
181
|
+
fs.copyFileSync(mainFile, worktreeFile);
|
|
182
|
+
const worktreeStats = fs.statSync(worktreeFile);
|
|
183
|
+
console.debug(`[WorktreeManager] Synced ${file} from main repo to worktree`, {
|
|
184
|
+
mainFileSize: mainStats.size,
|
|
185
|
+
worktreeFileSize: worktreeStats.size,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.debug(`[WorktreeManager] Main file does not exist: ${mainFile}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// STEP 2: Copy config.json
|
|
193
|
+
// ========================
|
|
194
|
+
// Copy sudocode configuration to maintain consistency
|
|
195
|
+
const mainConfig = path.join(mainSudocodeDir, "config.json");
|
|
196
|
+
const worktreeConfig = path.join(worktreeSudocodeDir, "config.json");
|
|
197
|
+
if (fs.existsSync(mainConfig)) {
|
|
198
|
+
fs.copyFileSync(mainConfig, worktreeConfig);
|
|
199
|
+
console.debug(`[WorktreeManager] Copied config.json from main repo to worktree`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.debug(`[WorktreeManager] No config.json to copy from main repo`);
|
|
203
|
+
}
|
|
204
|
+
// STEP 3: Initialize local database in worktree
|
|
205
|
+
// ==============================================
|
|
206
|
+
// Create a brand new SQLite database in the worktree and import the JSONL
|
|
207
|
+
// files we just copied. This gives the worktree its own isolated database
|
|
208
|
+
// with the complete current state.
|
|
209
|
+
//
|
|
210
|
+
// Important: This database is completely separate from the main repo's DB.
|
|
211
|
+
// All MCP/CLI operations in the worktree will use THIS database, not the main one.
|
|
212
|
+
const worktreeDbPath = path.join(worktreeSudocodeDir, "cache.db");
|
|
213
|
+
// Initialize database with CLI's initDatabase (creates all tables)
|
|
214
|
+
const db = initDatabase({ path: worktreeDbPath, verbose: false });
|
|
215
|
+
try {
|
|
216
|
+
await importFromJSONL(db, {
|
|
217
|
+
inputDir: worktreeSudocodeDir,
|
|
218
|
+
});
|
|
219
|
+
console.debug(`[WorktreeManager] Successfully initialized local database in worktree at ${worktreeDbPath}`);
|
|
220
|
+
// Verify database was created
|
|
221
|
+
if (fs.existsSync(worktreeDbPath)) {
|
|
222
|
+
const dbStats = fs.statSync(worktreeDbPath);
|
|
223
|
+
console.debug("[WorktreeManager] Database file created", {
|
|
224
|
+
path: worktreeDbPath,
|
|
225
|
+
size: dbStats.size,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.error("[WorktreeManager] ERROR: Database file was not created!");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
console.error("[WorktreeManager] Failed to initialize database", error);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
db.close();
|
|
238
|
+
}
|
|
239
|
+
console.debug("[WorktreeManager] Worktree environment setup complete");
|
|
240
|
+
}
|
|
241
|
+
async ensureWorktreeExists(repoPath, branchName, worktreePath) {
|
|
242
|
+
// Get lock for this specific path
|
|
243
|
+
const lock = this.getLock(worktreePath);
|
|
244
|
+
const release = await lock.acquire();
|
|
245
|
+
try {
|
|
246
|
+
// Check if already exists and valid
|
|
247
|
+
if (await this.isWorktreeValid(repoPath, worktreePath)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Recreate worktree
|
|
251
|
+
await this.recreateWorktree(repoPath, branchName, worktreePath);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
release();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async cleanupWorktree(worktreePath, repoPath) {
|
|
258
|
+
// Get lock for this specific path
|
|
259
|
+
const lock = this.getLock(worktreePath);
|
|
260
|
+
const release = await lock.acquire();
|
|
261
|
+
try {
|
|
262
|
+
// Infer repoPath if not provided (try to find from worktree)
|
|
263
|
+
const effectiveRepoPath = repoPath || (await this.inferRepoPath(worktreePath));
|
|
264
|
+
if (!effectiveRepoPath) {
|
|
265
|
+
// Can't determine repo path, just cleanup the directory
|
|
266
|
+
if (fs.existsSync(worktreePath)) {
|
|
267
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Get worktree info to find branch name (for optional deletion)
|
|
272
|
+
let branchName;
|
|
273
|
+
try {
|
|
274
|
+
const worktrees = await this.git.worktreeList(effectiveRepoPath);
|
|
275
|
+
// Normalize paths for comparison (resolves symlinks like /var -> /private/var on macOS)
|
|
276
|
+
const normalizedWorktreePath = fs.realpathSync(worktreePath);
|
|
277
|
+
const worktreeInfo = worktrees.find((w) => {
|
|
278
|
+
try {
|
|
279
|
+
const normalizedGitPath = fs.realpathSync(w.path);
|
|
280
|
+
return normalizedGitPath === normalizedWorktreePath;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// If path doesn't exist, try direct comparison
|
|
284
|
+
return w.path === worktreePath;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
if (worktreeInfo) {
|
|
288
|
+
branchName = worktreeInfo.branch;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
// Ignore errors, branch deletion is optional
|
|
293
|
+
}
|
|
294
|
+
// 1. Remove git worktree registration
|
|
295
|
+
try {
|
|
296
|
+
await this.git.worktreeRemove(effectiveRepoPath, worktreePath, true);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
// Worktree might already be removed or invalid, continue cleanup
|
|
300
|
+
}
|
|
301
|
+
// 2. Force cleanup metadata directory
|
|
302
|
+
const worktreeName = path.basename(worktreePath);
|
|
303
|
+
const metadataPath = path.join(effectiveRepoPath, ".git", "worktrees", worktreeName);
|
|
304
|
+
if (fs.existsSync(metadataPath)) {
|
|
305
|
+
fs.rmSync(metadataPath, { recursive: true, force: true });
|
|
306
|
+
}
|
|
307
|
+
// 3. Remove filesystem directory
|
|
308
|
+
if (fs.existsSync(worktreePath)) {
|
|
309
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
// 4. Prune stale worktree entries
|
|
312
|
+
try {
|
|
313
|
+
await this.git.worktreePrune(effectiveRepoPath);
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
// Prune is best-effort, continue even if it fails
|
|
317
|
+
}
|
|
318
|
+
// 5. Delete branch if configured
|
|
319
|
+
if (this.config.autoDeleteBranches &&
|
|
320
|
+
branchName &&
|
|
321
|
+
branchName !== "(detached)") {
|
|
322
|
+
try {
|
|
323
|
+
await this.git.deleteBranch(effectiveRepoPath, branchName, true);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
// Branch deletion is optional, don't fail the cleanup
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
release();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async isWorktreeValid(repoPath, worktreePath) {
|
|
335
|
+
try {
|
|
336
|
+
// 1. Check filesystem path exists
|
|
337
|
+
if (!fs.existsSync(worktreePath)) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
// 2. Check worktree is registered in git metadata
|
|
341
|
+
const worktrees = await this.git.worktreeList(repoPath);
|
|
342
|
+
// Normalize paths for comparison (resolves symlinks like /var -> /private/var on macOS)
|
|
343
|
+
const normalizedWorktreePath = fs.realpathSync(worktreePath);
|
|
344
|
+
const isRegistered = worktrees.some((w) => {
|
|
345
|
+
try {
|
|
346
|
+
const normalizedGitPath = fs.realpathSync(w.path);
|
|
347
|
+
return normalizedGitPath === normalizedWorktreePath;
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// If path doesn't exist, try direct comparison
|
|
351
|
+
return w.path === worktreePath;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
return isRegistered;
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
// On any error, consider invalid
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async listWorktrees(repoPath) {
|
|
362
|
+
return await this.git.worktreeList(repoPath);
|
|
363
|
+
}
|
|
364
|
+
getConfig() {
|
|
365
|
+
return { ...this.config };
|
|
366
|
+
}
|
|
367
|
+
async isValidRepo(repoPath) {
|
|
368
|
+
return this.git.isValidRepo(repoPath);
|
|
369
|
+
}
|
|
370
|
+
async listBranches(repoPath) {
|
|
371
|
+
return this.git.listBranches(repoPath);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Recreate a worktree (internal method)
|
|
375
|
+
*
|
|
376
|
+
* @param repoPath - Path to repository
|
|
377
|
+
* @param branchName - Branch name
|
|
378
|
+
* @param worktreePath - Worktree path
|
|
379
|
+
*/
|
|
380
|
+
async recreateWorktree(repoPath, branchName, worktreePath) {
|
|
381
|
+
// 1. Comprehensive cleanup of existing worktree
|
|
382
|
+
await this.cleanupWorktree(worktreePath, repoPath);
|
|
383
|
+
// 2. Create parent directory if needed
|
|
384
|
+
const parentDir = path.dirname(worktreePath);
|
|
385
|
+
if (!fs.existsSync(parentDir)) {
|
|
386
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
387
|
+
}
|
|
388
|
+
// 3. Create worktree with retry logic
|
|
389
|
+
let lastError;
|
|
390
|
+
const maxRetries = 1;
|
|
391
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
392
|
+
try {
|
|
393
|
+
await this.git.worktreeAdd(repoPath, worktreePath, branchName);
|
|
394
|
+
// Apply sparse-checkout if configured
|
|
395
|
+
if (this.config.enableSparseCheckout &&
|
|
396
|
+
this.config.sparseCheckoutPatterns) {
|
|
397
|
+
await this.git.configureSparseCheckout(worktreePath, this.config.sparseCheckoutPatterns);
|
|
398
|
+
}
|
|
399
|
+
// Validate creation
|
|
400
|
+
if (!fs.existsSync(worktreePath)) {
|
|
401
|
+
throw new WorktreeError(`Worktree creation succeeded but path does not exist: ${worktreePath}`, WorktreeErrorCode.REPOSITORY_ERROR);
|
|
402
|
+
}
|
|
403
|
+
// Setup isolated worktree environment (see setupWorktreeEnvironment for details)
|
|
404
|
+
await this.setupWorktreeEnvironment(repoPath, worktreePath);
|
|
405
|
+
return; // Success!
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
lastError = error;
|
|
409
|
+
if (attempt < maxRetries) {
|
|
410
|
+
// Cleanup metadata and try again
|
|
411
|
+
const worktreeName = path.basename(worktreePath);
|
|
412
|
+
const metadataPath = path.join(repoPath, ".git", "worktrees", worktreeName);
|
|
413
|
+
if (fs.existsSync(metadataPath)) {
|
|
414
|
+
fs.rmSync(metadataPath, { recursive: true, force: true });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// All retries failed
|
|
420
|
+
throw new WorktreeError(`Failed to recreate worktree after ${maxRetries + 1} attempts: ${lastError}`, WorktreeErrorCode.REPOSITORY_ERROR, lastError);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Infer git repository path from a worktree
|
|
424
|
+
* Uses git rev-parse --git-common-dir
|
|
425
|
+
*
|
|
426
|
+
* @param worktreePath - Path to worktree
|
|
427
|
+
* @returns Repository path or undefined
|
|
428
|
+
*/
|
|
429
|
+
async inferRepoPath(worktreePath) {
|
|
430
|
+
try {
|
|
431
|
+
if (!fs.existsSync(worktreePath)) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
// Try to use git to find the common git directory
|
|
435
|
+
const gitCommonDir = execSync("git rev-parse --git-common-dir", {
|
|
436
|
+
cwd: worktreePath,
|
|
437
|
+
encoding: "utf8",
|
|
438
|
+
}).trim();
|
|
439
|
+
// git-common-dir gives us the .git directory
|
|
440
|
+
// We need the working directory (parent of .git)
|
|
441
|
+
const gitDirPath = path.resolve(worktreePath, gitCommonDir);
|
|
442
|
+
if (path.basename(gitDirPath) === ".git") {
|
|
443
|
+
return path.dirname(gitDirPath);
|
|
444
|
+
}
|
|
445
|
+
return gitDirPath;
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
//# sourceMappingURL=manager.js.map
|