gitxelectron 1.2.6 → 1.2.8

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/out/main/index.js CHANGED
@@ -4,6 +4,8 @@ const path = require("path");
4
4
  const fs = require("fs/promises");
5
5
  const simpleGit = require("simple-git");
6
6
  const crypto = require("crypto");
7
+ const child_process = require("child_process");
8
+ const readline = require("readline");
7
9
  const generativeAi = require("@google/generative-ai");
8
10
  const OpenAI = require("openai");
9
11
  const chokidar = require("chokidar");
@@ -26,6 +28,7 @@ function _interopNamespaceDefault(e) {
26
28
  }
27
29
  const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
28
30
  const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
31
+ const readline__namespace = /* @__PURE__ */ _interopNamespaceDefault(readline);
29
32
  const GITHUB_CLIENT_ID = "Ov23livWfiaPADGiwSXq";
30
33
  let accounts = [];
31
34
  const activeSessions = /* @__PURE__ */ new Map();
@@ -690,9 +693,56 @@ async function initializeRepo(sessionId, dir) {
690
693
  }
691
694
  }
692
695
  async function getStatus(sessionId) {
693
- const { git } = getGitSession(sessionId);
694
- const status = await git.status();
695
- return JSON.parse(JSON.stringify(status));
696
+ const { git, repoPath } = getGitSession(sessionId);
697
+ return new Promise((resolve, reject) => {
698
+ const child = child_process.spawn("git", ["status", "--porcelain"], { cwd: repoPath });
699
+ let count = 0;
700
+ let isMassive = false;
701
+ child.stdout.on("data", (chunk) => {
702
+ for (let i = 0; i < chunk.length; ++i) {
703
+ if (chunk[i] === 10) {
704
+ count++;
705
+ if (count > 1e4) {
706
+ isMassive = true;
707
+ child.kill();
708
+ break;
709
+ }
710
+ }
711
+ }
712
+ });
713
+ child.on("close", async () => {
714
+ if (isMassive) {
715
+ resolve({
716
+ not_added: [],
717
+ created: [],
718
+ deleted: [],
719
+ modified: [],
720
+ conflicted: [],
721
+ renamed: [],
722
+ staged: [],
723
+ files: [],
724
+ current: "main",
725
+ ahead: 0,
726
+ behind: 0,
727
+ tracking: null,
728
+ isMassive: true,
729
+ totalChanges: count
730
+ });
731
+ } else {
732
+ try {
733
+ const status = await git.status();
734
+ const result = JSON.parse(JSON.stringify(status));
735
+ result.isMassive = false;
736
+ result.totalChanges = count;
737
+ if (!result.staged) result.staged = [];
738
+ resolve(result);
739
+ } catch (e) {
740
+ reject(e);
741
+ }
742
+ }
743
+ });
744
+ child.on("error", reject);
745
+ });
696
746
  }
697
747
  async function stageFiles(sessionId, files) {
698
748
  const { git, repoPath } = getGitSession(sessionId);
@@ -727,7 +777,15 @@ async function stageFiles(sessionId, files) {
727
777
  const chunkSize = 50;
728
778
  for (let i = 0; i < fileArray.length; i += chunkSize) {
729
779
  const chunk = fileArray.slice(i, i + chunkSize);
730
- await git.add(["--force", ...chunk]);
780
+ try {
781
+ await git.raw(["-c", "core.safecrlf=false", "-c", "core.autocrlf=false", "add", "--force", ...chunk]);
782
+ } catch (e) {
783
+ if (e.message && e.message.includes("LF will be replaced by CRLF") && !e.message.includes("fatal:")) {
784
+ console.warn("Ignored CRLF warning during stage");
785
+ } else {
786
+ throw e;
787
+ }
788
+ }
731
789
  }
732
790
  return true;
733
791
  }
@@ -779,6 +837,123 @@ function injectAuthToken(sessionId, remoteUrl) {
779
837
  }
780
838
  return remoteUrl;
781
839
  }
840
+ async function pushMassiveRepository(sessionId, remote, branch, chunkSize = 5e3, onProgress, onDetailedProgress) {
841
+ const { repoPath } = getGitSession(sessionId);
842
+ if (onProgress) onProgress("Starting rapid file count to prepare massive chunking...");
843
+ const totalFiles = await new Promise((resolve) => {
844
+ const child2 = child_process.spawn("git", ["status", "--porcelain"], { cwd: repoPath });
845
+ let count = 0;
846
+ child2.stdout.on("data", (chunk) => {
847
+ for (let i = 0; i < chunk.length; ++i) {
848
+ if (chunk[i] === 10) count++;
849
+ }
850
+ });
851
+ child2.on("close", () => resolve(count));
852
+ child2.on("error", () => resolve(0));
853
+ });
854
+ if (totalFiles === 0) {
855
+ if (onProgress) onProgress("No files to push.");
856
+ return true;
857
+ }
858
+ if (onProgress) onProgress(`Total files to push: ${totalFiles}. Starting chunked upload...`);
859
+ const { git } = getGitSession(sessionId);
860
+ const remotes = await getRemotes(sessionId);
861
+ const remoteObj = remotes.find((r) => r.name === remote);
862
+ if (onProgress) onProgress("Starting massive repository push...");
863
+ await git.addConfig("http.postBuffer", "1048576000");
864
+ await git.addConfig("core.safecrlf", "false");
865
+ await git.addConfig("core.autocrlf", "false");
866
+ let authUrl = remote;
867
+ if (remoteObj && remoteObj.refs.push) {
868
+ authUrl = injectAuthToken(sessionId, remoteObj.refs.push);
869
+ }
870
+ const child = child_process.spawn("git", ["status", "--porcelain"], { cwd: repoPath });
871
+ const rl = readline__namespace.createInterface({
872
+ input: child.stdout,
873
+ crlfDelay: Infinity
874
+ });
875
+ let currentChunk = [];
876
+ let chunkCount = 0;
877
+ let processedFiles = 0;
878
+ const startTime = Date.now();
879
+ const processChunk = async (chunk) => {
880
+ chunkCount++;
881
+ processedFiles += chunk.length;
882
+ const percentage = Math.min(100, processedFiles / totalFiles * 100);
883
+ const elapsedMs = Date.now() - startTime;
884
+ const msPerFile = elapsedMs / processedFiles;
885
+ const remainingFiles = totalFiles - processedFiles;
886
+ const etaMs = msPerFile * remainingFiles;
887
+ const formatEta = (ms) => {
888
+ if (!isFinite(ms) || ms < 0) return "calculating...";
889
+ const totalSec = Math.floor(ms / 1e3);
890
+ const min = Math.floor(totalSec / 60);
891
+ const sec = totalSec % 60;
892
+ if (min > 0) return `${min}m ${sec}s left`;
893
+ return `${sec}s left`;
894
+ };
895
+ const etaStr = formatEta(etaMs);
896
+ if (onProgress) onProgress(`Processing chunk ${chunkCount} (${chunk.length} files)... ETA: ${etaStr}`);
897
+ if (onDetailedProgress) {
898
+ onDetailedProgress({
899
+ processed: processedFiles,
900
+ total: totalFiles,
901
+ percentage,
902
+ eta: etaStr
903
+ });
904
+ }
905
+ const tempFile = path__namespace.join(repoPath, ".git", "massive_chunk.txt");
906
+ await fs__namespace.writeFile(tempFile, chunk.join("\n"), "utf-8");
907
+ try {
908
+ await git.raw(["-c", "core.safecrlf=false", "add", "--force", "--pathspec-from-file=" + tempFile]);
909
+ } catch (e) {
910
+ if (e.message && e.message.includes("fatal:")) {
911
+ throw e;
912
+ }
913
+ } finally {
914
+ try {
915
+ await fs__namespace.unlink(tempFile);
916
+ } catch (e) {
917
+ }
918
+ }
919
+ try {
920
+ await git.commit(`Auto-sync chunk ${chunkCount} (Massive Push)`);
921
+ } catch (e) {
922
+ }
923
+ try {
924
+ await git.raw(["-c", "credential.helper=", "push", authUrl, branch]);
925
+ } catch (err) {
926
+ if (err.message && (err.message.includes("non-fast-forward") || err.message.includes("fetch first") || err.message.includes("branch is behind"))) {
927
+ await git.pull(authUrl, branch, { "--rebase": null, "--autostash": null });
928
+ await git.raw(["-c", "credential.helper=", "push", authUrl, branch]);
929
+ } else {
930
+ if (err.message && !err.message.includes("nothing to commit")) {
931
+ throw err;
932
+ }
933
+ }
934
+ }
935
+ };
936
+ for await (const line of rl) {
937
+ if (line.length < 3) continue;
938
+ let file = line.substring(3).trim();
939
+ if (file.startsWith('"') && file.endsWith('"')) {
940
+ file = file.substring(1, file.length - 1);
941
+ }
942
+ if (!file) continue;
943
+ currentChunk.push(file);
944
+ if (currentChunk.length >= chunkSize) {
945
+ rl.pause();
946
+ await processChunk(currentChunk);
947
+ currentChunk = [];
948
+ rl.resume();
949
+ }
950
+ }
951
+ if (currentChunk.length > 0) {
952
+ await processChunk(currentChunk);
953
+ }
954
+ if (onProgress) onProgress("✅ Massive push completed successfully!");
955
+ return true;
956
+ }
782
957
  async function pushToRemote(sessionId, remote, branch) {
783
958
  const remotes = await getRemotes(sessionId);
784
959
  const remoteObj = remotes.find((r) => r.name === remote);
@@ -1127,7 +1302,15 @@ async function commitUniversalWorkspace(workspacePath, message, branch) {
1127
1302
  GIT_TERMINAL_PROMPT: "0",
1128
1303
  GIT_ASKPASS: "echo"
1129
1304
  });
1130
- await git.add(["--sparse", "."]);
1305
+ try {
1306
+ await git.raw(["-c", "core.safecrlf=false", "-c", "core.autocrlf=false", "add", "--sparse", "."]);
1307
+ } catch (e) {
1308
+ if (e.message && e.message.includes("LF will be replaced by CRLF") && !e.message.includes("fatal:")) {
1309
+ console.warn("Ignored CRLF warning in universal workspace");
1310
+ } else {
1311
+ throw e;
1312
+ }
1313
+ }
1131
1314
  await git.commit(message);
1132
1315
  try {
1133
1316
  await git.raw(["-c", "credential.helper=", "push", "origin", branch]);
@@ -1193,11 +1376,11 @@ async function cloneRepository(sessionId, remoteUrl, destPath) {
1193
1376
  } else {
1194
1377
  await existingGit.raw(["remote", "add", "origin", authUrl]);
1195
1378
  }
1196
- await existingGit.raw(["-c", "credential.helper=", "fetch", "origin"]);
1379
+ await existingGit.raw(["-c", "credential.helper=", "fetch", "--progress", "origin"]);
1197
1380
  const st = await existingGit.status();
1198
1381
  const branch = st.current || "main";
1199
1382
  try {
1200
- await existingGit.raw(["-c", "credential.helper=", "pull", "origin", branch]);
1383
+ await existingGit.raw(["-c", "credential.helper=", "pull", "--progress", "origin", branch]);
1201
1384
  } catch (e) {
1202
1385
  }
1203
1386
  return destPath;
@@ -1207,7 +1390,7 @@ async function cloneRepository(sessionId, remoteUrl, destPath) {
1207
1390
  }
1208
1391
  if (destExists && destIsEmpty) {
1209
1392
  const emptyDirGit = createSafeGit(destPath, sessionId);
1210
- await emptyDirGit.raw(["-c", "credential.helper=", "clone", authUrl, "."]);
1393
+ await emptyDirGit.raw(["-c", "credential.helper=", "clone", "--progress", authUrl, "."]);
1211
1394
  try {
1212
1395
  await createSafeGit(void 0, sessionId).raw(["config", "--global", "--add", "safe.directory", destPath]);
1213
1396
  } catch (e) {
@@ -1222,7 +1405,7 @@ async function cloneRepository(sessionId, remoteUrl, destPath) {
1222
1405
  const baseDir = path__namespace.dirname(destPath);
1223
1406
  const targetDirName = path__namespace.basename(destPath);
1224
1407
  const parentGit = createSafeGit(baseDir, sessionId);
1225
- await parentGit.raw(["-c", "credential.helper=", "clone", authUrl, targetDirName]);
1408
+ await parentGit.raw(["-c", "credential.helper=", "clone", "--progress", authUrl, targetDirName]);
1226
1409
  try {
1227
1410
  await createSafeGit(void 0, sessionId).raw(["config", "--global", "--add", "safe.directory", destPath]);
1228
1411
  } catch (e) {
@@ -1243,6 +1426,16 @@ async function cloneRepository(sessionId, remoteUrl, destPath) {
1243
1426
  throw new Error(cleanMsg);
1244
1427
  }
1245
1428
  }
1429
+ async function removeGitMetadata(_sessionId, repoPath) {
1430
+ try {
1431
+ const gitDir = path__namespace.join(repoPath, ".git");
1432
+ await fs__namespace.rm(gitDir, { recursive: true, force: true });
1433
+ return true;
1434
+ } catch (e) {
1435
+ console.error("Failed to remove git metadata:", e);
1436
+ return false;
1437
+ }
1438
+ }
1246
1439
  const SYSTEM_PROMPT = `You are an autonomous Git Healer Agent for the "GitElectron" app.
1247
1440
  The user has encountered a Git error during a push, pull, or merge operation and you are executing as a background process to silently fix it.
1248
1441
  You will receive the Git Status and the Raw Error Message.
@@ -1517,13 +1710,15 @@ let isResolving = false;
1517
1710
  let globalOnLog = null;
1518
1711
  let globalOnErrorApproval = null;
1519
1712
  let globalOnResolveComplete = null;
1713
+ let globalOnMassiveProgress = null;
1520
1714
  const DEBOUNCE_MS = 3e3;
1521
1715
  const changedFiles = /* @__PURE__ */ new Set();
1522
- function toggleAutoSync(sessionId, repoPath, enabled, onLog, onErrorApproval, onResolveComplete) {
1716
+ function toggleAutoSync(sessionId, repoPath, enabled, onLog, onErrorApproval, onResolveComplete, onMassiveProgress) {
1523
1717
  isAutoSyncEnabled = enabled;
1524
1718
  globalOnLog = onLog;
1525
1719
  if (onErrorApproval) globalOnErrorApproval = onErrorApproval;
1526
1720
  if (onResolveComplete) globalOnResolveComplete = onResolveComplete;
1721
+ if (onMassiveProgress) globalOnMassiveProgress = onMassiveProgress;
1527
1722
  if (currentWatcher) {
1528
1723
  currentWatcher.close();
1529
1724
  currentWatcher = null;
@@ -1553,7 +1748,11 @@ function toggleAutoSync(sessionId, repoPath, enabled, onLog, onErrorApproval, on
1553
1748
  currentWatcher.on("all", (_event, filePath) => {
1554
1749
  if (!isAutoSyncEnabled || isResolving) return;
1555
1750
  const relativePath = path__namespace.relative(repoPath, filePath).replace(/\\/g, "/");
1556
- changedFiles.add(relativePath);
1751
+ if (changedFiles.size < 1e4) {
1752
+ changedFiles.add(relativePath);
1753
+ } else if (!changedFiles.has("__MASSIVE__")) {
1754
+ changedFiles.add("__MASSIVE__");
1755
+ }
1557
1756
  if (syncTimeout) {
1558
1757
  clearTimeout(syncTimeout);
1559
1758
  }
@@ -1574,9 +1773,29 @@ async function triggerSyncSequence(sessionId, _repoPath, onLog) {
1574
1773
  onLog("No valid files to sync (temp files filtered).");
1575
1774
  return;
1576
1775
  }
1577
- await stageFiles(sessionId, existingFiles);
1776
+ if (changedFiles.has("__MASSIVE__")) {
1777
+ onLog("Massive file changes detected in watcher. Checking status...");
1778
+ }
1578
1779
  const status = await getStatus(sessionId);
1579
- const branch = status.current || "main";
1780
+ if (status.isMassive) {
1781
+ onLog(`🚨 Massive repository change detected! Engaging chunked sync mode...`);
1782
+ const branch2 = status.current || "main";
1783
+ const success = await pushMassiveRepository(
1784
+ sessionId,
1785
+ "origin",
1786
+ branch2,
1787
+ 5e3,
1788
+ onLog,
1789
+ globalOnMassiveProgress || void 0
1790
+ );
1791
+ if (success) {
1792
+ onLog(`✅ Successfully pushed massive repository sync!`);
1793
+ }
1794
+ return;
1795
+ }
1796
+ await stageFiles(sessionId, existingFiles);
1797
+ const finalStatus = await getStatus(sessionId);
1798
+ const branch = finalStatus.current || "main";
1580
1799
  onLog("Generating AI commit message...");
1581
1800
  let commitMsg = `Auto-sync: update ${filesToSync.length > 1 ? filesToSync.length + " files" : filesToSync[0]}`;
1582
1801
  try {
@@ -1688,7 +1907,15 @@ async function applyAiFixAndResume(sessionId, resolvedCode) {
1688
1907
  if (resolvedCode) {
1689
1908
  for (const [file, content] of Object.entries(resolvedCode)) {
1690
1909
  await fs__namespace.writeFile(path__namespace.join(repoPath, file), content, "utf-8");
1691
- await git.add(["--force", file]);
1910
+ try {
1911
+ await git.raw(["-c", "core.safecrlf=false", "-c", "core.autocrlf=false", "add", "--force", file]);
1912
+ } catch (e) {
1913
+ if (e.message && e.message.includes("LF will be replaced by CRLF") && !e.message.includes("fatal:")) {
1914
+ console.warn("Ignored CRLF warning during AI fix apply");
1915
+ } else {
1916
+ throw e;
1917
+ }
1918
+ }
1692
1919
  }
1693
1920
  }
1694
1921
  try {
@@ -1851,6 +2078,17 @@ electron.app.whenReady().then(async () => {
1851
2078
  return await commitChanges(event.sender.id, message);
1852
2079
  });
1853
2080
  electron.ipcMain.handle("push-to-remote", async (event, remote, branch) => {
2081
+ const status = await getStatus(event.sender.id);
2082
+ if (status.isMassive) {
2083
+ return await pushMassiveRepository(
2084
+ event.sender.id,
2085
+ remote,
2086
+ branch,
2087
+ 5e3,
2088
+ void 0,
2089
+ (data) => event.sender.send("massive-push-progress", data)
2090
+ );
2091
+ }
1854
2092
  return await pushToRemote(event.sender.id, remote, branch);
1855
2093
  });
1856
2094
  electron.ipcMain.handle("pull-from-remote", async (event, remote, branch) => {
@@ -2040,6 +2278,9 @@ electron.app.whenReady().then(async () => {
2040
2278
  electron.ipcMain.handle("clone-repository", async (event, remoteUrl, destPath) => {
2041
2279
  return await cloneRepository(event.sender.id, remoteUrl, destPath);
2042
2280
  });
2281
+ electron.ipcMain.handle("remove-git-metadata", async (event, repoPath) => {
2282
+ return await removeGitMetadata(event.sender.id, repoPath);
2283
+ });
2043
2284
  electron.ipcMain.handle("check-path-exists", async (_event, targetPath) => {
2044
2285
  if (!targetPath) return false;
2045
2286
  try {
@@ -2073,15 +2314,10 @@ electron.app.whenReady().then(async () => {
2073
2314
  event.sender.id,
2074
2315
  repoPath,
2075
2316
  enabled,
2076
- (msg) => {
2077
- event.sender.send("auto-sync-log", msg);
2078
- },
2079
- (payload) => {
2080
- event.sender.send("auto-sync-error-approval", payload);
2081
- },
2082
- () => {
2083
- event.sender.send("auto-sync-resolved");
2084
- }
2317
+ (msg) => event.sender.send("auto-sync-log", msg),
2318
+ (payload) => event.sender.send("auto-sync-error-approval", payload),
2319
+ () => event.sender.send("auto-sync-resolved"),
2320
+ (data) => event.sender.send("massive-push-progress", data)
2085
2321
  );
2086
2322
  });
2087
2323
  electron.ipcMain.handle("approve-ai-fix", async (event, resolvedCode) => {
@@ -13,6 +13,7 @@ const api = {
13
13
  selectDirectory: () => electron.ipcRenderer.invoke("select-directory"),
14
14
  setRepoPath: (path) => electron.ipcRenderer.invoke("set-repo-path", path),
15
15
  initRepo: (path) => electron.ipcRenderer.invoke("init-repo", path),
16
+ removeGitMetadata: (repoPath) => electron.ipcRenderer.invoke("remove-git-metadata", repoPath),
16
17
  getStatus: () => electron.ipcRenderer.invoke("get-status"),
17
18
  stageFiles: (files) => electron.ipcRenderer.invoke("stage-files", files),
18
19
  unstageFiles: (files) => electron.ipcRenderer.invoke("unstage-files", files),
@@ -93,6 +94,10 @@ const api = {
93
94
  electron.ipcRenderer.removeAllListeners("auto-sync-resolved");
94
95
  electron.ipcRenderer.on("auto-sync-resolved", () => callback());
95
96
  },
97
+ onMassivePushProgress: (callback) => {
98
+ electron.ipcRenderer.removeAllListeners("massive-push-progress");
99
+ electron.ipcRenderer.on("massive-push-progress", (_event, data) => callback(data));
100
+ },
96
101
  getAppVersion: () => electron.ipcRenderer.invoke("get-app-version"),
97
102
  // AI Fixer API
98
103
  askAi: (provider, apiKey, errorMsg, gitStatus, repoPath) => electron.ipcRenderer.invoke("ask-ai", provider, apiKey, errorMsg, gitStatus, repoPath),