@wundr.io/cli 1.0.1 → 1.0.4

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 (249) hide show
  1. package/dist/ai/ai-service.d.ts +152 -0
  2. package/dist/ai/ai-service.d.ts.map +1 -0
  3. package/dist/ai/ai-service.js +430 -0
  4. package/dist/ai/ai-service.js.map +1 -0
  5. package/dist/ai/claude-client.d.ts +130 -0
  6. package/dist/ai/claude-client.d.ts.map +1 -0
  7. package/dist/ai/claude-client.js +340 -0
  8. package/dist/ai/claude-client.js.map +1 -0
  9. package/dist/ai/conversation-manager.d.ts +164 -0
  10. package/dist/ai/conversation-manager.d.ts.map +1 -0
  11. package/dist/ai/conversation-manager.js +614 -0
  12. package/dist/ai/conversation-manager.js.map +1 -0
  13. package/dist/ai/index.d.ts +5 -0
  14. package/dist/ai/index.d.ts.map +1 -0
  15. package/dist/ai/index.js +8 -0
  16. package/dist/ai/index.js.map +1 -0
  17. package/dist/cli.d.ts +36 -0
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/cli.js +192 -0
  20. package/dist/cli.js.map +1 -0
  21. package/dist/commands/ai.d.ts +89 -0
  22. package/dist/commands/ai.d.ts.map +1 -0
  23. package/dist/commands/ai.js +799 -0
  24. package/dist/commands/ai.js.map +1 -0
  25. package/dist/commands/alignment.d.ts +78 -0
  26. package/dist/commands/alignment.d.ts.map +1 -0
  27. package/dist/commands/alignment.js +817 -0
  28. package/dist/commands/alignment.js.map +1 -0
  29. package/dist/commands/analyze-optimized.d.ts +14 -0
  30. package/dist/commands/analyze-optimized.d.ts.map +1 -0
  31. package/dist/commands/analyze-optimized.js +600 -0
  32. package/dist/commands/analyze-optimized.js.map +1 -0
  33. package/dist/commands/analyze.d.ts +65 -0
  34. package/dist/commands/analyze.d.ts.map +1 -0
  35. package/dist/commands/analyze.js +435 -0
  36. package/dist/commands/analyze.js.map +1 -0
  37. package/dist/commands/batch.d.ts +71 -0
  38. package/dist/commands/batch.d.ts.map +1 -0
  39. package/dist/commands/batch.js +738 -0
  40. package/dist/commands/batch.js.map +1 -0
  41. package/dist/commands/chat.d.ts +71 -0
  42. package/dist/commands/chat.d.ts.map +1 -0
  43. package/dist/commands/chat.js +674 -0
  44. package/dist/commands/chat.js.map +1 -0
  45. package/dist/commands/claude-init.d.ts +28 -0
  46. package/dist/commands/claude-init.d.ts.map +1 -0
  47. package/dist/commands/claude-init.js +591 -0
  48. package/dist/commands/claude-init.js.map +1 -0
  49. package/dist/commands/claude-setup.d.ts +119 -0
  50. package/dist/commands/claude-setup.d.ts.map +1 -0
  51. package/dist/commands/claude-setup.js +1073 -0
  52. package/dist/commands/claude-setup.js.map +1 -0
  53. package/dist/commands/computer-setup-commands.d.ts +53 -0
  54. package/dist/commands/computer-setup-commands.d.ts.map +1 -0
  55. package/dist/commands/computer-setup-commands.js +705 -0
  56. package/dist/commands/computer-setup-commands.js.map +1 -0
  57. package/dist/commands/computer-setup.d.ts +7 -0
  58. package/dist/commands/computer-setup.d.ts.map +1 -0
  59. package/dist/commands/computer-setup.js +849 -0
  60. package/dist/commands/computer-setup.js.map +1 -0
  61. package/dist/commands/create-command.d.ts +7 -0
  62. package/dist/commands/create-command.d.ts.map +1 -0
  63. package/dist/commands/create-command.js +158 -0
  64. package/dist/commands/create-command.js.map +1 -0
  65. package/dist/commands/create.d.ts +74 -0
  66. package/dist/commands/create.d.ts.map +1 -0
  67. package/dist/commands/create.js +556 -0
  68. package/dist/commands/create.js.map +1 -0
  69. package/dist/commands/dashboard.d.ts +91 -0
  70. package/dist/commands/dashboard.d.ts.map +1 -0
  71. package/dist/commands/dashboard.js +538 -0
  72. package/dist/commands/dashboard.js.map +1 -0
  73. package/dist/commands/govern.d.ts +70 -0
  74. package/dist/commands/govern.d.ts.map +1 -0
  75. package/dist/commands/govern.js +481 -0
  76. package/dist/commands/govern.js.map +1 -0
  77. package/dist/commands/governance.d.ts +17 -0
  78. package/dist/commands/governance.d.ts.map +1 -0
  79. package/dist/commands/governance.js +703 -0
  80. package/dist/commands/governance.js.map +1 -0
  81. package/dist/commands/guardian.d.ts +20 -0
  82. package/dist/commands/guardian.d.ts.map +1 -0
  83. package/dist/commands/guardian.js +597 -0
  84. package/dist/commands/guardian.js.map +1 -0
  85. package/dist/commands/init.d.ts +59 -0
  86. package/dist/commands/init.d.ts.map +1 -0
  87. package/dist/commands/init.js +650 -0
  88. package/dist/commands/init.js.map +1 -0
  89. package/dist/commands/performance-optimizer.d.ts +30 -0
  90. package/dist/commands/performance-optimizer.d.ts.map +1 -0
  91. package/dist/commands/performance-optimizer.js +650 -0
  92. package/dist/commands/performance-optimizer.js.map +1 -0
  93. package/dist/commands/plugins.d.ts +87 -0
  94. package/dist/commands/plugins.d.ts.map +1 -0
  95. package/dist/commands/plugins.js +685 -0
  96. package/dist/commands/plugins.js.map +1 -0
  97. package/dist/commands/rag.d.ts +7 -0
  98. package/dist/commands/rag.d.ts.map +1 -0
  99. package/dist/commands/rag.js +748 -0
  100. package/dist/commands/rag.js.map +1 -0
  101. package/dist/commands/session.d.ts +41 -0
  102. package/dist/commands/session.d.ts.map +1 -0
  103. package/dist/commands/session.js +441 -0
  104. package/dist/commands/session.js.map +1 -0
  105. package/dist/commands/setup.d.ts +29 -0
  106. package/dist/commands/setup.d.ts.map +1 -0
  107. package/dist/commands/setup.js +397 -0
  108. package/dist/commands/setup.js.map +1 -0
  109. package/dist/commands/test-init.d.ts +9 -0
  110. package/dist/commands/test-init.d.ts.map +1 -0
  111. package/dist/commands/test-init.js +222 -0
  112. package/dist/commands/test-init.js.map +1 -0
  113. package/dist/commands/test.d.ts +25 -0
  114. package/dist/commands/test.d.ts.map +1 -0
  115. package/dist/commands/test.js +217 -0
  116. package/dist/commands/test.js.map +1 -0
  117. package/dist/commands/vp.d.ts +7 -0
  118. package/dist/commands/vp.d.ts.map +1 -0
  119. package/dist/commands/vp.js +571 -0
  120. package/dist/commands/vp.js.map +1 -0
  121. package/dist/commands/watch.d.ts +76 -0
  122. package/dist/commands/watch.d.ts.map +1 -0
  123. package/dist/commands/watch.js +613 -0
  124. package/dist/commands/watch.js.map +1 -0
  125. package/dist/commands/worktree.d.ts +63 -0
  126. package/dist/commands/worktree.d.ts.map +1 -0
  127. package/dist/commands/worktree.js +774 -0
  128. package/dist/commands/worktree.js.map +1 -0
  129. package/dist/context/context-manager.d.ts +155 -0
  130. package/dist/context/context-manager.d.ts.map +1 -0
  131. package/dist/context/context-manager.js +383 -0
  132. package/dist/context/context-manager.js.map +1 -0
  133. package/dist/context/index.d.ts +3 -0
  134. package/dist/context/index.d.ts.map +1 -0
  135. package/dist/context/index.js +6 -0
  136. package/dist/context/index.js.map +1 -0
  137. package/dist/context/session-manager.d.ts +207 -0
  138. package/dist/context/session-manager.d.ts.map +1 -0
  139. package/dist/context/session-manager.js +686 -0
  140. package/dist/context/session-manager.js.map +1 -0
  141. package/dist/index.d.ts +8 -0
  142. package/dist/index.d.ts.map +1 -0
  143. package/dist/index.js +51 -0
  144. package/dist/index.js.map +1 -0
  145. package/dist/interactive/interactive-mode.d.ts +76 -0
  146. package/dist/interactive/interactive-mode.d.ts.map +1 -0
  147. package/dist/interactive/interactive-mode.js +732 -0
  148. package/dist/interactive/interactive-mode.js.map +1 -0
  149. package/dist/nlp/command-mapper.d.ts +174 -0
  150. package/dist/nlp/command-mapper.d.ts.map +1 -0
  151. package/dist/nlp/command-mapper.js +624 -0
  152. package/dist/nlp/command-mapper.js.map +1 -0
  153. package/dist/nlp/command-parser.d.ts +106 -0
  154. package/dist/nlp/command-parser.d.ts.map +1 -0
  155. package/dist/nlp/command-parser.js +417 -0
  156. package/dist/nlp/command-parser.js.map +1 -0
  157. package/dist/nlp/index.d.ts +5 -0
  158. package/dist/nlp/index.d.ts.map +1 -0
  159. package/dist/nlp/index.js +8 -0
  160. package/dist/nlp/index.js.map +1 -0
  161. package/dist/nlp/intent-classifier.d.ts +59 -0
  162. package/dist/nlp/intent-classifier.d.ts.map +1 -0
  163. package/dist/nlp/intent-classifier.js +384 -0
  164. package/dist/nlp/intent-classifier.js.map +1 -0
  165. package/dist/nlp/intent-parser.d.ts +152 -0
  166. package/dist/nlp/intent-parser.d.ts.map +1 -0
  167. package/dist/nlp/intent-parser.js +744 -0
  168. package/dist/nlp/intent-parser.js.map +1 -0
  169. package/dist/plugins/plugin-manager.d.ts +120 -0
  170. package/dist/plugins/plugin-manager.d.ts.map +1 -0
  171. package/dist/plugins/plugin-manager.js +595 -0
  172. package/dist/plugins/plugin-manager.js.map +1 -0
  173. package/dist/types/index.d.ts +224 -0
  174. package/dist/types/index.d.ts.map +1 -0
  175. package/dist/types/index.js +3 -0
  176. package/dist/types/index.js.map +1 -0
  177. package/dist/utils/backup-rollback-manager.d.ts +72 -0
  178. package/dist/utils/backup-rollback-manager.d.ts.map +1 -0
  179. package/dist/utils/backup-rollback-manager.js +289 -0
  180. package/dist/utils/backup-rollback-manager.js.map +1 -0
  181. package/dist/utils/claude-config-installer.d.ts +94 -0
  182. package/dist/utils/claude-config-installer.d.ts.map +1 -0
  183. package/dist/utils/claude-config-installer.js +628 -0
  184. package/dist/utils/claude-config-installer.js.map +1 -0
  185. package/dist/utils/config-manager.d.ts +73 -0
  186. package/dist/utils/config-manager.d.ts.map +1 -0
  187. package/dist/utils/config-manager.js +339 -0
  188. package/dist/utils/config-manager.js.map +1 -0
  189. package/dist/utils/error-handler.d.ts +46 -0
  190. package/dist/utils/error-handler.d.ts.map +1 -0
  191. package/dist/utils/error-handler.js +169 -0
  192. package/dist/utils/error-handler.js.map +1 -0
  193. package/dist/utils/logger.d.ts +25 -0
  194. package/dist/utils/logger.d.ts.map +1 -0
  195. package/dist/utils/logger.js +105 -0
  196. package/dist/utils/logger.js.map +1 -0
  197. package/package.json +6 -4
  198. package/src/ai/ai-service.ts +22 -19
  199. package/src/ai/claude-client.ts +20 -16
  200. package/src/ai/conversation-manager.ts +37 -30
  201. package/src/cli.ts +46 -17
  202. package/src/commands/ai.ts +196 -88
  203. package/src/commands/alignment.ts +1212 -0
  204. package/src/commands/analyze-optimized.ts +394 -89
  205. package/src/commands/analyze.ts +22 -20
  206. package/src/commands/batch.ts +41 -38
  207. package/src/commands/chat.ts +37 -34
  208. package/src/commands/claude-init.ts +38 -30
  209. package/src/commands/claude-setup.ts +692 -97
  210. package/src/commands/computer-setup-commands.ts +45 -39
  211. package/src/commands/computer-setup.ts +490 -20
  212. package/src/commands/create-command.ts +7 -7
  213. package/src/commands/create.ts +36 -33
  214. package/src/commands/dashboard.ts +33 -28
  215. package/src/commands/govern.ts +34 -29
  216. package/src/commands/governance.ts +1005 -0
  217. package/src/commands/guardian.ts +887 -0
  218. package/src/commands/init.ts +112 -22
  219. package/src/commands/performance-optimizer.ts +48 -42
  220. package/src/commands/plugins.ts +35 -32
  221. package/src/commands/project-update.ts +1053 -0
  222. package/src/commands/rag.ts +904 -0
  223. package/src/commands/session.ts +631 -0
  224. package/src/commands/setup.ts +35 -31
  225. package/src/commands/test-init.ts +6 -5
  226. package/src/commands/test.ts +7 -6
  227. package/src/commands/vp.ts +762 -0
  228. package/src/commands/watch.ts +44 -33
  229. package/src/commands/worktree.ts +1057 -0
  230. package/src/context/context-manager.ts +15 -12
  231. package/src/context/session-manager.ts +51 -40
  232. package/src/index.ts +7 -6
  233. package/src/interactive/interactive-mode.ts +25 -18
  234. package/src/lib/conflict-resolution.ts +28 -0
  235. package/src/lib/merge-strategy.ts +28 -0
  236. package/src/lib/safety-mechanisms.ts +47 -0
  237. package/src/lib/state-detection.ts +28 -0
  238. package/src/nlp/command-mapper.ts +35 -30
  239. package/src/nlp/command-parser.ts +20 -17
  240. package/src/nlp/intent-classifier.ts +7 -7
  241. package/src/nlp/intent-parser.ts +61 -49
  242. package/src/plugins/plugin-manager.ts +27 -23
  243. package/src/types/index.ts +1 -1
  244. package/src/types/modules.d.ts +1 -0
  245. package/src/utils/backup-rollback-manager.ts +13 -11
  246. package/src/utils/claude-config-installer.ts +18 -16
  247. package/src/utils/config-manager.ts +12 -9
  248. package/src/utils/error-handler.ts +5 -3
  249. package/src/utils/logger.ts +35 -12
@@ -0,0 +1,1057 @@
1
+ /**
2
+ * Worktree Management CLI Commands
3
+ *
4
+ * Manages git worktrees for multi-agent development including
5
+ * listing, creating, switching, cleanup, and synchronization.
6
+ *
7
+ * @module commands/worktree
8
+ */
9
+
10
+ import { exec } from 'child_process';
11
+ import * as fs from 'fs/promises';
12
+ import * as os from 'os';
13
+ import * as path from 'path';
14
+ import { promisify } from 'util';
15
+
16
+ import chalk from 'chalk';
17
+ import { Command } from 'commander';
18
+ import ora from 'ora';
19
+
20
+ const execAsync = promisify(exec);
21
+
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
+
26
+ // Types
27
+ export type WorktreeStatus =
28
+ | 'pending'
29
+ | 'creating'
30
+ | 'active'
31
+ | 'paused'
32
+ | 'syncing'
33
+ | 'cleanup'
34
+ | 'error'
35
+ | 'destroyed';
36
+
37
+ export interface WorktreeEntry {
38
+ taskId: string;
39
+ branchName: string;
40
+ worktreePath: string;
41
+ sessionId: string;
42
+ status: WorktreeStatus;
43
+ createdAt: string;
44
+ lastAccessedAt?: string;
45
+ parentWorktreePath?: string;
46
+ metadata?: Record<string, unknown>;
47
+ }
48
+
49
+ export interface WorktreeState {
50
+ version: string;
51
+ lastUpdated: string;
52
+ repoPath: string;
53
+ worktrees: WorktreeEntry[];
54
+ }
55
+
56
+ export interface GitWorktreeInfo {
57
+ path: string;
58
+ branch: string;
59
+ commit: string;
60
+ isMain: boolean;
61
+ isLocked: boolean;
62
+ lastModified?: Date;
63
+ }
64
+
65
+ export interface CleanupResult {
66
+ success: boolean;
67
+ removedCount: number;
68
+ removedPaths: string[];
69
+ skippedPaths: string[];
70
+ skipReasons: Map<string, string>;
71
+ errors: Array<{ path: string; message: string }>;
72
+ freedBytes?: number;
73
+ }
74
+
75
+ export interface SyncResult {
76
+ success: boolean;
77
+ message: string;
78
+ worktreePath: string;
79
+ branchName?: string;
80
+ stashedChanges?: boolean;
81
+ commitsUpdated?: number;
82
+ error?: Error;
83
+ timestamp: Date;
84
+ }
85
+
86
+ // Utility functions
87
+ function getTimestamp(): string {
88
+ return new Date().toISOString();
89
+ }
90
+
91
+ function padRight(str: string, length: number): string {
92
+ return str.length >= length
93
+ ? str.substring(0, length)
94
+ : str + ' '.repeat(length - str.length);
95
+ }
96
+
97
+ function truncate(str: string, length: number): string {
98
+ if (str.length <= length) {
99
+ return str;
100
+ }
101
+ return str.substring(0, length - 3) + '...';
102
+ }
103
+
104
+ function formatAge(dateStr: string): string {
105
+ const date = new Date(dateStr);
106
+ const now = Date.now();
107
+ const diffMs = now - date.getTime();
108
+ const diffMins = Math.floor(diffMs / (1000 * 60));
109
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
110
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
111
+
112
+ if (diffDays > 0) {
113
+ return `${diffDays}d ago`;
114
+ }
115
+ if (diffHours > 0) {
116
+ return `${diffHours}h ago`;
117
+ }
118
+ if (diffMins > 0) {
119
+ return `${diffMins}m ago`;
120
+ }
121
+ return 'just now';
122
+ }
123
+
124
+ function generateSessionId(): string {
125
+ const timestamp = Date.now().toString(36);
126
+ const random = Math.random().toString(36).substring(2, 8);
127
+ return `session-${timestamp}-${random}`;
128
+ }
129
+
130
+ function generateBranchName(taskId: string): string {
131
+ const sanitized = taskId
132
+ .toLowerCase()
133
+ .replace(/[^a-z0-9-]/g, '-')
134
+ .replace(/-+/g, '-')
135
+ .replace(/^-|-$/g, '');
136
+ return `task/${sanitized}`;
137
+ }
138
+
139
+ async function ensureStateDir(): Promise<void> {
140
+ await fs.mkdir(WORKTREE_STATE_DIR, { recursive: true });
141
+ }
142
+
143
+ async function loadWorktreeState(): Promise<WorktreeState> {
144
+ try {
145
+ await ensureStateDir();
146
+ const content = await fs.readFile(WORKTREE_STATE_FILE, 'utf-8');
147
+ return JSON.parse(content) as WorktreeState;
148
+ } catch {
149
+ // Return empty state if file doesn't exist
150
+ return {
151
+ version: '1.0.0',
152
+ lastUpdated: getTimestamp(),
153
+ repoPath: process.cwd(),
154
+ worktrees: [],
155
+ };
156
+ }
157
+ }
158
+
159
+ async function saveWorktreeState(state: WorktreeState): Promise<void> {
160
+ await ensureStateDir();
161
+ state.lastUpdated = getTimestamp();
162
+ await fs.writeFile(WORKTREE_STATE_FILE, JSON.stringify(state, null, 2));
163
+ }
164
+
165
+ async function getGitRepoRoot(): Promise<string> {
166
+ try {
167
+ const { stdout } = await execAsync('git rev-parse --show-toplevel', {
168
+ timeout: 10000,
169
+ });
170
+ return stdout.trim();
171
+ } catch {
172
+ return process.cwd();
173
+ }
174
+ }
175
+
176
+ async function executeGitCommand(
177
+ command: string,
178
+ args: string[],
179
+ cwd: string,
180
+ ): Promise<string> {
181
+ const fullCommand = `git ${command} ${args.join(' ')}`;
182
+ try {
183
+ const { stdout } = await execAsync(fullCommand, { cwd, timeout: 60000 });
184
+ return stdout.trim();
185
+ } catch (error) {
186
+ const errorMessage = error instanceof Error ? error.message : String(error);
187
+ throw new Error(`Git command failed: ${fullCommand}\n${errorMessage}`);
188
+ }
189
+ }
190
+
191
+ async function listGitWorktrees(repoPath: string): Promise<GitWorktreeInfo[]> {
192
+ const worktrees: GitWorktreeInfo[] = [];
193
+
194
+ try {
195
+ const output = await executeGitCommand(
196
+ 'worktree',
197
+ ['list', '--porcelain'],
198
+ repoPath,
199
+ );
200
+ const entries = output.split('\n\n').filter(Boolean);
201
+
202
+ for (const entry of entries) {
203
+ const lines = entry.split('\n');
204
+ const info: Partial<GitWorktreeInfo> = {
205
+ isMain: false,
206
+ isLocked: false,
207
+ };
208
+
209
+ for (const line of lines) {
210
+ if (line.startsWith('worktree ')) {
211
+ info.path = line.substring('worktree '.length).trim();
212
+ } else if (line.startsWith('HEAD ')) {
213
+ info.commit = line.substring('HEAD '.length).trim();
214
+ } else if (line.startsWith('branch ')) {
215
+ info.branch = line
216
+ .substring('branch '.length)
217
+ .trim()
218
+ .replace('refs/heads/', '');
219
+ } else if (line === 'bare') {
220
+ info.isMain = true;
221
+ } else if (line === 'locked') {
222
+ info.isLocked = true;
223
+ }
224
+ }
225
+
226
+ if (info.path) {
227
+ // Check if this is the main worktree
228
+ if (info.path === repoPath) {
229
+ info.isMain = true;
230
+ }
231
+
232
+ // Get last modified time
233
+ try {
234
+ const stats = await fs.stat(info.path);
235
+ info.lastModified = stats.mtime;
236
+ } catch {
237
+ // Path might not exist for orphaned worktrees
238
+ }
239
+
240
+ worktrees.push(info as GitWorktreeInfo);
241
+ }
242
+ }
243
+ } catch {
244
+ // Return empty list if git command fails
245
+ }
246
+
247
+ return worktrees;
248
+ }
249
+
250
+ async function hasUncommittedChanges(worktreePath: string): Promise<boolean> {
251
+ try {
252
+ const { stdout } = await execAsync('git status --porcelain', {
253
+ cwd: worktreePath,
254
+ timeout: 10000,
255
+ });
256
+ return stdout.trim().length > 0;
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
261
+
262
+ function getStatusColor(status: WorktreeStatus): (str: string) => string {
263
+ switch (status) {
264
+ case 'active':
265
+ return chalk.green;
266
+ case 'paused':
267
+ return chalk.yellow;
268
+ case 'syncing':
269
+ return chalk.blue;
270
+ case 'creating':
271
+ return chalk.cyan;
272
+ case 'cleanup':
273
+ return chalk.magenta;
274
+ case 'error':
275
+ return chalk.red;
276
+ case 'destroyed':
277
+ return chalk.gray;
278
+ case 'pending':
279
+ return chalk.white;
280
+ default:
281
+ return chalk.white;
282
+ }
283
+ }
284
+
285
+ function getStatusIcon(status: WorktreeStatus): string {
286
+ switch (status) {
287
+ case 'active':
288
+ return '[ACTIVE]';
289
+ case 'paused':
290
+ return '[PAUSED]';
291
+ case 'syncing':
292
+ return '[SYNCING]';
293
+ case 'creating':
294
+ return '[CREATING]';
295
+ case 'cleanup':
296
+ return '[CLEANUP]';
297
+ case 'error':
298
+ return '[ERROR]';
299
+ case 'destroyed':
300
+ return '[DESTROYED]';
301
+ case 'pending':
302
+ return '[PENDING]';
303
+ default:
304
+ return '[UNKNOWN]';
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Create the worktree command with all subcommands
310
+ */
311
+ export function createWorktreeCommand(): Command {
312
+ const command = new Command('worktree')
313
+ .alias('wt')
314
+ .description('Manage git worktrees for multi-agent development')
315
+ .addHelpText(
316
+ 'after',
317
+ chalk.gray(`
318
+ Examples:
319
+ ${chalk.green('wundr worktree list')} List all worktrees
320
+ ${chalk.green('wundr wt list --session <id>')} List worktrees for a session
321
+ ${chalk.green('wundr worktree create <taskId>')} Create new worktree for a task
322
+ ${chalk.green('wundr worktree switch <taskId>')} Switch to an existing worktree
323
+ ${chalk.green('wundr worktree cleanup --dry-run')} Preview what would be cleaned up
324
+ ${chalk.green('wundr worktree sync')} Sync all worktrees from remote
325
+ ${chalk.green('wundr worktree sync <taskId>')} Sync specific worktree
326
+ `),
327
+ );
328
+
329
+ // List command (default)
330
+ command
331
+ .command('list', { isDefault: true })
332
+ .description('List all worktrees')
333
+ .option('-s, --session <id>', 'Filter by session ID')
334
+ .option(
335
+ '--status <status>',
336
+ 'Filter by status (active, paused, syncing, error, etc.)',
337
+ )
338
+ .option('-f, --format <format>', 'Output format (table, json)', 'table')
339
+ .action(async options => {
340
+ await listWorktrees(options);
341
+ });
342
+
343
+ // Create command (ccswitch create)
344
+ command
345
+ .command('create <taskId>')
346
+ .description('Create a new worktree for a task')
347
+ .option('-b, --base <branch>', 'Base branch to create from', 'main')
348
+ .action(async (taskId, options) => {
349
+ await createWorktree(taskId, options);
350
+ });
351
+
352
+ // Switch command (ccswitch switch)
353
+ command
354
+ .command('switch <taskId>')
355
+ .description('Switch to an existing worktree')
356
+ .action(async taskId => {
357
+ await switchWorktree(taskId);
358
+ });
359
+
360
+ // Cleanup command
361
+ command
362
+ .command('cleanup')
363
+ .description('Clean up stale worktrees')
364
+ .option(
365
+ '--dry-run',
366
+ 'Preview what would be cleaned up without making changes',
367
+ )
368
+ .option('--force', 'Force cleanup even with uncommitted changes')
369
+ .option('--age <days>', 'Clean up worktrees older than specified days', '7')
370
+ .action(async options => {
371
+ await cleanupWorktrees(options);
372
+ });
373
+
374
+ // Sync command
375
+ command
376
+ .command('sync [taskId]')
377
+ .description('Sync worktree(s) from remote')
378
+ .option('--all', 'Sync all worktrees')
379
+ .action(async (taskId, options) => {
380
+ await syncWorktrees(taskId, options);
381
+ });
382
+
383
+ return command;
384
+ }
385
+
386
+ // Command implementations
387
+
388
+ async function listWorktrees(options: {
389
+ session?: string;
390
+ status?: string;
391
+ format?: 'table' | 'json';
392
+ }): Promise<void> {
393
+ const spinner = ora('Loading worktrees...').start();
394
+
395
+ try {
396
+ const state = await loadWorktreeState();
397
+ let worktrees = state.worktrees;
398
+
399
+ // Filter by session ID if provided
400
+ if (options.session) {
401
+ worktrees = worktrees.filter(wt => wt.sessionId === options.session);
402
+ }
403
+
404
+ // Filter by status if provided
405
+ if (options.status) {
406
+ worktrees = worktrees.filter(wt => wt.status === options.status);
407
+ }
408
+
409
+ spinner.stop();
410
+
411
+ if (options.format === 'json') {
412
+ console.log(
413
+ JSON.stringify(
414
+ {
415
+ timestamp: getTimestamp(),
416
+ count: worktrees.length,
417
+ worktrees: worktrees,
418
+ },
419
+ null,
420
+ 2,
421
+ ),
422
+ );
423
+ return;
424
+ }
425
+
426
+ console.log(chalk.cyan('\nWorktree List'));
427
+ console.log(chalk.gray('='.repeat(110)));
428
+
429
+ if (worktrees.length === 0) {
430
+ console.log(chalk.yellow('\nNo worktrees found.'));
431
+ if (options.session) {
432
+ console.log(chalk.gray(`No worktrees for session: ${options.session}`));
433
+ }
434
+ console.log('');
435
+ return;
436
+ }
437
+
438
+ // Table header
439
+ console.log(
440
+ chalk.cyan(
441
+ padRight('Task ID', 20) +
442
+ padRight('Branch', 25) +
443
+ padRight('Status', 12) +
444
+ padRight('Created', 12) +
445
+ padRight('Path', 40),
446
+ ),
447
+ );
448
+ console.log(chalk.gray('-'.repeat(110)));
449
+
450
+ // Table rows
451
+ for (const wt of worktrees) {
452
+ const statusColor = getStatusColor(wt.status);
453
+ const createdAge = formatAge(wt.createdAt);
454
+ const worktreePath = truncate(wt.worktreePath, 38);
455
+ const branchName = truncate(wt.branchName, 23);
456
+
457
+ console.log(
458
+ padRight(wt.taskId, 20) +
459
+ chalk.blue(padRight(branchName, 25)) +
460
+ statusColor(padRight(getStatusIcon(wt.status), 12)) +
461
+ padRight(createdAge, 12) +
462
+ chalk.gray(padRight(worktreePath, 40)),
463
+ );
464
+ }
465
+
466
+ console.log(chalk.gray('-'.repeat(110)));
467
+ console.log(chalk.gray(`Total: ${worktrees.length} worktree(s)`));
468
+ console.log('');
469
+ } catch (error) {
470
+ spinner.fail('Failed to load worktrees');
471
+ console.error(
472
+ chalk.red(error instanceof Error ? error.message : String(error)),
473
+ );
474
+ }
475
+ }
476
+
477
+ async function createWorktree(
478
+ taskId: string,
479
+ options: { base?: string },
480
+ ): Promise<void> {
481
+ const spinner = ora(`Creating worktree for task: ${taskId}...`).start();
482
+
483
+ try {
484
+ const state = await loadWorktreeState();
485
+ const repoPath = await getGitRepoRoot();
486
+ state.repoPath = repoPath;
487
+
488
+ // Check if worktree already exists
489
+ const existing = state.worktrees.find(wt => wt.taskId === taskId);
490
+ if (existing) {
491
+ spinner.fail(`Worktree already exists for task: ${taskId}`);
492
+ console.log(
493
+ chalk.yellow(`Use "wundr worktree switch ${taskId}" to switch to it.`),
494
+ );
495
+ return;
496
+ }
497
+
498
+ const baseBranch = options.base || 'main';
499
+ const branchName = generateBranchName(taskId);
500
+ const sessionId = generateSessionId();
501
+ const worktreeRoot = path.join(repoPath, '.git-worktrees', 'sessions');
502
+ const worktreePath = path.join(worktreeRoot, taskId);
503
+
504
+ // Ensure worktree root exists
505
+ await fs.mkdir(worktreeRoot, { recursive: true });
506
+
507
+ // Fetch latest from remote
508
+ try {
509
+ await executeGitCommand('fetch', ['origin', baseBranch], repoPath);
510
+ } catch {
511
+ // Continue even if fetch fails (might be offline)
512
+ }
513
+
514
+ // Create the worktree with a new branch
515
+ await executeGitCommand(
516
+ 'worktree',
517
+ ['add', '-b', branchName, worktreePath, `origin/${baseBranch}`],
518
+ repoPath,
519
+ );
520
+
521
+ // Save to state
522
+ const entry: WorktreeEntry = {
523
+ taskId,
524
+ branchName,
525
+ worktreePath,
526
+ sessionId,
527
+ status: 'active',
528
+ createdAt: getTimestamp(),
529
+ lastAccessedAt: getTimestamp(),
530
+ };
531
+
532
+ state.worktrees.push(entry);
533
+ await saveWorktreeState(state);
534
+
535
+ spinner.succeed(`Worktree created for task: ${taskId}`);
536
+ console.log('');
537
+ console.log(chalk.white(' Task ID: ') + chalk.green(taskId));
538
+ console.log(chalk.white(' Branch: ') + chalk.blue(branchName));
539
+ console.log(chalk.white(' Path: ') + chalk.gray(worktreePath));
540
+ console.log('');
541
+ console.log(
542
+ chalk.gray(`Use "wundr worktree switch ${taskId}" to start working.`),
543
+ );
544
+ console.log('');
545
+ } catch (error) {
546
+ spinner.fail('Failed to create worktree');
547
+ console.error(
548
+ chalk.red(error instanceof Error ? error.message : String(error)),
549
+ );
550
+ }
551
+ }
552
+
553
+ async function switchWorktree(taskId: string): Promise<void> {
554
+ const spinner = ora(`Switching to worktree: ${taskId}...`).start();
555
+
556
+ try {
557
+ const state = await loadWorktreeState();
558
+
559
+ // Find the worktree
560
+ const worktree = state.worktrees.find(wt => wt.taskId === taskId);
561
+ if (!worktree) {
562
+ spinner.fail(`Worktree not found: ${taskId}`);
563
+ console.log(
564
+ chalk.yellow(`Use "wundr worktree create ${taskId}" to create it.`),
565
+ );
566
+ return;
567
+ }
568
+
569
+ // Check if worktree path exists
570
+ try {
571
+ await fs.access(worktree.worktreePath);
572
+ } catch {
573
+ spinner.fail(`Worktree directory not found: ${worktree.worktreePath}`);
574
+ console.log(
575
+ chalk.yellow(
576
+ 'The worktree may have been deleted. Consider running cleanup.',
577
+ ),
578
+ );
579
+ return;
580
+ }
581
+
582
+ // Update status
583
+ worktree.status = 'active';
584
+ worktree.lastAccessedAt = getTimestamp();
585
+ await saveWorktreeState(state);
586
+
587
+ spinner.succeed(`Switched to worktree: ${taskId}`);
588
+ console.log('');
589
+ console.log(chalk.white(' Task ID: ') + chalk.green(taskId));
590
+ console.log(
591
+ chalk.white(' Branch: ') + chalk.blue(worktree.branchName),
592
+ );
593
+ console.log(
594
+ chalk.white(' Path: ') + chalk.gray(worktree.worktreePath),
595
+ );
596
+ console.log('');
597
+ console.log(chalk.cyan('To navigate to this worktree, run:'));
598
+ console.log(chalk.white(` cd ${worktree.worktreePath}`));
599
+ console.log('');
600
+ } catch (error) {
601
+ spinner.fail('Failed to switch worktree');
602
+ console.error(
603
+ chalk.red(error instanceof Error ? error.message : String(error)),
604
+ );
605
+ }
606
+ }
607
+
608
+ async function cleanupWorktrees(options: {
609
+ dryRun?: boolean;
610
+ force?: boolean;
611
+ age?: string;
612
+ }): Promise<void> {
613
+ const maxAgeDays = parseInt(options.age || '7', 10);
614
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
615
+ const spinner = ora(
616
+ options.dryRun ? 'Analyzing worktrees...' : 'Cleaning up worktrees...',
617
+ ).start();
618
+
619
+ try {
620
+ const state = await loadWorktreeState();
621
+ const repoPath = state.repoPath || (await getGitRepoRoot());
622
+ const now = Date.now();
623
+
624
+ // Get actual git worktrees
625
+ const gitWorktrees = await listGitWorktrees(repoPath);
626
+
627
+ // Categorize worktrees for cleanup
628
+ const toRemove: Array<{
629
+ entry?: WorktreeEntry;
630
+ gitInfo?: GitWorktreeInfo;
631
+ reason: string;
632
+ }> = [];
633
+ const toSkip: Array<{
634
+ entry?: WorktreeEntry;
635
+ gitInfo?: GitWorktreeInfo;
636
+ reason: string;
637
+ }> = [];
638
+
639
+ // Check state entries
640
+ for (const entry of state.worktrees) {
641
+ const createdAt = new Date(entry.createdAt).getTime();
642
+ const age = now - createdAt;
643
+ const gitInfo = gitWorktrees.find(g => g.path === entry.worktreePath);
644
+
645
+ // Check if worktree still exists
646
+ try {
647
+ await fs.access(entry.worktreePath);
648
+ } catch {
649
+ toRemove.push({ entry, reason: 'Directory no longer exists' });
650
+ continue;
651
+ }
652
+
653
+ // Check if too old
654
+ if (age > maxAgeMs) {
655
+ // Check for uncommitted changes
656
+ const hasChanges = await hasUncommittedChanges(entry.worktreePath);
657
+ if (hasChanges && !options.force) {
658
+ toSkip.push({ entry, gitInfo, reason: 'Has uncommitted changes' });
659
+ } else if (gitInfo?.isLocked && !options.force) {
660
+ toSkip.push({ entry, gitInfo, reason: 'Worktree is locked' });
661
+ } else {
662
+ toRemove.push({
663
+ entry,
664
+ gitInfo,
665
+ reason: `Older than ${maxAgeDays} days`,
666
+ });
667
+ }
668
+ }
669
+
670
+ // Check error status
671
+ if (entry.status === 'error' && !options.force) {
672
+ toRemove.push({ entry, gitInfo, reason: 'In error state' });
673
+ }
674
+ }
675
+
676
+ spinner.stop();
677
+
678
+ if (options.dryRun) {
679
+ // Show preview
680
+ console.log(chalk.cyan('\nCleanup Preview (Dry Run)'));
681
+ console.log(chalk.gray('='.repeat(80)));
682
+
683
+ if (toRemove.length === 0) {
684
+ console.log(chalk.green('\nNo worktrees need to be cleaned up.'));
685
+ console.log('');
686
+ return;
687
+ }
688
+
689
+ console.log(
690
+ chalk.yellow(`\nWorktrees to be removed: ${toRemove.length}`),
691
+ );
692
+ console.log(chalk.gray('-'.repeat(80)));
693
+
694
+ for (const { entry, reason } of toRemove) {
695
+ if (entry) {
696
+ console.log(chalk.white(' Task ID: ') + chalk.green(entry.taskId));
697
+ console.log(
698
+ chalk.white(' Path: ') + chalk.gray(entry.worktreePath),
699
+ );
700
+ console.log(
701
+ chalk.white(' Branch: ') + chalk.blue(entry.branchName),
702
+ );
703
+ console.log(chalk.white(' Reason: ') + chalk.yellow(reason));
704
+ console.log('');
705
+ }
706
+ }
707
+
708
+ if (toSkip.length > 0) {
709
+ console.log(
710
+ chalk.yellow(`\nWorktrees to be skipped: ${toSkip.length}`),
711
+ );
712
+ console.log(chalk.gray('-'.repeat(80)));
713
+
714
+ for (const { entry, reason } of toSkip) {
715
+ if (entry) {
716
+ console.log(chalk.white(' Task ID: ') + chalk.green(entry.taskId));
717
+ console.log(
718
+ chalk.white(' Path: ') + chalk.gray(entry.worktreePath),
719
+ );
720
+ console.log(chalk.white(' Reason: ') + chalk.gray(reason));
721
+ console.log('');
722
+ }
723
+ }
724
+ }
725
+
726
+ console.log(chalk.gray('-'.repeat(80)));
727
+ console.log(
728
+ chalk.gray('Run without --dry-run to perform actual cleanup.'),
729
+ );
730
+ console.log('');
731
+ } else {
732
+ // Perform actual cleanup
733
+ const result: CleanupResult = {
734
+ success: true,
735
+ removedCount: 0,
736
+ removedPaths: [],
737
+ skippedPaths: [],
738
+ skipReasons: new Map(),
739
+ errors: [],
740
+ };
741
+
742
+ for (const { entry, reason } of toRemove) {
743
+ if (!entry) {
744
+ continue;
745
+ }
746
+
747
+ try {
748
+ // Remove the git worktree
749
+ const forceFlag = options.force ? '--force' : '';
750
+ await executeGitCommand(
751
+ 'worktree',
752
+ ['remove', forceFlag, entry.worktreePath].filter(Boolean),
753
+ repoPath,
754
+ );
755
+
756
+ result.removedCount++;
757
+ result.removedPaths.push(entry.worktreePath);
758
+ } catch (error) {
759
+ // Try manual removal if git worktree remove fails
760
+ if (options.force) {
761
+ try {
762
+ await fs.rm(entry.worktreePath, { recursive: true, force: true });
763
+ await executeGitCommand('worktree', ['prune'], repoPath);
764
+ result.removedCount++;
765
+ result.removedPaths.push(entry.worktreePath);
766
+ } catch (rmError) {
767
+ result.errors.push({
768
+ path: entry.worktreePath,
769
+ message:
770
+ rmError instanceof Error ? rmError.message : String(rmError),
771
+ });
772
+ }
773
+ } else {
774
+ result.errors.push({
775
+ path: entry.worktreePath,
776
+ message: error instanceof Error ? error.message : String(error),
777
+ });
778
+ }
779
+ }
780
+ }
781
+
782
+ for (const { entry, reason } of toSkip) {
783
+ if (entry) {
784
+ result.skippedPaths.push(entry.worktreePath);
785
+ result.skipReasons.set(entry.worktreePath, reason);
786
+ }
787
+ }
788
+
789
+ // Update state file to remove cleaned worktrees
790
+ const removedPaths = new Set(result.removedPaths);
791
+ state.worktrees = state.worktrees.filter(
792
+ wt => !removedPaths.has(wt.worktreePath),
793
+ );
794
+ await saveWorktreeState(state);
795
+
796
+ // Show results
797
+ console.log(chalk.cyan('\nCleanup Results'));
798
+ console.log(chalk.gray('='.repeat(80)));
799
+
800
+ if (result.removedCount === 0 && result.errors.length === 0) {
801
+ console.log(chalk.green('\nNo worktrees needed to be cleaned up.'));
802
+ console.log('');
803
+ return;
804
+ }
805
+
806
+ if (result.removedCount > 0) {
807
+ console.log(
808
+ chalk.green(`\nRemoved ${result.removedCount} worktree(s):`),
809
+ );
810
+ for (const removedPath of result.removedPaths) {
811
+ console.log(chalk.gray(` - ${removedPath}`));
812
+ }
813
+ }
814
+
815
+ if (result.skippedPaths.length > 0) {
816
+ console.log(
817
+ chalk.yellow(`\nSkipped ${result.skippedPaths.length} worktree(s):`),
818
+ );
819
+ for (const skippedPath of result.skippedPaths) {
820
+ const reason = result.skipReasons.get(skippedPath) || 'unknown';
821
+ console.log(chalk.gray(` - ${skippedPath}: ${reason}`));
822
+ }
823
+ }
824
+
825
+ if (result.errors.length > 0) {
826
+ console.log(chalk.red(`\nErrors (${result.errors.length}):`));
827
+ for (const error of result.errors) {
828
+ console.log(chalk.red(` - ${error.path}: ${error.message}`));
829
+ }
830
+ }
831
+
832
+ console.log('');
833
+ }
834
+ } catch (error) {
835
+ spinner.fail('Failed to cleanup worktrees');
836
+ console.error(
837
+ chalk.red(error instanceof Error ? error.message : String(error)),
838
+ );
839
+ }
840
+ }
841
+
842
+ async function syncWorktrees(
843
+ taskId: string | undefined,
844
+ options: { all?: boolean },
845
+ ): Promise<void> {
846
+ const spinner = ora(
847
+ taskId ? `Syncing worktree: ${taskId}...` : 'Syncing worktrees...',
848
+ ).start();
849
+
850
+ try {
851
+ const state = await loadWorktreeState();
852
+ const repoPath = state.repoPath || (await getGitRepoRoot());
853
+
854
+ const syncSingleWorktree = async (
855
+ worktree: WorktreeEntry,
856
+ ): Promise<SyncResult> => {
857
+ const timestamp = new Date();
858
+ let stashedChanges = false;
859
+
860
+ try {
861
+ // Check for uncommitted changes
862
+ const hasChanges = await hasUncommittedChanges(worktree.worktreePath);
863
+
864
+ if (hasChanges) {
865
+ // Stash changes
866
+ await executeGitCommand(
867
+ 'stash',
868
+ ['push', '-m', 'auto-stash before sync'],
869
+ worktree.worktreePath,
870
+ );
871
+ stashedChanges = true;
872
+ }
873
+
874
+ // Fetch from remote
875
+ await executeGitCommand('fetch', ['origin'], worktree.worktreePath);
876
+
877
+ // Get current branch
878
+ const branchName = await executeGitCommand(
879
+ 'rev-parse',
880
+ ['--abbrev-ref', 'HEAD'],
881
+ worktree.worktreePath,
882
+ );
883
+
884
+ // Try fast-forward pull
885
+ try {
886
+ await executeGitCommand(
887
+ 'pull',
888
+ ['--ff-only', 'origin', branchName],
889
+ worktree.worktreePath,
890
+ );
891
+ } catch {
892
+ // Fast-forward not possible, try rebase
893
+ await executeGitCommand(
894
+ 'rebase',
895
+ [`origin/${branchName}`],
896
+ worktree.worktreePath,
897
+ );
898
+ }
899
+
900
+ // Count commits updated (approximate)
901
+ let commitsUpdated = 0;
902
+ try {
903
+ const { stdout } = await execAsync(
904
+ `git rev-list --count origin/${branchName}..HEAD`,
905
+ { cwd: worktree.worktreePath, timeout: 10000 },
906
+ );
907
+ commitsUpdated = parseInt(stdout.trim(), 10) || 0;
908
+ } catch {
909
+ // Ignore count errors
910
+ }
911
+
912
+ // Restore stashed changes
913
+ if (stashedChanges) {
914
+ try {
915
+ await executeGitCommand('stash', ['pop'], worktree.worktreePath);
916
+ } catch {
917
+ // Stash pop might have conflicts
918
+ }
919
+ }
920
+
921
+ return {
922
+ success: true,
923
+ message: 'Successfully synced',
924
+ worktreePath: worktree.worktreePath,
925
+ branchName,
926
+ stashedChanges,
927
+ commitsUpdated,
928
+ timestamp,
929
+ };
930
+ } catch (error) {
931
+ // Try to restore stash on failure
932
+ if (stashedChanges) {
933
+ try {
934
+ await executeGitCommand('stash', ['pop'], worktree.worktreePath);
935
+ } catch {
936
+ // Ignore
937
+ }
938
+ }
939
+
940
+ return {
941
+ success: false,
942
+ message: error instanceof Error ? error.message : String(error),
943
+ worktreePath: worktree.worktreePath,
944
+ stashedChanges,
945
+ timestamp,
946
+ error: error instanceof Error ? error : new Error(String(error)),
947
+ };
948
+ }
949
+ };
950
+
951
+ if (taskId) {
952
+ // Sync specific worktree
953
+ const worktree = state.worktrees.find(wt => wt.taskId === taskId);
954
+ if (!worktree) {
955
+ spinner.fail(`Worktree not found: ${taskId}`);
956
+ return;
957
+ }
958
+
959
+ // Update status to syncing
960
+ worktree.status = 'syncing';
961
+ await saveWorktreeState(state);
962
+
963
+ const result = await syncSingleWorktree(worktree);
964
+
965
+ // Update status based on result
966
+ worktree.status = result.success ? 'active' : 'error';
967
+ worktree.lastAccessedAt = getTimestamp();
968
+ await saveWorktreeState(state);
969
+
970
+ spinner.stop();
971
+
972
+ if (result.success) {
973
+ console.log(chalk.green(`\nSuccessfully synced worktree: ${taskId}`));
974
+ console.log(
975
+ chalk.white(' Branch: ') +
976
+ chalk.blue(result.branchName || 'unknown'),
977
+ );
978
+ console.log(
979
+ chalk.white(' Commits: ') +
980
+ chalk.cyan(`${result.commitsUpdated || 0} updated`),
981
+ );
982
+ if (result.stashedChanges) {
983
+ console.log(
984
+ chalk.yellow(' Note: Local changes were stashed and restored.'),
985
+ );
986
+ }
987
+ } else {
988
+ console.log(chalk.red(`\nFailed to sync worktree: ${taskId}`));
989
+ console.log(chalk.red(` Error: ${result.message}`));
990
+ }
991
+ console.log('');
992
+ } else if (options.all || !taskId) {
993
+ // Sync all active worktrees
994
+ const activeWorktrees = state.worktrees.filter(
995
+ wt => wt.status === 'active' || wt.status === 'paused',
996
+ );
997
+
998
+ if (activeWorktrees.length === 0) {
999
+ spinner.stop();
1000
+ console.log(chalk.yellow('\nNo active worktrees to sync.'));
1001
+ console.log('');
1002
+ return;
1003
+ }
1004
+
1005
+ spinner.text = `Syncing ${activeWorktrees.length} worktree(s)...`;
1006
+
1007
+ const results: Array<{ taskId: string; result: SyncResult }> = [];
1008
+
1009
+ for (const wt of activeWorktrees) {
1010
+ wt.status = 'syncing';
1011
+ await saveWorktreeState(state);
1012
+
1013
+ const result = await syncSingleWorktree(wt);
1014
+ wt.status = result.success ? 'active' : 'error';
1015
+ wt.lastAccessedAt = getTimestamp();
1016
+ results.push({ taskId: wt.taskId, result });
1017
+ }
1018
+
1019
+ await saveWorktreeState(state);
1020
+ spinner.stop();
1021
+
1022
+ console.log(chalk.cyan('\nSync Results'));
1023
+ console.log(chalk.gray('='.repeat(80)));
1024
+
1025
+ const successful = results.filter(r => r.result.success);
1026
+ const failed = results.filter(r => !r.result.success);
1027
+
1028
+ if (successful.length > 0) {
1029
+ console.log(chalk.green(`\nSuccessfully synced: ${successful.length}`));
1030
+ for (const { taskId: tid, result } of successful) {
1031
+ console.log(
1032
+ chalk.gray(
1033
+ ` - ${tid}: ${result.commitsUpdated || 0} commit(s) updated`,
1034
+ ),
1035
+ );
1036
+ }
1037
+ }
1038
+
1039
+ if (failed.length > 0) {
1040
+ console.log(chalk.red(`\nFailed to sync: ${failed.length}`));
1041
+ for (const { taskId: tid, result } of failed) {
1042
+ console.log(chalk.red(` - ${tid}: ${result.message}`));
1043
+ }
1044
+ }
1045
+
1046
+ console.log('');
1047
+ }
1048
+ } catch (error) {
1049
+ spinner.fail('Failed to sync worktrees');
1050
+ console.error(
1051
+ chalk.red(error instanceof Error ? error.message : String(error)),
1052
+ );
1053
+ }
1054
+ }
1055
+
1056
+ // Export for registration
1057
+ export default createWorktreeCommand;