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/README.md +2 -0
- package/dist/components/App.d.ts +4 -6
- package/dist/components/App.d.ts.map +1 -1
- package/dist/components/BranchCreationWizard.d.ts.map +1 -1
- package/dist/components/OpenEditorWizard.d.ts.map +1 -1
- package/dist/components/StatusBar.d.ts +4 -0
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/WorktreeStatusView.d.ts +3 -6
- package/dist/components/WorktreeStatusView.d.ts.map +1 -1
- package/dist/index.js +347 -88
- package/dist/index.js.map +4 -4
- package/dist/mcp-server.js +68 -36
- package/dist/mcp-server.js.map +4 -4
- package/dist/services/InteractiveUIService.d.ts +6 -6
- package/dist/services/InteractiveUIService.d.ts.map +1 -1
- package/dist/services/git.service.d.ts +3 -1
- package/dist/services/git.service.d.ts.map +1 -1
- package/dist/services/worktree-sync.service.d.ts +5 -2
- package/dist/services/worktree-sync.service.d.ts.map +1 -1
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/app-events.d.ts +10 -0
- package/dist/utils/app-events.d.ts.map +1 -1
- package/dist/utils/config-generator.d.ts.map +1 -1
- package/dist/utils/git-progress.d.ts +5 -5
- package/package.json +28 -27
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
|
|
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
|
|
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,
|
|
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 = ({
|
|
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
|
-
|
|
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
|
|
1672
|
-
if (
|
|
1673
|
-
selectedRepoIndexRef.current =
|
|
1741
|
+
const selectedRepo2 = filteredProjects[selectedProjectIndex];
|
|
1742
|
+
if (selectedRepo2) {
|
|
1743
|
+
selectedRepoIndexRef.current = selectedRepo2.index;
|
|
1674
1744
|
setStep("VIEW_STATUS");
|
|
1675
|
-
loadStatus(
|
|
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
|
-
|
|
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 {
|
|
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"
|
|
2053
|
+
} else if (input2 === "c") {
|
|
1956
2054
|
setShowBranchWizard(true);
|
|
1957
|
-
} else if (input2 === "o"
|
|
2055
|
+
} else if (input2 === "o") {
|
|
1958
2056
|
setShowOpenEditorWizard(true);
|
|
1959
|
-
} else if (input2 === "w" &&
|
|
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
|
|
2015
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 = {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
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
|
|
5999
|
-
}
|
|
6000
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
6603
|
-
|
|
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
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
7116
|
+
services.map((service) => {
|
|
7117
|
+
const repoName = service.config.name || service.config.repoUrl;
|
|
7118
|
+
return this.limit(async () => {
|
|
6875
7119
|
service.clearRecordedSkips();
|
|
6876
|
-
|
|
6877
|
-
|
|
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 =
|
|
7555
|
+
const limit = pLimit4(maxParallel);
|
|
7297
7556
|
if (runOnce) {
|
|
7298
7557
|
globalLogger.info(`
|
|
7299
7558
|
\u{1F504} Syncing ${repositories.length} repositories...`);
|