sync-worktrees 2.0.0 → 2.2.0

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/index.js CHANGED
@@ -1,9 +1,99 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import * as path8 from "path";
4
+ import * as path10 from "path";
5
5
  import { confirm as confirm2 } from "@inquirer/prompts";
6
6
  import * as cron2 from "node-cron";
7
+ import pLimit2 from "p-limit";
8
+
9
+ // src/constants.ts
10
+ var GIT_CONSTANTS = {
11
+ REMOTE_PREFIX: "origin/",
12
+ REMOTE_NAME: "origin",
13
+ HEAD_REF: "/HEAD",
14
+ DEFAULT_BRANCH: "main",
15
+ COMMON_DEFAULT_BRANCHES: ["main", "master", "develop", "trunk"],
16
+ BARE_DIR_NAME: ".bare",
17
+ DIVERGED_DIR_NAME: ".diverged",
18
+ LFS_HEADER: "version https://git-lfs.github.com/spec/",
19
+ SUBMODULE_STATUS_ADDED: "+",
20
+ SUBMODULE_STATUS_REMOVED: "-",
21
+ GITDIR_PREFIX: "gitdir:",
22
+ GIT_CHECK_IGNORE_NO_MATCH: "exit code: 1",
23
+ REFS: {
24
+ HEADS: "refs/heads/",
25
+ REMOTES: "refs/remotes/origin",
26
+ REMOTES_ORIGIN: "refs/remotes/origin/*"
27
+ },
28
+ FETCH_CONFIG: "+refs/heads/*:refs/remotes/origin/*"
29
+ };
30
+ var GIT_OPERATIONS = {
31
+ MERGE_HEAD: "MERGE_HEAD",
32
+ CHERRY_PICK_HEAD: "CHERRY_PICK_HEAD",
33
+ REVERT_HEAD: "REVERT_HEAD",
34
+ BISECT_LOG: "BISECT_LOG",
35
+ REBASE_MERGE: "rebase-merge",
36
+ REBASE_APPLY: "rebase-apply"
37
+ };
38
+ var DEFAULT_CONFIG = {
39
+ CRON_SCHEDULE: "0 * * * *",
40
+ RETRY: {
41
+ MAX_ATTEMPTS: 3,
42
+ MAX_LFS_RETRIES: 2,
43
+ INITIAL_DELAY_MS: 1e3,
44
+ MAX_DELAY_MS: 3e4,
45
+ BACKOFF_MULTIPLIER: 2,
46
+ JITTER_MS: 500
47
+ },
48
+ PARALLELISM: {
49
+ MAX_REPOSITORIES: 2,
50
+ MAX_WORKTREE_CREATION: 1,
51
+ MAX_WORKTREE_UPDATES: 3,
52
+ MAX_WORKTREE_REMOVAL: 3,
53
+ MAX_STATUS_CHECKS: 20,
54
+ MAX_SAFE_TOTAL_CONCURRENT_OPS: 100
55
+ },
56
+ UPDATE_EXISTING_WORKTREES: true
57
+ };
58
+ var ERROR_MESSAGES = {
59
+ GIT_NOT_INITIALIZED: "Git service not initialized. Call initialize() first.",
60
+ ALREADY_EXISTS: "already exists",
61
+ ALREADY_REGISTERED: "already registered worktree",
62
+ FAST_FORWARD_FAILED: [
63
+ "Not possible to fast-forward",
64
+ "fatal: Not possible to fast-forward, aborting",
65
+ "cannot fast-forward"
66
+ ],
67
+ NO_UPSTREAM: [
68
+ "fatal: no upstream configured",
69
+ "no upstream configured for branch",
70
+ "fatal: ambiguous argument",
71
+ "unknown revision or path"
72
+ ],
73
+ LFS_ERROR: ["smudge filter lfs failed", "git-lfs", "LFS"],
74
+ EXDEV: "EXDEV"
75
+ };
76
+ var ENV_CONSTANTS = {
77
+ GIT_LFS_SKIP_SMUDGE: "GIT_LFS_SKIP_SMUDGE",
78
+ NODE_ENV_TEST: "test"
79
+ };
80
+ var PATH_CONSTANTS = {
81
+ GIT_DIR: ".git",
82
+ README: "README"
83
+ };
84
+ var CONFIG_CONSTANTS = {
85
+ WILDCARD_PATTERN: ".*"
86
+ };
87
+ var METADATA_CONSTANTS = {
88
+ MAX_HISTORY_ENTRIES: 10,
89
+ METADATA_FILENAME: "sync-metadata.json",
90
+ WORKTREE_METADATA_PATH: ".git/worktrees",
91
+ DIVERGED_INFO_FILE: ".diverged-info.json",
92
+ DIVERGED_REASON: "diverged-history-with-changes",
93
+ ACTION_CREATED: "created",
94
+ ACTION_UPDATED: "updated",
95
+ ACTION_FETCHED: "fetched"
96
+ };
7
97
 
8
98
  // src/services/config-loader.service.ts
9
99
  import * as fs from "fs/promises";
@@ -73,6 +163,9 @@ var ConfigLoaderService = class {
73
163
  if (repoObj.runOnce !== void 0 && typeof repoObj.runOnce !== "boolean") {
74
164
  throw new Error(`Repository '${repoObj.name}' has invalid 'runOnce' property`);
75
165
  }
166
+ if (repoObj.filesToCopyOnBranchCreate !== void 0) {
167
+ this.validateFilesToCopyConfig(repoObj.filesToCopyOnBranchCreate, `Repository '${repoObj.name}'`);
168
+ }
76
169
  });
77
170
  if (configObj.defaults) {
78
171
  if (typeof configObj.defaults !== "object") {
@@ -88,6 +181,9 @@ var ConfigLoaderService = class {
88
181
  if (defaults.retry !== void 0 && typeof defaults.retry !== "object") {
89
182
  throw new Error("Invalid 'retry' in defaults");
90
183
  }
184
+ if (defaults.filesToCopyOnBranchCreate !== void 0) {
185
+ this.validateFilesToCopyConfig(defaults.filesToCopyOnBranchCreate, "defaults");
186
+ }
91
187
  }
92
188
  if (configObj.retry !== void 0) {
93
189
  if (typeof configObj.retry !== "object") {
@@ -114,13 +210,79 @@ var ConfigLoaderService = class {
114
210
  throw new Error("Invalid 'backoffMultiplier' in retry config");
115
211
  }
116
212
  }
213
+ if (configObj.parallelism !== void 0) {
214
+ this.validateParallelismConfig(configObj.parallelism, "global");
215
+ }
216
+ if (configObj.defaults && typeof configObj.defaults === "object") {
217
+ const defaults = configObj.defaults;
218
+ if (defaults.parallelism !== void 0) {
219
+ this.validateParallelismConfig(defaults.parallelism, "defaults");
220
+ }
221
+ }
222
+ }
223
+ validateParallelismConfig(parallelism, context) {
224
+ if (typeof parallelism !== "object" || parallelism === null) {
225
+ throw new Error(`'parallelism' in ${context} must be an object`);
226
+ }
227
+ const config = parallelism;
228
+ if (config.maxRepositories !== void 0) {
229
+ if (typeof config.maxRepositories !== "number" || config.maxRepositories < 1) {
230
+ throw new Error(`Invalid 'maxRepositories' in ${context} parallelism config. Must be a positive number`);
231
+ }
232
+ }
233
+ if (config.maxWorktreeCreation !== void 0) {
234
+ if (typeof config.maxWorktreeCreation !== "number" || config.maxWorktreeCreation < 1) {
235
+ throw new Error(`Invalid 'maxWorktreeCreation' in ${context} parallelism config. Must be a positive number`);
236
+ }
237
+ }
238
+ if (config.maxWorktreeUpdates !== void 0) {
239
+ if (typeof config.maxWorktreeUpdates !== "number" || config.maxWorktreeUpdates < 1) {
240
+ throw new Error(`Invalid 'maxWorktreeUpdates' in ${context} parallelism config. Must be a positive number`);
241
+ }
242
+ }
243
+ if (config.maxWorktreeRemoval !== void 0) {
244
+ if (typeof config.maxWorktreeRemoval !== "number" || config.maxWorktreeRemoval < 1) {
245
+ throw new Error(`Invalid 'maxWorktreeRemoval' in ${context} parallelism config. Must be a positive number`);
246
+ }
247
+ }
248
+ if (config.maxStatusChecks !== void 0) {
249
+ if (typeof config.maxStatusChecks !== "number" || config.maxStatusChecks < 1) {
250
+ throw new Error(`Invalid 'maxStatusChecks' in ${context} parallelism config. Must be a positive number`);
251
+ }
252
+ }
253
+ const maxRepos = config.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
254
+ const maxCreation = config.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
255
+ const maxUpdates = config.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES;
256
+ const maxRemoval = config.maxWorktreeRemoval ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_REMOVAL;
257
+ const maxStatus = config.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
258
+ const maxPerRepoOps = maxCreation + maxUpdates + maxRemoval + maxStatus;
259
+ const totalMaxConcurrent = maxRepos * maxPerRepoOps;
260
+ if (totalMaxConcurrent > DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS) {
261
+ const safeMaxRepos = Math.floor(DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS / maxPerRepoOps);
262
+ throw new Error(
263
+ `Total concurrent operations (${totalMaxConcurrent}) exceeds safe limit (${DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS}). With current per-repository limits (creation: ${maxCreation}, updates: ${maxUpdates}, removal: ${maxRemoval}, status: ${maxStatus}), maximum safe maxRepositories is ${safeMaxRepos}. Consider reducing maxRepositories or lowering per-operation limits.`
264
+ );
265
+ }
266
+ }
267
+ validateFilesToCopyConfig(filesToCopy, context) {
268
+ if (!Array.isArray(filesToCopy)) {
269
+ throw new Error(`'filesToCopyOnBranchCreate' in ${context} must be an array`);
270
+ }
271
+ for (let i = 0; i < filesToCopy.length; i++) {
272
+ const pattern = filesToCopy[i];
273
+ if (typeof pattern !== "string" || pattern.trim() === "") {
274
+ throw new Error(
275
+ `'filesToCopyOnBranchCreate' in ${context} must contain only non-empty strings (invalid at index ${i})`
276
+ );
277
+ }
278
+ }
117
279
  }
118
280
  resolveRepositoryConfig(repo, defaults, configDir, globalRetry) {
119
281
  const resolved = {
120
282
  name: repo.name,
121
283
  repoUrl: repo.repoUrl,
122
284
  worktreeDir: this.resolvePath(repo.worktreeDir, configDir),
123
- cronSchedule: repo.cronSchedule ?? defaults?.cronSchedule ?? "0 * * * *",
285
+ cronSchedule: repo.cronSchedule ?? defaults?.cronSchedule ?? DEFAULT_CONFIG.CRON_SCHEDULE,
124
286
  runOnce: repo.runOnce ?? defaults?.runOnce ?? false
125
287
  };
126
288
  if (repo.bareRepoDir) {
@@ -139,9 +301,18 @@ var ConfigLoaderService = class {
139
301
  ...repo.retry || {}
140
302
  };
141
303
  }
304
+ if (repo.parallelism || defaults?.parallelism) {
305
+ resolved.parallelism = {
306
+ ...defaults?.parallelism || {},
307
+ ...repo.parallelism || {}
308
+ };
309
+ }
142
310
  if (repo.updateExistingWorktrees !== void 0 || defaults?.updateExistingWorktrees !== void 0) {
143
311
  resolved.updateExistingWorktrees = repo.updateExistingWorktrees ?? defaults?.updateExistingWorktrees ?? true;
144
312
  }
313
+ if (repo.filesToCopyOnBranchCreate || defaults?.filesToCopyOnBranchCreate) {
314
+ resolved.filesToCopyOnBranchCreate = repo.filesToCopyOnBranchCreate ?? defaults?.filesToCopyOnBranchCreate;
315
+ }
145
316
  return resolved;
146
317
  }
147
318
  resolvePath(inputPath, baseDir) {
@@ -158,7 +329,7 @@ var ConfigLoaderService = class {
158
329
  return repositories.filter((repo) => {
159
330
  return patterns.some((pattern) => {
160
331
  if (pattern.includes("*")) {
161
- const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
332
+ const regex = new RegExp("^" + pattern.replace(/\*/g, CONFIG_CONSTANTS.WILDCARD_PATTERN) + "$");
162
333
  return regex.test(repo.name);
163
334
  }
164
335
  return repo.name === pattern;
@@ -168,13 +339,15 @@ var ConfigLoaderService = class {
168
339
  };
169
340
 
170
341
  // src/services/InteractiveUIService.tsx
171
- import React4 from "react";
342
+ import React7 from "react";
343
+ import * as path7 from "path";
172
344
  import { render } from "ink";
173
345
  import * as cron from "node-cron";
346
+ import { spawn } from "child_process";
174
347
 
175
348
  // src/components/App.tsx
176
- import React3, { useState as useState2, useEffect as useEffect2, useCallback } from "react";
177
- import { Box as Box3, useInput as useInput2 } from "ink";
349
+ import React6, { useState as useState5, useEffect as useEffect5, useCallback as useCallback3, useRef as useRef3 } from "react";
350
+ import { Box as Box6, useInput as useInput5, useStdout } from "ink";
178
351
 
179
352
  // src/components/StatusBar.tsx
180
353
  import React, { useState, useEffect } from "react";
@@ -210,7 +383,7 @@ var StatusBar = ({ status, repositoryCount, lastSyncTime, cronSchedule, diskSpac
210
383
  const getStatusIcon = () => {
211
384
  return status === "syncing" ? "\u27F3" : "\u2713";
212
385
  };
213
- return /* @__PURE__ */ React.createElement(Box, { borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, { bold: true }, getStatusIcon(), " Status:", " ", /* @__PURE__ */ React.createElement(Text, { color: getStatusColor() }, status === "syncing" ? "Syncing..." : "Running")), /* @__PURE__ */ React.createElement(Text, null, "Repositories: ", /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, repositoryCount))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Last Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(lastSyncTime))), cronSchedule && /* @__PURE__ */ React.createElement(Text, null, "Next Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(nextSyncTime)))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Disk Space: ", /* @__PURE__ */ React.createElement(Text, { color: "magenta" }, diskSpaceUsed || "Calculating...")))));
386
+ return /* @__PURE__ */ React.createElement(Box, { borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, { bold: true }, getStatusIcon(), " Status:", " ", /* @__PURE__ */ React.createElement(Text, { color: getStatusColor() }, status === "syncing" ? "Syncing..." : "Running")), /* @__PURE__ */ React.createElement(Text, null, "Repositories: ", /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, repositoryCount))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Last Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(lastSyncTime))), cronSchedule && /* @__PURE__ */ React.createElement(Text, null, "Next Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(nextSyncTime)))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Disk Space: ", /* @__PURE__ */ React.createElement(Text, { color: "magenta" }, diskSpaceUsed || "Calculating...")), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "s"), "ync", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "c"), "reate", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "o"), "pen", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "r"), "eload", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "?"), "help", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "q"), "uit"))));
214
387
  };
215
388
  var StatusBar_default = StatusBar;
216
389
 
@@ -221,27 +394,632 @@ var HelpModal = ({ onClose }) => {
221
394
  useInput(() => {
222
395
  onClose();
223
396
  });
224
- return /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", alignItems: "center", flexDirection: "column", marginTop: 2, marginBottom: 2 }, /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u{1F333} sync-worktrees - Keyboard Shortcuts")), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "?"), /* @__PURE__ */ React2.createElement(Text2, null, " or "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "h")), /* @__PURE__ */ React2.createElement(Text2, null, "Toggle this help screen")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "s")), /* @__PURE__ */ React2.createElement(Text2, null, "Manually trigger sync for all repositories")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "r")), /* @__PURE__ */ React2.createElement(Text2, null, "Reload configuration and re-sync all repos")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "q"), /* @__PURE__ */ React2.createElement(Text2, null, " or "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "Esc")), /* @__PURE__ */ React2.createElement(Text2, null, "Gracefully quit"))), /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Press any key to close"))));
397
+ return /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", alignItems: "center", flexDirection: "column", marginTop: 2, marginBottom: 2 }, /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u{1F333} sync-worktrees - Keyboard Shortcuts")), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green", dimColor: true }, "Navigation"), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "j"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u2193")), /* @__PURE__ */ React2.createElement(Text2, null, "Scroll down one line")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "k"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u2191")), /* @__PURE__ */ React2.createElement(Text2, null, "Scroll up one line")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "gg")), /* @__PURE__ */ React2.createElement(Text2, null, "Jump to top")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "G")), /* @__PURE__ */ React2.createElement(Text2, null, "Jump to bottom (re-enables auto-scroll)")), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green", dimColor: true }, "Actions")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "s")), /* @__PURE__ */ React2.createElement(Text2, null, "Manually trigger sync for all repositories")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "c")), /* @__PURE__ */ React2.createElement(Text2, null, "Create a new branch")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "o")), /* @__PURE__ */ React2.createElement(Text2, null, "Open editor in worktree")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "r")), /* @__PURE__ */ React2.createElement(Text2, null, "Reload configuration and re-sync all repos")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "?"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "h")), /* @__PURE__ */ React2.createElement(Text2, null, "Toggle this help screen")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "q"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "Esc")), /* @__PURE__ */ React2.createElement(Text2, null, "Gracefully quit"))), /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Press any key to close"))));
225
398
  };
226
399
  var HelpModal_default = HelpModal;
227
400
 
228
- // src/components/App.tsx
229
- var App = ({ repositoryCount, cronSchedule, onManualSync, onReload, onQuit }) => {
230
- const [showHelp, setShowHelp] = useState2(false);
231
- const [status, setStatus] = useState2("idle");
232
- const [lastSyncTime, setLastSyncTime] = useState2(null);
233
- const [diskSpaceUsed, setDiskSpaceUsed] = useState2(null);
401
+ // src/components/BranchCreationWizard.tsx
402
+ import React3, { useState as useState2, useEffect as useEffect2, useCallback } from "react";
403
+ import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
404
+ var isValidGitBranchName = (name) => {
405
+ if (!name.trim()) {
406
+ return { valid: false, error: "Branch name cannot be empty" };
407
+ }
408
+ if (name.startsWith("-")) {
409
+ return { valid: false, error: "Branch name cannot start with '-'" };
410
+ }
411
+ if (name.endsWith(".lock")) {
412
+ return { valid: false, error: "Branch name cannot end with '.lock'" };
413
+ }
414
+ if (name.includes("..")) {
415
+ return { valid: false, error: "Branch name cannot contain '..'" };
416
+ }
417
+ if (name.includes("@{")) {
418
+ return { valid: false, error: "Branch name cannot contain '@{'" };
419
+ }
420
+ if (name.startsWith(".") || name.endsWith(".")) {
421
+ return { valid: false, error: "Branch name cannot start or end with '.'" };
422
+ }
423
+ if (name.includes("//")) {
424
+ return { valid: false, error: "Branch name cannot contain consecutive slashes" };
425
+ }
426
+ if (/[\x00-\x1f\x7f~^:?*\[\\]/.test(name)) {
427
+ return { valid: false, error: "Branch name contains invalid characters" };
428
+ }
429
+ return { valid: true };
430
+ };
431
+ var BranchCreationWizard = ({
432
+ repositories,
433
+ getBranchesForRepo,
434
+ getDefaultBranchForRepo,
435
+ createAndPushBranch,
436
+ onClose,
437
+ onComplete
438
+ }) => {
439
+ const [step, setStep] = useState2(repositories.length > 1 ? "SELECT_PROJECT" : "SELECT_BRANCH");
440
+ const [selectedProjectIndex, setSelectedProjectIndex] = useState2(repositories.length === 1 ? 0 : 0);
441
+ const [branches, setBranches] = useState2([]);
442
+ const [defaultBranch, setDefaultBranch] = useState2("");
443
+ const [selectedBranchIndex, setSelectedBranchIndex] = useState2(0);
444
+ const [branchName, setBranchName] = useState2("");
445
+ const [existingSuffix, setExistingSuffix] = useState2(null);
446
+ const [validationError, setValidationError] = useState2(null);
447
+ const [result, setResult] = useState2(null);
448
+ const [loading, setLoading] = useState2(false);
449
+ const loadBranches = useCallback(
450
+ async (repoIndex) => {
451
+ setLoading(true);
452
+ try {
453
+ const branchList = await getBranchesForRepo(repoIndex);
454
+ const defaultBr = getDefaultBranchForRepo(repoIndex);
455
+ setBranches(branchList);
456
+ setDefaultBranch(defaultBr);
457
+ const defaultIndex = branchList.indexOf(defaultBr);
458
+ setSelectedBranchIndex(defaultIndex >= 0 ? defaultIndex : 0);
459
+ } catch {
460
+ setBranches([]);
461
+ }
462
+ setLoading(false);
463
+ },
464
+ [getBranchesForRepo, getDefaultBranchForRepo]
465
+ );
466
+ const checkBranchExists = useCallback(
467
+ (name) => {
468
+ if (!name.trim()) {
469
+ setExistingSuffix(null);
470
+ setValidationError(null);
471
+ return;
472
+ }
473
+ const validation = isValidGitBranchName(name);
474
+ if (!validation.valid) {
475
+ setValidationError(validation.error ?? null);
476
+ setExistingSuffix(null);
477
+ return;
478
+ }
479
+ setValidationError(null);
480
+ let suffix = 0;
481
+ let testName = name;
482
+ while (branches.includes(testName)) {
483
+ suffix++;
484
+ testName = `${name}-${suffix}`;
485
+ }
486
+ setExistingSuffix(suffix > 0 ? suffix : null);
487
+ },
488
+ [branches]
489
+ );
490
+ useEffect2(() => {
491
+ if (step === "SELECT_BRANCH" && branches.length === 0 && !loading) {
492
+ loadBranches(selectedProjectIndex);
493
+ }
494
+ }, [step, selectedProjectIndex, branches.length, loading, loadBranches]);
495
+ useEffect2(() => {
496
+ if (step === "ENTER_NAME") {
497
+ checkBranchExists(branchName);
498
+ }
499
+ }, [branchName, step, checkBranchExists]);
500
+ const handleCreateBranch = async () => {
501
+ const trimmedName = branchName.trim();
502
+ if (!trimmedName) return;
503
+ const validation = isValidGitBranchName(trimmedName);
504
+ if (!validation.valid) {
505
+ setValidationError(validation.error ?? null);
506
+ return;
507
+ }
508
+ setStep("CREATING");
509
+ const baseBranch = branches[selectedBranchIndex];
510
+ const createResult = await createAndPushBranch(selectedProjectIndex, baseBranch, trimmedName);
511
+ setResult(createResult);
512
+ setStep("RESULT");
513
+ };
234
514
  useInput2((input2, key) => {
515
+ if (step === "CREATING") return;
516
+ if (key.escape) {
517
+ if (step === "SELECT_PROJECT") {
518
+ onClose();
519
+ } else if (step === "SELECT_BRANCH") {
520
+ if (repositories.length > 1) {
521
+ setBranches([]);
522
+ setStep("SELECT_PROJECT");
523
+ } else {
524
+ onClose();
525
+ }
526
+ } else if (step === "ENTER_NAME") {
527
+ setBranchName("");
528
+ setExistingSuffix(null);
529
+ setStep("SELECT_BRANCH");
530
+ } else if (step === "RESULT") {
531
+ const context = result?.success ? {
532
+ repoIndex: selectedProjectIndex,
533
+ baseBranch: branches[selectedBranchIndex],
534
+ newBranch: result.finalName
535
+ } : void 0;
536
+ onComplete(result?.success ?? false, context);
537
+ }
538
+ return;
539
+ }
540
+ if (step === "SELECT_PROJECT") {
541
+ if (key.upArrow) {
542
+ setSelectedProjectIndex((prev) => Math.max(0, prev - 1));
543
+ } else if (key.downArrow) {
544
+ setSelectedProjectIndex((prev) => Math.min(repositories.length - 1, prev + 1));
545
+ } else if (key.return) {
546
+ loadBranches(selectedProjectIndex);
547
+ setStep("SELECT_BRANCH");
548
+ }
549
+ } else if (step === "SELECT_BRANCH") {
550
+ if (key.upArrow) {
551
+ setSelectedBranchIndex((prev) => Math.max(0, prev - 1));
552
+ } else if (key.downArrow) {
553
+ setSelectedBranchIndex((prev) => Math.min(branches.length - 1, prev + 1));
554
+ } else if (key.return && branches.length > 0) {
555
+ setStep("ENTER_NAME");
556
+ }
557
+ } else if (step === "ENTER_NAME") {
558
+ if (key.return && branchName.trim()) {
559
+ void handleCreateBranch();
560
+ } else if (key.backspace || key.delete) {
561
+ setBranchName((prev) => prev.slice(0, -1));
562
+ } else if (input2 && !key.ctrl && !key.meta) {
563
+ const validChar = /^[a-zA-Z0-9/_-]$/.test(input2);
564
+ if (validChar) {
565
+ setBranchName((prev) => prev + input2);
566
+ }
567
+ }
568
+ } else if (step === "RESULT") {
569
+ const context = result?.success ? {
570
+ repoIndex: selectedProjectIndex,
571
+ baseBranch: branches[selectedBranchIndex],
572
+ newBranch: result.finalName
573
+ } : void 0;
574
+ onComplete(result?.success ?? false, context);
575
+ }
576
+ });
577
+ const getStepNumber = () => {
578
+ if (repositories.length === 1) {
579
+ if (step === "SELECT_BRANCH") return 1;
580
+ if (step === "ENTER_NAME") return 2;
581
+ return 2;
582
+ }
583
+ if (step === "SELECT_PROJECT") return 1;
584
+ if (step === "SELECT_BRANCH") return 2;
585
+ if (step === "ENTER_NAME") return 3;
586
+ return 3;
587
+ };
588
+ const getTotalSteps = () => repositories.length === 1 ? 2 : 3;
589
+ const renderProjectSelection = () => /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Select repository:"), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, repositories.map((repo, idx) => /* @__PURE__ */ React3.createElement(Box3, { key: repo.index }, /* @__PURE__ */ React3.createElement(Text3, { color: idx === selectedProjectIndex ? "cyan" : void 0 }, idx === selectedProjectIndex ? "> " : " ", repo.name)))));
590
+ const renderBranchSelection = () => {
591
+ if (loading) {
592
+ return /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Loading branches...");
593
+ }
594
+ if (branches.length === 0) {
595
+ return /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, "No branches found");
596
+ }
597
+ const visibleCount = 8;
598
+ const halfVisible = Math.floor(visibleCount / 2);
599
+ let startIdx = Math.max(0, selectedBranchIndex - halfVisible);
600
+ const endIdx = Math.min(branches.length, startIdx + visibleCount);
601
+ if (endIdx - startIdx < visibleCount) {
602
+ startIdx = Math.max(0, endIdx - visibleCount);
603
+ }
604
+ const visibleBranches = branches.slice(startIdx, endIdx);
605
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Select base branch:"), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, startIdx > 0 && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ..."), visibleBranches.map((branch, idx) => {
606
+ const actualIdx = startIdx + idx;
607
+ const isSelected = actualIdx === selectedBranchIndex;
608
+ const isDefault = branch === defaultBranch;
609
+ return /* @__PURE__ */ React3.createElement(Box3, { key: branch }, /* @__PURE__ */ React3.createElement(Text3, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", branch, isDefault && /* @__PURE__ */ React3.createElement(Text3, { color: "green" }, " (default)")));
610
+ }), endIdx < branches.length && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ...")));
611
+ };
612
+ const renderNameInput = () => {
613
+ const baseBranch = branches[selectedBranchIndex] || "";
614
+ const finalName = existingSuffix !== null ? `${branchName}-${existingSuffix}` : branchName;
615
+ const endsWithSlash = branchName.endsWith("/");
616
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Base branch: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, baseBranch)), /* @__PURE__ */ React3.createElement(Text3, null, "Enter new branch name:"), /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, "> "), /* @__PURE__ */ React3.createElement(Text3, null, branchName), /* @__PURE__ */ React3.createElement(Text3, { color: "gray" }, "|")), validationError && /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, validationError), !validationError && endsWithSlash && /* @__PURE__ */ React3.createElement(Text3, { color: "yellow", dimColor: true }, "Hint: consecutive slashes (//) are not allowed"), !validationError && !endsWithSlash && existingSuffix !== null && branchName && /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Name exists, will create: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, finalName)));
617
+ };
618
+ const renderCreating = () => /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Creating branch..."), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Please wait while the branch is created and pushed to remote."));
619
+ const renderResult = () => {
620
+ if (!result) return null;
621
+ if (result.success) {
622
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "green" }, "Branch created successfully!"), /* @__PURE__ */ React3.createElement(Text3, null, "Created: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, result.finalName)), /* @__PURE__ */ React3.createElement(Text3, null, "From: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, branches[selectedBranchIndex])), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Syncing now to create the worktree..."));
623
+ }
624
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, "Failed to create branch"), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, result.error));
625
+ };
626
+ const renderContent = () => {
627
+ switch (step) {
628
+ case "SELECT_PROJECT":
629
+ return renderProjectSelection();
630
+ case "SELECT_BRANCH":
631
+ return renderBranchSelection();
632
+ case "ENTER_NAME":
633
+ return renderNameInput();
634
+ case "CREATING":
635
+ return renderCreating();
636
+ case "RESULT":
637
+ return renderResult();
638
+ }
639
+ };
640
+ const renderFooter = () => {
641
+ if (step === "CREATING") return null;
642
+ if (step === "RESULT") {
643
+ return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Press any key to continue");
644
+ }
645
+ if (step === "ENTER_NAME") {
646
+ return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Enter to create \u2022 ESC to go back");
647
+ }
648
+ return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u2191/\u2193 to navigate \u2022 Enter to select \u2022 ESC to cancel");
649
+ };
650
+ return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Box3, { borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "\u{1F33F} Create New Branch", " ", step !== "CREATING" && step !== "RESULT" && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), repositories.length > 1 && step !== "SELECT_PROJECT" && step !== "CREATING" && step !== "RESULT" && /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Repository: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, repositories[selectedProjectIndex].name))), renderContent(), /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, renderFooter())));
651
+ };
652
+ var BranchCreationWizard_default = BranchCreationWizard;
653
+
654
+ // src/components/OpenEditorWizard.tsx
655
+ import React4, { useState as useState3, useEffect as useEffect3, useMemo, useCallback as useCallback2, useRef } from "react";
656
+ import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
657
+ var OpenEditorWizard = ({
658
+ repositories,
659
+ getWorktreesForRepo,
660
+ openEditorInWorktree,
661
+ onClose
662
+ }) => {
663
+ const [step, setStep] = useState3(repositories.length > 1 ? "SELECT_PROJECT" : "SELECT_WORKTREE");
664
+ const [selectedProjectIndex, setSelectedProjectIndex] = useState3(0);
665
+ const [projectFilter, setProjectFilter] = useState3("");
666
+ const selectedRepoIndexRef = useRef(repositories.length === 1 ? 0 : -1);
667
+ const [worktrees, setWorktrees] = useState3([]);
668
+ const [selectedWorktreeIndex, setSelectedWorktreeIndex] = useState3(0);
669
+ const [worktreeFilter, setWorktreeFilter] = useState3("");
670
+ const [loading, setLoading] = useState3(false);
671
+ const [error, setError] = useState3(null);
672
+ const filteredProjects = useMemo(() => {
673
+ if (!projectFilter) return repositories;
674
+ const lowerFilter = projectFilter.toLowerCase();
675
+ return repositories.filter((repo) => repo.name.toLowerCase().includes(lowerFilter));
676
+ }, [repositories, projectFilter]);
677
+ const filteredWorktrees = useMemo(() => {
678
+ if (!worktreeFilter) return worktrees;
679
+ const lowerFilter = worktreeFilter.toLowerCase();
680
+ return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerFilter));
681
+ }, [worktrees, worktreeFilter]);
682
+ const loadWorktrees = useCallback2(
683
+ async (repoIndex) => {
684
+ setLoading(true);
685
+ try {
686
+ const wts = await getWorktreesForRepo(repoIndex);
687
+ setWorktrees(wts);
688
+ setSelectedWorktreeIndex(0);
689
+ } catch (err) {
690
+ setError(`Failed to load worktrees: ${err}`);
691
+ setStep("ERROR");
692
+ }
693
+ setLoading(false);
694
+ },
695
+ [getWorktreesForRepo]
696
+ );
697
+ useEffect3(() => {
698
+ if (step === "SELECT_WORKTREE" && worktrees.length === 0 && !loading && selectedRepoIndexRef.current >= 0) {
699
+ loadWorktrees(selectedRepoIndexRef.current);
700
+ }
701
+ }, [step, worktrees.length, loading, loadWorktrees]);
702
+ const handleOpenEditor = () => {
703
+ const worktree = filteredWorktrees[selectedWorktreeIndex];
704
+ if (!worktree) return;
705
+ setStep("OPENING");
706
+ const result = openEditorInWorktree(worktree.path);
707
+ if (result.success) {
708
+ onClose();
709
+ } else {
710
+ setError(result.error || "Failed to open editor");
711
+ setStep("ERROR");
712
+ }
713
+ };
714
+ useInput3((input2, key) => {
715
+ if (step === "OPENING") return;
716
+ if (key.escape) {
717
+ if (step === "SELECT_PROJECT") {
718
+ onClose();
719
+ } else if (step === "SELECT_WORKTREE") {
720
+ if (repositories.length > 1) {
721
+ setWorktrees([]);
722
+ setWorktreeFilter("");
723
+ selectedRepoIndexRef.current = -1;
724
+ setStep("SELECT_PROJECT");
725
+ } else {
726
+ onClose();
727
+ }
728
+ } else if (step === "ERROR") {
729
+ onClose();
730
+ }
731
+ return;
732
+ }
733
+ if (step === "SELECT_PROJECT") {
734
+ if (key.upArrow) {
735
+ setSelectedProjectIndex((prev) => Math.max(0, prev - 1));
736
+ } else if (key.downArrow) {
737
+ setSelectedProjectIndex((prev) => Math.min(filteredProjects.length - 1, prev + 1));
738
+ } else if (key.return && filteredProjects.length > 0) {
739
+ const selectedRepo = filteredProjects[selectedProjectIndex];
740
+ if (selectedRepo) {
741
+ selectedRepoIndexRef.current = selectedRepo.index;
742
+ setStep("SELECT_WORKTREE");
743
+ loadWorktrees(selectedRepo.index);
744
+ }
745
+ } else if (key.backspace || key.delete) {
746
+ setProjectFilter((prev) => prev.slice(0, -1));
747
+ setSelectedProjectIndex(0);
748
+ } else if (input2 && !key.ctrl && !key.meta) {
749
+ setProjectFilter((prev) => prev + input2);
750
+ setSelectedProjectIndex(0);
751
+ }
752
+ } else if (step === "SELECT_WORKTREE") {
753
+ if (key.upArrow) {
754
+ setSelectedWorktreeIndex((prev) => Math.max(0, prev - 1));
755
+ } else if (key.downArrow) {
756
+ setSelectedWorktreeIndex((prev) => Math.min(filteredWorktrees.length - 1, prev + 1));
757
+ } else if (key.return && filteredWorktrees.length > 0) {
758
+ handleOpenEditor();
759
+ } else if (key.backspace || key.delete) {
760
+ setWorktreeFilter((prev) => prev.slice(0, -1));
761
+ setSelectedWorktreeIndex(0);
762
+ } else if (input2 && !key.ctrl && !key.meta) {
763
+ setWorktreeFilter((prev) => prev + input2);
764
+ setSelectedWorktreeIndex(0);
765
+ }
766
+ } else if (step === "ERROR") {
767
+ onClose();
768
+ }
769
+ });
770
+ const getStepNumber = () => {
771
+ if (repositories.length === 1) {
772
+ return 1;
773
+ }
774
+ return step === "SELECT_PROJECT" ? 1 : 2;
775
+ };
776
+ const getTotalSteps = () => repositories.length === 1 ? 1 : 2;
777
+ const renderProjectSelection = () => {
778
+ const visibleCount = 8;
779
+ const halfVisible = Math.floor(visibleCount / 2);
780
+ let startIdx = Math.max(0, selectedProjectIndex - halfVisible);
781
+ const endIdx = Math.min(filteredProjects.length, startIdx + visibleCount);
782
+ if (endIdx - startIdx < visibleCount) {
783
+ startIdx = Math.max(0, endIdx - visibleCount);
784
+ }
785
+ const visibleProjects = filteredProjects.slice(startIdx, endIdx);
786
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Select repository:"), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, null, "Filter: "), /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, projectFilter || "_"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ", "(", filteredProjects.length, "/", repositories.length, " matches)")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, filteredProjects.length === 0 ? /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "No matches") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, startIdx > 0 && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."), visibleProjects.map((repo, idx) => {
787
+ const actualIdx = startIdx + idx;
788
+ const isSelected = actualIdx === selectedProjectIndex;
789
+ return /* @__PURE__ */ React4.createElement(Box4, { key: repo.index }, /* @__PURE__ */ React4.createElement(Text4, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", repo.name));
790
+ }), endIdx < filteredProjects.length && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."))));
791
+ };
792
+ const renderWorktreeSelection = () => {
793
+ if (loading) {
794
+ return /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "Loading worktrees...");
795
+ }
796
+ if (worktrees.length === 0) {
797
+ return /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, "No worktrees found");
798
+ }
799
+ const visibleCount = 8;
800
+ const halfVisible = Math.floor(visibleCount / 2);
801
+ let startIdx = Math.max(0, selectedWorktreeIndex - halfVisible);
802
+ const endIdx = Math.min(filteredWorktrees.length, startIdx + visibleCount);
803
+ if (endIdx - startIdx < visibleCount) {
804
+ startIdx = Math.max(0, endIdx - visibleCount);
805
+ }
806
+ const visibleWorktrees = filteredWorktrees.slice(startIdx, endIdx);
807
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Select worktree:"), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, null, "Filter: "), /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, worktreeFilter || "_"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ", "(", filteredWorktrees.length, "/", worktrees.length, " matches)")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, filteredWorktrees.length === 0 ? /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "No matches") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, startIdx > 0 && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."), visibleWorktrees.map((wt, idx) => {
808
+ const actualIdx = startIdx + idx;
809
+ const isSelected = actualIdx === selectedWorktreeIndex;
810
+ return /* @__PURE__ */ React4.createElement(Box4, { key: wt.path }, /* @__PURE__ */ React4.createElement(Text4, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", wt.branch));
811
+ }), endIdx < filteredWorktrees.length && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."))));
812
+ };
813
+ const renderOpening = () => /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "Opening editor..."));
814
+ const renderError = () => /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, "Error: ", error), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Press any key to close"));
815
+ const renderContent = () => {
816
+ switch (step) {
817
+ case "SELECT_PROJECT":
818
+ return renderProjectSelection();
819
+ case "SELECT_WORKTREE":
820
+ return renderWorktreeSelection();
821
+ case "OPENING":
822
+ return renderOpening();
823
+ case "ERROR":
824
+ return renderError();
825
+ }
826
+ };
827
+ const renderFooter = () => {
828
+ if (step === "OPENING") return null;
829
+ if (step === "ERROR") return null;
830
+ return /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "\u2191/\u2193 navigate \u2022 Type to filter \u2022 Enter to select \u2022 ESC to cancel");
831
+ };
832
+ return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "blue", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "blue" }, "\u{1F4C2} Open in Editor", " ", step !== "OPENING" && step !== "ERROR" && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), repositories.length > 1 && step === "SELECT_WORKTREE" && !loading && selectedRepoIndexRef.current >= 0 && /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Repository: ", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, repositories.find((r) => r.index === selectedRepoIndexRef.current)?.name))), renderContent(), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1 }, renderFooter())));
833
+ };
834
+ var OpenEditorWizard_default = OpenEditorWizard;
835
+
836
+ // src/components/LogPanel.tsx
837
+ import React5, { useState as useState4, useEffect as useEffect4, useRef as useRef2 } from "react";
838
+ import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
839
+ var LogPanel = ({ logs, height, isActive }) => {
840
+ const [scrollOffset, setScrollOffset] = useState4(0);
841
+ const [autoScroll, setAutoScroll] = useState4(true);
842
+ const [pendingG, setPendingG] = useState4(false);
843
+ const gTimeoutRef = useRef2(null);
844
+ const borderLines = 2;
845
+ const headerLine = 1;
846
+ const visibleLines = Math.max(1, height - borderLines - headerLine);
847
+ const maxOffset = Math.max(0, logs.length - visibleLines);
848
+ useEffect4(() => {
849
+ if (autoScroll) {
850
+ setScrollOffset(maxOffset);
851
+ }
852
+ }, [logs.length, maxOffset, autoScroll]);
853
+ useEffect4(() => {
854
+ return () => {
855
+ if (gTimeoutRef.current) {
856
+ clearTimeout(gTimeoutRef.current);
857
+ }
858
+ };
859
+ }, []);
860
+ useInput4(
861
+ (input2, key) => {
862
+ if (!isActive) return;
863
+ if (key.upArrow || input2 === "k") {
864
+ setScrollOffset((prev) => Math.max(0, prev - 1));
865
+ setAutoScroll(false);
866
+ setPendingG(false);
867
+ } else if (key.downArrow || input2 === "j") {
868
+ setScrollOffset((prev) => {
869
+ const newOffset = Math.min(maxOffset, prev + 1);
870
+ if (newOffset >= maxOffset) {
871
+ setAutoScroll(true);
872
+ }
873
+ return newOffset;
874
+ });
875
+ setPendingG(false);
876
+ } else if (key.pageUp) {
877
+ setScrollOffset((prev) => Math.max(0, prev - visibleLines));
878
+ setAutoScroll(false);
879
+ setPendingG(false);
880
+ } else if (key.pageDown) {
881
+ setScrollOffset((prev) => {
882
+ const newOffset = Math.min(maxOffset, prev + visibleLines);
883
+ if (newOffset >= maxOffset) {
884
+ setAutoScroll(true);
885
+ }
886
+ return newOffset;
887
+ });
888
+ setPendingG(false);
889
+ } else if (input2 === "g") {
890
+ if (pendingG) {
891
+ setScrollOffset(0);
892
+ setAutoScroll(false);
893
+ setPendingG(false);
894
+ if (gTimeoutRef.current) {
895
+ clearTimeout(gTimeoutRef.current);
896
+ gTimeoutRef.current = null;
897
+ }
898
+ } else {
899
+ setPendingG(true);
900
+ gTimeoutRef.current = setTimeout(() => {
901
+ setPendingG(false);
902
+ }, 500);
903
+ }
904
+ } else if (input2 === "G") {
905
+ setScrollOffset(maxOffset);
906
+ setAutoScroll(true);
907
+ setPendingG(false);
908
+ }
909
+ },
910
+ { isActive }
911
+ );
912
+ const getLogColor = (level) => {
913
+ switch (level) {
914
+ case "error":
915
+ return "red";
916
+ case "warn":
917
+ return "yellow";
918
+ default:
919
+ return void 0;
920
+ }
921
+ };
922
+ const visibleLogs = logs.slice(scrollOffset, scrollOffset + visibleLines);
923
+ const hasMoreAbove = scrollOffset > 0;
924
+ const hasMoreBelow = scrollOffset + visibleLines < logs.length;
925
+ const aboveCount = scrollOffset;
926
+ const belowCount = logs.length - scrollOffset - visibleLines;
927
+ const emptyLines = Math.max(0, visibleLines - visibleLogs.length);
928
+ return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "single", flexDirection: "column", flexGrow: 1, paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, { bold: true }, "\u{1F4CB} Logs ", logs.length > 0 && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(", logs.length, " entries)")), isActive && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, hasMoreAbove || hasMoreBelow ? "\u2191/\u2193 scroll" : "", " ", autoScroll ? "(auto)" : "")), hasMoreAbove && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2191 ", aboveCount, " more above"), visibleLogs.map((log) => /* @__PURE__ */ React5.createElement(Text5, { key: log.id, color: getLogColor(log.level), wrap: "truncate" }, log.message)), Array.from({ length: emptyLines }).map((_, i) => /* @__PURE__ */ React5.createElement(Text5, { key: `empty-${i}` }, " ")), hasMoreBelow && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2193 ", belowCount, " more below"));
929
+ };
930
+ var LogPanel_default = LogPanel;
931
+
932
+ // src/utils/app-events.ts
933
+ var AppEventEmitter = class {
934
+ listeners = /* @__PURE__ */ new Map();
935
+ on(event, callback) {
936
+ if (!this.listeners.has(event)) {
937
+ this.listeners.set(event, /* @__PURE__ */ new Set());
938
+ }
939
+ this.listeners.get(event).add(callback);
940
+ return () => {
941
+ this.listeners.get(event)?.delete(callback);
942
+ };
943
+ }
944
+ emit(event, ...args) {
945
+ const callbacks = this.listeners.get(event);
946
+ if (callbacks) {
947
+ for (const callback of callbacks) {
948
+ try {
949
+ callback(args[0]);
950
+ } catch {
951
+ }
952
+ }
953
+ }
954
+ }
955
+ removeAllListeners() {
956
+ this.listeners.clear();
957
+ }
958
+ };
959
+ var appEvents = new AppEventEmitter();
960
+
961
+ // src/components/App.tsx
962
+ var MAX_LOG_ENTRIES = 5e3;
963
+ var App = ({
964
+ repositoryCount,
965
+ cronSchedule,
966
+ onManualSync,
967
+ onReload,
968
+ onQuit,
969
+ getRepositoryList,
970
+ getBranchesForRepo,
971
+ getDefaultBranchForRepo,
972
+ createAndPushBranch,
973
+ getWorktreesForRepo,
974
+ openEditorInWorktree,
975
+ copyBranchFiles,
976
+ createWorktreeForBranch
977
+ }) => {
978
+ const [showHelp, setShowHelp] = useState5(false);
979
+ const [showBranchWizard, setShowBranchWizard] = useState5(false);
980
+ const [showOpenEditorWizard, setShowOpenEditorWizard] = useState5(false);
981
+ const [status, setStatus] = useState5("idle");
982
+ const [lastSyncTime, setLastSyncTime] = useState5(null);
983
+ const [diskSpaceUsed, setDiskSpaceUsed] = useState5(null);
984
+ const [logs, setLogs] = useState5([]);
985
+ const { stdout } = useStdout();
986
+ const addLog = useCallback3((message, level = "info") => {
987
+ setLogs((prev) => {
988
+ const newLogs = [
989
+ ...prev,
990
+ {
991
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
992
+ message,
993
+ level,
994
+ timestamp: /* @__PURE__ */ new Date()
995
+ }
996
+ ];
997
+ if (newLogs.length > MAX_LOG_ENTRIES) {
998
+ return newLogs.slice(-MAX_LOG_ENTRIES);
999
+ }
1000
+ return newLogs;
1001
+ });
1002
+ }, []);
1003
+ const addLogRef = useRef3(addLog);
1004
+ addLogRef.current = addLog;
1005
+ useInput5((input2) => {
235
1006
  if (showHelp) {
236
1007
  if (input2 === "?" || input2 === "h") {
237
1008
  setShowHelp(false);
238
1009
  }
239
1010
  return;
240
1011
  }
241
- if (key.escape || input2 === "q") {
1012
+ if (showBranchWizard || showOpenEditorWizard) {
1013
+ return;
1014
+ }
1015
+ if (input2 === "q") {
242
1016
  void onQuit();
243
1017
  } else if (input2 === "?" || input2 === "h") {
244
1018
  setShowHelp(true);
1019
+ } else if (input2 === "c" && status === "idle") {
1020
+ setShowBranchWizard(true);
1021
+ } else if (input2 === "o" && status === "idle") {
1022
+ setShowOpenEditorWizard(true);
245
1023
  } else if (input2 === "s" && status !== "syncing") {
246
1024
  setStatus("syncing");
247
1025
  void (async () => {
@@ -264,21 +1042,71 @@ var App = ({ repositoryCount, cronSchedule, onManualSync, onReload, onQuit }) =>
264
1042
  })();
265
1043
  }
266
1044
  });
267
- const updateLastSyncTime = useCallback(() => {
1045
+ const updateLastSyncTime = useCallback3(() => {
268
1046
  setLastSyncTime(/* @__PURE__ */ new Date());
269
1047
  setStatus("idle");
270
1048
  }, []);
271
- useEffect2(() => {
272
- globalThis.__inkAppMethods = {
273
- updateLastSyncTime,
274
- setStatus,
275
- setDiskSpace: setDiskSpaceUsed
276
- };
1049
+ useEffect5(() => {
1050
+ const unsubscribers = [
1051
+ appEvents.on("updateLastSyncTime", () => {
1052
+ setLastSyncTime(/* @__PURE__ */ new Date());
1053
+ setStatus("idle");
1054
+ }),
1055
+ appEvents.on("setStatus", (newStatus) => {
1056
+ setStatus(newStatus);
1057
+ }),
1058
+ appEvents.on("setDiskSpace", (diskSpace) => {
1059
+ setDiskSpaceUsed(diskSpace);
1060
+ }),
1061
+ appEvents.on("addLog", ({ message, level }) => {
1062
+ addLogRef.current(message, level);
1063
+ })
1064
+ ];
1065
+ appEvents.emit("uiReady");
277
1066
  return () => {
278
- delete globalThis.__inkAppMethods;
1067
+ unsubscribers.forEach((unsub) => unsub());
279
1068
  };
280
- }, [updateLastSyncTime, setStatus]);
281
- return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React3.createElement(
1069
+ }, []);
1070
+ const statusBarHeight = 5;
1071
+ const terminalRows = stdout.rows ?? 24;
1072
+ const logPanelHeight = Math.max(5, terminalRows - statusBarHeight);
1073
+ const showModal = showHelp || showBranchWizard || showOpenEditorWizard;
1074
+ return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", minHeight: terminalRows }, !showModal && /* @__PURE__ */ React6.createElement(LogPanel_default, { logs, height: logPanelHeight, isActive: !showModal }), showHelp && /* @__PURE__ */ React6.createElement(HelpModal_default, { onClose: () => setShowHelp(false) }), showBranchWizard && /* @__PURE__ */ React6.createElement(
1075
+ BranchCreationWizard_default,
1076
+ {
1077
+ repositories: getRepositoryList(),
1078
+ getBranchesForRepo,
1079
+ getDefaultBranchForRepo,
1080
+ createAndPushBranch,
1081
+ onClose: () => setShowBranchWizard(false),
1082
+ onComplete: (success, context) => {
1083
+ setShowBranchWizard(false);
1084
+ if (success && context) {
1085
+ setStatus("syncing");
1086
+ void (async () => {
1087
+ try {
1088
+ await createWorktreeForBranch(context.repoIndex, context.newBranch);
1089
+ if (copyBranchFiles) {
1090
+ await copyBranchFiles(context.repoIndex, context.baseBranch, context.newBranch);
1091
+ }
1092
+ } catch (error) {
1093
+ console.error("Failed to create worktree:", error);
1094
+ } finally {
1095
+ setStatus("idle");
1096
+ }
1097
+ })();
1098
+ }
1099
+ }
1100
+ }
1101
+ ), showOpenEditorWizard && /* @__PURE__ */ React6.createElement(
1102
+ OpenEditorWizard_default,
1103
+ {
1104
+ repositories: getRepositoryList(),
1105
+ getWorktreesForRepo,
1106
+ openEditorInWorktree,
1107
+ onClose: () => setShowOpenEditorWizard(false)
1108
+ }
1109
+ ), /* @__PURE__ */ React6.createElement(
282
1110
  StatusBar_default,
283
1111
  {
284
1112
  status,
@@ -287,13 +1115,14 @@ var App = ({ repositoryCount, cronSchedule, onManualSync, onReload, onQuit }) =>
287
1115
  cronSchedule,
288
1116
  diskSpaceUsed: diskSpaceUsed ?? void 0
289
1117
  }
290
- ), showHelp && /* @__PURE__ */ React3.createElement(HelpModal_default, { onClose: () => setShowHelp(false) }));
1118
+ ));
291
1119
  };
292
1120
  var App_default = App;
293
1121
 
294
1122
  // src/services/worktree-sync.service.ts
295
1123
  import * as fs5 from "fs/promises";
296
1124
  import * as path5 from "path";
1125
+ import pLimit from "p-limit";
297
1126
 
298
1127
  // src/utils/date-filter.ts
299
1128
  function parseDuration(durationStr) {
@@ -373,6 +1202,7 @@ var DEFAULT_OPTIONS = {
373
1202
  maxDelayMs: 6e5,
374
1203
  // 10 minutes
375
1204
  backoffMultiplier: 2,
1205
+ jitterMs: 0,
376
1206
  shouldRetry: (error, context) => {
377
1207
  const err = error;
378
1208
  if (isLfsErrorFromError(error)) {
@@ -430,7 +1260,9 @@ async function retry(fn, options = {}) {
430
1260
  if (lfsContext.isLfsError && opts.lfsRetryHandler) {
431
1261
  opts.lfsRetryHandler(lfsContext);
432
1262
  }
433
- const delay = Math.min(opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1), opts.maxDelayMs);
1263
+ const baseDelay = Math.min(opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1), opts.maxDelayMs);
1264
+ const jitter = opts.jitterMs > 0 ? Math.random() * opts.jitterMs : 0;
1265
+ const delay = baseDelay + jitter;
434
1266
  opts.onRetry(error, attempt, lfsContext);
435
1267
  await new Promise((resolve6) => setTimeout(resolve6, delay));
436
1268
  attempt++;
@@ -438,6 +1270,106 @@ async function retry(fn, options = {}) {
438
1270
  }
439
1271
  }
440
1272
 
1273
+ // src/utils/timing.ts
1274
+ import Table from "cli-table3";
1275
+ var Timer = class {
1276
+ startTime;
1277
+ endTime;
1278
+ constructor() {
1279
+ this.startTime = Date.now();
1280
+ }
1281
+ stop() {
1282
+ this.endTime = Date.now();
1283
+ return this.getDuration();
1284
+ }
1285
+ getDuration() {
1286
+ const end = this.endTime ?? Date.now();
1287
+ return end - this.startTime;
1288
+ }
1289
+ };
1290
+ var PhaseTimer = class {
1291
+ phases = /* @__PURE__ */ new Map();
1292
+ currentPhase;
1293
+ startPhase(name, parallelism) {
1294
+ if (this.currentPhase) {
1295
+ this.endPhase();
1296
+ }
1297
+ this.currentPhase = name;
1298
+ this.phases.set(name, { timer: new Timer(), parallelism });
1299
+ }
1300
+ endPhase() {
1301
+ if (this.currentPhase) {
1302
+ const phase = this.phases.get(this.currentPhase);
1303
+ if (phase) {
1304
+ phase.timer.stop();
1305
+ }
1306
+ this.currentPhase = void 0;
1307
+ }
1308
+ }
1309
+ setPhaseCount(name, count) {
1310
+ const phase = this.phases.get(name);
1311
+ if (phase) {
1312
+ phase.count = count;
1313
+ }
1314
+ }
1315
+ getResults() {
1316
+ if (this.currentPhase) {
1317
+ this.endPhase();
1318
+ }
1319
+ const results = [];
1320
+ for (const [name, { timer, count, parallelism }] of this.phases.entries()) {
1321
+ const duration = timer.getDuration();
1322
+ const result = {
1323
+ name,
1324
+ duration,
1325
+ count
1326
+ };
1327
+ if (count && count > 0 && parallelism && parallelism > 1) {
1328
+ const batches = Math.ceil(count / parallelism);
1329
+ const avgTimePerBatch = duration / batches;
1330
+ const theoreticalSequentialTime = count * avgTimePerBatch;
1331
+ result.efficiency = theoreticalSequentialTime > 0 ? Math.round(theoreticalSequentialTime / duration * 100) : 100;
1332
+ }
1333
+ results.push(result);
1334
+ }
1335
+ return results;
1336
+ }
1337
+ };
1338
+ function formatDuration2(ms) {
1339
+ if (ms < 1e3) {
1340
+ return `${ms}ms`;
1341
+ }
1342
+ if (ms < 6e4) {
1343
+ return `${(ms / 1e3).toFixed(1)}s`;
1344
+ }
1345
+ const minutes = Math.floor(ms / 6e4);
1346
+ const seconds = Math.floor(ms % 6e4 / 1e3);
1347
+ return `${minutes}m ${seconds}s`;
1348
+ }
1349
+ function formatTimingTable(totalDuration, phaseResults, repoName) {
1350
+ const header = repoName ? `Performance Summary - [${repoName}]` : "Performance Summary";
1351
+ const table = new Table({
1352
+ head: ["Operation", "Duration", "Efficiency"],
1353
+ colWidths: [35, 12, 12],
1354
+ style: {
1355
+ head: ["cyan", "bold"],
1356
+ border: ["gray"]
1357
+ }
1358
+ });
1359
+ table.push([{ colSpan: 3, content: header, hAlign: "center" }]);
1360
+ table.push(["Total Sync", formatDuration2(totalDuration), ""]);
1361
+ for (let i = 0; i < phaseResults.length; i++) {
1362
+ const result = phaseResults[i];
1363
+ const isLast = i === phaseResults.length - 1;
1364
+ const countStr = result.count ? ` (${result.count})` : "";
1365
+ const prefix = isLast ? "\u2514\u2500" : "\u251C\u2500";
1366
+ const name = ` ${prefix} ${result.name}${countStr}`;
1367
+ const efficiency = result.efficiency ? `${result.efficiency}%` : "";
1368
+ table.push([name, formatDuration2(result.duration), efficiency]);
1369
+ }
1370
+ return table.toString();
1371
+ }
1372
+
441
1373
  // src/services/git.service.ts
442
1374
  import * as fs4 from "fs/promises";
443
1375
  import * as path4 from "path";
@@ -469,29 +1401,86 @@ function getDefaultBareRepoDir(repoUrl, baseDir = ".bare") {
469
1401
  return `${baseDir}/${repoName}`;
470
1402
  }
471
1403
 
1404
+ // src/services/logger.service.ts
1405
+ var Logger = class _Logger {
1406
+ repoName;
1407
+ debugEnabled;
1408
+ outputFn;
1409
+ constructor(options = {}) {
1410
+ this.repoName = options.repoName;
1411
+ this.debugEnabled = options.debug ?? false;
1412
+ this.outputFn = options.outputFn;
1413
+ }
1414
+ prefix() {
1415
+ return this.repoName ? `[${this.repoName}] ` : "";
1416
+ }
1417
+ debug(message, ...args) {
1418
+ if (!this.debugEnabled) return;
1419
+ const formattedMessage = this.prefix() + this.formatMessage(message, args);
1420
+ if (this.outputFn) {
1421
+ this.outputFn(formattedMessage, "debug");
1422
+ } else {
1423
+ console.log(formattedMessage);
1424
+ }
1425
+ }
1426
+ info(message, ...args) {
1427
+ const formattedMessage = this.prefix() + this.formatMessage(message, args);
1428
+ if (this.outputFn) {
1429
+ this.outputFn(formattedMessage, "info");
1430
+ } else {
1431
+ console.log(formattedMessage);
1432
+ }
1433
+ }
1434
+ warn(message, ...args) {
1435
+ const formattedMessage = this.prefix() + this.formatMessage(message, args);
1436
+ if (this.outputFn) {
1437
+ this.outputFn(formattedMessage, "warn");
1438
+ } else {
1439
+ console.warn(formattedMessage);
1440
+ }
1441
+ }
1442
+ error(message, error) {
1443
+ let formattedMessage = this.prefix() + message;
1444
+ if (error instanceof Error) {
1445
+ formattedMessage += ` ${error.message}`;
1446
+ } else if (error) {
1447
+ formattedMessage += ` ${String(error)}`;
1448
+ }
1449
+ if (this.outputFn) {
1450
+ this.outputFn(formattedMessage, "error");
1451
+ } else {
1452
+ if (error instanceof Error) {
1453
+ console.error(this.prefix() + message, error);
1454
+ } else if (error) {
1455
+ console.error(this.prefix() + message, error);
1456
+ } else {
1457
+ console.error(this.prefix() + message);
1458
+ }
1459
+ }
1460
+ }
1461
+ table(content) {
1462
+ const formattedMessage = "\n" + content + "\n";
1463
+ if (this.outputFn) {
1464
+ this.outputFn(formattedMessage, "info");
1465
+ } else {
1466
+ console.log(formattedMessage);
1467
+ }
1468
+ }
1469
+ formatMessage(message, args) {
1470
+ if (args.length === 0) {
1471
+ return message;
1472
+ }
1473
+ return args.reduce((msg, arg) => msg.replace("%s", String(arg)), message);
1474
+ }
1475
+ static createDefault(repoName, debug) {
1476
+ return new _Logger({ repoName, debug });
1477
+ }
1478
+ };
1479
+
472
1480
  // src/services/worktree-metadata.service.ts
473
1481
  import * as fs2 from "fs/promises";
474
1482
  import * as path2 from "path";
475
1483
  import simpleGit from "simple-git";
476
-
477
- // src/constants.ts
478
- var GIT_OPERATIONS = {
479
- MERGE_HEAD: "MERGE_HEAD",
480
- CHERRY_PICK_HEAD: "CHERRY_PICK_HEAD",
481
- REVERT_HEAD: "REVERT_HEAD",
482
- BISECT_LOG: "BISECT_LOG",
483
- REBASE_MERGE: "rebase-merge",
484
- REBASE_APPLY: "rebase-apply"
485
- };
486
- var PATH_CONSTANTS = {
487
- GIT_DIR: ".git",
488
- README: "README"
489
- };
490
- var METADATA_CONSTANTS = {
491
- MAX_HISTORY_ENTRIES: 10
492
- };
493
-
494
- // src/services/worktree-metadata.service.ts
495
1484
  var WorktreeMetadataService = class {
496
1485
  /**
497
1486
  * Gets the internal worktree directory name from a worktree path.
@@ -502,7 +1491,12 @@ var WorktreeMetadataService = class {
502
1491
  return path2.basename(worktreePath);
503
1492
  }
504
1493
  async getMetadataPath(bareRepoPath, worktreeName) {
505
- return path2.join(bareRepoPath, ".git", "worktrees", worktreeName, "sync-metadata.json");
1494
+ return path2.join(
1495
+ bareRepoPath,
1496
+ METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
1497
+ worktreeName,
1498
+ METADATA_CONSTANTS.METADATA_FILENAME
1499
+ );
506
1500
  }
507
1501
  async getMetadataPathFromWorktreePath(bareRepoPath, worktreePath) {
508
1502
  const worktreeDirName = this.getWorktreeDirectoryName(worktreePath);
@@ -537,7 +1531,12 @@ var WorktreeMetadataService = class {
537
1531
  const branchName = path2.basename(worktreePath);
538
1532
  const parentDir = path2.dirname(worktreePath);
539
1533
  const possibleBranchWithSlash = path2.join(path2.basename(parentDir), branchName);
540
- const oldPath = path2.join(bareRepoPath, ".git", "worktrees", possibleBranchWithSlash, "sync-metadata.json");
1534
+ const oldPath = path2.join(
1535
+ bareRepoPath,
1536
+ METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
1537
+ possibleBranchWithSlash,
1538
+ METADATA_CONSTANTS.METADATA_FILENAME
1539
+ );
541
1540
  const content = await fs2.readFile(oldPath, "utf-8");
542
1541
  const metadata = JSON.parse(content);
543
1542
  if (!await this.validateMetadata(metadata)) {
@@ -620,7 +1619,7 @@ var WorktreeMetadataService = class {
620
1619
  }
621
1620
  } catch {
622
1621
  }
623
- const parentBranch = defaultBranch || "main";
1622
+ const parentBranch = defaultBranch || GIT_CONSTANTS.DEFAULT_BRANCH;
624
1623
  await this.createInitialMetadataFromPath(
625
1624
  bareRepoPath,
626
1625
  worktreePath,
@@ -736,9 +1735,9 @@ var WorktreeError = class extends SyncWorktreesError {
736
1735
  }
737
1736
  };
738
1737
  var WorktreeNotCleanError = class extends WorktreeError {
739
- constructor(path9, reasons) {
740
- super(`Worktree at '${path9}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
741
- this.path = path9;
1738
+ constructor(path11, reasons) {
1739
+ super(`Worktree at '${path11}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
1740
+ this.path = path11;
742
1741
  this.reasons = reasons;
743
1742
  }
744
1743
  };
@@ -853,7 +1852,7 @@ var WorktreeStatusService = class {
853
1852
  const lines = result.split("\n").filter((line) => line.trim());
854
1853
  for (const line of lines) {
855
1854
  const firstChar = line.charAt(0);
856
- if (firstChar === "+" || firstChar === "-") {
1855
+ if (firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_ADDED || firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_REMOVED) {
857
1856
  return true;
858
1857
  }
859
1858
  }
@@ -951,7 +1950,7 @@ var WorktreeStatusService = class {
951
1950
  const modifiedSubmodules = [];
952
1951
  for (const line of lines) {
953
1952
  const firstChar = line.charAt(0);
954
- if (firstChar === "+" || firstChar === "-") {
1953
+ if (firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_ADDED || firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_REMOVED) {
955
1954
  const match = line.match(/^[+-]\s*(\S+)/);
956
1955
  if (match) {
957
1956
  modifiedSubmodules.push(match[1]);
@@ -1000,7 +1999,7 @@ var WorktreeStatusService = class {
1000
1999
  return files.filter((f) => !ignoredFiles.has(f));
1001
2000
  } catch (error) {
1002
2001
  const errorMessage = getErrorMessage(error);
1003
- if (errorMessage.includes("exit code: 1")) {
2002
+ if (errorMessage.includes(GIT_CONSTANTS.GIT_CHECK_IGNORE_NO_MATCH)) {
1004
2003
  return files;
1005
2004
  }
1006
2005
  console.warn(`Warning: Could not check gitignore status for files in ${worktreePath}: ${errorMessage}`);
@@ -1021,7 +2020,7 @@ var WorktreeStatusService = class {
1021
2020
  const stat4 = await fs3.stat(gitPath);
1022
2021
  if (stat4.isFile()) {
1023
2022
  const content = await fs3.readFile(gitPath, "utf-8");
1024
- const gitdirMatch = content.match(/^gitdir:\s*(.+)$/m);
2023
+ const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
1025
2024
  if (gitdirMatch) {
1026
2025
  return path3.resolve(worktreePath, gitdirMatch[1].trim());
1027
2026
  }
@@ -1043,31 +2042,37 @@ var WorktreeStatusService = class {
1043
2042
 
1044
2043
  // src/services/git.service.ts
1045
2044
  var GitService = class {
1046
- constructor(config) {
2045
+ constructor(config, logger) {
1047
2046
  this.config = config;
2047
+ this.logger = logger ?? Logger.createDefault(void 0, config.debug);
1048
2048
  this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
1049
- this.mainWorktreePath = path4.join(this.config.worktreeDir, "main");
2049
+ this.mainWorktreePath = path4.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
1050
2050
  this.metadataService = new WorktreeMetadataService();
1051
2051
  this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs });
1052
2052
  }
1053
2053
  git = null;
1054
2054
  bareRepoPath;
1055
2055
  mainWorktreePath;
1056
- defaultBranch = "main";
2056
+ defaultBranch = GIT_CONSTANTS.DEFAULT_BRANCH;
1057
2057
  // Will be updated after detection
1058
2058
  metadataService;
1059
2059
  statusService;
2060
+ logger;
2061
+ updateLogger(logger) {
2062
+ this.logger = logger;
2063
+ }
1060
2064
  async initialize() {
1061
2065
  const { repoUrl } = this.config;
2066
+ let needsClone = false;
1062
2067
  try {
1063
2068
  await fs4.access(path4.join(this.bareRepoPath, "HEAD"));
1064
- console.log(`Bare repository at "${this.bareRepoPath}" already exists. Using it.`);
1065
2069
  } catch {
1066
- console.log(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
2070
+ needsClone = true;
2071
+ this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
1067
2072
  await fs4.mkdir(path4.dirname(this.bareRepoPath), { recursive: true });
1068
- const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3();
2073
+ const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3();
1069
2074
  await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
1070
- console.log("\u2705 Clone successful.");
2075
+ this.logger.info("\u2705 Clone successful.");
1071
2076
  }
1072
2077
  const bareGit = simpleGit3(this.bareRepoPath);
1073
2078
  try {
@@ -1079,11 +2084,12 @@ var GitService = class {
1079
2084
  } catch {
1080
2085
  await bareGit.addConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*");
1081
2086
  }
1082
- console.log("Fetching remote branches...");
1083
- await bareGit.fetch(["--all"]);
2087
+ if (needsClone) {
2088
+ this.logger.info("Fetching remote branches...");
2089
+ await bareGit.fetch(["--all"]);
2090
+ }
1084
2091
  this.defaultBranch = await this.detectDefaultBranch(bareGit);
1085
2092
  this.mainWorktreePath = path4.join(this.config.worktreeDir, this.defaultBranch);
1086
- console.log(`Detected default branch: ${this.defaultBranch}`);
1087
2093
  let needsMainWorktree = true;
1088
2094
  try {
1089
2095
  const worktrees = await this.getWorktreesFromBare(bareGit);
@@ -1091,7 +2097,7 @@ var GitService = class {
1091
2097
  } catch {
1092
2098
  }
1093
2099
  if (needsMainWorktree) {
1094
- console.log(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
2100
+ this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
1095
2101
  await fs4.mkdir(this.config.worktreeDir, { recursive: true });
1096
2102
  const absoluteWorktreePath = path4.resolve(this.mainWorktreePath);
1097
2103
  try {
@@ -1099,7 +2105,7 @@ var GitService = class {
1099
2105
  const defaultBranchExists = branches.all.includes(this.defaultBranch);
1100
2106
  if (defaultBranchExists) {
1101
2107
  await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
1102
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(absoluteWorktreePath);
2108
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(absoluteWorktreePath);
1103
2109
  await worktreeGit.branch(["--set-upstream-to", `origin/${this.defaultBranch}`, this.defaultBranch]);
1104
2110
  } else {
1105
2111
  await bareGit.raw([
@@ -1115,17 +2121,17 @@ var GitService = class {
1115
2121
  } catch (error) {
1116
2122
  const errorMessage = getErrorMessage(error);
1117
2123
  if (errorMessage.includes("already exists")) {
1118
- console.log(
2124
+ this.logger.info(
1119
2125
  `${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`
1120
2126
  );
1121
2127
  } else {
1122
- console.warn(`Failed to create ${this.defaultBranch} worktree with tracking, using simple add: ${error}`);
2128
+ this.logger.warn(`Failed to create ${this.defaultBranch} worktree with tracking, using simple add: ${error}`);
1123
2129
  try {
1124
2130
  await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
1125
2131
  } catch (fallbackError) {
1126
2132
  const fallbackErrorMessage = getErrorMessage(fallbackError);
1127
2133
  if (fallbackErrorMessage.includes("already exists")) {
1128
- console.log(
2134
+ this.logger.info(
1129
2135
  `${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`
1130
2136
  );
1131
2137
  } else {
@@ -1139,8 +2145,8 @@ var GitService = class {
1139
2145
  (w) => path4.resolve(w.path) === path4.resolve(this.mainWorktreePath)
1140
2146
  );
1141
2147
  if (!mainWorktreeRegistered) {
1142
- if (process.env.NODE_ENV !== "test") {
1143
- console.warn(`Main worktree was created but not found in worktree list. This may cause issues.`);
2148
+ if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
2149
+ this.logger.warn(`Main worktree was created but not found in worktree list. This may cause issues.`);
1144
2150
  }
1145
2151
  }
1146
2152
  }
@@ -1153,14 +2159,17 @@ var GitService = class {
1153
2159
  }
1154
2160
  return this.git;
1155
2161
  }
2162
+ isInitialized() {
2163
+ return this.git !== null;
2164
+ }
1156
2165
  getDefaultBranch() {
1157
2166
  return this.defaultBranch;
1158
2167
  }
1159
2168
  async fetchAll() {
1160
2169
  const git = this.getGit();
1161
- console.log("Fetching latest data from remote...");
2170
+ this.logger.info("Fetching latest data from remote...");
1162
2171
  if (this.isLfsSkipEnabled()) {
1163
- await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["--all", "--prune"]);
2172
+ await git.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }).fetch(["--all", "--prune"]);
1164
2173
  } else {
1165
2174
  await git.fetch(["--all", "--prune"]);
1166
2175
  }
@@ -1168,7 +2177,7 @@ var GitService = class {
1168
2177
  async fetchBranch(branchName) {
1169
2178
  const git = this.getGit();
1170
2179
  if (this.isLfsSkipEnabled()) {
1171
- await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["origin", branchName, "--prune"]);
2180
+ await git.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }).fetch(["origin", branchName, "--prune"]);
1172
2181
  } else {
1173
2182
  await git.fetch(["origin", branchName, "--prune"]);
1174
2183
  }
@@ -1211,7 +2220,7 @@ var GitService = class {
1211
2220
  return;
1212
2221
  }
1213
2222
  if (this.config.debug) {
1214
- console.log(` - Verifying ${lfsFileList.length} LFS files are downloaded...`);
2223
+ this.logger.info(` - Verifying ${lfsFileList.length} LFS files are downloaded...`);
1215
2224
  }
1216
2225
  const sampleSize = Math.min(5, lfsFileList.length);
1217
2226
  const samplesToCheck = [];
@@ -1233,7 +2242,7 @@ var GitService = class {
1233
2242
  const buffer = Buffer.alloc(200);
1234
2243
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
1235
2244
  const header = buffer.subarray(0, bytesRead).toString("utf8");
1236
- if (header.startsWith("version https://git-lfs.github.com/spec/")) {
2245
+ if (header.startsWith(GIT_CONSTANTS.LFS_HEADER)) {
1237
2246
  allDownloaded = false;
1238
2247
  notDownloaded.push(file);
1239
2248
  }
@@ -1247,7 +2256,7 @@ var GitService = class {
1247
2256
  }
1248
2257
  if (allDownloaded) {
1249
2258
  if (this.config.debug) {
1250
- console.log(` - \u2705 LFS files verified (${samplesToCheck.length} samples checked)`);
2259
+ this.logger.info(` - \u2705 LFS files verified (${samplesToCheck.length} samples checked)`);
1251
2260
  }
1252
2261
  return;
1253
2262
  }
@@ -1256,16 +2265,16 @@ var GitService = class {
1256
2265
  await new Promise((resolve6) => setTimeout(resolve6, retryDelay));
1257
2266
  }
1258
2267
  }
1259
- console.warn(
2268
+ this.logger.warn(
1260
2269
  ` - \u26A0\uFE0F Warning: Some LFS files may not be fully downloaded after ${maxRetries} seconds. This might cause issues if tools access the worktree immediately.`
1261
2270
  );
1262
2271
  } catch (error) {
1263
- console.warn(` - \u26A0\uFE0F Warning: Could not verify LFS files for '${branchName}': ${error}`);
2272
+ this.logger.warn(` - \u26A0\uFE0F Warning: Could not verify LFS files for '${branchName}': ${error}`);
1264
2273
  }
1265
2274
  }
1266
2275
  async createWorktreeMetadata(bareGit, worktreePath, branchName) {
1267
2276
  try {
1268
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
2277
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
1269
2278
  const currentCommit = await worktreeGit.revparse(["HEAD"]);
1270
2279
  const parentCommit = await bareGit.revparse([this.defaultBranch]);
1271
2280
  await this.metadataService.createInitialMetadataFromPath(
@@ -1277,12 +2286,12 @@ var GitService = class {
1277
2286
  parentCommit.trim()
1278
2287
  );
1279
2288
  } catch (metadataError) {
1280
- console.error(` - \u274C Failed to create metadata for '${branchName}': ${metadataError}`);
2289
+ this.logger.error(` - \u274C Failed to create metadata for '${branchName}': ${metadataError}`);
1281
2290
  throw new Error(`Metadata creation failed for ${branchName}. This worktree cannot be auto-managed.`);
1282
2291
  }
1283
2292
  }
1284
2293
  async addWorktree(branchName, worktreePath) {
1285
- const bareGit = this.isLfsSkipEnabled() ? simpleGit3(this.bareRepoPath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(this.bareRepoPath);
2294
+ const bareGit = this.isLfsSkipEnabled() ? simpleGit3(this.bareRepoPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(this.bareRepoPath);
1286
2295
  const absoluteWorktreePath = path4.resolve(worktreePath);
1287
2296
  await fs4.mkdir(path4.dirname(absoluteWorktreePath), { recursive: true });
1288
2297
  try {
@@ -1290,10 +2299,10 @@ var GitService = class {
1290
2299
  const worktrees = await this.getWorktreesFromBare(bareGit);
1291
2300
  const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
1292
2301
  if (isValidWorktree) {
1293
- console.log(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
2302
+ this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
1294
2303
  return;
1295
2304
  } else {
1296
- console.log(` - Cleaning up orphaned directory at '${absoluteWorktreePath}'`);
2305
+ this.logger.info(` - Cleaning up orphaned directory at '${absoluteWorktreePath}'`);
1297
2306
  await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
1298
2307
  }
1299
2308
  } catch {
@@ -1303,7 +2312,7 @@ var GitService = class {
1303
2312
  const localBranchExists = branches.all.includes(branchName);
1304
2313
  if (localBranchExists || branchName.includes("/")) {
1305
2314
  await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
1306
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(absoluteWorktreePath);
2315
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(absoluteWorktreePath);
1307
2316
  await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
1308
2317
  } else {
1309
2318
  await bareGit.raw([
@@ -1316,7 +2325,7 @@ var GitService = class {
1316
2325
  `origin/${branchName}`
1317
2326
  ]);
1318
2327
  }
1319
- console.log(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
2328
+ this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
1320
2329
  if (!this.isLfsSkipEnabled()) {
1321
2330
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
1322
2331
  }
@@ -1327,7 +2336,13 @@ var GitService = class {
1327
2336
  throw error;
1328
2337
  }
1329
2338
  if (errorMessage.includes("already registered worktree")) {
1330
- console.warn(` - Worktree already registered but missing. Pruning and retrying...`);
2339
+ const worktrees = await this.getWorktreesFromBare(bareGit);
2340
+ const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
2341
+ if (existingWorktree && !existingWorktree.isPrunable) {
2342
+ this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
2343
+ return;
2344
+ }
2345
+ this.logger.warn(` - Worktree already registered but missing. Pruning and retrying...`);
1331
2346
  await bareGit.raw(["worktree", "prune"]);
1332
2347
  try {
1333
2348
  await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
@@ -1343,53 +2358,66 @@ var GitService = class {
1343
2358
  absoluteWorktreePath,
1344
2359
  `origin/${branchName}`
1345
2360
  ]);
1346
- console.log(` - Created worktree for '${branchName}' after pruning`);
2361
+ this.logger.info(` - Created worktree for '${branchName}' after pruning`);
1347
2362
  if (!this.isLfsSkipEnabled()) {
1348
2363
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
1349
2364
  }
1350
2365
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
1351
2366
  return;
1352
2367
  } catch (retryError) {
1353
- console.error(` - Failed to create worktree after pruning: ${retryError}`);
2368
+ this.logger.error(` - Failed to create worktree after pruning: ${retryError}`);
1354
2369
  throw retryError;
1355
2370
  }
1356
2371
  }
1357
- console.warn(` - Failed to create worktree with tracking, falling back to simple add: ${error}`);
2372
+ this.logger.warn(` - Failed to create worktree with tracking, falling back to simple add: ${error}`);
1358
2373
  try {
1359
2374
  await fs4.access(absoluteWorktreePath);
1360
2375
  const worktrees = await this.getWorktreesFromBare(bareGit);
1361
2376
  const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
1362
2377
  if (isValidWorktree) {
1363
- console.log(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
2378
+ this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
1364
2379
  return;
1365
2380
  } else {
1366
- console.log(` - Cleaning up orphaned directory at '${absoluteWorktreePath}' before fallback attempt`);
2381
+ this.logger.info(` - Cleaning up orphaned directory at '${absoluteWorktreePath}' before fallback attempt`);
1367
2382
  await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
1368
2383
  }
1369
2384
  } catch {
1370
2385
  }
1371
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
1372
- console.log(` - Created worktree for '${branchName}' (without tracking)`);
1373
- if (!this.isLfsSkipEnabled()) {
1374
- await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
2386
+ try {
2387
+ await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
2388
+ this.logger.info(` - Created worktree for '${branchName}' (without tracking)`);
2389
+ if (!this.isLfsSkipEnabled()) {
2390
+ await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
2391
+ }
2392
+ await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
2393
+ } catch (fallbackError) {
2394
+ const fallbackErrorMessage = getErrorMessage(fallbackError);
2395
+ if (fallbackErrorMessage.includes("already registered worktree")) {
2396
+ const worktrees = await this.getWorktreesFromBare(bareGit);
2397
+ const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
2398
+ if (existingWorktree && !existingWorktree.isPrunable) {
2399
+ this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
2400
+ return;
2401
+ }
2402
+ }
2403
+ throw fallbackError;
1375
2404
  }
1376
- await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
1377
2405
  }
1378
2406
  }
1379
2407
  async removeWorktree(worktreePath) {
1380
2408
  const bareGit = simpleGit3(this.bareRepoPath);
1381
2409
  await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
1382
- console.log(` - \u2705 Safely removed stale worktree at '${worktreePath}'.`);
2410
+ this.logger.info(` - \u2705 Safely removed stale worktree at '${worktreePath}'.`);
1383
2411
  try {
1384
2412
  await this.metadataService.deleteMetadataFromPath(this.bareRepoPath, worktreePath);
1385
2413
  } catch (metadataError) {
1386
- console.warn(`Failed to delete metadata for worktree: ${metadataError}`);
2414
+ this.logger.warn(`Failed to delete metadata for worktree: ${metadataError}`);
1387
2415
  }
1388
2416
  }
1389
2417
  async pruneWorktrees() {
1390
2418
  const bareGit = simpleGit3(this.bareRepoPath);
1391
2419
  await bareGit.raw(["worktree", "prune"]);
1392
- console.log("Pruned worktree metadata.");
2420
+ this.logger.info("Pruned worktree metadata.");
1393
2421
  }
1394
2422
  async checkWorktreeStatus(worktreePath) {
1395
2423
  const worktreeGit = simpleGit3(worktreePath);
@@ -1428,7 +2456,7 @@ var GitService = class {
1428
2456
  const unpushedCount = parseInt(result.trim(), 10);
1429
2457
  return unpushedCount > 0;
1430
2458
  } catch (error) {
1431
- console.error(`Error checking unpushed commits: ${error}`);
2459
+ this.logger.error(`Error checking unpushed commits: ${error}`);
1432
2460
  return false;
1433
2461
  }
1434
2462
  }
@@ -1466,7 +2494,7 @@ var GitService = class {
1466
2494
  }
1467
2495
  return false;
1468
2496
  }
1469
- console.error(
2497
+ this.logger.error(
1470
2498
  `Unexpected error checking upstream status for ${worktreePath}. This might indicate a real issue rather than a missing upstream. Error: ${errorMessage}`
1471
2499
  );
1472
2500
  return false;
@@ -1478,7 +2506,7 @@ var GitService = class {
1478
2506
  const stashList = await worktreeGit.stashList();
1479
2507
  return stashList.total > 0;
1480
2508
  } catch (error) {
1481
- console.error(`Error checking stash: ${error}`);
2509
+ this.logger.error(`Error checking stash: ${error}`);
1482
2510
  return true;
1483
2511
  }
1484
2512
  }
@@ -1544,7 +2572,7 @@ var GitService = class {
1544
2572
  } catch {
1545
2573
  try {
1546
2574
  const remoteBranches = await bareGit.branch(["-r"]);
1547
- const commonDefaults = ["main", "master", "develop", "trunk"];
2575
+ const commonDefaults = GIT_CONSTANTS.COMMON_DEFAULT_BRANCHES;
1548
2576
  for (const defaultName of commonDefaults) {
1549
2577
  if (remoteBranches.all.some((branch) => branch === `origin/${defaultName}`)) {
1550
2578
  return defaultName;
@@ -1554,10 +2582,10 @@ var GitService = class {
1554
2582
  }
1555
2583
  }
1556
2584
  }
1557
- return "main";
2585
+ return GIT_CONSTANTS.DEFAULT_BRANCH;
1558
2586
  }
1559
2587
  isLfsSkipEnabled() {
1560
- return this.config.skipLfs || process.env.GIT_LFS_SKIP_SMUDGE === "1";
2588
+ return this.config.skipLfs || process.env[ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE] === "1";
1561
2589
  }
1562
2590
  async getWorktrees() {
1563
2591
  const bareGit = simpleGit3(this.bareRepoPath);
@@ -1579,7 +2607,7 @@ var GitService = class {
1579
2607
  }
1580
2608
  }
1581
2609
  async updateWorktree(worktreePath) {
1582
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
2610
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
1583
2611
  const branchSummary = await worktreeGit.branch();
1584
2612
  const currentBranch = branchSummary.current;
1585
2613
  await worktreeGit.merge([`origin/${currentBranch}`, "--ff-only"]);
@@ -1597,14 +2625,14 @@ var GitService = class {
1597
2625
  this.defaultBranch
1598
2626
  );
1599
2627
  } catch (metadataError) {
1600
- console.warn(`Failed to update metadata for worktree: ${metadataError}`);
2628
+ this.logger.warn(`Failed to update metadata for worktree: ${metadataError}`);
1601
2629
  }
1602
2630
  }
1603
2631
  async hasDivergedHistory(worktreePath, expectedBranch) {
1604
2632
  const worktreeGit = simpleGit3(worktreePath);
1605
2633
  const branchInfo = await worktreeGit.branch();
1606
2634
  if (branchInfo.current !== expectedBranch) {
1607
- console.warn(`Branch mismatch in hasDivergedHistory: expected ${expectedBranch}, got ${branchInfo.current}`);
2635
+ this.logger.warn(`Branch mismatch in hasDivergedHistory: expected ${expectedBranch}, got ${branchInfo.current}`);
1608
2636
  return false;
1609
2637
  }
1610
2638
  try {
@@ -1626,6 +2654,18 @@ var GitService = class {
1626
2654
  return false;
1627
2655
  }
1628
2656
  }
2657
+ async isLocalAheadOfRemote(worktreePath, branch) {
2658
+ const worktreeGit = simpleGit3(worktreePath);
2659
+ try {
2660
+ const mergeBase = await worktreeGit.raw(["merge-base", "HEAD", `origin/${branch}`]);
2661
+ const mergeBaseSha = mergeBase.trim();
2662
+ const remoteSha = await worktreeGit.revparse([`origin/${branch}`]);
2663
+ const remoteShaTrimmed = remoteSha.trim();
2664
+ return mergeBaseSha === remoteShaTrimmed;
2665
+ } catch {
2666
+ return false;
2667
+ }
2668
+ }
1629
2669
  async compareTreeContent(worktreePath, branch) {
1630
2670
  const worktreeGit = simpleGit3(worktreePath);
1631
2671
  try {
@@ -1633,12 +2673,12 @@ var GitService = class {
1633
2673
  const remoteTree = await worktreeGit.raw(["rev-parse", `origin/${branch}^{tree}`]);
1634
2674
  return localTree.trim() === remoteTree.trim();
1635
2675
  } catch (error) {
1636
- console.error(`Error comparing tree content: ${error}`);
2676
+ this.logger.error(`Error comparing tree content: ${error}`);
1637
2677
  return false;
1638
2678
  }
1639
2679
  }
1640
2680
  async resetToUpstream(worktreePath, branch) {
1641
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
2681
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
1642
2682
  await worktreeGit.reset(["--hard", `origin/${branch}`]);
1643
2683
  try {
1644
2684
  const currentCommit = await worktreeGit.revparse(["HEAD"]);
@@ -1650,7 +2690,7 @@ var GitService = class {
1650
2690
  this.defaultBranch
1651
2691
  );
1652
2692
  } catch (metadataError) {
1653
- console.warn(`Failed to update metadata after reset: ${metadataError}`);
2693
+ this.logger.warn(`Failed to update metadata after reset: ${metadataError}`);
1654
2694
  }
1655
2695
  }
1656
2696
  async getCurrentCommit(worktreePath) {
@@ -1663,6 +2703,29 @@ var GitService = class {
1663
2703
  const commit = await git.revparse([ref]);
1664
2704
  return commit.trim();
1665
2705
  }
2706
+ async branchExists(branchName) {
2707
+ const bareGit = simpleGit3(this.bareRepoPath);
2708
+ const localBranches = await bareGit.branch();
2709
+ const local = localBranches.all.includes(branchName);
2710
+ const remoteBranches = await bareGit.branch(["-r"]);
2711
+ const remote = remoteBranches.all.includes(`origin/${branchName}`);
2712
+ return { local, remote };
2713
+ }
2714
+ async getLocalBranches() {
2715
+ const bareGit = simpleGit3(this.bareRepoPath);
2716
+ const branches = await bareGit.branch();
2717
+ return branches.all;
2718
+ }
2719
+ async createBranch(branchName, baseBranch) {
2720
+ const bareGit = simpleGit3(this.bareRepoPath);
2721
+ await bareGit.raw(["branch", branchName, `origin/${baseBranch}`]);
2722
+ this.logger.info(`Created branch '${branchName}' from '${baseBranch}'`);
2723
+ }
2724
+ async pushBranch(branchName) {
2725
+ const bareGit = simpleGit3(this.bareRepoPath);
2726
+ await bareGit.push(["origin", `${branchName}:${branchName}`, "-u"]);
2727
+ this.logger.info(`Pushed branch '${branchName}' to remote`);
2728
+ }
1666
2729
  async getWorktreeMetadata(worktreePath) {
1667
2730
  return this.metadataService.loadMetadataFromPath(this.bareRepoPath, worktreePath);
1668
2731
  }
@@ -1678,17 +2741,27 @@ var GitService = class {
1678
2741
  currentWorktree.branch = line.substring(7).replace("refs/heads/", "");
1679
2742
  } else if (line === "detached") {
1680
2743
  currentWorktree.detached = true;
2744
+ } else if (line === "prunable") {
2745
+ currentWorktree.prunable = true;
1681
2746
  } else if (line.trim() === "") {
1682
2747
  if (currentWorktree.path) {
1683
2748
  if (currentWorktree.branch && !currentWorktree.detached) {
1684
- worktrees.push({ path: currentWorktree.path, branch: currentWorktree.branch });
2749
+ worktrees.push({
2750
+ path: currentWorktree.path,
2751
+ branch: currentWorktree.branch,
2752
+ isPrunable: currentWorktree.prunable || false
2753
+ });
1685
2754
  }
1686
2755
  }
1687
2756
  currentWorktree = {};
1688
2757
  }
1689
2758
  }
1690
2759
  if (currentWorktree.path && currentWorktree.branch && !currentWorktree.detached) {
1691
- worktrees.push({ path: currentWorktree.path, branch: currentWorktree.branch });
2760
+ worktrees.push({
2761
+ path: currentWorktree.path,
2762
+ branch: currentWorktree.branch,
2763
+ isPrunable: currentWorktree.prunable || false
2764
+ });
1692
2765
  }
1693
2766
  return worktrees;
1694
2767
  }
@@ -1698,23 +2771,37 @@ var GitService = class {
1698
2771
  var WorktreeSyncService = class {
1699
2772
  constructor(config) {
1700
2773
  this.config = config;
1701
- this.gitService = new GitService(config);
2774
+ this.logger = config.logger ?? Logger.createDefault(void 0, config.debug);
2775
+ this.gitService = new GitService(config, this.logger);
1702
2776
  }
1703
2777
  gitService;
2778
+ logger;
1704
2779
  syncInProgress = false;
1705
2780
  async initialize() {
1706
2781
  await this.gitService.initialize();
1707
2782
  }
2783
+ isInitialized() {
2784
+ return this.gitService.isInitialized();
2785
+ }
1708
2786
  isSyncInProgress() {
1709
2787
  return this.syncInProgress;
1710
2788
  }
2789
+ getGitService() {
2790
+ return this.gitService;
2791
+ }
2792
+ updateLogger(logger) {
2793
+ this.logger = logger;
2794
+ this.gitService.updateLogger(logger);
2795
+ }
1711
2796
  async sync() {
1712
2797
  if (this.syncInProgress) {
1713
- console.warn("\u26A0\uFE0F Sync already in progress, skipping...");
2798
+ this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
1714
2799
  return;
1715
2800
  }
1716
2801
  this.syncInProgress = true;
1717
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
2802
+ this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
2803
+ const totalTimer = new Timer();
2804
+ const phaseTimer = new PhaseTimer();
1718
2805
  let lfsSkipEnabled = false;
1719
2806
  const retryOptions = {
1720
2807
  maxAttempts: this.config.retry?.maxAttempts ?? 3,
@@ -1724,18 +2811,18 @@ var WorktreeSyncService = class {
1724
2811
  backoffMultiplier: this.config.retry?.backoffMultiplier ?? 2,
1725
2812
  onRetry: (error, attempt, context) => {
1726
2813
  const errorMessage = getErrorMessage(error);
1727
- console.log(`
2814
+ this.logger.info(`
1728
2815
  \u26A0\uFE0F Sync attempt ${attempt} failed: ${errorMessage}`);
1729
2816
  if (context?.isLfsError && !this.config.skipLfs) {
1730
- console.log(`\u{1F504} LFS error detected. Will retry with LFS skipped...`);
2817
+ this.logger.info(`\u{1F504} LFS error detected. Will retry with LFS skipped...`);
1731
2818
  } else {
1732
- console.log(`\u{1F504} Retrying synchronization...
2819
+ this.logger.info(`\u{1F504} Retrying synchronization...
1733
2820
  `);
1734
2821
  }
1735
2822
  },
1736
2823
  lfsRetryHandler: () => {
1737
2824
  if (!this.config.skipLfs && !lfsSkipEnabled) {
1738
- console.log("\u26A0\uFE0F Temporarily disabling LFS downloads for this sync...");
2825
+ this.logger.info("\u26A0\uFE0F Temporarily disabling LFS downloads for this sync...");
1739
2826
  process.env.GIT_LFS_SKIP_SMUDGE = "1";
1740
2827
  lfsSkipEnabled = true;
1741
2828
  }
@@ -1744,14 +2831,15 @@ var WorktreeSyncService = class {
1744
2831
  try {
1745
2832
  await retry(async () => {
1746
2833
  await this.gitService.pruneWorktrees();
1747
- console.log("Step 1: Fetching latest data from remote...");
2834
+ this.logger.info("Step 1: Fetching latest data from remote...");
2835
+ phaseTimer.startPhase("Phase 1: Fetch");
1748
2836
  try {
1749
2837
  await this.gitService.fetchAll();
1750
2838
  } catch (fetchError) {
1751
2839
  const errorMessage = getErrorMessage(fetchError);
1752
2840
  if (isLfsError(errorMessage) && !lfsSkipEnabled && !this.config.skipLfs) {
1753
- console.log("\u26A0\uFE0F Fetch all failed due to LFS error. Attempting branch-by-branch fetch...");
1754
- console.log("\u26A0\uFE0F Temporarily disabling LFS downloads for branch-by-branch fetch...");
2841
+ this.logger.info("\u26A0\uFE0F Fetch all failed due to LFS error. Attempting branch-by-branch fetch...");
2842
+ this.logger.info("\u26A0\uFE0F Temporarily disabling LFS downloads for branch-by-branch fetch...");
1755
2843
  process.env.GIT_LFS_SKIP_SMUDGE = "1";
1756
2844
  lfsSkipEnabled = true;
1757
2845
  await this.fetchBranchByBranch();
@@ -1759,143 +2847,221 @@ var WorktreeSyncService = class {
1759
2847
  throw fetchError;
1760
2848
  }
1761
2849
  }
2850
+ phaseTimer.endPhase();
1762
2851
  let remoteBranches;
1763
2852
  if (this.config.branchMaxAge) {
1764
2853
  const branchesWithActivity = await this.gitService.getRemoteBranchesWithActivity();
1765
2854
  const filteredBranches = filterBranchesByAge(branchesWithActivity, this.config.branchMaxAge);
1766
2855
  remoteBranches = filteredBranches.map((b) => b.branch);
1767
- console.log(`Found ${branchesWithActivity.length} remote branches.`);
1768
- console.log(
2856
+ this.logger.info(`Found ${branchesWithActivity.length} remote branches.`);
2857
+ this.logger.info(
1769
2858
  `After filtering by age (${formatDuration(this.config.branchMaxAge)}): ${remoteBranches.length} branches.`
1770
2859
  );
1771
2860
  if (branchesWithActivity.length > remoteBranches.length) {
1772
2861
  const excludedCount = branchesWithActivity.length - remoteBranches.length;
1773
- console.log(` - Excluded ${excludedCount} stale branches.`);
2862
+ this.logger.info(` - Excluded ${excludedCount} stale branches.`);
1774
2863
  }
1775
2864
  } else {
1776
2865
  remoteBranches = await this.gitService.getRemoteBranches();
1777
- console.log(`Found ${remoteBranches.length} remote branches.`);
2866
+ this.logger.info(`Found ${remoteBranches.length} remote branches.`);
1778
2867
  }
1779
2868
  const defaultBranch = this.gitService.getDefaultBranch();
1780
2869
  if (!remoteBranches.includes(defaultBranch)) {
1781
2870
  remoteBranches.push(defaultBranch);
1782
- console.log(`Ensuring default branch '${defaultBranch}' is retained.`);
2871
+ this.logger.info(`Ensuring default branch '${defaultBranch}' is retained.`);
1783
2872
  }
1784
2873
  await fs5.mkdir(this.config.worktreeDir, { recursive: true });
1785
2874
  const worktrees = await this.gitService.getWorktrees();
1786
2875
  const worktreeBranches = worktrees.map((w) => w.branch);
1787
- console.log(`Found ${worktrees.length} existing Git worktrees.`);
2876
+ this.logger.info(`Found ${worktrees.length} existing Git worktrees.`);
1788
2877
  await this.cleanupOrphanedDirectories(worktrees);
1789
- await this.createNewWorktrees(remoteBranches, worktreeBranches, defaultBranch);
1790
- await this.pruneOldWorktrees(remoteBranches, worktreeBranches);
2878
+ await this.createNewWorktreesWithTiming(remoteBranches, worktreeBranches, defaultBranch, phaseTimer);
2879
+ await this.pruneOldWorktreesWithTiming(remoteBranches, worktreeBranches, phaseTimer);
1791
2880
  if (this.config.updateExistingWorktrees !== false) {
1792
- await this.updateExistingWorktrees(worktrees, remoteBranches);
2881
+ await this.updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer);
1793
2882
  }
2883
+ phaseTimer.startPhase("Phase 5: Cleanup");
1794
2884
  await this.gitService.pruneWorktrees();
1795
- console.log("Step 5: Pruned worktree metadata.");
2885
+ this.logger.info("Step 5: Pruned worktree metadata.");
2886
+ phaseTimer.endPhase();
1796
2887
  }, retryOptions);
1797
2888
  } catch (error) {
1798
- console.error("\n\u274C Error during worktree synchronization after all retry attempts:", error);
2889
+ this.logger.error("\n\u274C Error during worktree synchronization after all retry attempts:", error);
1799
2890
  throw error;
1800
2891
  } finally {
1801
2892
  if (lfsSkipEnabled && !this.config.skipLfs) {
1802
2893
  delete process.env.GIT_LFS_SKIP_SMUDGE;
1803
2894
  }
1804
2895
  this.syncInProgress = false;
1805
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
2896
+ this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
1806
2897
  `);
2898
+ if (this.config.debug) {
2899
+ const totalDuration = totalTimer.stop();
2900
+ const phaseResults = phaseTimer.getResults();
2901
+ const repoName = this.config.name;
2902
+ this.logger.table(formatTimingTable(totalDuration, phaseResults, repoName));
2903
+ }
1807
2904
  }
1808
2905
  }
2906
+ async createNewWorktreesWithTiming(remoteBranches, existingWorktreeBranches, defaultBranch, phaseTimer) {
2907
+ const maxConcurrent = this.config.parallelism?.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
2908
+ phaseTimer.startPhase("Phase 2: Create", maxConcurrent);
2909
+ await this.createNewWorktrees(remoteBranches, existingWorktreeBranches, defaultBranch);
2910
+ const newBranches = remoteBranches.filter((b) => !existingWorktreeBranches.includes(b)).filter((b) => b !== defaultBranch);
2911
+ phaseTimer.setPhaseCount("Phase 2: Create", newBranches.length);
2912
+ phaseTimer.endPhase();
2913
+ }
1809
2914
  async createNewWorktrees(remoteBranches, existingWorktreeBranches, defaultBranch) {
1810
2915
  const newBranches = remoteBranches.filter((b) => !existingWorktreeBranches.includes(b)).filter((b) => b !== defaultBranch);
1811
2916
  if (newBranches.length > 0) {
1812
- console.log(`Step 2: Creating new worktrees for: ${newBranches.join(", ")}`);
1813
- for (const branchName of newBranches) {
1814
- const worktreePath = path5.join(this.config.worktreeDir, branchName);
1815
- await this.gitService.addWorktree(branchName, worktreePath);
1816
- }
2917
+ this.logger.info(`Step 2: Creating ${newBranches.length} new worktrees...`);
2918
+ const maxConcurrent = this.config.parallelism?.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
2919
+ const limit = pLimit(maxConcurrent);
2920
+ const results = await Promise.allSettled(
2921
+ newBranches.map(
2922
+ (branchName) => limit(async () => {
2923
+ const worktreePath = path5.join(this.config.worktreeDir, branchName);
2924
+ try {
2925
+ await this.gitService.addWorktree(branchName, worktreePath);
2926
+ this.logger.info(` \u2705 Created worktree for '${branchName}'`);
2927
+ } catch (error) {
2928
+ this.logger.error(` \u274C Failed to create worktree for '${branchName}':`, getErrorMessage(error));
2929
+ throw error;
2930
+ }
2931
+ })
2932
+ )
2933
+ );
2934
+ const successCount = results.filter((r) => r.status === "fulfilled").length;
2935
+ this.logger.info(` Created ${successCount}/${newBranches.length} worktrees successfully`);
1817
2936
  } else {
1818
- console.log("Step 2: No new branches to create worktrees for.");
2937
+ this.logger.info("Step 2: No new branches to create worktrees for.");
1819
2938
  }
1820
2939
  }
2940
+ async pruneOldWorktreesWithTiming(remoteBranches, existingWorktreeBranches, phaseTimer) {
2941
+ const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
2942
+ phaseTimer.startPhase("Phase 3: Prune", maxConcurrent);
2943
+ await this.pruneOldWorktrees(remoteBranches, existingWorktreeBranches);
2944
+ const deletedBranches = existingWorktreeBranches.filter((branch) => !remoteBranches.includes(branch));
2945
+ phaseTimer.setPhaseCount("Phase 3: Prune", deletedBranches.length);
2946
+ phaseTimer.endPhase();
2947
+ }
1821
2948
  async pruneOldWorktrees(remoteBranches, existingWorktreeBranches) {
1822
2949
  const deletedBranches = existingWorktreeBranches.filter((branch) => !remoteBranches.includes(branch));
1823
2950
  if (deletedBranches.length > 0) {
1824
- console.log(`Step 3: Checking for stale worktrees to prune: ${deletedBranches.join(", ")}`);
1825
- for (const branchName of deletedBranches) {
1826
- const worktreePath = path5.join(this.config.worktreeDir, branchName);
1827
- try {
1828
- const status = await this.gitService.getFullWorktreeStatus(worktreePath, this.config.debug);
2951
+ this.logger.info(`Step 3: Checking ${deletedBranches.length} stale worktrees to prune...`);
2952
+ const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
2953
+ const limit = pLimit(maxConcurrent);
2954
+ const statusResults = await Promise.allSettled(
2955
+ deletedBranches.map(
2956
+ (branchName) => limit(async () => {
2957
+ const worktreePath = path5.join(this.config.worktreeDir, branchName);
2958
+ const status = await this.gitService.getFullWorktreeStatus(worktreePath, this.config.debug);
2959
+ return { branchName, worktreePath, status };
2960
+ })
2961
+ )
2962
+ );
2963
+ const toRemove = [];
2964
+ const toSkip = [];
2965
+ for (const result of statusResults) {
2966
+ if (result.status === "fulfilled") {
2967
+ const { branchName, worktreePath, status } = result.value;
1829
2968
  if (status.canRemove) {
1830
- await this.gitService.removeWorktree(worktreePath);
2969
+ toRemove.push({ branchName, worktreePath });
1831
2970
  } else {
1832
- if (status.upstreamGone && status.hasUnpushedCommits) {
1833
- console.warn(` - \u26A0\uFE0F Cannot automatically remove '${branchName}' - upstream branch was deleted.`);
1834
- console.log(` Please review manually: cd ${worktreePath} && git log`);
1835
- console.log(
1836
- ` If changes were squash-merged, you can safely remove with: git worktree remove ${worktreePath}`
1837
- );
1838
- } else {
1839
- console.log(` - \u26A0\uFE0F Skipping removal of '${branchName}' due to: ${status.reasons.join(", ")}.`);
1840
- }
1841
- if (this.config.debug && status.details) {
1842
- this.logDebugDetails(branchName, status.details);
1843
- }
2971
+ toSkip.push({ branchName, status });
1844
2972
  }
1845
- } catch (error) {
1846
- console.error(` - Error checking worktree '${branchName}':`, error);
2973
+ } else {
2974
+ this.logger.error(` - Error checking worktree:`, result.reason);
2975
+ }
2976
+ }
2977
+ if (toRemove.length > 0) {
2978
+ const removeLimit = pLimit(
2979
+ this.config.parallelism?.maxWorktreeRemoval ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_REMOVAL
2980
+ );
2981
+ const removeResults = await Promise.allSettled(
2982
+ toRemove.map(
2983
+ ({ branchName, worktreePath }) => removeLimit(async () => {
2984
+ try {
2985
+ await this.gitService.removeWorktree(worktreePath);
2986
+ this.logger.info(` \u2705 Removed worktree for '${branchName}'`);
2987
+ } catch (error) {
2988
+ this.logger.error(` \u274C Failed to remove worktree for '${branchName}':`, getErrorMessage(error));
2989
+ throw error;
2990
+ }
2991
+ })
2992
+ )
2993
+ );
2994
+ const removedCount = removeResults.filter((r) => r.status === "fulfilled").length;
2995
+ this.logger.info(` Removed ${removedCount}/${toRemove.length} worktrees successfully`);
2996
+ }
2997
+ if (toSkip.length > 0) {
2998
+ this.logger.info(` Skipped ${toSkip.length} worktree(s) with local changes or unpushed commits`);
2999
+ }
3000
+ for (const { branchName, status } of toSkip) {
3001
+ if (status.upstreamGone && status.hasUnpushedCommits) {
3002
+ const worktreePath = path5.join(this.config.worktreeDir, branchName);
3003
+ this.logger.warn(` - \u26A0\uFE0F Cannot automatically remove '${branchName}' - upstream branch was deleted.`);
3004
+ this.logger.info(` Please review manually: cd ${worktreePath} && git log`);
3005
+ this.logger.info(
3006
+ ` If changes were squash-merged, you can safely remove with: git worktree remove ${worktreePath}`
3007
+ );
3008
+ } else {
3009
+ this.logger.info(` - \u26A0\uFE0F Skipping removal of '${branchName}' due to: ${status.reasons.join(", ")}.`);
3010
+ }
3011
+ if (this.config.debug && status.details) {
3012
+ this.logDebugDetails(branchName, status.details);
1847
3013
  }
1848
3014
  }
1849
3015
  } else {
1850
- console.log("Step 3: No stale worktrees to prune.");
3016
+ this.logger.info("Step 3: No stale worktrees to prune.");
1851
3017
  }
1852
3018
  }
1853
3019
  logDebugDetails(branchName, details) {
1854
- console.log(`
3020
+ this.logger.info(`
1855
3021
  \u{1F50D} Debug details for '${branchName}':`);
1856
3022
  if (details.modifiedFiles > 0 && details.modifiedFilesList) {
1857
- console.log(` - Modified files (${details.modifiedFiles}):`);
1858
- details.modifiedFilesList.forEach((file) => console.log(` \u2022 ${file}`));
3023
+ this.logger.info(` - Modified files (${details.modifiedFiles}):`);
3024
+ details.modifiedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1859
3025
  }
1860
3026
  if (details.deletedFiles > 0 && details.deletedFilesList) {
1861
- console.log(` - Deleted files (${details.deletedFiles}):`);
1862
- details.deletedFilesList.forEach((file) => console.log(` \u2022 ${file}`));
3027
+ this.logger.info(` - Deleted files (${details.deletedFiles}):`);
3028
+ details.deletedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1863
3029
  }
1864
3030
  if (details.renamedFiles > 0 && details.renamedFilesList) {
1865
- console.log(` - Renamed files (${details.renamedFiles}):`);
1866
- details.renamedFilesList.forEach((file) => console.log(` \u2022 ${file.from} \u2192 ${file.to}`));
3031
+ this.logger.info(` - Renamed files (${details.renamedFiles}):`);
3032
+ details.renamedFilesList.forEach((file) => this.logger.info(` \u2022 ${file.from} \u2192 ${file.to}`));
1867
3033
  }
1868
3034
  if (details.createdFiles > 0 && details.createdFilesList) {
1869
- console.log(` - Created files (${details.createdFiles}):`);
1870
- details.createdFilesList.forEach((file) => console.log(` \u2022 ${file}`));
3035
+ this.logger.info(` - Created files (${details.createdFiles}):`);
3036
+ details.createdFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1871
3037
  }
1872
3038
  if (details.conflictedFiles > 0 && details.conflictedFilesList) {
1873
- console.log(` - Conflicted files (${details.conflictedFiles}):`);
1874
- details.conflictedFilesList.forEach((file) => console.log(` \u2022 ${file}`));
3039
+ this.logger.info(` - Conflicted files (${details.conflictedFiles}):`);
3040
+ details.conflictedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1875
3041
  }
1876
3042
  if (details.untrackedFiles > 0 && details.untrackedFilesList) {
1877
- console.log(` - Untracked files (not ignored) (${details.untrackedFiles}):`);
1878
- details.untrackedFilesList.forEach((file) => console.log(` \u2022 ${file}`));
3043
+ this.logger.info(` - Untracked files (not ignored) (${details.untrackedFiles}):`);
3044
+ details.untrackedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1879
3045
  }
1880
3046
  if (details.unpushedCommitCount !== void 0 && details.unpushedCommitCount > 0) {
1881
- console.log(` - Unpushed commits: ${details.unpushedCommitCount}`);
3047
+ this.logger.info(` - Unpushed commits: ${details.unpushedCommitCount}`);
1882
3048
  }
1883
3049
  if (details.stashCount !== void 0 && details.stashCount > 0) {
1884
- console.log(` - Stashed changes: ${details.stashCount}`);
3050
+ this.logger.info(` - Stashed changes: ${details.stashCount}`);
1885
3051
  }
1886
3052
  if (details.operationType) {
1887
- console.log(` - Operation in progress: ${details.operationType}`);
3053
+ this.logger.info(` - Operation in progress: ${details.operationType}`);
1888
3054
  }
1889
3055
  if (details.modifiedSubmodules && details.modifiedSubmodules.length > 0) {
1890
- console.log(` - Modified submodules (${details.modifiedSubmodules.length}):`);
1891
- details.modifiedSubmodules.forEach((submodule) => console.log(` \u2022 ${submodule}`));
3056
+ this.logger.info(` - Modified submodules (${details.modifiedSubmodules.length}):`);
3057
+ details.modifiedSubmodules.forEach((submodule) => this.logger.info(` \u2022 ${submodule}`));
1892
3058
  }
1893
- console.log("");
3059
+ this.logger.info("");
1894
3060
  }
1895
3061
  async fetchBranchByBranch() {
1896
- console.log("Fetching branches individually to isolate LFS errors...");
3062
+ this.logger.info("Fetching branches individually to isolate LFS errors...");
1897
3063
  const remoteBranches = await this.gitService.getRemoteBranches();
1898
- console.log(`Found ${remoteBranches.length} remote branches to fetch.`);
3064
+ this.logger.info(`Found ${remoteBranches.length} remote branches to fetch.`);
1899
3065
  const failedBranches = [];
1900
3066
  let successCount = 0;
1901
3067
  for (const branch of remoteBranches) {
@@ -1904,79 +3070,109 @@ var WorktreeSyncService = class {
1904
3070
  successCount++;
1905
3071
  } catch (error) {
1906
3072
  const errorMessage = getErrorMessage(error);
1907
- console.log(` \u26A0\uFE0F Failed to fetch branch '${branch}': ${errorMessage}`);
3073
+ this.logger.info(` \u26A0\uFE0F Failed to fetch branch '${branch}': ${errorMessage}`);
1908
3074
  failedBranches.push(branch);
1909
3075
  }
1910
3076
  }
1911
- console.log(`Branch-by-branch fetch completed: ${successCount}/${remoteBranches.length} successful`);
3077
+ this.logger.info(`Branch-by-branch fetch completed: ${successCount}/${remoteBranches.length} successful`);
1912
3078
  if (failedBranches.length > 0) {
1913
- console.log(`\u26A0\uFE0F Failed to fetch ${failedBranches.length} branches due to errors.`);
1914
- console.log(` These branches will be skipped: ${failedBranches.join(", ")}`);
3079
+ this.logger.info(`\u26A0\uFE0F Failed to fetch ${failedBranches.length} branches due to errors.`);
3080
+ this.logger.info(` These branches will be skipped: ${failedBranches.join(", ")}`);
1915
3081
  }
1916
3082
  }
3083
+ async updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer) {
3084
+ const maxConcurrent = this.config.parallelism?.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES;
3085
+ phaseTimer.startPhase("Phase 4: Update", maxConcurrent);
3086
+ await this.updateExistingWorktrees(worktrees, remoteBranches);
3087
+ const activeWorktrees = worktrees.filter((w) => remoteBranches.includes(w.branch));
3088
+ phaseTimer.setPhaseCount("Phase 4: Update", activeWorktrees.length);
3089
+ phaseTimer.endPhase();
3090
+ }
1917
3091
  async updateExistingWorktrees(worktrees, remoteBranches) {
1918
- const worktreesToUpdate = [];
1919
- console.log("Step 4: Checking for worktrees that need updates...");
1920
- const divergedDir = path5.join(this.config.worktreeDir, ".diverged");
3092
+ this.logger.info("Step 4: Checking for worktrees that need updates...");
3093
+ const divergedDir = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
1921
3094
  try {
1922
3095
  const diverged = await fs5.readdir(divergedDir);
1923
3096
  if (diverged.length > 0) {
1924
- console.log(`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path5.relative(process.cwd(), divergedDir)}`);
3097
+ this.logger.info(
3098
+ `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path5.relative(process.cwd(), divergedDir)}`
3099
+ );
1925
3100
  }
1926
3101
  } catch {
1927
3102
  }
1928
3103
  const activeWorktrees = worktrees.filter((w) => remoteBranches.includes(w.branch));
1929
- for (const worktree of activeWorktrees) {
1930
- try {
1931
- try {
1932
- await fs5.access(worktree.path);
1933
- } catch {
1934
- continue;
1935
- }
1936
- const hasOp = await this.gitService.hasOperationInProgress(worktree.path);
1937
- if (hasOp) {
1938
- continue;
1939
- }
1940
- const isClean = await this.gitService.checkWorktreeStatus(worktree.path);
1941
- if (!isClean) {
1942
- continue;
1943
- }
1944
- const canFastForward = await this.gitService.canFastForward(worktree.path, worktree.branch);
1945
- if (!canFastForward) {
1946
- await this.handleDivergedBranch(worktree);
1947
- continue;
1948
- }
1949
- const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
1950
- if (isBehind) {
1951
- worktreesToUpdate.push(worktree);
1952
- }
1953
- } catch (error) {
1954
- console.error(` - Error checking worktree '${worktree.branch}':`, error);
3104
+ const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
3105
+ const limit = pLimit(maxConcurrent);
3106
+ const checkResults = await Promise.allSettled(
3107
+ activeWorktrees.map(
3108
+ (worktree) => limit(async () => {
3109
+ try {
3110
+ await fs5.access(worktree.path);
3111
+ } catch {
3112
+ return null;
3113
+ }
3114
+ const hasOp = await this.gitService.hasOperationInProgress(worktree.path);
3115
+ if (hasOp) return null;
3116
+ const isClean = await this.gitService.checkWorktreeStatus(worktree.path);
3117
+ if (!isClean) return null;
3118
+ const canFastForward = await this.gitService.canFastForward(worktree.path, worktree.branch);
3119
+ if (!canFastForward) {
3120
+ const isAhead = await this.gitService.isLocalAheadOfRemote(worktree.path, worktree.branch);
3121
+ if (isAhead) {
3122
+ this.logger.info(`\u23ED\uFE0F Skipping '${worktree.branch}' - has unpushed commits`);
3123
+ return null;
3124
+ }
3125
+ await this.handleDivergedBranch(worktree);
3126
+ return null;
3127
+ }
3128
+ const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
3129
+ return isBehind ? worktree : null;
3130
+ })
3131
+ )
3132
+ );
3133
+ const worktreesToUpdate = [];
3134
+ for (const result of checkResults) {
3135
+ if (result.status === "fulfilled" && result.value) {
3136
+ worktreesToUpdate.push(result.value);
3137
+ } else if (result.status === "rejected") {
3138
+ this.logger.error(` - Error checking worktree:`, result.reason);
1955
3139
  }
1956
3140
  }
1957
3141
  if (worktreesToUpdate.length > 0) {
1958
- console.log(` - Found ${worktreesToUpdate.length} worktrees behind their upstream branches.`);
1959
- for (const worktree of worktreesToUpdate) {
1960
- try {
1961
- console.log(` - Updating worktree '${worktree.branch}'...`);
1962
- await this.gitService.updateWorktree(worktree.path);
1963
- console.log(` \u2705 Successfully updated '${worktree.branch}'.`);
1964
- } catch (error) {
1965
- const errorMessage = getErrorMessage(error);
1966
- if (errorMessage.includes("Not possible to fast-forward") || errorMessage.includes("fatal: Not possible to fast-forward, aborting") || errorMessage.includes("cannot fast-forward")) {
1967
- console.log(` \u26A0\uFE0F Branch '${worktree.branch}' cannot be fast-forwarded. Checking for divergence...`);
3142
+ this.logger.info(` - Found ${worktreesToUpdate.length} worktrees behind their upstream branches.`);
3143
+ const updateLimit = pLimit(
3144
+ this.config.parallelism?.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES
3145
+ );
3146
+ const updateResults = await Promise.allSettled(
3147
+ worktreesToUpdate.map(
3148
+ (worktree) => updateLimit(async () => {
1968
3149
  try {
1969
- await this.handleDivergedBranch(worktree);
1970
- } catch (divergedError) {
1971
- console.error(` \u274C Failed to handle diverged branch '${worktree.branch}':`, divergedError);
3150
+ this.logger.info(` - Updating worktree '${worktree.branch}'...`);
3151
+ await this.gitService.updateWorktree(worktree.path);
3152
+ this.logger.info(` \u2705 Successfully updated '${worktree.branch}'.`);
3153
+ } catch (error) {
3154
+ const errorMessage = getErrorMessage(error);
3155
+ if (ERROR_MESSAGES.FAST_FORWARD_FAILED.some((msg) => errorMessage.includes(msg))) {
3156
+ this.logger.info(
3157
+ ` \u26A0\uFE0F Branch '${worktree.branch}' cannot be fast-forwarded. Checking for divergence...`
3158
+ );
3159
+ try {
3160
+ await this.handleDivergedBranch(worktree);
3161
+ } catch (divergedError) {
3162
+ this.logger.error(` \u274C Failed to handle diverged branch '${worktree.branch}':`, divergedError);
3163
+ }
3164
+ } else {
3165
+ this.logger.error(` \u274C Failed to update '${worktree.branch}':`, error);
3166
+ }
3167
+ throw error;
1972
3168
  }
1973
- } else {
1974
- console.error(` \u274C Failed to update '${worktree.branch}':`, error);
1975
- }
1976
- }
1977
- }
3169
+ })
3170
+ )
3171
+ );
3172
+ const successCount = updateResults.filter((r) => r.status === "fulfilled").length;
3173
+ this.logger.info(` Updated ${successCount}/${worktreesToUpdate.length} worktrees successfully`);
1978
3174
  } else {
1979
- console.log(" - All worktrees are up to date.");
3175
+ this.logger.info(" - All worktrees are up to date.");
1980
3176
  }
1981
3177
  }
1982
3178
  async cleanupOrphanedDirectories(worktrees) {
@@ -1994,50 +3190,50 @@ var WorktreeSyncService = class {
1994
3190
  }
1995
3191
  }
1996
3192
  if (orphanedDirs.length > 0) {
1997
- console.log(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
3193
+ this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
1998
3194
  for (const dir of orphanedDirs) {
1999
3195
  const dirPath = path5.join(this.config.worktreeDir, dir);
2000
3196
  try {
2001
3197
  const stat4 = await fs5.stat(dirPath);
2002
3198
  if (stat4.isDirectory()) {
2003
3199
  await fs5.rm(dirPath, { recursive: true, force: true });
2004
- console.log(` - Removed orphaned directory: ${dir}`);
3200
+ this.logger.info(` - Removed orphaned directory: ${dir}`);
2005
3201
  }
2006
3202
  } catch (error) {
2007
- console.error(` - Failed to remove orphaned directory ${dir}:`, error);
3203
+ this.logger.error(` - Failed to remove orphaned directory ${dir}:`, error);
2008
3204
  }
2009
3205
  }
2010
3206
  }
2011
3207
  } catch (error) {
2012
- console.error("Error during orphaned directory cleanup:", error);
3208
+ this.logger.error("Error during orphaned directory cleanup:", error);
2013
3209
  }
2014
3210
  }
2015
3211
  async handleDivergedBranch(worktree) {
2016
- console.log(`\u26A0\uFE0F Branch '${worktree.branch}' has diverged from upstream. Analyzing...`);
3212
+ this.logger.info(`\u26A0\uFE0F Branch '${worktree.branch}' has diverged from upstream. Analyzing...`);
2017
3213
  const treesIdentical = await this.gitService.compareTreeContent(worktree.path, worktree.branch);
2018
3214
  if (treesIdentical) {
2019
- console.log(`\u2705 Branch '${worktree.branch}' was rebased but files are identical. Resetting to upstream...`);
3215
+ this.logger.info(`\u2705 Branch '${worktree.branch}' was rebased but files are identical. Resetting to upstream...`);
2020
3216
  await this.gitService.resetToUpstream(worktree.path, worktree.branch);
2021
- console.log(` Successfully updated '${worktree.branch}' to match upstream.`);
3217
+ this.logger.info(` Successfully updated '${worktree.branch}' to match upstream.`);
2022
3218
  } else {
2023
3219
  const hasLocalChanges = await this.hasLocalChangesSinceLastSync(worktree.path);
2024
3220
  if (!hasLocalChanges) {
2025
- console.log(
3221
+ this.logger.info(
2026
3222
  `\u2705 Branch '${worktree.branch}' has diverged but you made no local changes. Resetting to upstream...`
2027
3223
  );
2028
3224
  await this.gitService.resetToUpstream(worktree.path, worktree.branch);
2029
- console.log(` Successfully updated '${worktree.branch}' to match upstream.`);
3225
+ this.logger.info(` Successfully updated '${worktree.branch}' to match upstream.`);
2030
3226
  } else {
2031
- console.log(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
3227
+ this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
2032
3228
  const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
2033
3229
  const relativePath = path5.relative(process.cwd(), divergedPath);
2034
- console.log(` Moved to: ${relativePath}`);
2035
- console.log(` Your local changes are preserved. To review:`);
2036
- console.log(` cd ${relativePath}`);
2037
- console.log(` git diff origin/${worktree.branch}`);
3230
+ this.logger.info(` Moved to: ${relativePath}`);
3231
+ this.logger.info(` Your local changes are preserved. To review:`);
3232
+ this.logger.info(` cd ${relativePath}`);
3233
+ this.logger.info(` git diff origin/${worktree.branch}`);
2038
3234
  await this.gitService.removeWorktree(worktree.path);
2039
3235
  await this.gitService.addWorktree(worktree.branch, worktree.path);
2040
- console.log(` Created fresh worktree from upstream at: ${worktree.path}`);
3236
+ this.logger.info(` Created fresh worktree from upstream at: ${worktree.path}`);
2041
3237
  }
2042
3238
  }
2043
3239
  }
@@ -2054,9 +3250,9 @@ var WorktreeSyncService = class {
2054
3250
  }
2055
3251
  }
2056
3252
  async divergeWorktree(worktreePath, branchName) {
2057
- const divergedBaseDir = path5.join(this.config.worktreeDir, ".diverged");
3253
+ const divergedBaseDir = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
2058
3254
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2059
- const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
3255
+ const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
2060
3256
  const safeBranchName = branchName.replace(/\//g, "-");
2061
3257
  const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
2062
3258
  const divergedPath = path5.join(divergedBaseDir, divergedName);
@@ -2075,7 +3271,7 @@ var WorktreeSyncService = class {
2075
3271
  const metadata = {
2076
3272
  originalBranch: branchName,
2077
3273
  divergedAt: (/* @__PURE__ */ new Date()).toISOString(),
2078
- reason: "diverged-history-with-changes",
3274
+ reason: METADATA_CONSTANTS.DIVERGED_REASON,
2079
3275
  originalPath: worktreePath,
2080
3276
  localCommit: await this.gitService.getCurrentCommit(divergedPath),
2081
3277
  remoteCommit: await this.gitService.getRemoteCommit(`origin/${branchName}`),
@@ -2086,11 +3282,92 @@ var WorktreeSyncService = class {
2086
3282
 
2087
3283
  Original worktree location: ${worktreePath}`
2088
3284
  };
2089
- await fs5.writeFile(path5.join(divergedPath, ".diverged-info.json"), JSON.stringify(metadata, null, 2));
3285
+ await fs5.writeFile(
3286
+ path5.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
3287
+ JSON.stringify(metadata, null, 2)
3288
+ );
2090
3289
  return divergedPath;
2091
3290
  }
2092
3291
  };
2093
3292
 
3293
+ // src/services/file-copy.service.ts
3294
+ import * as fs6 from "fs/promises";
3295
+ import * as path6 from "path";
3296
+ import { glob } from "glob";
3297
+ var DEFAULT_IGNORE_PATTERNS = [
3298
+ "**/node_modules/**",
3299
+ "**/.git/**",
3300
+ "**/dist/**",
3301
+ "**/build/**",
3302
+ "**/.next/**",
3303
+ "**/coverage/**"
3304
+ ];
3305
+ var FileCopyService = class {
3306
+ /**
3307
+ * Copy files matching patterns from source to destination directory.
3308
+ * Skips files that already exist at destination.
3309
+ * Preserves directory structure relative to source.
3310
+ */
3311
+ async copyFiles(sourceDir, destDir, patterns) {
3312
+ const result = {
3313
+ copied: [],
3314
+ skipped: [],
3315
+ errors: []
3316
+ };
3317
+ if (!patterns || patterns.length === 0) {
3318
+ return result;
3319
+ }
3320
+ const filesToCopy = await this.expandPatterns(sourceDir, patterns);
3321
+ for (const relativePath of filesToCopy) {
3322
+ const sourcePath = path6.join(sourceDir, relativePath);
3323
+ const destPath = path6.join(destDir, relativePath);
3324
+ try {
3325
+ const copied = await this.copyFile(sourcePath, destPath);
3326
+ if (copied) {
3327
+ result.copied.push(relativePath);
3328
+ } else {
3329
+ result.skipped.push(relativePath);
3330
+ }
3331
+ } catch (error) {
3332
+ result.errors.push({
3333
+ file: relativePath,
3334
+ error: error instanceof Error ? error.message : String(error)
3335
+ });
3336
+ }
3337
+ }
3338
+ return result;
3339
+ }
3340
+ async expandPatterns(sourceDir, patterns) {
3341
+ const allFiles = /* @__PURE__ */ new Set();
3342
+ for (const pattern of patterns) {
3343
+ try {
3344
+ const matches = await glob(pattern, {
3345
+ cwd: sourceDir,
3346
+ nodir: true,
3347
+ dot: true,
3348
+ ignore: DEFAULT_IGNORE_PATTERNS
3349
+ });
3350
+ for (const match of matches) {
3351
+ allFiles.add(match);
3352
+ }
3353
+ } catch {
3354
+ }
3355
+ }
3356
+ return Array.from(allFiles);
3357
+ }
3358
+ async copyFile(sourcePath, destPath) {
3359
+ try {
3360
+ await fs6.access(destPath);
3361
+ return false;
3362
+ } catch {
3363
+ }
3364
+ const destDir = path6.dirname(destPath);
3365
+ await fs6.mkdir(destDir, { recursive: true });
3366
+ await fs6.copyFile(sourcePath, destPath);
3367
+ return true;
3368
+ }
3369
+ };
3370
+
2094
3371
  // src/utils/disk-space.ts
2095
3372
  import fastFolderSize from "fast-folder-size";
2096
3373
  async function calculateDirectorySize(dirPath) {
@@ -2139,9 +3416,8 @@ var InteractiveUIService = class {
2139
3416
  cronSchedule;
2140
3417
  cronJobs = [];
2141
3418
  repositoryCount;
2142
- originalConsoleLog;
2143
- originalConsoleWarn;
2144
- originalConsoleError;
3419
+ logBuffer = [];
3420
+ uiReady = false;
2145
3421
  constructor(syncServices, configPath, cronSchedule) {
2146
3422
  if (syncServices.length === 0) {
2147
3423
  throw new Error("InteractiveUIService requires at least one WorktreeSyncService");
@@ -2150,31 +3426,52 @@ var InteractiveUIService = class {
2150
3426
  this.configPath = configPath;
2151
3427
  this.cronSchedule = cronSchedule;
2152
3428
  this.repositoryCount = syncServices.length;
2153
- this.originalConsoleLog = console.log.bind(console);
2154
- this.originalConsoleWarn = console.warn.bind(console);
2155
- this.originalConsoleError = console.error.bind(console);
2156
- this.redirectConsole();
2157
3429
  this.setupCronJobs();
3430
+ this.startBufferFlushCheck();
2158
3431
  this.renderUI();
3432
+ this.injectLoggersIntoServices();
3433
+ setTimeout(() => {
3434
+ this.addLog("\u{1F680} sync-worktrees UI initialized", "info");
3435
+ }, 100);
3436
+ }
3437
+ startBufferFlushCheck() {
3438
+ const unsubscribe = appEvents.on("uiReady", () => {
3439
+ this.uiReady = true;
3440
+ this.flushLogBuffer();
3441
+ unsubscribe();
3442
+ });
2159
3443
  }
2160
- redirectConsole() {
2161
- console.log = (...args) => {
2162
- const message = args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ");
2163
- this.originalConsoleLog(message);
2164
- };
2165
- console.warn = (...args) => {
2166
- const message = args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ");
2167
- this.originalConsoleWarn(message);
2168
- };
2169
- console.error = (...args) => {
2170
- const message = args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ");
2171
- this.originalConsoleError(message);
3444
+ createOutputFn() {
3445
+ return (message, level) => {
3446
+ const uiLevel = level === "debug" ? "info" : level;
3447
+ this.addLog(message, uiLevel);
2172
3448
  };
2173
3449
  }
2174
- restoreConsole() {
2175
- console.log = this.originalConsoleLog;
2176
- console.warn = this.originalConsoleWarn;
2177
- console.error = this.originalConsoleError;
3450
+ injectLoggersIntoServices() {
3451
+ const outputFn = this.createOutputFn();
3452
+ for (const service of this.syncServices) {
3453
+ const config = service.config;
3454
+ service.updateLogger(
3455
+ new Logger({
3456
+ repoName: config.name,
3457
+ debug: config.debug,
3458
+ outputFn
3459
+ })
3460
+ );
3461
+ }
3462
+ }
3463
+ addLog(message, level = "info") {
3464
+ if (this.uiReady) {
3465
+ appEvents.emit("addLog", { message, level });
3466
+ } else {
3467
+ this.logBuffer.push({ message, level });
3468
+ }
3469
+ }
3470
+ flushLogBuffer() {
3471
+ for (const log of this.logBuffer) {
3472
+ appEvents.emit("addLog", { message: log.message, level: log.level });
3473
+ }
3474
+ this.logBuffer = [];
2178
3475
  }
2179
3476
  setupCronJobs() {
2180
3477
  if (!this.cronSchedule) {
@@ -2188,6 +3485,9 @@ var InteractiveUIService = class {
2188
3485
  const task = cron.schedule(schedule3, async () => {
2189
3486
  this.setStatus("syncing");
2190
3487
  try {
3488
+ if (!service.isInitialized()) {
3489
+ await service.initialize();
3490
+ }
2191
3491
  await service.sync();
2192
3492
  } catch (error) {
2193
3493
  console.error(`Error syncing: ${error.message}`);
@@ -2211,28 +3511,42 @@ var InteractiveUIService = class {
2211
3511
  this.app.unmount();
2212
3512
  }
2213
3513
  this.app = render(
2214
- /* @__PURE__ */ React4.createElement(
3514
+ /* @__PURE__ */ React7.createElement(
2215
3515
  App_default,
2216
3516
  {
2217
3517
  repositoryCount: this.repositoryCount,
2218
3518
  cronSchedule: this.cronSchedule,
2219
3519
  onManualSync: () => this.handleManualSync(),
2220
3520
  onReload: () => this.handleReload(),
2221
- onQuit: () => this.handleQuit()
3521
+ onQuit: () => this.handleQuit(),
3522
+ getRepositoryList: () => this.getRepositoryList(),
3523
+ getBranchesForRepo: (index) => this.getBranchesForRepo(index),
3524
+ getDefaultBranchForRepo: (index) => this.getDefaultBranchForRepo(index),
3525
+ createAndPushBranch: (repoIndex, baseBranch, branchName) => this.createAndPushBranch(repoIndex, baseBranch, branchName),
3526
+ getWorktreesForRepo: (index) => this.getWorktreesForRepo(index),
3527
+ openEditorInWorktree: (path11) => this.openEditorInWorktree(path11),
3528
+ copyBranchFiles: (repoIndex, baseBranch, targetBranch) => this.copyBranchFiles(repoIndex, baseBranch, targetBranch),
3529
+ createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName)
2222
3530
  }
2223
3531
  )
2224
3532
  );
2225
3533
  }
2226
3534
  async handleManualSync() {
3535
+ await this.triggerInitialSync();
3536
+ }
3537
+ async triggerInitialSync() {
2227
3538
  this.setStatus("syncing");
2228
3539
  try {
2229
3540
  for (const service of this.syncServices) {
3541
+ if (!service.isInitialized()) {
3542
+ await service.initialize();
3543
+ }
2230
3544
  await service.sync();
2231
3545
  }
2232
3546
  this.updateLastSyncTime();
2233
3547
  await this.calculateAndUpdateDiskSpace();
2234
3548
  } catch (error) {
2235
- console.error("Manual sync failed:", error);
3549
+ console.error("Sync failed:", error);
2236
3550
  } finally {
2237
3551
  this.setStatus("idle");
2238
3552
  }
@@ -2316,22 +3630,13 @@ var InteractiveUIService = class {
2316
3630
  }
2317
3631
  }
2318
3632
  updateLastSyncTime() {
2319
- const methods = globalThis.__inkAppMethods;
2320
- if (methods && methods.updateLastSyncTime) {
2321
- methods.updateLastSyncTime();
2322
- }
3633
+ appEvents.emit("updateLastSyncTime");
2323
3634
  }
2324
3635
  setStatus(status) {
2325
- const methods = globalThis.__inkAppMethods;
2326
- if (methods && methods.setStatus) {
2327
- methods.setStatus(status);
2328
- }
3636
+ appEvents.emit("setStatus", status);
2329
3637
  }
2330
3638
  setDiskSpace(diskSpace) {
2331
- const methods = globalThis.__inkAppMethods;
2332
- if (methods && methods.setDiskSpace) {
2333
- methods.setDiskSpace(diskSpace);
2334
- }
3639
+ appEvents.emit("setDiskSpace", diskSpace);
2335
3640
  }
2336
3641
  async calculateAndUpdateDiskSpace() {
2337
3642
  try {
@@ -2346,14 +3651,137 @@ var InteractiveUIService = class {
2346
3651
  this.setDiskSpace("N/A");
2347
3652
  }
2348
3653
  }
3654
+ getRepositoryList() {
3655
+ return this.syncServices.map((service, index) => ({
3656
+ index,
3657
+ name: service.config.name || `repo-${index}`,
3658
+ repoUrl: service.config.repoUrl
3659
+ }));
3660
+ }
3661
+ async getBranchesForRepo(repoIndex) {
3662
+ if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
3663
+ throw new Error(`Invalid repository index: ${repoIndex}`);
3664
+ }
3665
+ const service = this.syncServices[repoIndex];
3666
+ const gitService = service.getGitService();
3667
+ return gitService.getRemoteBranches();
3668
+ }
3669
+ getDefaultBranchForRepo(repoIndex) {
3670
+ if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
3671
+ throw new Error(`Invalid repository index: ${repoIndex}`);
3672
+ }
3673
+ const service = this.syncServices[repoIndex];
3674
+ const gitService = service.getGitService();
3675
+ return gitService.getDefaultBranch();
3676
+ }
3677
+ async createAndPushBranch(repoIndex, baseBranch, branchName) {
3678
+ if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
3679
+ return { success: false, finalName: branchName, error: `Invalid repository index: ${repoIndex}` };
3680
+ }
3681
+ const service = this.syncServices[repoIndex];
3682
+ const gitService = service.getGitService();
3683
+ try {
3684
+ let finalName = branchName;
3685
+ let suffix = 0;
3686
+ while (true) {
3687
+ const exists = await gitService.branchExists(finalName);
3688
+ if (!exists.local && !exists.remote) {
3689
+ break;
3690
+ }
3691
+ suffix++;
3692
+ finalName = `${branchName}-${suffix}`;
3693
+ }
3694
+ await gitService.createBranch(finalName, baseBranch);
3695
+ await gitService.pushBranch(finalName);
3696
+ return { success: true, finalName };
3697
+ } catch (error) {
3698
+ const errorMessage = error instanceof Error ? error.message : String(error);
3699
+ return { success: false, finalName: branchName, error: errorMessage };
3700
+ }
3701
+ }
3702
+ async getWorktreesForRepo(repoIndex) {
3703
+ if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
3704
+ throw new Error(`Invalid repository index: ${repoIndex}`);
3705
+ }
3706
+ const service = this.syncServices[repoIndex];
3707
+ const gitService = service.getGitService();
3708
+ return gitService.getWorktrees();
3709
+ }
3710
+ async createWorktreeForBranch(repoIndex, branchName) {
3711
+ if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
3712
+ throw new Error(`Invalid repository index: ${repoIndex}`);
3713
+ }
3714
+ const service = this.syncServices[repoIndex];
3715
+ const gitService = service.getGitService();
3716
+ const worktreeDir = service.config.worktreeDir;
3717
+ const worktreePath = path7.join(worktreeDir, branchName);
3718
+ await gitService.addWorktree(branchName, worktreePath);
3719
+ }
3720
+ openEditorInWorktree(worktreePath) {
3721
+ const editor = process.env.EDITOR || process.env.VISUAL || "code";
3722
+ try {
3723
+ const child = spawn(editor, [worktreePath], {
3724
+ detached: true,
3725
+ stdio: "ignore"
3726
+ });
3727
+ child.on("error", (err) => {
3728
+ this.addLog(`Failed to open editor '${editor}': ${err.message}`, "error");
3729
+ this.addLog("Set EDITOR or VISUAL environment variable to your preferred editor", "warn");
3730
+ });
3731
+ child.unref();
3732
+ return { success: true };
3733
+ } catch (err) {
3734
+ const errorMessage = err instanceof Error ? err.message : String(err);
3735
+ this.addLog(`Failed to open editor '${editor}': ${errorMessage}`, "error");
3736
+ return { success: false, error: errorMessage };
3737
+ }
3738
+ }
3739
+ async copyBranchFiles(repoIndex, baseBranch, targetBranch) {
3740
+ if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
3741
+ return;
3742
+ }
3743
+ const service = this.syncServices[repoIndex];
3744
+ const config = service.config;
3745
+ if (!config.filesToCopyOnBranchCreate?.length) {
3746
+ return;
3747
+ }
3748
+ const gitService = service.getGitService();
3749
+ const worktrees = await gitService.getWorktrees();
3750
+ const sourceWorktree = worktrees.find((w) => w.branch === baseBranch);
3751
+ const targetWorktree = worktrees.find((w) => w.branch === targetBranch);
3752
+ if (!sourceWorktree || !targetWorktree) {
3753
+ console.warn(`Could not find worktrees for file copy: source=${baseBranch}, target=${targetBranch}`);
3754
+ return;
3755
+ }
3756
+ const fileCopyService = new FileCopyService();
3757
+ try {
3758
+ const result = await fileCopyService.copyFiles(
3759
+ sourceWorktree.path,
3760
+ targetWorktree.path,
3761
+ config.filesToCopyOnBranchCreate
3762
+ );
3763
+ if (result.copied.length > 0) {
3764
+ console.log(`\u{1F4CB} Copied ${result.copied.length} file(s) to new branch: ${result.copied.join(", ")}`);
3765
+ }
3766
+ if (result.errors.length > 0) {
3767
+ console.warn(`\u26A0\uFE0F Failed to copy ${result.errors.length} file(s):`);
3768
+ for (const err of result.errors) {
3769
+ console.warn(` - ${err.file}: ${err.error}`);
3770
+ }
3771
+ }
3772
+ } catch (error) {
3773
+ console.error(`Failed to copy files to new branch: ${error}`);
3774
+ }
3775
+ }
2349
3776
  destroy() {
2350
3777
  this.cancelCronJobs();
2351
- this.restoreConsole();
2352
3778
  if (this.app) {
2353
3779
  this.app.unmount();
2354
3780
  this.app = null;
2355
3781
  }
2356
- delete globalThis.__inkAppMethods;
3782
+ appEvents.removeAllListeners();
3783
+ this.uiReady = false;
3784
+ this.logBuffer = [];
2357
3785
  }
2358
3786
  };
2359
3787
 
@@ -2412,6 +3840,10 @@ function parseArguments() {
2412
3840
  type: "boolean",
2413
3841
  description: "Enable debug mode to show detailed reasons why worktrees are not cleaned up.",
2414
3842
  default: false
3843
+ }).option("sync-on-start", {
3844
+ type: "boolean",
3845
+ description: "Run sync immediately when starting the interactive UI (config mode only).",
3846
+ default: false
2415
3847
  }).help().alias("help", "h").parseSync();
2416
3848
  return {
2417
3849
  config: argv.config,
@@ -2425,7 +3857,8 @@ function parseArguments() {
2425
3857
  branchMaxAge: argv.branchMaxAge,
2426
3858
  skipLfs: argv.skipLfs,
2427
3859
  noUpdateExisting: argv["no-update-existing"],
2428
- debug: argv.debug
3860
+ debug: argv.debug,
3861
+ syncOnStart: argv["sync-on-start"]
2429
3862
  };
2430
3863
  }
2431
3864
  function isInteractiveMode(config) {
@@ -2463,12 +3896,12 @@ function reconstructCliCommand(config) {
2463
3896
  }
2464
3897
 
2465
3898
  // src/utils/interactive.ts
2466
- import * as path7 from "path";
3899
+ import * as path9 from "path";
2467
3900
  import { confirm, input, select } from "@inquirer/prompts";
2468
3901
 
2469
3902
  // src/utils/config-generator.ts
2470
- import * as fs6 from "fs/promises";
2471
- import * as path6 from "path";
3903
+ import * as fs7 from "fs/promises";
3904
+ import * as path8 from "path";
2472
3905
  function serializeToESM(obj, indent = 0) {
2473
3906
  const spaces = " ".repeat(indent);
2474
3907
  const innerSpaces = " ".repeat(indent + 2);
@@ -2498,9 +3931,9 @@ ${spaces}}`;
2498
3931
  return String(obj);
2499
3932
  }
2500
3933
  async function generateConfigFile(config, configPath) {
2501
- const configDir = path6.dirname(configPath);
2502
- await fs6.mkdir(configDir, { recursive: true });
2503
- const worktreeDirRelative = path6.relative(configDir, config.worktreeDir);
3934
+ const configDir = path8.dirname(configPath);
3935
+ await fs7.mkdir(configDir, { recursive: true });
3936
+ const worktreeDirRelative = path8.relative(configDir, config.worktreeDir);
2504
3937
  const useRelativeWorktree = !worktreeDirRelative.startsWith("../../../");
2505
3938
  const repoName = extractRepoNameFromUrl(config.repoUrl);
2506
3939
  const repository = {
@@ -2509,7 +3942,7 @@ async function generateConfigFile(config, configPath) {
2509
3942
  worktreeDir: useRelativeWorktree ? `./${worktreeDirRelative}` : config.worktreeDir
2510
3943
  };
2511
3944
  if (config.bareRepoDir) {
2512
- const bareRepoDirRelative = path6.relative(configDir, config.bareRepoDir);
3945
+ const bareRepoDirRelative = path8.relative(configDir, config.bareRepoDir);
2513
3946
  const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
2514
3947
  repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : config.bareRepoDir;
2515
3948
  }
@@ -2527,10 +3960,10 @@ async function generateConfigFile(config, configPath) {
2527
3960
 
2528
3961
  export default ${serializeToESM(configObject)};
2529
3962
  `;
2530
- await fs6.writeFile(configPath, configContent, "utf-8");
3963
+ await fs7.writeFile(configPath, configContent, "utf-8");
2531
3964
  }
2532
3965
  function getDefaultConfigPath() {
2533
- return path6.join(process.cwd(), "sync-worktrees.config.js");
3966
+ return path8.join(process.cwd(), "sync-worktrees.config.js");
2534
3967
  }
2535
3968
 
2536
3969
  // src/utils/interactive.ts
@@ -2572,8 +4005,8 @@ async function promptForConfig(partialConfig) {
2572
4005
  if (!worktreeDir.trim() && defaultWorktreeDir) {
2573
4006
  worktreeDir = defaultWorktreeDir;
2574
4007
  }
2575
- if (!path7.isAbsolute(worktreeDir)) {
2576
- worktreeDir = path7.resolve(worktreeDir);
4008
+ if (!path9.isAbsolute(worktreeDir)) {
4009
+ worktreeDir = path9.resolve(worktreeDir);
2577
4010
  }
2578
4011
  }
2579
4012
  let bareRepoDir = partialConfig.bareRepoDir;
@@ -2592,8 +4025,8 @@ async function promptForConfig(partialConfig) {
2592
4025
  return true;
2593
4026
  }
2594
4027
  });
2595
- if (!path7.isAbsolute(bareRepoDir)) {
2596
- bareRepoDir = path7.resolve(bareRepoDir);
4028
+ if (!path9.isAbsolute(bareRepoDir)) {
4029
+ bareRepoDir = path9.resolve(bareRepoDir);
2597
4030
  }
2598
4031
  }
2599
4032
  let runOnce = partialConfig.runOnce;
@@ -2664,8 +4097,8 @@ async function promptForConfig(partialConfig) {
2664
4097
  return true;
2665
4098
  }
2666
4099
  });
2667
- if (!path7.isAbsolute(configPath)) {
2668
- configPath = path7.resolve(configPath);
4100
+ if (!path9.isAbsolute(configPath)) {
4101
+ configPath = path9.resolve(configPath);
2669
4102
  }
2670
4103
  try {
2671
4104
  await generateConfigFile(finalConfig, configPath);
@@ -2673,7 +4106,7 @@ async function promptForConfig(partialConfig) {
2673
4106
  \u2705 Configuration saved to: ${configPath}`);
2674
4107
  console.log(`
2675
4108
  \u{1F4A1} You can now use this config file with:`);
2676
- console.log(` sync-worktrees --config ${path7.relative(process.cwd(), configPath)}`);
4109
+ console.log(` sync-worktrees --config ${path9.relative(process.cwd(), configPath)}`);
2677
4110
  console.log("");
2678
4111
  } catch (error) {
2679
4112
  console.error(`
@@ -2685,14 +4118,18 @@ async function promptForConfig(partialConfig) {
2685
4118
 
2686
4119
  // src/index.ts
2687
4120
  async function runSingleRepository(config) {
2688
- console.log("\n\u{1F4CB} CLI Command (for future reference):");
2689
- console.log(` ${reconstructCliCommand(config)}`);
2690
- console.log("");
4121
+ const logger = Logger.createDefault(void 0, config.debug);
4122
+ logger.info("\n\u{1F4CB} CLI Command (for future reference):");
4123
+ logger.info(` ${reconstructCliCommand(config)}`);
4124
+ logger.info("");
4125
+ if (!config.logger) {
4126
+ config.logger = logger;
4127
+ }
2691
4128
  const syncService = new WorktreeSyncService(config);
2692
4129
  try {
2693
4130
  await syncService.initialize();
2694
4131
  if (config.runOnce) {
2695
- console.log("Running the sync process once as requested by --runOnce flag.");
4132
+ logger.info("Running the sync process once as requested by --runOnce flag.");
2696
4133
  await syncService.sync();
2697
4134
  } else {
2698
4135
  const uiService = new InteractiveUIService([syncService], void 0, config.cronSchedule);
@@ -2706,43 +4143,76 @@ async function runSingleRepository(config) {
2706
4143
  uiService.updateLastSyncTime();
2707
4144
  void uiService.calculateAndUpdateDiskSpace();
2708
4145
  } catch (error) {
2709
- console.error(`Error during scheduled sync: ${error.message}`);
4146
+ logger.error(`Error during scheduled sync: ${error.message}`, error);
2710
4147
  uiService.setStatus("idle");
2711
4148
  }
2712
4149
  });
2713
4150
  }
2714
4151
  } catch (error) {
2715
- console.error("\u274C Fatal Error during initialization:", error.message);
4152
+ logger.error("\u274C Fatal Error during initialization:", error);
2716
4153
  process.exit(1);
2717
4154
  }
2718
4155
  }
2719
- async function runMultipleRepositories(repositories, runOnce, configPath) {
4156
+ async function runMultipleRepositories(repositories, runOnce, configPath, maxParallel, syncOnStart) {
2720
4157
  const services = /* @__PURE__ */ new Map();
2721
- console.log(`
4158
+ const globalLogger = Logger.createDefault();
4159
+ const limit = pLimit2(maxParallel ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
4160
+ if (runOnce) {
4161
+ globalLogger.info(`
2722
4162
  \u{1F504} Syncing ${repositories.length} repositories...`);
2723
- for (const repoConfig of repositories) {
2724
- console.log(`
4163
+ const initResults = await Promise.allSettled(
4164
+ repositories.map(
4165
+ (repoConfig) => limit(async () => {
4166
+ const repoLogger = Logger.createDefault(repoConfig.name, repoConfig.debug);
4167
+ repoLogger.info(`
2725
4168
  \u{1F4E6} Repository: ${repoConfig.name}`);
2726
- console.log(` URL: ${repoConfig.repoUrl}`);
2727
- console.log(` Worktrees: ${repoConfig.worktreeDir}`);
2728
- if (repoConfig.bareRepoDir) {
2729
- console.log(` Bare repo: ${repoConfig.bareRepoDir}`);
4169
+ repoLogger.info(` URL: ${repoConfig.repoUrl}`);
4170
+ repoLogger.info(` Worktrees: ${repoConfig.worktreeDir}`);
4171
+ if (repoConfig.bareRepoDir) {
4172
+ repoLogger.info(` Bare repo: ${repoConfig.bareRepoDir}`);
4173
+ }
4174
+ if (!repoConfig.logger) {
4175
+ repoConfig.logger = repoLogger;
4176
+ }
4177
+ const syncService = new WorktreeSyncService(repoConfig);
4178
+ await syncService.initialize();
4179
+ return { name: repoConfig.name, service: syncService };
4180
+ })
4181
+ )
4182
+ );
4183
+ const servicesToSync = [];
4184
+ for (const result of initResults) {
4185
+ if (result.status === "fulfilled") {
4186
+ services.set(result.value.name, result.value.service);
4187
+ servicesToSync.push(result.value);
4188
+ } else {
4189
+ globalLogger.error(`\u274C Failed to initialize repository:`, result.reason);
4190
+ }
2730
4191
  }
2731
- const syncService = new WorktreeSyncService(repoConfig);
2732
- services.set(repoConfig.name, syncService);
2733
- try {
2734
- await syncService.initialize();
2735
- await syncService.sync();
2736
- } catch (error) {
2737
- console.error(`\u274C Error syncing repository '${repoConfig.name}':`, error.message);
4192
+ const syncResults = await Promise.allSettled(
4193
+ servicesToSync.map(
4194
+ ({ name, service }) => limit(async () => {
4195
+ try {
4196
+ await service.sync();
4197
+ } catch (error) {
4198
+ globalLogger.error(`\u274C Error syncing repository '${name}':`, error);
4199
+ throw error;
4200
+ }
4201
+ })
4202
+ )
4203
+ );
4204
+ const successCount = syncResults.filter((r) => r.status === "fulfilled").length;
4205
+ globalLogger.info(`
4206
+ \u2705 Successfully synced ${successCount}/${servicesToSync.length} repositories`);
4207
+ } else {
4208
+ for (const repoConfig of repositories) {
4209
+ const syncService = new WorktreeSyncService(repoConfig);
4210
+ services.set(repoConfig.name, syncService);
2738
4211
  }
2739
- }
2740
- if (!runOnce) {
2741
4212
  const uniqueSchedules = [...new Set(repositories.map((r) => r.cronSchedule))];
2742
4213
  const displaySchedule = uniqueSchedules.length === 1 ? uniqueSchedules[0] : void 0;
2743
4214
  const allServices = Array.from(services.values());
2744
4215
  const uiService = new InteractiveUIService(allServices, configPath, displaySchedule);
2745
- uiService.updateLastSyncTime();
2746
4216
  void uiService.calculateAndUpdateDiskSpace();
2747
4217
  const cronJobs = /* @__PURE__ */ new Map();
2748
4218
  for (const repoConfig of repositories) {
@@ -2753,25 +4223,35 @@ async function runMultipleRepositories(repositories, runOnce, configPath) {
2753
4223
  cron2.schedule(repoConfig.cronSchedule, async () => {
2754
4224
  const reposToSync = repositories.filter((r) => r.cronSchedule === repoConfig.cronSchedule);
2755
4225
  uiService.setStatus("syncing");
2756
- for (const repo of reposToSync) {
2757
- const service = services.get(repo.name);
2758
- if (!service) continue;
2759
- console.log(`Running scheduled sync for: ${repo.name}`);
2760
- try {
2761
- await service.sync();
2762
- } catch (error) {
2763
- console.error(`Error syncing '${repo.name}': ${error.message}`);
2764
- }
2765
- }
4226
+ await Promise.allSettled(
4227
+ reposToSync.map(
4228
+ (repo) => limit(async () => {
4229
+ const service = services.get(repo.name);
4230
+ if (!service) return;
4231
+ uiService.addLog(`Running scheduled sync for: ${repo.name}`);
4232
+ try {
4233
+ if (!service.isInitialized()) {
4234
+ await service.initialize();
4235
+ }
4236
+ await service.sync();
4237
+ } catch (error) {
4238
+ uiService.addLog(`Error syncing '${repo.name}': ${error}`, "error");
4239
+ }
4240
+ })
4241
+ )
4242
+ );
2766
4243
  uiService.updateLastSyncTime();
2767
4244
  void uiService.calculateAndUpdateDiskSpace();
2768
4245
  });
2769
4246
  }
2770
4247
  }
2771
- console.log(`All ${repositories.length} repositories scheduled`);
4248
+ uiService.addLog(`\u{1F4CB} ${repositories.length} repositories configured`);
2772
4249
  for (const [schedule3] of cronJobs) {
2773
4250
  const repoCount = repositories.filter((r) => r.cronSchedule === schedule3).length;
2774
- console.log(`${schedule3}: ${repoCount} repository(ies)`);
4251
+ uiService.addLog(`\u23F0 ${schedule3}: ${repoCount} repository(ies)`);
4252
+ }
4253
+ if (syncOnStart) {
4254
+ await uiService.triggerInitialSync();
2775
4255
  }
2776
4256
  }
2777
4257
  }
@@ -2779,7 +4259,7 @@ async function listRepositories(configPath, filter) {
2779
4259
  const configLoader = new ConfigLoaderService();
2780
4260
  try {
2781
4261
  const configFile = await configLoader.loadConfigFile(configPath);
2782
- const configDir = path8.dirname(path8.resolve(configPath));
4262
+ const configDir = path10.dirname(path10.resolve(configPath));
2783
4263
  let repositories = configFile.repositories.map(
2784
4264
  (repo) => configLoader.resolveRepositoryConfig(repo, configFile.defaults, configDir, configFile.retry)
2785
4265
  );
@@ -2820,7 +4300,7 @@ async function main() {
2820
4300
  }
2821
4301
  try {
2822
4302
  const configFile = await configLoader.loadConfigFile(options.config);
2823
- const configDir = path8.dirname(path8.resolve(options.config));
4303
+ const configDir = path10.dirname(path10.resolve(options.config));
2824
4304
  let repositories = configFile.repositories.map(
2825
4305
  (repo) => configLoader.resolveRepositoryConfig(repo, configFile.defaults, configDir, configFile.retry)
2826
4306
  );
@@ -2844,7 +4324,8 @@ async function main() {
2844
4324
  debug: true
2845
4325
  }));
2846
4326
  }
2847
- await runMultipleRepositories(repositories, globalRunOnce, options.config);
4327
+ const maxParallel = configFile.parallelism?.maxRepositories ?? configFile.defaults?.parallelism?.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
4328
+ await runMultipleRepositories(repositories, globalRunOnce, options.config, maxParallel, options.syncOnStart);
2848
4329
  } catch (error) {
2849
4330
  if (error instanceof Error && error.message.includes("Config file not found")) {
2850
4331
  console.error(`