@wundr.io/cli 1.0.12 → 1.0.14
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/ai/ai-service.d.ts +152 -0
- package/dist/ai/ai-service.d.ts.map +1 -0
- package/dist/ai/ai-service.js +430 -0
- package/dist/ai/ai-service.js.map +1 -0
- package/dist/ai/claude-client.d.ts +130 -0
- package/dist/ai/claude-client.d.ts.map +1 -0
- package/dist/ai/claude-client.js +340 -0
- package/dist/ai/claude-client.js.map +1 -0
- package/dist/ai/conversation-manager.d.ts +164 -0
- package/dist/ai/conversation-manager.d.ts.map +1 -0
- package/dist/ai/conversation-manager.js +614 -0
- package/dist/ai/conversation-manager.js.map +1 -0
- package/dist/ai/index.d.ts +5 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +8 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/cli.d.ts +36 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +192 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/ai.d.ts +89 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +954 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/alignment.d.ts +78 -0
- package/dist/commands/alignment.d.ts.map +1 -0
- package/dist/commands/alignment.js +817 -0
- package/dist/commands/alignment.js.map +1 -0
- package/dist/commands/analyze-optimized.d.ts +14 -0
- package/dist/commands/analyze-optimized.d.ts.map +1 -0
- package/dist/commands/analyze-optimized.js +609 -0
- package/dist/commands/analyze-optimized.js.map +1 -0
- package/dist/commands/analyze.d.ts +65 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +435 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/batch.d.ts +93 -0
- package/dist/commands/batch.d.ts.map +1 -0
- package/dist/commands/batch.js +854 -0
- package/dist/commands/batch.js.map +1 -0
- package/dist/commands/chat.d.ts +72 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +678 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/claude-init.d.ts +28 -0
- package/dist/commands/claude-init.d.ts.map +1 -0
- package/dist/commands/claude-init.js +591 -0
- package/dist/commands/claude-init.js.map +1 -0
- package/dist/commands/claude-setup.d.ts +119 -0
- package/dist/commands/claude-setup.d.ts.map +1 -0
- package/dist/commands/claude-setup.js +1079 -0
- package/dist/commands/claude-setup.js.map +1 -0
- package/dist/commands/computer-setup.d.ts +8 -0
- package/dist/commands/computer-setup.d.ts.map +1 -0
- package/dist/commands/computer-setup.js +877 -0
- package/dist/commands/computer-setup.js.map +1 -0
- package/dist/commands/create-command.d.ts +7 -0
- package/dist/commands/create-command.d.ts.map +1 -0
- package/dist/commands/create-command.js +158 -0
- package/dist/commands/create-command.js.map +1 -0
- package/dist/commands/create.d.ts +74 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +556 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dashboard.d.ts +91 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +538 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/govern.d.ts +70 -0
- package/dist/commands/govern.d.ts.map +1 -0
- package/dist/commands/govern.js +481 -0
- package/dist/commands/govern.js.map +1 -0
- package/dist/commands/governance.d.ts +17 -0
- package/dist/commands/governance.d.ts.map +1 -0
- package/dist/commands/governance.js +703 -0
- package/dist/commands/governance.js.map +1 -0
- package/dist/commands/guardian.d.ts +20 -0
- package/dist/commands/guardian.d.ts.map +1 -0
- package/dist/commands/guardian.js +597 -0
- package/dist/commands/guardian.js.map +1 -0
- package/dist/commands/init.d.ts +59 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +650 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/orchestrator.d.ts +7 -0
- package/dist/commands/orchestrator.d.ts.map +1 -0
- package/dist/commands/orchestrator.js +578 -0
- package/dist/commands/orchestrator.js.map +1 -0
- package/dist/commands/performance-optimizer.d.ts +30 -0
- package/dist/commands/performance-optimizer.d.ts.map +1 -0
- package/dist/commands/performance-optimizer.js +650 -0
- package/dist/commands/performance-optimizer.js.map +1 -0
- package/dist/commands/plugins.d.ts +87 -0
- package/dist/commands/plugins.d.ts.map +1 -0
- package/dist/commands/plugins.js +685 -0
- package/dist/commands/plugins.js.map +1 -0
- package/dist/commands/rag.d.ts +7 -0
- package/dist/commands/rag.d.ts.map +1 -0
- package/dist/commands/rag.js +751 -0
- package/dist/commands/rag.js.map +1 -0
- package/dist/commands/session.d.ts +41 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +441 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/setup.d.ts +24 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +172 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/test-init.d.ts +9 -0
- package/dist/commands/test-init.d.ts.map +1 -0
- package/dist/commands/test-init.js +222 -0
- package/dist/commands/test-init.js.map +1 -0
- package/dist/commands/test.d.ts +25 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +217 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/watch.d.ts +76 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +613 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/commands/worktree.d.ts +63 -0
- package/dist/commands/worktree.d.ts.map +1 -0
- package/dist/commands/worktree.js +774 -0
- package/dist/commands/worktree.js.map +1 -0
- package/dist/context/context-manager.d.ts +155 -0
- package/dist/context/context-manager.d.ts.map +1 -0
- package/dist/context/context-manager.js +383 -0
- package/dist/context/context-manager.js.map +1 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +6 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/session-manager.d.ts +207 -0
- package/dist/context/session-manager.d.ts.map +1 -0
- package/dist/context/session-manager.js +686 -0
- package/dist/context/session-manager.js.map +1 -0
- package/dist/framework/command-interface.d.ts +349 -0
- package/dist/framework/command-interface.d.ts.map +1 -0
- package/dist/framework/command-interface.js +101 -0
- package/dist/framework/command-interface.js.map +1 -0
- package/dist/framework/command-registry.d.ts +173 -0
- package/dist/framework/command-registry.d.ts.map +1 -0
- package/dist/framework/command-registry.js +734 -0
- package/dist/framework/command-registry.js.map +1 -0
- package/dist/framework/completion-exporter.d.ts +79 -0
- package/dist/framework/completion-exporter.d.ts.map +1 -0
- package/dist/framework/completion-exporter.js +259 -0
- package/dist/framework/completion-exporter.js.map +1 -0
- package/dist/framework/debug-logger.d.ts +163 -0
- package/dist/framework/debug-logger.d.ts.map +1 -0
- package/dist/framework/debug-logger.js +373 -0
- package/dist/framework/debug-logger.js.map +1 -0
- package/dist/framework/error-handler.d.ts +196 -0
- package/dist/framework/error-handler.d.ts.map +1 -0
- package/dist/framework/error-handler.js +613 -0
- package/dist/framework/error-handler.js.map +1 -0
- package/dist/framework/help-generator.d.ts +78 -0
- package/dist/framework/help-generator.d.ts.map +1 -0
- package/dist/framework/help-generator.js +414 -0
- package/dist/framework/help-generator.js.map +1 -0
- package/dist/framework/index.d.ts +62 -0
- package/dist/framework/index.d.ts.map +1 -0
- package/dist/framework/index.js +95 -0
- package/dist/framework/index.js.map +1 -0
- package/dist/framework/interactive-repl.d.ts +138 -0
- package/dist/framework/interactive-repl.d.ts.map +1 -0
- package/dist/framework/interactive-repl.js +567 -0
- package/dist/framework/interactive-repl.js.map +1 -0
- package/dist/framework/output-formatter.d.ts +274 -0
- package/dist/framework/output-formatter.d.ts.map +1 -0
- package/dist/framework/output-formatter.js +545 -0
- package/dist/framework/output-formatter.js.map +1 -0
- package/dist/framework/progress-manager.d.ts +192 -0
- package/dist/framework/progress-manager.d.ts.map +1 -0
- package/dist/framework/progress-manager.js +408 -0
- package/dist/framework/progress-manager.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive/interactive-mode.d.ts +76 -0
- package/dist/interactive/interactive-mode.d.ts.map +1 -0
- package/dist/interactive/interactive-mode.js +732 -0
- package/dist/interactive/interactive-mode.js.map +1 -0
- package/dist/nlp/command-mapper.d.ts +174 -0
- package/dist/nlp/command-mapper.d.ts.map +1 -0
- package/dist/nlp/command-mapper.js +624 -0
- package/dist/nlp/command-mapper.js.map +1 -0
- package/dist/nlp/command-parser.d.ts +106 -0
- package/dist/nlp/command-parser.d.ts.map +1 -0
- package/dist/nlp/command-parser.js +417 -0
- package/dist/nlp/command-parser.js.map +1 -0
- package/dist/nlp/index.d.ts +5 -0
- package/dist/nlp/index.d.ts.map +1 -0
- package/dist/nlp/index.js +8 -0
- package/dist/nlp/index.js.map +1 -0
- package/dist/nlp/intent-classifier.d.ts +59 -0
- package/dist/nlp/intent-classifier.d.ts.map +1 -0
- package/dist/nlp/intent-classifier.js +384 -0
- package/dist/nlp/intent-classifier.js.map +1 -0
- package/dist/nlp/intent-parser.d.ts +152 -0
- package/dist/nlp/intent-parser.d.ts.map +1 -0
- package/dist/nlp/intent-parser.js +746 -0
- package/dist/nlp/intent-parser.js.map +1 -0
- package/dist/plugins/plugin-manager.d.ts +121 -0
- package/dist/plugins/plugin-manager.d.ts.map +1 -0
- package/dist/plugins/plugin-manager.js +606 -0
- package/dist/plugins/plugin-manager.js.map +1 -0
- package/dist/types/index.d.ts +224 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/backup-rollback-manager.d.ts +72 -0
- package/dist/utils/backup-rollback-manager.d.ts.map +1 -0
- package/dist/utils/backup-rollback-manager.js +288 -0
- package/dist/utils/backup-rollback-manager.js.map +1 -0
- package/dist/utils/claude-config-installer.d.ts +98 -0
- package/dist/utils/claude-config-installer.d.ts.map +1 -0
- package/dist/utils/claude-config-installer.js +678 -0
- package/dist/utils/claude-config-installer.js.map +1 -0
- package/dist/utils/config-manager.d.ts +73 -0
- package/dist/utils/config-manager.d.ts.map +1 -0
- package/dist/utils/config-manager.js +339 -0
- package/dist/utils/config-manager.js.map +1 -0
- package/dist/utils/error-handler.d.ts +46 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js +169 -0
- package/dist/utils/error-handler.js.map +1 -0
- package/dist/utils/logger.d.ts +25 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +6 -6
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Worktree Management CLI Commands
|
|
4
|
+
*
|
|
5
|
+
* Manages git worktrees for multi-agent development including
|
|
6
|
+
* listing, creating, switching, cleanup, and synchronization.
|
|
7
|
+
*
|
|
8
|
+
* @module commands/worktree
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.createWorktreeCommand = createWorktreeCommand;
|
|
12
|
+
const tslib_1 = require("tslib");
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const fs = tslib_1.__importStar(require("fs/promises"));
|
|
15
|
+
const os = tslib_1.__importStar(require("os"));
|
|
16
|
+
const path = tslib_1.__importStar(require("path"));
|
|
17
|
+
const util_1 = require("util");
|
|
18
|
+
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
19
|
+
const commander_1 = require("commander");
|
|
20
|
+
const ora_1 = tslib_1.__importDefault(require("ora"));
|
|
21
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
22
|
+
// Constants
|
|
23
|
+
const WORKTREE_STATE_DIR = path.join(os.homedir(), '.wundr', 'worktrees');
|
|
24
|
+
const WORKTREE_STATE_FILE = path.join(WORKTREE_STATE_DIR, 'state.json');
|
|
25
|
+
// Utility functions
|
|
26
|
+
function getTimestamp() {
|
|
27
|
+
return new Date().toISOString();
|
|
28
|
+
}
|
|
29
|
+
function padRight(str, length) {
|
|
30
|
+
return str.length >= length
|
|
31
|
+
? str.substring(0, length)
|
|
32
|
+
: str + ' '.repeat(length - str.length);
|
|
33
|
+
}
|
|
34
|
+
function truncate(str, length) {
|
|
35
|
+
if (str.length <= length) {
|
|
36
|
+
return str;
|
|
37
|
+
}
|
|
38
|
+
return str.substring(0, length - 3) + '...';
|
|
39
|
+
}
|
|
40
|
+
function formatAge(dateStr) {
|
|
41
|
+
const date = new Date(dateStr);
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const diffMs = now - date.getTime();
|
|
44
|
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
45
|
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
46
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
47
|
+
if (diffDays > 0) {
|
|
48
|
+
return `${diffDays}d ago`;
|
|
49
|
+
}
|
|
50
|
+
if (diffHours > 0) {
|
|
51
|
+
return `${diffHours}h ago`;
|
|
52
|
+
}
|
|
53
|
+
if (diffMins > 0) {
|
|
54
|
+
return `${diffMins}m ago`;
|
|
55
|
+
}
|
|
56
|
+
return 'just now';
|
|
57
|
+
}
|
|
58
|
+
function generateSessionId() {
|
|
59
|
+
const timestamp = Date.now().toString(36);
|
|
60
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
61
|
+
return `session-${timestamp}-${random}`;
|
|
62
|
+
}
|
|
63
|
+
function generateBranchName(taskId) {
|
|
64
|
+
const sanitized = taskId
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
67
|
+
.replace(/-+/g, '-')
|
|
68
|
+
.replace(/^-|-$/g, '');
|
|
69
|
+
return `task/${sanitized}`;
|
|
70
|
+
}
|
|
71
|
+
async function ensureStateDir() {
|
|
72
|
+
await fs.mkdir(WORKTREE_STATE_DIR, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
async function loadWorktreeState() {
|
|
75
|
+
try {
|
|
76
|
+
await ensureStateDir();
|
|
77
|
+
const content = await fs.readFile(WORKTREE_STATE_FILE, 'utf-8');
|
|
78
|
+
return JSON.parse(content);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Return empty state if file doesn't exist
|
|
82
|
+
return {
|
|
83
|
+
version: '1.0.0',
|
|
84
|
+
lastUpdated: getTimestamp(),
|
|
85
|
+
repoPath: process.cwd(),
|
|
86
|
+
worktrees: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function saveWorktreeState(state) {
|
|
91
|
+
await ensureStateDir();
|
|
92
|
+
state.lastUpdated = getTimestamp();
|
|
93
|
+
await fs.writeFile(WORKTREE_STATE_FILE, JSON.stringify(state, null, 2));
|
|
94
|
+
}
|
|
95
|
+
async function getGitRepoRoot() {
|
|
96
|
+
try {
|
|
97
|
+
const { stdout } = await execAsync('git rev-parse --show-toplevel', {
|
|
98
|
+
timeout: 10000,
|
|
99
|
+
});
|
|
100
|
+
return stdout.trim();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return process.cwd();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function executeGitCommand(command, args, cwd) {
|
|
107
|
+
const fullCommand = `git ${command} ${args.join(' ')}`;
|
|
108
|
+
try {
|
|
109
|
+
const { stdout } = await execAsync(fullCommand, { cwd, timeout: 60000 });
|
|
110
|
+
return stdout.trim();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
114
|
+
throw new Error(`Git command failed: ${fullCommand}\n${errorMessage}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function listGitWorktrees(repoPath) {
|
|
118
|
+
const worktrees = [];
|
|
119
|
+
try {
|
|
120
|
+
const output = await executeGitCommand('worktree', ['list', '--porcelain'], repoPath);
|
|
121
|
+
const entries = output.split('\n\n').filter(Boolean);
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const lines = entry.split('\n');
|
|
124
|
+
const info = {
|
|
125
|
+
isMain: false,
|
|
126
|
+
isLocked: false,
|
|
127
|
+
};
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
if (line.startsWith('worktree ')) {
|
|
130
|
+
info.path = line.substring('worktree '.length).trim();
|
|
131
|
+
}
|
|
132
|
+
else if (line.startsWith('HEAD ')) {
|
|
133
|
+
info.commit = line.substring('HEAD '.length).trim();
|
|
134
|
+
}
|
|
135
|
+
else if (line.startsWith('branch ')) {
|
|
136
|
+
info.branch = line
|
|
137
|
+
.substring('branch '.length)
|
|
138
|
+
.trim()
|
|
139
|
+
.replace('refs/heads/', '');
|
|
140
|
+
}
|
|
141
|
+
else if (line === 'bare') {
|
|
142
|
+
info.isMain = true;
|
|
143
|
+
}
|
|
144
|
+
else if (line === 'locked') {
|
|
145
|
+
info.isLocked = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (info.path) {
|
|
149
|
+
// Check if this is the main worktree
|
|
150
|
+
if (info.path === repoPath) {
|
|
151
|
+
info.isMain = true;
|
|
152
|
+
}
|
|
153
|
+
// Get last modified time
|
|
154
|
+
try {
|
|
155
|
+
const stats = await fs.stat(info.path);
|
|
156
|
+
info.lastModified = stats.mtime;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Path might not exist for orphaned worktrees
|
|
160
|
+
}
|
|
161
|
+
worktrees.push(info);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Return empty list if git command fails
|
|
167
|
+
}
|
|
168
|
+
return worktrees;
|
|
169
|
+
}
|
|
170
|
+
async function hasUncommittedChanges(worktreePath) {
|
|
171
|
+
try {
|
|
172
|
+
const { stdout } = await execAsync('git status --porcelain', {
|
|
173
|
+
cwd: worktreePath,
|
|
174
|
+
timeout: 10000,
|
|
175
|
+
});
|
|
176
|
+
return stdout.trim().length > 0;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function getStatusColor(status) {
|
|
183
|
+
switch (status) {
|
|
184
|
+
case 'active':
|
|
185
|
+
return chalk_1.default.green;
|
|
186
|
+
case 'paused':
|
|
187
|
+
return chalk_1.default.yellow;
|
|
188
|
+
case 'syncing':
|
|
189
|
+
return chalk_1.default.blue;
|
|
190
|
+
case 'creating':
|
|
191
|
+
return chalk_1.default.cyan;
|
|
192
|
+
case 'cleanup':
|
|
193
|
+
return chalk_1.default.magenta;
|
|
194
|
+
case 'error':
|
|
195
|
+
return chalk_1.default.red;
|
|
196
|
+
case 'destroyed':
|
|
197
|
+
return chalk_1.default.gray;
|
|
198
|
+
case 'pending':
|
|
199
|
+
return chalk_1.default.white;
|
|
200
|
+
default:
|
|
201
|
+
return chalk_1.default.white;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function getStatusIcon(status) {
|
|
205
|
+
switch (status) {
|
|
206
|
+
case 'active':
|
|
207
|
+
return '[ACTIVE]';
|
|
208
|
+
case 'paused':
|
|
209
|
+
return '[PAUSED]';
|
|
210
|
+
case 'syncing':
|
|
211
|
+
return '[SYNCING]';
|
|
212
|
+
case 'creating':
|
|
213
|
+
return '[CREATING]';
|
|
214
|
+
case 'cleanup':
|
|
215
|
+
return '[CLEANUP]';
|
|
216
|
+
case 'error':
|
|
217
|
+
return '[ERROR]';
|
|
218
|
+
case 'destroyed':
|
|
219
|
+
return '[DESTROYED]';
|
|
220
|
+
case 'pending':
|
|
221
|
+
return '[PENDING]';
|
|
222
|
+
default:
|
|
223
|
+
return '[UNKNOWN]';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Create the worktree command with all subcommands
|
|
228
|
+
*/
|
|
229
|
+
function createWorktreeCommand() {
|
|
230
|
+
const command = new commander_1.Command('worktree')
|
|
231
|
+
.alias('wt')
|
|
232
|
+
.description('Manage git worktrees for multi-agent development')
|
|
233
|
+
.addHelpText('after', chalk_1.default.gray(`
|
|
234
|
+
Examples:
|
|
235
|
+
${chalk_1.default.green('wundr worktree list')} List all worktrees
|
|
236
|
+
${chalk_1.default.green('wundr wt list --session <id>')} List worktrees for a session
|
|
237
|
+
${chalk_1.default.green('wundr worktree create <taskId>')} Create new worktree for a task
|
|
238
|
+
${chalk_1.default.green('wundr worktree switch <taskId>')} Switch to an existing worktree
|
|
239
|
+
${chalk_1.default.green('wundr worktree cleanup --dry-run')} Preview what would be cleaned up
|
|
240
|
+
${chalk_1.default.green('wundr worktree sync')} Sync all worktrees from remote
|
|
241
|
+
${chalk_1.default.green('wundr worktree sync <taskId>')} Sync specific worktree
|
|
242
|
+
`));
|
|
243
|
+
// List command (default)
|
|
244
|
+
command
|
|
245
|
+
.command('list', { isDefault: true })
|
|
246
|
+
.description('List all worktrees')
|
|
247
|
+
.option('-s, --session <id>', 'Filter by session ID')
|
|
248
|
+
.option('--status <status>', 'Filter by status (active, paused, syncing, error, etc.)')
|
|
249
|
+
.option('-f, --format <format>', 'Output format (table, json)', 'table')
|
|
250
|
+
.action(async (options) => {
|
|
251
|
+
await listWorktrees(options);
|
|
252
|
+
});
|
|
253
|
+
// Create command (ccswitch create)
|
|
254
|
+
command
|
|
255
|
+
.command('create <taskId>')
|
|
256
|
+
.description('Create a new worktree for a task')
|
|
257
|
+
.option('-b, --base <branch>', 'Base branch to create from', 'main')
|
|
258
|
+
.action(async (taskId, options) => {
|
|
259
|
+
await createWorktree(taskId, options);
|
|
260
|
+
});
|
|
261
|
+
// Switch command (ccswitch switch)
|
|
262
|
+
command
|
|
263
|
+
.command('switch <taskId>')
|
|
264
|
+
.description('Switch to an existing worktree')
|
|
265
|
+
.action(async (taskId) => {
|
|
266
|
+
await switchWorktree(taskId);
|
|
267
|
+
});
|
|
268
|
+
// Cleanup command
|
|
269
|
+
command
|
|
270
|
+
.command('cleanup')
|
|
271
|
+
.description('Clean up stale worktrees')
|
|
272
|
+
.option('--dry-run', 'Preview what would be cleaned up without making changes')
|
|
273
|
+
.option('--force', 'Force cleanup even with uncommitted changes')
|
|
274
|
+
.option('--age <days>', 'Clean up worktrees older than specified days', '7')
|
|
275
|
+
.action(async (options) => {
|
|
276
|
+
await cleanupWorktrees(options);
|
|
277
|
+
});
|
|
278
|
+
// Sync command
|
|
279
|
+
command
|
|
280
|
+
.command('sync [taskId]')
|
|
281
|
+
.description('Sync worktree(s) from remote')
|
|
282
|
+
.option('--all', 'Sync all worktrees')
|
|
283
|
+
.action(async (taskId, options) => {
|
|
284
|
+
await syncWorktrees(taskId, options);
|
|
285
|
+
});
|
|
286
|
+
return command;
|
|
287
|
+
}
|
|
288
|
+
// Command implementations
|
|
289
|
+
async function listWorktrees(options) {
|
|
290
|
+
const spinner = (0, ora_1.default)('Loading worktrees...').start();
|
|
291
|
+
try {
|
|
292
|
+
const state = await loadWorktreeState();
|
|
293
|
+
let worktrees = state.worktrees;
|
|
294
|
+
// Filter by session ID if provided
|
|
295
|
+
if (options.session) {
|
|
296
|
+
worktrees = worktrees.filter(wt => wt.sessionId === options.session);
|
|
297
|
+
}
|
|
298
|
+
// Filter by status if provided
|
|
299
|
+
if (options.status) {
|
|
300
|
+
worktrees = worktrees.filter(wt => wt.status === options.status);
|
|
301
|
+
}
|
|
302
|
+
spinner.stop();
|
|
303
|
+
if (options.format === 'json') {
|
|
304
|
+
console.log(JSON.stringify({
|
|
305
|
+
timestamp: getTimestamp(),
|
|
306
|
+
count: worktrees.length,
|
|
307
|
+
worktrees: worktrees,
|
|
308
|
+
}, null, 2));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
console.log(chalk_1.default.cyan('\nWorktree List'));
|
|
312
|
+
console.log(chalk_1.default.gray('='.repeat(110)));
|
|
313
|
+
if (worktrees.length === 0) {
|
|
314
|
+
console.log(chalk_1.default.yellow('\nNo worktrees found.'));
|
|
315
|
+
if (options.session) {
|
|
316
|
+
console.log(chalk_1.default.gray(`No worktrees for session: ${options.session}`));
|
|
317
|
+
}
|
|
318
|
+
console.log('');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// Table header
|
|
322
|
+
console.log(chalk_1.default.cyan(padRight('Task ID', 20) +
|
|
323
|
+
padRight('Branch', 25) +
|
|
324
|
+
padRight('Status', 12) +
|
|
325
|
+
padRight('Created', 12) +
|
|
326
|
+
padRight('Path', 40)));
|
|
327
|
+
console.log(chalk_1.default.gray('-'.repeat(110)));
|
|
328
|
+
// Table rows
|
|
329
|
+
for (const wt of worktrees) {
|
|
330
|
+
const statusColor = getStatusColor(wt.status);
|
|
331
|
+
const createdAge = formatAge(wt.createdAt);
|
|
332
|
+
const worktreePath = truncate(wt.worktreePath, 38);
|
|
333
|
+
const branchName = truncate(wt.branchName, 23);
|
|
334
|
+
console.log(padRight(wt.taskId, 20) +
|
|
335
|
+
chalk_1.default.blue(padRight(branchName, 25)) +
|
|
336
|
+
statusColor(padRight(getStatusIcon(wt.status), 12)) +
|
|
337
|
+
padRight(createdAge, 12) +
|
|
338
|
+
chalk_1.default.gray(padRight(worktreePath, 40)));
|
|
339
|
+
}
|
|
340
|
+
console.log(chalk_1.default.gray('-'.repeat(110)));
|
|
341
|
+
console.log(chalk_1.default.gray(`Total: ${worktrees.length} worktree(s)`));
|
|
342
|
+
console.log('');
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
spinner.fail('Failed to load worktrees');
|
|
346
|
+
console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function createWorktree(taskId, options) {
|
|
350
|
+
const spinner = (0, ora_1.default)(`Creating worktree for task: ${taskId}...`).start();
|
|
351
|
+
try {
|
|
352
|
+
const state = await loadWorktreeState();
|
|
353
|
+
const repoPath = await getGitRepoRoot();
|
|
354
|
+
state.repoPath = repoPath;
|
|
355
|
+
// Check if worktree already exists
|
|
356
|
+
const existing = state.worktrees.find(wt => wt.taskId === taskId);
|
|
357
|
+
if (existing) {
|
|
358
|
+
spinner.fail(`Worktree already exists for task: ${taskId}`);
|
|
359
|
+
console.log(chalk_1.default.yellow(`Use "wundr worktree switch ${taskId}" to switch to it.`));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const baseBranch = options.base || 'main';
|
|
363
|
+
const branchName = generateBranchName(taskId);
|
|
364
|
+
const sessionId = generateSessionId();
|
|
365
|
+
const worktreeRoot = path.join(repoPath, '.git-worktrees', 'sessions');
|
|
366
|
+
const worktreePath = path.join(worktreeRoot, taskId);
|
|
367
|
+
// Ensure worktree root exists
|
|
368
|
+
await fs.mkdir(worktreeRoot, { recursive: true });
|
|
369
|
+
// Fetch latest from remote
|
|
370
|
+
try {
|
|
371
|
+
await executeGitCommand('fetch', ['origin', baseBranch], repoPath);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// Continue even if fetch fails (might be offline)
|
|
375
|
+
}
|
|
376
|
+
// Create the worktree with a new branch
|
|
377
|
+
await executeGitCommand('worktree', ['add', '-b', branchName, worktreePath, `origin/${baseBranch}`], repoPath);
|
|
378
|
+
// Save to state
|
|
379
|
+
const entry = {
|
|
380
|
+
taskId,
|
|
381
|
+
branchName,
|
|
382
|
+
worktreePath,
|
|
383
|
+
sessionId,
|
|
384
|
+
status: 'active',
|
|
385
|
+
createdAt: getTimestamp(),
|
|
386
|
+
lastAccessedAt: getTimestamp(),
|
|
387
|
+
};
|
|
388
|
+
state.worktrees.push(entry);
|
|
389
|
+
await saveWorktreeState(state);
|
|
390
|
+
spinner.succeed(`Worktree created for task: ${taskId}`);
|
|
391
|
+
console.log('');
|
|
392
|
+
console.log(chalk_1.default.white(' Task ID: ') + chalk_1.default.green(taskId));
|
|
393
|
+
console.log(chalk_1.default.white(' Branch: ') + chalk_1.default.blue(branchName));
|
|
394
|
+
console.log(chalk_1.default.white(' Path: ') + chalk_1.default.gray(worktreePath));
|
|
395
|
+
console.log('');
|
|
396
|
+
console.log(chalk_1.default.gray(`Use "wundr worktree switch ${taskId}" to start working.`));
|
|
397
|
+
console.log('');
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
spinner.fail('Failed to create worktree');
|
|
401
|
+
console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function switchWorktree(taskId) {
|
|
405
|
+
const spinner = (0, ora_1.default)(`Switching to worktree: ${taskId}...`).start();
|
|
406
|
+
try {
|
|
407
|
+
const state = await loadWorktreeState();
|
|
408
|
+
// Find the worktree
|
|
409
|
+
const worktree = state.worktrees.find(wt => wt.taskId === taskId);
|
|
410
|
+
if (!worktree) {
|
|
411
|
+
spinner.fail(`Worktree not found: ${taskId}`);
|
|
412
|
+
console.log(chalk_1.default.yellow(`Use "wundr worktree create ${taskId}" to create it.`));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// Check if worktree path exists
|
|
416
|
+
try {
|
|
417
|
+
await fs.access(worktree.worktreePath);
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
spinner.fail(`Worktree directory not found: ${worktree.worktreePath}`);
|
|
421
|
+
console.log(chalk_1.default.yellow('The worktree may have been deleted. Consider running cleanup.'));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// Update status
|
|
425
|
+
worktree.status = 'active';
|
|
426
|
+
worktree.lastAccessedAt = getTimestamp();
|
|
427
|
+
await saveWorktreeState(state);
|
|
428
|
+
spinner.succeed(`Switched to worktree: ${taskId}`);
|
|
429
|
+
console.log('');
|
|
430
|
+
console.log(chalk_1.default.white(' Task ID: ') + chalk_1.default.green(taskId));
|
|
431
|
+
console.log(chalk_1.default.white(' Branch: ') + chalk_1.default.blue(worktree.branchName));
|
|
432
|
+
console.log(chalk_1.default.white(' Path: ') + chalk_1.default.gray(worktree.worktreePath));
|
|
433
|
+
console.log('');
|
|
434
|
+
console.log(chalk_1.default.cyan('To navigate to this worktree, run:'));
|
|
435
|
+
console.log(chalk_1.default.white(` cd ${worktree.worktreePath}`));
|
|
436
|
+
console.log('');
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
spinner.fail('Failed to switch worktree');
|
|
440
|
+
console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function cleanupWorktrees(options) {
|
|
444
|
+
const maxAgeDays = parseInt(options.age || '7', 10);
|
|
445
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
446
|
+
const spinner = (0, ora_1.default)(options.dryRun ? 'Analyzing worktrees...' : 'Cleaning up worktrees...').start();
|
|
447
|
+
try {
|
|
448
|
+
const state = await loadWorktreeState();
|
|
449
|
+
const repoPath = state.repoPath || (await getGitRepoRoot());
|
|
450
|
+
const now = Date.now();
|
|
451
|
+
// Get actual git worktrees
|
|
452
|
+
const gitWorktrees = await listGitWorktrees(repoPath);
|
|
453
|
+
// Categorize worktrees for cleanup
|
|
454
|
+
const toRemove = [];
|
|
455
|
+
const toSkip = [];
|
|
456
|
+
// Check state entries
|
|
457
|
+
for (const entry of state.worktrees) {
|
|
458
|
+
const createdAt = new Date(entry.createdAt).getTime();
|
|
459
|
+
const age = now - createdAt;
|
|
460
|
+
const gitInfo = gitWorktrees.find(g => g.path === entry.worktreePath);
|
|
461
|
+
// Check if worktree still exists
|
|
462
|
+
try {
|
|
463
|
+
await fs.access(entry.worktreePath);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
toRemove.push({ entry, reason: 'Directory no longer exists' });
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
// Check if too old
|
|
470
|
+
if (age > maxAgeMs) {
|
|
471
|
+
// Check for uncommitted changes
|
|
472
|
+
const hasChanges = await hasUncommittedChanges(entry.worktreePath);
|
|
473
|
+
if (hasChanges && !options.force) {
|
|
474
|
+
toSkip.push({ entry, gitInfo, reason: 'Has uncommitted changes' });
|
|
475
|
+
}
|
|
476
|
+
else if (gitInfo?.isLocked && !options.force) {
|
|
477
|
+
toSkip.push({ entry, gitInfo, reason: 'Worktree is locked' });
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
toRemove.push({
|
|
481
|
+
entry,
|
|
482
|
+
gitInfo,
|
|
483
|
+
reason: `Older than ${maxAgeDays} days`,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Check error status
|
|
488
|
+
if (entry.status === 'error' && !options.force) {
|
|
489
|
+
toRemove.push({ entry, gitInfo, reason: 'In error state' });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
spinner.stop();
|
|
493
|
+
if (options.dryRun) {
|
|
494
|
+
// Show preview
|
|
495
|
+
console.log(chalk_1.default.cyan('\nCleanup Preview (Dry Run)'));
|
|
496
|
+
console.log(chalk_1.default.gray('='.repeat(80)));
|
|
497
|
+
if (toRemove.length === 0) {
|
|
498
|
+
console.log(chalk_1.default.green('\nNo worktrees need to be cleaned up.'));
|
|
499
|
+
console.log('');
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
console.log(chalk_1.default.yellow(`\nWorktrees to be removed: ${toRemove.length}`));
|
|
503
|
+
console.log(chalk_1.default.gray('-'.repeat(80)));
|
|
504
|
+
for (const { entry, reason } of toRemove) {
|
|
505
|
+
if (entry) {
|
|
506
|
+
console.log(chalk_1.default.white(' Task ID: ') + chalk_1.default.green(entry.taskId));
|
|
507
|
+
console.log(chalk_1.default.white(' Path: ') + chalk_1.default.gray(entry.worktreePath));
|
|
508
|
+
console.log(chalk_1.default.white(' Branch: ') + chalk_1.default.blue(entry.branchName));
|
|
509
|
+
console.log(chalk_1.default.white(' Reason: ') + chalk_1.default.yellow(reason));
|
|
510
|
+
console.log('');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (toSkip.length > 0) {
|
|
514
|
+
console.log(chalk_1.default.yellow(`\nWorktrees to be skipped: ${toSkip.length}`));
|
|
515
|
+
console.log(chalk_1.default.gray('-'.repeat(80)));
|
|
516
|
+
for (const { entry, reason } of toSkip) {
|
|
517
|
+
if (entry) {
|
|
518
|
+
console.log(chalk_1.default.white(' Task ID: ') + chalk_1.default.green(entry.taskId));
|
|
519
|
+
console.log(chalk_1.default.white(' Path: ') + chalk_1.default.gray(entry.worktreePath));
|
|
520
|
+
console.log(chalk_1.default.white(' Reason: ') + chalk_1.default.gray(reason));
|
|
521
|
+
console.log('');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
console.log(chalk_1.default.gray('-'.repeat(80)));
|
|
526
|
+
console.log(chalk_1.default.gray('Run without --dry-run to perform actual cleanup.'));
|
|
527
|
+
console.log('');
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
// Perform actual cleanup
|
|
531
|
+
const result = {
|
|
532
|
+
success: true,
|
|
533
|
+
removedCount: 0,
|
|
534
|
+
removedPaths: [],
|
|
535
|
+
skippedPaths: [],
|
|
536
|
+
skipReasons: new Map(),
|
|
537
|
+
errors: [],
|
|
538
|
+
};
|
|
539
|
+
for (const { entry, reason } of toRemove) {
|
|
540
|
+
if (!entry) {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
// Remove the git worktree
|
|
545
|
+
const forceFlag = options.force ? '--force' : '';
|
|
546
|
+
await executeGitCommand('worktree', ['remove', forceFlag, entry.worktreePath].filter(Boolean), repoPath);
|
|
547
|
+
result.removedCount++;
|
|
548
|
+
result.removedPaths.push(entry.worktreePath);
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
// Try manual removal if git worktree remove fails
|
|
552
|
+
if (options.force) {
|
|
553
|
+
try {
|
|
554
|
+
await fs.rm(entry.worktreePath, { recursive: true, force: true });
|
|
555
|
+
await executeGitCommand('worktree', ['prune'], repoPath);
|
|
556
|
+
result.removedCount++;
|
|
557
|
+
result.removedPaths.push(entry.worktreePath);
|
|
558
|
+
}
|
|
559
|
+
catch (rmError) {
|
|
560
|
+
result.errors.push({
|
|
561
|
+
path: entry.worktreePath,
|
|
562
|
+
message: rmError instanceof Error ? rmError.message : String(rmError),
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
result.errors.push({
|
|
568
|
+
path: entry.worktreePath,
|
|
569
|
+
message: error instanceof Error ? error.message : String(error),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
for (const { entry, reason } of toSkip) {
|
|
575
|
+
if (entry) {
|
|
576
|
+
result.skippedPaths.push(entry.worktreePath);
|
|
577
|
+
result.skipReasons.set(entry.worktreePath, reason);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Update state file to remove cleaned worktrees
|
|
581
|
+
const removedPaths = new Set(result.removedPaths);
|
|
582
|
+
state.worktrees = state.worktrees.filter(wt => !removedPaths.has(wt.worktreePath));
|
|
583
|
+
await saveWorktreeState(state);
|
|
584
|
+
// Show results
|
|
585
|
+
console.log(chalk_1.default.cyan('\nCleanup Results'));
|
|
586
|
+
console.log(chalk_1.default.gray('='.repeat(80)));
|
|
587
|
+
if (result.removedCount === 0 && result.errors.length === 0) {
|
|
588
|
+
console.log(chalk_1.default.green('\nNo worktrees needed to be cleaned up.'));
|
|
589
|
+
console.log('');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (result.removedCount > 0) {
|
|
593
|
+
console.log(chalk_1.default.green(`\nRemoved ${result.removedCount} worktree(s):`));
|
|
594
|
+
for (const removedPath of result.removedPaths) {
|
|
595
|
+
console.log(chalk_1.default.gray(` - ${removedPath}`));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (result.skippedPaths.length > 0) {
|
|
599
|
+
console.log(chalk_1.default.yellow(`\nSkipped ${result.skippedPaths.length} worktree(s):`));
|
|
600
|
+
for (const skippedPath of result.skippedPaths) {
|
|
601
|
+
const reason = result.skipReasons.get(skippedPath) || 'unknown';
|
|
602
|
+
console.log(chalk_1.default.gray(` - ${skippedPath}: ${reason}`));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (result.errors.length > 0) {
|
|
606
|
+
console.log(chalk_1.default.red(`\nErrors (${result.errors.length}):`));
|
|
607
|
+
for (const error of result.errors) {
|
|
608
|
+
console.log(chalk_1.default.red(` - ${error.path}: ${error.message}`));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
console.log('');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
spinner.fail('Failed to cleanup worktrees');
|
|
616
|
+
console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async function syncWorktrees(taskId, options) {
|
|
620
|
+
const spinner = (0, ora_1.default)(taskId ? `Syncing worktree: ${taskId}...` : 'Syncing worktrees...').start();
|
|
621
|
+
try {
|
|
622
|
+
const state = await loadWorktreeState();
|
|
623
|
+
const repoPath = state.repoPath || (await getGitRepoRoot());
|
|
624
|
+
const syncSingleWorktree = async (worktree) => {
|
|
625
|
+
const timestamp = new Date();
|
|
626
|
+
let stashedChanges = false;
|
|
627
|
+
try {
|
|
628
|
+
// Check for uncommitted changes
|
|
629
|
+
const hasChanges = await hasUncommittedChanges(worktree.worktreePath);
|
|
630
|
+
if (hasChanges) {
|
|
631
|
+
// Stash changes
|
|
632
|
+
await executeGitCommand('stash', ['push', '-m', 'auto-stash before sync'], worktree.worktreePath);
|
|
633
|
+
stashedChanges = true;
|
|
634
|
+
}
|
|
635
|
+
// Fetch from remote
|
|
636
|
+
await executeGitCommand('fetch', ['origin'], worktree.worktreePath);
|
|
637
|
+
// Get current branch
|
|
638
|
+
const branchName = await executeGitCommand('rev-parse', ['--abbrev-ref', 'HEAD'], worktree.worktreePath);
|
|
639
|
+
// Try fast-forward pull
|
|
640
|
+
try {
|
|
641
|
+
await executeGitCommand('pull', ['--ff-only', 'origin', branchName], worktree.worktreePath);
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
// Fast-forward not possible, try rebase
|
|
645
|
+
await executeGitCommand('rebase', [`origin/${branchName}`], worktree.worktreePath);
|
|
646
|
+
}
|
|
647
|
+
// Count commits updated (approximate)
|
|
648
|
+
let commitsUpdated = 0;
|
|
649
|
+
try {
|
|
650
|
+
const { stdout } = await execAsync(`git rev-list --count origin/${branchName}..HEAD`, { cwd: worktree.worktreePath, timeout: 10000 });
|
|
651
|
+
commitsUpdated = parseInt(stdout.trim(), 10) || 0;
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Ignore count errors
|
|
655
|
+
}
|
|
656
|
+
// Restore stashed changes
|
|
657
|
+
if (stashedChanges) {
|
|
658
|
+
try {
|
|
659
|
+
await executeGitCommand('stash', ['pop'], worktree.worktreePath);
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
// Stash pop might have conflicts
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
success: true,
|
|
667
|
+
message: 'Successfully synced',
|
|
668
|
+
worktreePath: worktree.worktreePath,
|
|
669
|
+
branchName,
|
|
670
|
+
stashedChanges,
|
|
671
|
+
commitsUpdated,
|
|
672
|
+
timestamp,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
// Try to restore stash on failure
|
|
677
|
+
if (stashedChanges) {
|
|
678
|
+
try {
|
|
679
|
+
await executeGitCommand('stash', ['pop'], worktree.worktreePath);
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
// Ignore
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
success: false,
|
|
687
|
+
message: error instanceof Error ? error.message : String(error),
|
|
688
|
+
worktreePath: worktree.worktreePath,
|
|
689
|
+
stashedChanges,
|
|
690
|
+
timestamp,
|
|
691
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
if (taskId) {
|
|
696
|
+
// Sync specific worktree
|
|
697
|
+
const worktree = state.worktrees.find(wt => wt.taskId === taskId);
|
|
698
|
+
if (!worktree) {
|
|
699
|
+
spinner.fail(`Worktree not found: ${taskId}`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
// Update status to syncing
|
|
703
|
+
worktree.status = 'syncing';
|
|
704
|
+
await saveWorktreeState(state);
|
|
705
|
+
const result = await syncSingleWorktree(worktree);
|
|
706
|
+
// Update status based on result
|
|
707
|
+
worktree.status = result.success ? 'active' : 'error';
|
|
708
|
+
worktree.lastAccessedAt = getTimestamp();
|
|
709
|
+
await saveWorktreeState(state);
|
|
710
|
+
spinner.stop();
|
|
711
|
+
if (result.success) {
|
|
712
|
+
console.log(chalk_1.default.green(`\nSuccessfully synced worktree: ${taskId}`));
|
|
713
|
+
console.log(chalk_1.default.white(' Branch: ') +
|
|
714
|
+
chalk_1.default.blue(result.branchName || 'unknown'));
|
|
715
|
+
console.log(chalk_1.default.white(' Commits: ') +
|
|
716
|
+
chalk_1.default.cyan(`${result.commitsUpdated || 0} updated`));
|
|
717
|
+
if (result.stashedChanges) {
|
|
718
|
+
console.log(chalk_1.default.yellow(' Note: Local changes were stashed and restored.'));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
console.log(chalk_1.default.red(`\nFailed to sync worktree: ${taskId}`));
|
|
723
|
+
console.log(chalk_1.default.red(` Error: ${result.message}`));
|
|
724
|
+
}
|
|
725
|
+
console.log('');
|
|
726
|
+
}
|
|
727
|
+
else if (options.all || !taskId) {
|
|
728
|
+
// Sync all active worktrees
|
|
729
|
+
const activeWorktrees = state.worktrees.filter(wt => wt.status === 'active' || wt.status === 'paused');
|
|
730
|
+
if (activeWorktrees.length === 0) {
|
|
731
|
+
spinner.stop();
|
|
732
|
+
console.log(chalk_1.default.yellow('\nNo active worktrees to sync.'));
|
|
733
|
+
console.log('');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
spinner.text = `Syncing ${activeWorktrees.length} worktree(s)...`;
|
|
737
|
+
const results = [];
|
|
738
|
+
for (const wt of activeWorktrees) {
|
|
739
|
+
wt.status = 'syncing';
|
|
740
|
+
await saveWorktreeState(state);
|
|
741
|
+
const result = await syncSingleWorktree(wt);
|
|
742
|
+
wt.status = result.success ? 'active' : 'error';
|
|
743
|
+
wt.lastAccessedAt = getTimestamp();
|
|
744
|
+
results.push({ taskId: wt.taskId, result });
|
|
745
|
+
}
|
|
746
|
+
await saveWorktreeState(state);
|
|
747
|
+
spinner.stop();
|
|
748
|
+
console.log(chalk_1.default.cyan('\nSync Results'));
|
|
749
|
+
console.log(chalk_1.default.gray('='.repeat(80)));
|
|
750
|
+
const successful = results.filter(r => r.result.success);
|
|
751
|
+
const failed = results.filter(r => !r.result.success);
|
|
752
|
+
if (successful.length > 0) {
|
|
753
|
+
console.log(chalk_1.default.green(`\nSuccessfully synced: ${successful.length}`));
|
|
754
|
+
for (const { taskId: tid, result } of successful) {
|
|
755
|
+
console.log(chalk_1.default.gray(` - ${tid}: ${result.commitsUpdated || 0} commit(s) updated`));
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (failed.length > 0) {
|
|
759
|
+
console.log(chalk_1.default.red(`\nFailed to sync: ${failed.length}`));
|
|
760
|
+
for (const { taskId: tid, result } of failed) {
|
|
761
|
+
console.log(chalk_1.default.red(` - ${tid}: ${result.message}`));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
console.log('');
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
spinner.fail('Failed to sync worktrees');
|
|
769
|
+
console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// Export for registration
|
|
773
|
+
exports.default = createWorktreeCommand;
|
|
774
|
+
//# sourceMappingURL=worktree.js.map
|