sync-worktrees 4.0.0 → 4.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
@@ -4,7 +4,7 @@
4
4
  import { realpathSync as realpathSync2 } from "fs";
5
5
  import * as path17 from "path";
6
6
  import { fileURLToPath } from "url";
7
- import pLimit3 from "p-limit";
7
+ import pLimit4 from "p-limit";
8
8
 
9
9
  // src/constants.ts
10
10
  var GIT_CONSTANTS = {
@@ -141,6 +141,8 @@ var SyncWorktreesError = class extends Error {
141
141
  Caused by: ${cause.stack}`;
142
142
  }
143
143
  }
144
+ code;
145
+ cause;
144
146
  };
145
147
  var GitError = class extends SyncWorktreesError {
146
148
  constructor(message, code, cause) {
@@ -163,6 +165,8 @@ var WorktreeNotCleanError = class extends WorktreeError {
163
165
  this.path = path18;
164
166
  this.reasons = reasons;
165
167
  }
168
+ path;
169
+ reasons;
166
170
  };
167
171
  var ConfigError = class extends SyncWorktreesError {
168
172
  constructor(message, code, cause) {
@@ -175,18 +179,22 @@ var ConfigValidationError = class extends ConfigError {
175
179
  this.field = field;
176
180
  this.reason = reason;
177
181
  }
182
+ field;
183
+ reason;
178
184
  };
179
185
  var ConfigFileNotFoundError = class extends ConfigError {
180
186
  constructor(configPath) {
181
187
  super(`Config file not found: ${configPath}`, "FILE_NOT_FOUND");
182
188
  this.configPath = configPath;
183
189
  }
190
+ configPath;
184
191
  };
185
192
  var ConfigFileExistsError = class extends ConfigError {
186
193
  constructor(configPath) {
187
194
  super(`Config file already exists: ${configPath}`, "FILE_EXISTS");
188
195
  this.configPath = configPath;
189
196
  }
197
+ configPath;
190
198
  };
191
199
 
192
200
  // src/services/config-loader.service.ts
@@ -814,19 +822,28 @@ import React8 from "react";
814
822
  import * as path14 from "path";
815
823
  import { render } from "ink";
816
824
  import * as cron2 from "node-cron";
817
- import pLimit2 from "p-limit";
825
+ import pLimit3 from "p-limit";
818
826
  import { spawn as spawn2, spawnSync } from "child_process";
819
827
  import { existsSync as existsSync2 } from "fs";
820
828
 
821
829
  // src/components/App.tsx
822
830
  import React7, { useState as useState6, useEffect as useEffect6, useCallback as useCallback4, useRef as useRef5 } from "react";
823
- import { Box as Box7, useInput as useInput6, useStdout } from "ink";
831
+ import { Box as Box7, useInput as useInput6, useWindowSize } from "ink";
824
832
 
825
833
  // src/components/StatusBar.tsx
826
834
  import React, { useState, useEffect } from "react";
827
835
  import { Box, Text } from "ink";
828
836
  import { CronExpressionParser } from "cron-parser";
829
- var StatusBar = ({ status, repositoryCount, lastSyncTime, cronSchedule, diskSpaceUsed }) => {
837
+ var StatusBar = ({
838
+ status,
839
+ syncProgressEntries = [],
840
+ activeOps = [],
841
+ maxProgressLines = 2,
842
+ repositoryCount,
843
+ lastSyncTime,
844
+ cronSchedule,
845
+ diskSpaceUsed
846
+ }) => {
830
847
  const [nextSyncTime, setNextSyncTime] = useState(null);
831
848
  useEffect(() => {
832
849
  if (!cronSchedule) {
@@ -856,7 +873,14 @@ var StatusBar = ({ status, repositoryCount, lastSyncTime, cronSchedule, diskSpac
856
873
  const getStatusIcon = () => {
857
874
  return status === "syncing" ? "\u27F3" : "\u2713";
858
875
  };
859
- 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" }, "w"), "tree", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "r"), "eload", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "?"), "help", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "q"), "uit"))));
876
+ const formatProgress = (syncProgress) => `[${syncProgress.repo}] ${syncProgress.message}`;
877
+ const progressLineCount = Math.max(1, maxProgressLines);
878
+ const visibleProgress = syncProgressEntries.slice(-progressLineCount);
879
+ 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)))), status === "syncing" && Array.from({ length: progressLineCount }).map((_, index) => {
880
+ const entry = visibleProgress[index];
881
+ const message = entry ? formatProgress(entry) : index === 0 ? "waiting for progress events" : "";
882
+ return /* @__PURE__ */ React.createElement(Box, { key: index }, /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, message ? "Progress: " : " ", message && /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, message)));
883
+ }), activeOps.map((label, index) => /* @__PURE__ */ React.createElement(Box, { key: `op-${index}` }, /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "\u23F3 "), /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, label)))), /* @__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" }, "w"), "tree", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "r"), "eload", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "?"), "help", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "q"), "uit"))));
860
884
  };
861
885
  var StatusBar_default = StatusBar;
862
886
 
@@ -875,7 +899,7 @@ var HelpModal_default = HelpModal;
875
899
 
876
900
  // src/components/BranchCreationWizard.tsx
877
901
  import React3, { useState as useState2, useEffect as useEffect2, useCallback, useMemo, useRef } from "react";
878
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
902
+ import { Box as Box3, Text as Text3, useInput as useInput2, usePaste } from "ink";
879
903
 
880
904
  // src/utils/git-validation.ts
881
905
  function isValidGitBranchName(name) {
@@ -1141,6 +1165,17 @@ var BranchCreationWizard = ({
1141
1165
  onComplete(result?.success ?? false);
1142
1166
  }
1143
1167
  });
1168
+ usePaste((text) => {
1169
+ if (step === "SELECT_PROJECT") {
1170
+ setProjectFilter((prev) => prev + text);
1171
+ setSelectedProjectIndex(0);
1172
+ } else if (step === "SELECT_BRANCH") {
1173
+ setBranchFilter((prev) => prev + text);
1174
+ setSelectedBranchIndex(0);
1175
+ } else if (step === "ENTER_NAME") {
1176
+ setBranchName((prev) => prev + text.replace(/[^a-zA-Z0-9/._-]/g, ""));
1177
+ }
1178
+ });
1144
1179
  const getStepNumber = () => {
1145
1180
  if (repositories.length === 1) {
1146
1181
  if (step === "SELECT_BRANCH") return 1;
@@ -1234,7 +1269,7 @@ var BranchCreationWizard_default = BranchCreationWizard;
1234
1269
 
1235
1270
  // src/components/OpenEditorWizard.tsx
1236
1271
  import React4, { useState as useState3, useEffect as useEffect3, useMemo as useMemo2, useCallback as useCallback2, useRef as useRef2 } from "react";
1237
- import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
1272
+ import { Box as Box4, Text as Text4, useInput as useInput3, usePaste as usePaste2 } from "ink";
1238
1273
  var OpenEditorWizard = ({
1239
1274
  repositories,
1240
1275
  getWorktreesForRepo,
@@ -1352,6 +1387,15 @@ var OpenEditorWizard = ({
1352
1387
  onClose();
1353
1388
  }
1354
1389
  });
1390
+ usePaste2((text) => {
1391
+ if (step === "SELECT_PROJECT") {
1392
+ setProjectFilter((prev) => prev + text);
1393
+ setSelectedProjectIndex(0);
1394
+ } else if (step === "SELECT_WORKTREE") {
1395
+ setWorktreeFilter((prev) => prev + text);
1396
+ setSelectedWorktreeIndex(0);
1397
+ }
1398
+ });
1355
1399
  const getStepNumber = () => {
1356
1400
  if (repositories.length === 1) {
1357
1401
  return 1;
@@ -1423,7 +1467,7 @@ var OpenEditorWizard_default = OpenEditorWizard;
1423
1467
 
1424
1468
  // src/components/WorktreeStatusView.tsx
1425
1469
  import React5, { useState as useState4, useEffect as useEffect4, useMemo as useMemo3, useCallback as useCallback3, useRef as useRef3 } from "react";
1426
- import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
1470
+ import { Box as Box5, Text as Text5, useInput as useInput4, usePaste as usePaste3 } from "ink";
1427
1471
 
1428
1472
  // src/utils/lfs-error.ts
1429
1473
  function getErrorMessage(error) {
@@ -1526,6 +1570,7 @@ var formatDivergedDate = (dateStr) => {
1526
1570
  var WorktreeStatusView = ({
1527
1571
  repositories,
1528
1572
  getWorktreeStatusForRepo,
1573
+ getRepositoryDiskUsage,
1529
1574
  getDivergedDirectoriesForRepo,
1530
1575
  deleteDivergedDirectory,
1531
1576
  onClose
@@ -1540,6 +1585,8 @@ var WorktreeStatusView = ({
1540
1585
  const [entryFilter, setEntryFilter] = useState4("");
1541
1586
  const [expandedEntry, setExpandedEntry] = useState4(null);
1542
1587
  const [loading, setLoading] = useState4(false);
1588
+ const [repoDiskUsage, setRepoDiskUsage] = useState4({});
1589
+ const requestedDiskUsageRef = useRef3(/* @__PURE__ */ new Set());
1543
1590
  const [confirmDelete, setConfirmDelete] = useState4(null);
1544
1591
  const [deleting, setDeleting] = useState4(false);
1545
1592
  const [error, setError] = useState4(null);
@@ -1595,6 +1642,29 @@ var WorktreeStatusView = ({
1595
1642
  },
1596
1643
  [getWorktreeStatusForRepo, getDivergedDirectoriesForRepo]
1597
1644
  );
1645
+ useEffect4(() => {
1646
+ if (!getRepositoryDiskUsage) return void 0;
1647
+ let cancelled = false;
1648
+ const indexesToLoad = repositories.map((repo) => repo.index).filter((repoIndex) => !requestedDiskUsageRef.current.has(repoIndex));
1649
+ if (indexesToLoad.length === 0) return void 0;
1650
+ for (const repoIndex of indexesToLoad) {
1651
+ requestedDiskUsageRef.current.add(repoIndex);
1652
+ setRepoDiskUsage((prev) => ({ ...prev, [repoIndex]: { status: "loading" } }));
1653
+ void getRepositoryDiskUsage(repoIndex).then((usage) => {
1654
+ if (cancelled) return;
1655
+ setRepoDiskUsage((prev) => ({ ...prev, [repoIndex]: { status: "ready", usage } }));
1656
+ }).catch(() => {
1657
+ if (cancelled) return;
1658
+ setRepoDiskUsage((prev) => ({
1659
+ ...prev,
1660
+ [repoIndex]: { status: "error" }
1661
+ }));
1662
+ });
1663
+ }
1664
+ return () => {
1665
+ cancelled = true;
1666
+ };
1667
+ }, [repositories, getRepositoryDiskUsage]);
1598
1668
  useEffect4(() => {
1599
1669
  if (step === "VIEW_STATUS" && entries.length === 0 && !loading && selectedRepoIndexRef.current >= 0) {
1600
1670
  loadStatus(selectedRepoIndexRef.current);
@@ -1668,11 +1738,11 @@ var WorktreeStatusView = ({
1668
1738
  } else if (key.downArrow) {
1669
1739
  setSelectedProjectIndex((prev) => Math.min(filteredProjects.length - 1, prev + 1));
1670
1740
  } else if (key.return && filteredProjects.length > 0) {
1671
- const selectedRepo = filteredProjects[selectedProjectIndex];
1672
- if (selectedRepo) {
1673
- selectedRepoIndexRef.current = selectedRepo.index;
1741
+ const selectedRepo2 = filteredProjects[selectedProjectIndex];
1742
+ if (selectedRepo2) {
1743
+ selectedRepoIndexRef.current = selectedRepo2.index;
1674
1744
  setStep("VIEW_STATUS");
1675
- loadStatus(selectedRepo.index);
1745
+ loadStatus(selectedRepo2.index);
1676
1746
  }
1677
1747
  } else if (key.backspace || key.delete) {
1678
1748
  setProjectFilter((prev) => prev.slice(0, -1));
@@ -1703,6 +1773,17 @@ var WorktreeStatusView = ({
1703
1773
  onClose();
1704
1774
  }
1705
1775
  });
1776
+ usePaste3((text) => {
1777
+ if (confirmDelete !== null) return;
1778
+ if (step === "SELECT_PROJECT") {
1779
+ setProjectFilter((prev) => prev + text);
1780
+ setSelectedProjectIndex(0);
1781
+ } else if (step === "VIEW_STATUS" && !loading) {
1782
+ setEntryFilter((prev) => prev + text);
1783
+ setSelectedEntryIndex(0);
1784
+ setExpandedEntry(null);
1785
+ }
1786
+ });
1706
1787
  const getStepNumber = () => {
1707
1788
  if (repositories.length === 1) return 1;
1708
1789
  return step === "SELECT_PROJECT" ? 1 : 2;
@@ -1720,7 +1801,7 @@ var WorktreeStatusView = ({
1720
1801
  return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React5.createElement(Text5, null, "Select repository:"), /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Text5, null, "Filter: "), /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, projectFilter || "_"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " ", "(", filteredProjects.length, "/", repositories.length, " matches)")), /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, filteredProjects.length === 0 ? /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, "No matches") : /* @__PURE__ */ React5.createElement(React5.Fragment, null, startIdx > 0 && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " ..."), visibleProjects.map((repo, idx) => {
1721
1802
  const actualIdx = startIdx + idx;
1722
1803
  const isSelected = actualIdx === selectedProjectIndex;
1723
- return /* @__PURE__ */ React5.createElement(Box5, { key: repo.index }, /* @__PURE__ */ React5.createElement(Text5, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", repo.name));
1804
+ return /* @__PURE__ */ React5.createElement(Box5, { key: repo.index }, /* @__PURE__ */ React5.createElement(Text5, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " "), /* @__PURE__ */ React5.createElement(Box5, { width: 38 }, /* @__PURE__ */ React5.createElement(Text5, { color: isSelected ? "cyan" : void 0 }, repo.name)), getRepositoryDiskUsage && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " "), renderRepositoryDiskUsage(repo.index));
1724
1805
  }), endIdx < filteredProjects.length && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " ..."))));
1725
1806
  };
1726
1807
  const renderDetailPanel = (entry) => {
@@ -1731,6 +1812,17 @@ var WorktreeStatusView = ({
1731
1812
  const renderDivergedDetailPanel = (entry) => {
1732
1813
  return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", marginLeft: 4, marginTop: 0, marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "Path: ", entry.path), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " Original branch: ", entry.originalBranch), entry.divergedAt && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " Diverged: ", entry.divergedAt), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " Size: ", entry.sizeFormatted));
1733
1814
  };
1815
+ const renderRepositoryDiskUsage = (repoIndex) => {
1816
+ if (!getRepositoryDiskUsage) return null;
1817
+ const state = repoDiskUsage[repoIndex];
1818
+ if (!state || state.status === "loading") {
1819
+ return /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "Size: calculating...");
1820
+ }
1821
+ if (state.status === "error") {
1822
+ return /* @__PURE__ */ React5.createElement(Text5, { color: "red" }, "Size: N/A");
1823
+ }
1824
+ return /* @__PURE__ */ React5.createElement(Text5, null, "Size: ", /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, state.usage.sizeFormatted));
1825
+ };
1734
1826
  const renderStatusList = () => {
1735
1827
  if (loading) {
1736
1828
  return /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, "Loading worktree status...");
@@ -1784,7 +1876,8 @@ var WorktreeStatusView = ({
1784
1876
  }
1785
1877
  return /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, step === "VIEW_STATUS" ? isDivergedSelected ? "\u2191/\u2193 navigate \u2022 Type to filter \u2022 Enter to expand \u2022 d to delete \u2022 ESC to close" : "\u2191/\u2193 navigate \u2022 Type to filter \u2022 Enter to expand \u2022 ESC to close" : "\u2191/\u2193 navigate \u2022 Type to filter \u2022 Enter to select \u2022 ESC to cancel");
1786
1878
  };
1787
- return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, flexDirection: "column", width: 70 }, /* @__PURE__ */ React5.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "green" }, "\u{1F4CA} Worktree Status", " ", step !== "ERROR" && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), repositories.length > 1 && step === "VIEW_STATUS" && !loading && selectedRepoIndexRef.current >= 0 && /* @__PURE__ */ React5.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Text5, null, "Repository: ", /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, repositories.find((r) => r.index === selectedRepoIndexRef.current)?.name))), renderContent(), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1 }, renderFooter())));
1879
+ const selectedRepo = selectedRepoIndexRef.current >= 0 ? repositories.find((repo) => repo.index === selectedRepoIndexRef.current) : void 0;
1880
+ return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, flexDirection: "column", width: 70 }, /* @__PURE__ */ React5.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "green" }, "\u{1F4CA} Worktree Status", " ", step !== "ERROR" && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), step === "VIEW_STATUS" && selectedRepo && /* @__PURE__ */ React5.createElement(Box5, { marginBottom: 1 }, /* @__PURE__ */ React5.createElement(Text5, null, "Repository: ", /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, selectedRepo.name)), getRepositoryDiskUsage && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " "), renderRepositoryDiskUsage(selectedRepo.index)), renderContent(), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1 }, renderFooter())));
1788
1881
  };
1789
1882
  var WorktreeStatusView_default = WorktreeStatusView;
1790
1883
 
@@ -1893,7 +1986,9 @@ var App = ({
1893
1986
  onManualSync,
1894
1987
  onReload,
1895
1988
  onQuit,
1989
+ maxProgressLines = 2,
1896
1990
  getRepositoryList,
1991
+ getRepositoryDiskUsage,
1897
1992
  getBranchesForRepo,
1898
1993
  getDefaultBranchForRepo,
1899
1994
  fetchForRepo,
@@ -1913,12 +2008,15 @@ var App = ({
1913
2008
  const [showOpenEditorWizard, setShowOpenEditorWizard] = useState6(false);
1914
2009
  const [showWorktreeStatus, setShowWorktreeStatus] = useState6(false);
1915
2010
  const [status, setStatus] = useState6("idle");
2011
+ const [activeOps, setActiveOps] = useState6([]);
2012
+ const opIdRef = useRef5(0);
2013
+ const [syncProgressEntries, setSyncProgressEntries] = useState6([]);
1916
2014
  const [lastSyncTime, setLastSyncTime] = useState6(null);
1917
2015
  const [diskSpaceUsed, setDiskSpaceUsed] = useState6(null);
1918
2016
  const [logs, setLogs] = useState6([]);
1919
2017
  const [repoCount, setRepoCount] = useState6(repositoryCount);
1920
2018
  const [schedule2, setSchedule] = useState6(cronSchedule);
1921
- const { stdout } = useStdout();
2019
+ const { rows } = useWindowSize();
1922
2020
  const addLog = useCallback4((message, level = "info") => {
1923
2021
  setLogs((prev) => {
1924
2022
  const newLogs = [
@@ -1952,11 +2050,11 @@ var App = ({
1952
2050
  onQuit().catch((err) => console.error("Quit failed:", err));
1953
2051
  } else if (input2 === "?" || input2 === "h") {
1954
2052
  setShowHelp(true);
1955
- } else if (input2 === "c" && status === "idle") {
2053
+ } else if (input2 === "c") {
1956
2054
  setShowBranchWizard(true);
1957
- } else if (input2 === "o" && status === "idle") {
2055
+ } else if (input2 === "o") {
1958
2056
  setShowOpenEditorWizard(true);
1959
- } else if (input2 === "w" && status === "idle" && getWorktreeStatusForRepo) {
2057
+ } else if (input2 === "w" && getWorktreeStatusForRepo) {
1960
2058
  setShowWorktreeStatus(true);
1961
2059
  } else if (input2 === "s" && status !== "syncing") {
1962
2060
  setStatus("syncing");
@@ -1983,15 +2081,36 @@ var App = ({
1983
2081
  const updateLastSyncTime = useCallback4(() => {
1984
2082
  setLastSyncTime(/* @__PURE__ */ new Date());
1985
2083
  setStatus("idle");
2084
+ setSyncProgressEntries([]);
1986
2085
  }, []);
1987
2086
  useEffect6(() => {
1988
2087
  const unsubscribers = [
1989
2088
  events.on("updateLastSyncTime", () => {
1990
2089
  setLastSyncTime(/* @__PURE__ */ new Date());
1991
2090
  setStatus("idle");
2091
+ setSyncProgressEntries([]);
1992
2092
  }),
1993
2093
  events.on("setStatus", (newStatus) => {
1994
2094
  setStatus(newStatus);
2095
+ if (newStatus === "idle") {
2096
+ setSyncProgressEntries([]);
2097
+ }
2098
+ }),
2099
+ events.on("setSyncProgress", (progress) => {
2100
+ if (progress === null) {
2101
+ setSyncProgressEntries([]);
2102
+ return;
2103
+ }
2104
+ setSyncProgressEntries((prev) => {
2105
+ if (progress.completed) {
2106
+ return prev.filter((entry) => entry.repo !== progress.repo);
2107
+ }
2108
+ const existingIndex = prev.findIndex((entry) => entry.repo === progress.repo);
2109
+ if (existingIndex === -1) {
2110
+ return [...prev, progress];
2111
+ }
2112
+ return prev.map((entry, index) => index === existingIndex ? progress : entry);
2113
+ });
1995
2114
  }),
1996
2115
  events.on("setDiskSpace", (diskSpace) => {
1997
2116
  setDiskSpaceUsed(diskSpace);
@@ -2011,8 +2130,9 @@ var App = ({
2011
2130
  unsubscribers.forEach((unsub) => unsub());
2012
2131
  };
2013
2132
  }, []);
2014
- const statusBarHeight = 5;
2015
- const terminalRows = stdout.rows ?? 24;
2133
+ const progressLineCount = status === "syncing" ? Math.max(1, maxProgressLines) : 0;
2134
+ const statusBarHeight = 5 + progressLineCount + activeOps.length;
2135
+ const terminalRows = rows ?? 24;
2016
2136
  const logPanelHeight = Math.max(5, terminalRows - statusBarHeight);
2017
2137
  const showModal = showHelp || showBranchWizard || showOpenEditorWizard || showWorktreeStatus;
2018
2138
  return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", minHeight: terminalRows }, !showModal && /* @__PURE__ */ React7.createElement(LogPanel_default, { logs, height: logPanelHeight, isActive: !showModal }), showHelp && /* @__PURE__ */ React7.createElement(HelpModal_default, { onClose: () => setShowHelp(false) }), showBranchWizard && /* @__PURE__ */ React7.createElement(
@@ -2025,7 +2145,8 @@ var App = ({
2025
2145
  createAndPushBranch,
2026
2146
  onClose: () => setShowBranchWizard(false),
2027
2147
  onBranchCreated: (context) => {
2028
- setStatus("syncing");
2148
+ const opId = ++opIdRef.current;
2149
+ setActiveOps((prev) => [...prev, { id: opId, label: `Creating worktree ${context.newBranch}` }]);
2029
2150
  (async () => {
2030
2151
  try {
2031
2152
  await createWorktreeForBranch(context.repoIndex, context.newBranch);
@@ -2054,7 +2175,7 @@ var App = ({
2054
2175
  level: "error"
2055
2176
  });
2056
2177
  } finally {
2057
- setStatus("idle");
2178
+ setActiveOps((prev) => prev.filter((op) => op.id !== opId));
2058
2179
  }
2059
2180
  })().catch((err) => console.error("Branch creation unhandled error:", err));
2060
2181
  },
@@ -2076,6 +2197,7 @@ var App = ({
2076
2197
  {
2077
2198
  repositories: getRepositoryList(),
2078
2199
  getWorktreeStatusForRepo,
2200
+ getRepositoryDiskUsage,
2079
2201
  getDivergedDirectoriesForRepo,
2080
2202
  deleteDivergedDirectory,
2081
2203
  onClose: () => setShowWorktreeStatus(false)
@@ -2084,6 +2206,9 @@ var App = ({
2084
2206
  StatusBar_default,
2085
2207
  {
2086
2208
  status,
2209
+ syncProgressEntries,
2210
+ activeOps: activeOps.map((op) => op.label),
2211
+ maxProgressLines,
2087
2212
  repositoryCount: repoCount,
2088
2213
  lastSyncTime,
2089
2214
  cronSchedule: schedule2,
@@ -2093,6 +2218,9 @@ var App = ({
2093
2218
  };
2094
2219
  var App_default = App;
2095
2220
 
2221
+ // src/services/worktree-sync.service.ts
2222
+ import pLimit2 from "p-limit";
2223
+
2096
2224
  // src/utils/retry.ts
2097
2225
  var DEFAULT_OPTIONS = {
2098
2226
  maxAttempts: "unlimited",
@@ -2287,7 +2415,7 @@ function makeGitProgressHandler(logger, emitProgress) {
2287
2415
  lastBucket.set(key, bucket);
2288
2416
  const total = event.total > 0 ? `${event.processed}/${event.total}` : `${event.processed}`;
2289
2417
  const message = `${event.method} ${event.stage}: ${event.progress}% (${total})`;
2290
- logger.info(` \u21B3 ${message}`);
2418
+ logger.debug(` \u21B3 ${message}`);
2291
2419
  emitProgress?.({
2292
2420
  phase: event.method,
2293
2421
  message,
@@ -2493,6 +2621,7 @@ var SyncOutcomeAccumulator = class {
2493
2621
  constructor(options) {
2494
2622
  this.options = options;
2495
2623
  }
2624
+ options;
2496
2625
  counts = cloneCounts(EMPTY_COUNTS);
2497
2626
  actions = [];
2498
2627
  add(action) {
@@ -2569,6 +2698,9 @@ var CloneSyncService = class {
2569
2698
  this.progressEmitter = options.progressEmitter;
2570
2699
  this.onSkip = options.onSkip;
2571
2700
  }
2701
+ config;
2702
+ gitService;
2703
+ logger;
2572
2704
  initialized = false;
2573
2705
  resolvedBranch = null;
2574
2706
  branchCreatedActions;
@@ -3674,6 +3806,7 @@ var WorktreeStatusService = class {
3674
3806
  this.config = config;
3675
3807
  this.logger = logger ?? Logger.createDefault();
3676
3808
  }
3809
+ config;
3677
3810
  gitInstances = /* @__PURE__ */ new Map();
3678
3811
  logger;
3679
3812
  async checkWorktreeStatus(worktreePath) {
@@ -4022,8 +4155,9 @@ function sanitizeGitEnv(env) {
4022
4155
  return sanitized;
4023
4156
  }
4024
4157
  var GitService = class {
4025
- constructor(config, logger) {
4158
+ constructor(config, logger, progressEmitter) {
4026
4159
  this.config = config;
4160
+ this.progressEmitter = progressEmitter;
4027
4161
  this.logger = logger ?? Logger.createDefault(void 0, config.debug);
4028
4162
  this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
4029
4163
  this.mainWorktreePath = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
@@ -4031,6 +4165,8 @@ var GitService = class {
4031
4165
  this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
4032
4166
  this.sparseCheckoutService = new SparseCheckoutService(this.logger);
4033
4167
  }
4168
+ config;
4169
+ progressEmitter;
4034
4170
  git = null;
4035
4171
  bareRepoPath;
4036
4172
  mainWorktreePath;
@@ -4064,7 +4200,9 @@ var GitService = class {
4064
4200
  return git;
4065
4201
  }
4066
4202
  buildSimpleGitOptions(blockMs) {
4067
- const options = { progress: makeGitProgressHandler(this.logger) };
4203
+ const options = {
4204
+ progress: makeGitProgressHandler(this.logger, (event) => this.progressEmitter?.(event))
4205
+ };
4068
4206
  if (blockMs > 0) options.timeout = { block: blockMs };
4069
4207
  return options;
4070
4208
  }
@@ -4923,6 +5061,9 @@ var RepoOperationLock = class {
4923
5061
  this.gitService = gitService;
4924
5062
  this.logger = logger;
4925
5063
  }
5064
+ config;
5065
+ gitService;
5066
+ logger;
4926
5067
  updateLogger(logger) {
4927
5068
  this.logger = logger;
4928
5069
  }
@@ -4984,6 +5125,9 @@ var SyncRetryPolicy = class {
4984
5125
  this.gitService = gitService;
4985
5126
  this.logger = logger;
4986
5127
  }
5128
+ config;
5129
+ gitService;
5130
+ logger;
4987
5131
  updateLogger(logger) {
4988
5132
  this.logger = logger;
4989
5133
  }
@@ -5204,6 +5348,10 @@ var WorktreeModeSyncRunner = class {
5204
5348
  this.logger = logger;
5205
5349
  this.progressEmitter = progressEmitter;
5206
5350
  }
5351
+ config;
5352
+ gitService;
5353
+ logger;
5354
+ progressEmitter;
5207
5355
  pathResolution = new PathResolutionService();
5208
5356
  updateLogger(logger) {
5209
5357
  this.logger = logger;
@@ -5884,7 +6032,7 @@ var WorktreeSyncService = class {
5884
6032
  constructor(config) {
5885
6033
  this.config = config;
5886
6034
  this.logger = config.logger ?? Logger.createDefault(void 0, config.debug);
5887
- this.gitService = new GitService(config, this.logger);
6035
+ this.gitService = new GitService(config, this.logger, (event) => this.emitProgress(event));
5888
6036
  this.repoOperationLock = new RepoOperationLock(config, this.gitService, this.logger);
5889
6037
  this.retryPolicy = new SyncRetryPolicy(config, this.gitService, this.logger);
5890
6038
  this.worktreeModeSyncRunner = new WorktreeModeSyncRunner(
@@ -5902,10 +6050,15 @@ var WorktreeSyncService = class {
5902
6050
  });
5903
6051
  }
5904
6052
  }
6053
+ config;
5905
6054
  gitService;
5906
6055
  cloneSyncService = null;
5907
6056
  logger;
5908
- syncInProgress = false;
6057
+ // In-process FIFO serializer for all bare-repo-mutating operations (sync, init,
6058
+ // interactive create). One per repo. wait:true callers queue behind an in-flight op;
6059
+ // wait:false callers fail fast. The cross-process file lock (RepoOperationLock) is
6060
+ // acquired inside the mutex body for multi-process safety.
6061
+ repoMutex = pLimit2(1);
5909
6062
  progressEmitter = new ProgressEmitter();
5910
6063
  repoOperationLock;
5911
6064
  retryPolicy;
@@ -5957,7 +6110,7 @@ var WorktreeSyncService = class {
5957
6110
  return this.gitService.isInitialized();
5958
6111
  }
5959
6112
  isSyncInProgress() {
5960
- return this.syncInProgress;
6113
+ return this.repoMutex.activeCount + this.repoMutex.pendingCount > 0;
5961
6114
  }
5962
6115
  getGitService() {
5963
6116
  return this.gitService;
@@ -5973,34 +6126,31 @@ var WorktreeSyncService = class {
5973
6126
  onProgress(listener) {
5974
6127
  return this.progressEmitter.onProgress(listener);
5975
6128
  }
5976
- async runExclusiveRepoOperation(operation) {
5977
- if (this.syncInProgress) {
6129
+ async runExclusiveRepoOperation(operation, options = {}) {
6130
+ if (!options.wait && this.repoMutex.activeCount + this.repoMutex.pendingCount > 0) {
5978
6131
  this.logger.warn("\u26A0\uFE0F Another repository operation is already in progress, skipping...");
5979
6132
  return { started: false, reason: "in_progress" };
5980
6133
  }
5981
- this.syncInProgress = true;
5982
- let release;
5983
- try {
5984
- release = await this.repoOperationLock.acquire();
5985
- } catch (error) {
5986
- this.syncInProgress = false;
5987
- throw error;
5988
- }
5989
- if (release === null) {
5990
- this.syncInProgress = false;
5991
- this.logger.warn("\u26A0\uFE0F Another process holds the sync lock for this repo, skipping...");
5992
- return { started: false, reason: "locked" };
5993
- }
5994
- try {
5995
- return { started: true, value: await operation() };
5996
- } finally {
6134
+ return this.repoMutex(async () => {
6135
+ const release = await this.repoOperationLock.acquire();
6136
+ if (release === null) {
6137
+ this.logger.warn("\u26A0\uFE0F Another process holds the sync lock for this repo, skipping...");
6138
+ return { started: false, reason: "locked" };
6139
+ }
5997
6140
  try {
5998
- await release();
5999
- } catch (releaseError) {
6000
- this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
6141
+ return { started: true, value: await operation() };
6142
+ } finally {
6143
+ try {
6144
+ await release();
6145
+ } catch (releaseError) {
6146
+ this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
6147
+ }
6001
6148
  }
6002
- this.syncInProgress = false;
6003
- }
6149
+ });
6150
+ }
6151
+ // Interactive variant: queues behind any in-flight sync/op instead of failing fast.
6152
+ async runQueuedRepoOperation(operation) {
6153
+ return this.runExclusiveRepoOperation(operation, { wait: true });
6004
6154
  }
6005
6155
  emitProgress(event) {
6006
6156
  this.progressEmitter.emit(event);
@@ -6288,11 +6438,13 @@ var InteractiveUIService = class {
6288
6438
  branchCreatedActions = new BranchCreatedActionsService();
6289
6439
  pathResolution = new PathResolutionService();
6290
6440
  limit;
6441
+ maxProgressLines;
6291
6442
  reloadInProgress = false;
6292
6443
  isDestroyed = false;
6293
6444
  events;
6294
6445
  ownsEvents;
6295
6446
  unsubscribeCallbacks = [];
6447
+ progressUnsubscribers = [];
6296
6448
  constructor(syncServices, configPath, cronSchedule, maxParallel, events) {
6297
6449
  this.ownsEvents = events === void 0;
6298
6450
  this.events = events ?? new AppEventEmitter();
@@ -6303,9 +6455,11 @@ var InteractiveUIService = class {
6303
6455
  this.configPath = configPath;
6304
6456
  this.cronSchedule = cronSchedule;
6305
6457
  this.repositoryCount = syncServices.length;
6306
- this.limit = pLimit2(maxParallel ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
6458
+ this.maxProgressLines = Math.max(1, maxParallel ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
6459
+ this.limit = pLimit3(this.maxProgressLines);
6307
6460
  this.startBufferFlushCheck();
6308
6461
  this.renderUI();
6462
+ this.subscribeToServiceProgress();
6309
6463
  this.injectLoggersIntoServices();
6310
6464
  setTimeout(() => {
6311
6465
  this.addLog("\u{1F680} sync-worktrees UI initialized", "info");
@@ -6343,6 +6497,26 @@ var InteractiveUIService = class {
6343
6497
  );
6344
6498
  }
6345
6499
  }
6500
+ subscribeToServiceProgress() {
6501
+ for (const unsubscribe of this.progressUnsubscribers) {
6502
+ unsubscribe();
6503
+ }
6504
+ this.progressUnsubscribers = this.syncServices.map((service, index) => {
6505
+ const repoName = this.getRepoName(index);
6506
+ if (!service.onProgress) return () => void 0;
6507
+ return service.onProgress((event) => {
6508
+ if (this.isDestroyed) return;
6509
+ this.events.emit("setSyncProgress", {
6510
+ repo: repoName,
6511
+ phase: event.phase,
6512
+ message: event.message,
6513
+ progress: event.progress,
6514
+ processed: event.processed,
6515
+ total: event.total
6516
+ });
6517
+ });
6518
+ });
6519
+ }
6346
6520
  addLog(message, level = "info") {
6347
6521
  if (this.isDestroyed) return;
6348
6522
  if (this.uiReady) {
@@ -6395,6 +6569,7 @@ var InteractiveUIService = class {
6395
6569
  events: this.events,
6396
6570
  repositoryCount: this.repositoryCount,
6397
6571
  cronSchedule: this.cronSchedule,
6572
+ maxProgressLines: this.maxProgressLines,
6398
6573
  onManualSync: () => this.handleManualSync(),
6399
6574
  onReload: () => this.handleReload(),
6400
6575
  onQuit: () => this.handleQuit(),
@@ -6405,6 +6580,7 @@ var InteractiveUIService = class {
6405
6580
  createAndPushBranch: (repoIndex, baseBranch, branchName) => this.createAndPushBranch(repoIndex, baseBranch, branchName),
6406
6581
  getWorktreesForRepo: (index) => this.getWorktreesForRepo(index),
6407
6582
  getWorktreeStatusForRepo: (index) => this.getWorktreeStatusForRepo(index),
6583
+ getRepositoryDiskUsage: (index) => this.getRepositoryDiskUsage(index),
6408
6584
  getDivergedDirectoriesForRepo: (index) => this.getDivergedDirectoriesForRepo(index),
6409
6585
  deleteDivergedDirectory: (repoIndex, name) => this.deleteDivergedDirectory(repoIndex, name),
6410
6586
  openEditorInWorktree: (path18) => this.openEditorInWorktree(path18),
@@ -6413,7 +6589,11 @@ var InteractiveUIService = class {
6413
6589
  createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName),
6414
6590
  executeOnBranchCreatedHooks: (repoIndex, context) => this.executeOnBranchCreatedHooks(repoIndex, context)
6415
6591
  }
6416
- )
6592
+ ),
6593
+ {
6594
+ alternateScreen: true,
6595
+ incrementalRendering: true
6596
+ }
6417
6597
  );
6418
6598
  }
6419
6599
  async handleManualSync() {
@@ -6470,6 +6650,7 @@ var InteractiveUIService = class {
6470
6650
  cronJobsCancelled = true;
6471
6651
  this.syncServices = newServices;
6472
6652
  this.repositoryCount = this.syncServices.length;
6653
+ this.subscribeToServiceProgress();
6473
6654
  this.injectLoggersIntoServices();
6474
6655
  const uniqueSchedules = [...new Set(this.syncServices.map((s) => s.config.cronSchedule))];
6475
6656
  this.cronSchedule = uniqueSchedules.length === 1 ? uniqueSchedules[0] : void 0;
@@ -6546,6 +6727,9 @@ var InteractiveUIService = class {
6546
6727
  setStatus(status) {
6547
6728
  if (this.isDestroyed) return;
6548
6729
  this.events.emit("setStatus", status);
6730
+ if (status === "idle") {
6731
+ this.events.emit("setSyncProgress", null);
6732
+ }
6549
6733
  }
6550
6734
  setDiskSpace(diskSpace) {
6551
6735
  if (this.isDestroyed) return;
@@ -6575,6 +6759,46 @@ var InteractiveUIService = class {
6575
6759
  const service = this.syncServices[index];
6576
6760
  return service.config.name || `repo-${index}`;
6577
6761
  }
6762
+ async getRepositoryDiskUsage(repoIndex) {
6763
+ if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
6764
+ throw new Error(`Invalid repository index: ${repoIndex}`);
6765
+ }
6766
+ const service = this.syncServices[repoIndex];
6767
+ const config = service.config;
6768
+ const repoName = this.getRepoName(repoIndex);
6769
+ const mode = resolveMode(config);
6770
+ const sizeTargets = [
6771
+ ...mode === "worktree" ? [{ kind: "bare", path: config.bareRepoDir || getDefaultBareRepoDir(config.repoUrl) }] : [],
6772
+ { kind: "worktree", path: config.worktreeDir }
6773
+ ];
6774
+ let bareSizeBytes = 0;
6775
+ let worktreeSizeBytes = 0;
6776
+ const errors = [];
6777
+ for (const target of sizeTargets) {
6778
+ try {
6779
+ const size = await calculateDirectorySize(target.path);
6780
+ if (target.kind === "bare") {
6781
+ bareSizeBytes = size;
6782
+ } else {
6783
+ worktreeSizeBytes = size;
6784
+ }
6785
+ } catch (error) {
6786
+ errors.push(`${target.path}: ${error instanceof Error ? error.message : String(error)}`);
6787
+ }
6788
+ }
6789
+ const sizeBytes = bareSizeBytes + worktreeSizeBytes;
6790
+ const failedAllPaths = errors.length === sizeTargets.length;
6791
+ const partialFailure = errors.length > 0 && !failedAllPaths;
6792
+ return {
6793
+ repoIndex,
6794
+ repoName,
6795
+ sizeBytes: failedAllPaths ? null : sizeBytes,
6796
+ sizeFormatted: failedAllPaths ? "N/A" : partialFailure ? `\u2265${formatBytes(sizeBytes)}` : formatBytes(sizeBytes),
6797
+ bareSizeBytes,
6798
+ worktreeSizeBytes,
6799
+ error: errors.length > 0 ? errors.join("; ") : void 0
6800
+ };
6801
+ }
6578
6802
  async getBranchesForRepo(repoIndex) {
6579
6803
  if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
6580
6804
  throw new Error(`Invalid repository index: ${repoIndex}`);
@@ -6599,11 +6823,15 @@ var InteractiveUIService = class {
6599
6823
  throw new Error(`Invalid repository index: ${repoIndex}`);
6600
6824
  }
6601
6825
  const service = this.syncServices[repoIndex];
6602
- if (!service.isInitialized()) {
6603
- await service.initialize();
6826
+ const result = await service.runQueuedRepoOperation(async () => {
6827
+ if (!service.isInitialized()) {
6828
+ await service.initializeUnlocked();
6829
+ }
6830
+ await service.getGitService().fetchAll();
6831
+ });
6832
+ if (!result.started) {
6833
+ throw new Error("Another process holds the repository lock; fetch skipped. Try again.");
6604
6834
  }
6605
- const gitService = service.getGitService();
6606
- await gitService.fetchAll();
6607
6835
  }
6608
6836
  async createAndPushBranch(repoIndex, baseBranch, branchName) {
6609
6837
  if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
@@ -6611,25 +6839,35 @@ var InteractiveUIService = class {
6611
6839
  }
6612
6840
  const service = this.syncServices[repoIndex];
6613
6841
  const gitService = service.getGitService();
6614
- const maxAttempts = 10;
6615
- let finalName = branchName;
6616
- let suffix = 0;
6617
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
6618
- try {
6619
- await gitService.createBranch(finalName, baseBranch);
6620
- await gitService.pushBranch(finalName);
6621
- return { success: true, finalName };
6622
- } catch (error) {
6623
- const errorMessage = error instanceof Error ? error.message : String(error);
6624
- if (errorMessage.includes("already exists")) {
6625
- suffix++;
6626
- finalName = `${branchName}-${suffix}`;
6627
- continue;
6842
+ const result = await service.runQueuedRepoOperation(async () => {
6843
+ const maxAttempts = 10;
6844
+ let finalName = branchName;
6845
+ let suffix = 0;
6846
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
6847
+ try {
6848
+ await gitService.createBranch(finalName, baseBranch);
6849
+ await gitService.pushBranch(finalName);
6850
+ return { success: true, finalName };
6851
+ } catch (error) {
6852
+ const errorMessage = error instanceof Error ? error.message : String(error);
6853
+ if (errorMessage.includes("already exists")) {
6854
+ suffix++;
6855
+ finalName = `${branchName}-${suffix}`;
6856
+ continue;
6857
+ }
6858
+ return { success: false, finalName: branchName, error: errorMessage };
6628
6859
  }
6629
- return { success: false, finalName: branchName, error: errorMessage };
6630
6860
  }
6861
+ return { success: false, finalName: branchName, error: `Failed to create branch after ${maxAttempts} attempts` };
6862
+ });
6863
+ if (!result.started) {
6864
+ return {
6865
+ success: false,
6866
+ finalName: branchName,
6867
+ error: "Another process holds the repository lock; branch not created. Try again."
6868
+ };
6631
6869
  }
6632
- return { success: false, finalName: branchName, error: `Failed to create branch after ${maxAttempts} attempts` };
6870
+ return result.value;
6633
6871
  }
6634
6872
  async getWorktreesForRepo(repoIndex) {
6635
6873
  if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
@@ -6731,7 +6969,12 @@ var InteractiveUIService = class {
6731
6969
  const gitService = service.getGitService();
6732
6970
  const worktreeDir = service.config.worktreeDir;
6733
6971
  const worktreePath = this.pathResolution.getBranchWorktreePath(worktreeDir, branchName);
6734
- await gitService.addWorktree(branchName, worktreePath);
6972
+ const result = await service.runQueuedRepoOperation(async () => {
6973
+ await gitService.addWorktree(branchName, worktreePath);
6974
+ });
6975
+ if (!result.started) {
6976
+ throw new Error("Another process holds the repository lock; worktree not created. Try again.");
6977
+ }
6735
6978
  }
6736
6979
  openEditorInWorktree(worktreePath) {
6737
6980
  const editor = process.env.EDITOR || process.env.VISUAL || "code";
@@ -6870,19 +7113,28 @@ var InteractiveUIService = class {
6870
7113
  }
6871
7114
  async runSyncServices(services) {
6872
7115
  const syncResults = await Promise.allSettled(
6873
- services.map(
6874
- (service) => this.limit(async () => {
7116
+ services.map((service) => {
7117
+ const repoName = service.config.name || service.config.repoUrl;
7118
+ return this.limit(async () => {
6875
7119
  service.clearRecordedSkips();
6876
- if (!service.isInitialized()) {
6877
- await service.initialize();
7120
+ try {
7121
+ if (!service.isInitialized()) {
7122
+ await service.initialize();
7123
+ }
7124
+ const result = await service.sync();
7125
+ return { service, result };
7126
+ } finally {
7127
+ this.events.emit("setSyncProgress", {
7128
+ repo: repoName,
7129
+ phase: "complete",
7130
+ message: "Finished",
7131
+ completed: true
7132
+ });
6878
7133
  }
6879
- const result = await service.sync();
6880
- return { service, result };
6881
7134
  }).catch((error) => {
6882
- const repoName = service.config.name || service.config.repoUrl;
6883
7135
  throw Object.assign(error instanceof Error ? error : new Error(String(error)), { repoName });
6884
- })
6885
- )
7136
+ });
7137
+ })
6886
7138
  );
6887
7139
  const failures = [];
6888
7140
  const skipped = [];
@@ -6977,6 +7229,10 @@ var InteractiveUIService = class {
6977
7229
  unsubscribe();
6978
7230
  }
6979
7231
  this.unsubscribeCallbacks = [];
7232
+ for (const unsubscribe of this.progressUnsubscribers) {
7233
+ unsubscribe();
7234
+ }
7235
+ this.progressUnsubscribers = [];
6980
7236
  if (this.ownsEvents) {
6981
7237
  this.events.removeAllListeners();
6982
7238
  }
@@ -7106,11 +7362,14 @@ async function generateConfigFile(input2, configPath, options = {}) {
7106
7362
  const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
7107
7363
  repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : input2.bareRepoDir;
7108
7364
  }
7365
+ const defaults = {
7366
+ cronSchedule: input2.cronSchedule
7367
+ };
7368
+ if (input2.runOnce) {
7369
+ defaults.runOnce = input2.runOnce;
7370
+ }
7109
7371
  const configObject = {
7110
- defaults: {
7111
- cronSchedule: input2.cronSchedule,
7112
- runOnce: input2.runOnce
7113
- },
7372
+ defaults,
7114
7373
  repositories: [repository]
7115
7374
  };
7116
7375
  const configContent = `// @ts-check
@@ -7293,7 +7552,7 @@ async function runMultipleRepositories(configFile, repositories, configPath) {
7293
7552
  const globalLogger = Logger.createDefault();
7294
7553
  const runOnce = configFile.defaults?.runOnce ?? false;
7295
7554
  const maxParallel = configFile.parallelism?.maxRepositories ?? configFile.defaults?.parallelism?.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
7296
- const limit = pLimit3(maxParallel);
7555
+ const limit = pLimit4(maxParallel);
7297
7556
  if (runOnce) {
7298
7557
  globalLogger.info(`
7299
7558
  \u{1F504} Syncing ${repositories.length} repositories...`);