gitxelectron 1.1.7 → 1.1.9

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
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  const electron = require("electron");
3
3
  const path = require("path");
4
- const simpleGit = require("simple-git");
5
4
  const fs = require("fs/promises");
5
+ const simpleGit = require("simple-git");
6
6
  const crypto = require("crypto");
7
7
  const generativeAi = require("@google/generative-ai");
8
8
  const OpenAI = require("openai");
@@ -58,15 +58,23 @@ function cleanupSession(sessionId) {
58
58
  activeSessions.delete(sessionId);
59
59
  }
60
60
  function getToken(sessionId) {
61
- const accountId = activeSessions.get(sessionId);
61
+ let accountId = activeSessions.get(sessionId);
62
+ if (!accountId && accounts.length > 0) {
63
+ accountId = accounts[0].id;
64
+ activeSessions.set(sessionId, accountId);
65
+ }
62
66
  if (!accountId) return null;
63
67
  const acc = accounts.find((a) => a.id === accountId);
64
- return acc ? acc.token : null;
68
+ return acc ? acc.token : accounts.length > 0 ? accounts[0].token : null;
65
69
  }
66
70
  function getActiveAccount(sessionId) {
67
- const accountId = activeSessions.get(sessionId);
71
+ let accountId = activeSessions.get(sessionId);
72
+ if (!accountId && accounts.length > 0) {
73
+ accountId = accounts[0].id;
74
+ activeSessions.set(sessionId, accountId);
75
+ }
68
76
  if (!accountId) return null;
69
- return accounts.find((a) => a.id === accountId) || null;
77
+ return accounts.find((a) => a.id === accountId) || (accounts.length > 0 ? accounts[0] : null);
70
78
  }
71
79
  async function getAccounts(sessionId) {
72
80
  return {
@@ -288,6 +296,68 @@ async function getUserRepositories(sessionId) {
288
296
  }
289
297
  return await res.json();
290
298
  }
299
+ async function getCollaboratedRepos(sessionId) {
300
+ const token = getToken(sessionId);
301
+ if (!token) throw new Error("Not authenticated in this session");
302
+ const headers = {
303
+ Authorization: `Bearer ${token}`,
304
+ Accept: "application/vnd.github.v3+json",
305
+ "User-Agent": "gitelectron-App"
306
+ };
307
+ const res = await fetch(
308
+ "https://api.github.com/user/repos?affiliation=owner,collaborator,organization_member&per_page=100&sort=updated",
309
+ { headers }
310
+ );
311
+ if (!res.ok) {
312
+ const errorData = await res.json().catch(() => ({}));
313
+ throw new Error(errorData.message || "Failed to fetch collaborated repositories");
314
+ }
315
+ const allRepos = await res.json();
316
+ const account = getActiveAccount(sessionId);
317
+ const currentUsername = account?.username?.toLowerCase() || "";
318
+ const externalCollabRepos = [];
319
+ const ownedRepos = [];
320
+ for (const repo of allRepos) {
321
+ const ownerLogin = repo.owner?.login?.toLowerCase() || "";
322
+ if (ownerLogin !== currentUsername) {
323
+ externalCollabRepos.push(repo);
324
+ } else {
325
+ ownedRepos.push(repo);
326
+ }
327
+ }
328
+ const ownedCollabChecks = await Promise.allSettled(
329
+ ownedRepos.map(async (repo) => {
330
+ try {
331
+ const [collabRes, invRes] = await Promise.all([
332
+ fetch(`https://api.github.com/repos/${repo.owner.login}/${repo.name}/collaborators`, { headers }),
333
+ fetch(`https://api.github.com/repos/${repo.owner.login}/${repo.name}/invitations`, { headers })
334
+ ]);
335
+ const collabs = collabRes.ok ? await collabRes.json() : [];
336
+ const invs = invRes.ok ? await invRes.json() : [];
337
+ const hasOtherCollabs = Array.isArray(collabs) && collabs.some(
338
+ (c) => c.login && c.login.toLowerCase() !== currentUsername
339
+ );
340
+ const hasPendingInvs = Array.isArray(invs) && invs.length > 0;
341
+ if (hasOtherCollabs || hasPendingInvs) {
342
+ return {
343
+ ...repo,
344
+ collaboratorsList: collabs,
345
+ hasPendingInvitations: hasPendingInvs
346
+ };
347
+ }
348
+ } catch (err) {
349
+ }
350
+ return null;
351
+ })
352
+ );
353
+ const ownedCollabRepos = [];
354
+ for (const result of ownedCollabChecks) {
355
+ if (result.status === "fulfilled" && result.value) {
356
+ ownedCollabRepos.push(result.value);
357
+ }
358
+ }
359
+ return [...externalCollabRepos, ...ownedCollabRepos];
360
+ }
291
361
  async function createGitHubRepository(sessionId, name, description, isPrivate, autoInit = false, gitignoreTemplate = "", licenseTemplate = "") {
292
362
  const token = getToken(sessionId);
293
363
  if (!token) throw new Error("Not authenticated in this session");
@@ -390,7 +460,9 @@ async function removeCollaborator(sessionId, remoteUrl, usernameOrId, isInvitati
390
460
  async function searchGitHubUser(sessionId, username) {
391
461
  const token = getToken(sessionId);
392
462
  if (!token) throw new Error("Not authenticated");
393
- const res = await fetch(`https://api.github.com/users/${username}`, {
463
+ const trimmed = username.trim();
464
+ if (!trimmed) return null;
465
+ const res = await fetch(`https://api.github.com/users/${encodeURIComponent(trimmed)}`, {
394
466
  headers: {
395
467
  Authorization: `Bearer ${token}`,
396
468
  Accept: "application/vnd.github.v3+json",
@@ -407,17 +479,32 @@ async function searchGitHubUser(sessionId, username) {
407
479
  async function searchGitHubUsers(sessionId, query) {
408
480
  const token = getToken(sessionId);
409
481
  if (!token) throw new Error("Not authenticated");
410
- if (!query) return [];
411
- const res = await fetch(`https://api.github.com/search/users?q=${encodeURIComponent(query)}&per_page=5`, {
482
+ const trimmed = query.trim();
483
+ if (!trimmed) return [];
484
+ const searchPromise = fetch(
485
+ `https://api.github.com/search/users?q=${encodeURIComponent(trimmed)}+type:user&per_page=15`,
486
+ {
487
+ headers: {
488
+ Authorization: `Bearer ${token}`,
489
+ Accept: "application/vnd.github.v3+json",
490
+ "User-Agent": "gitelectron-App"
491
+ }
492
+ }
493
+ ).then((r) => r.ok ? r.json() : { items: [] }).catch(() => ({ items: [] }));
494
+ const exactPromise = /^[a-zA-Z0-9-]+$/.test(trimmed) ? fetch(`https://api.github.com/users/${encodeURIComponent(trimmed)}`, {
412
495
  headers: {
413
496
  Authorization: `Bearer ${token}`,
414
497
  Accept: "application/vnd.github.v3+json",
415
498
  "User-Agent": "gitelectron-App"
416
499
  }
417
- });
418
- if (!res.ok) throw new Error("Failed to search users");
419
- const data = await res.json();
420
- return data.items || [];
500
+ }).then((r) => r.ok ? r.json() : null).catch(() => null) : Promise.resolve(null);
501
+ const [searchData, exactUser] = await Promise.all([searchPromise, exactPromise]);
502
+ let items = searchData.items || [];
503
+ if (exactUser && exactUser.login) {
504
+ items = items.filter((u) => u.login.toLowerCase() !== exactUser.login.toLowerCase());
505
+ items.unshift(exactUser);
506
+ }
507
+ return items;
421
508
  }
422
509
  async function getRepositoryBranches(sessionId, owner, repo) {
423
510
  const token = getToken(sessionId);
@@ -499,6 +586,22 @@ function initGit(sessionId, baseDir) {
499
586
  });
500
587
  gitSessions.set(sessionId, { git, repoPath: baseDir });
501
588
  }
589
+ function createSafeGit(baseDir) {
590
+ const options = {
591
+ binary: "git",
592
+ maxConcurrentProcesses: 6,
593
+ unsafe: { allowUnsafeEditor: true, allowUnsafeAskPass: true, allowUnsafeCredentialHelper: true }
594
+ };
595
+ if (baseDir) {
596
+ options.baseDir = baseDir;
597
+ }
598
+ return simpleGit.simpleGit(options).env({
599
+ ...process.env,
600
+ GCM_INTERACTIVE: "false",
601
+ GIT_TERMINAL_PROMPT: "0",
602
+ GIT_ASKPASS: "echo"
603
+ });
604
+ }
502
605
  function cleanupGitSession(sessionId) {
503
606
  gitSessions.delete(sessionId);
504
607
  }
@@ -630,10 +733,20 @@ async function commitChanges(sessionId, message) {
630
733
  function injectAuthToken(sessionId, remoteUrl) {
631
734
  const token = getToken(sessionId);
632
735
  if (!token) return remoteUrl;
633
- if (remoteUrl.includes("github.com")) {
634
- let cleanUrl = remoteUrl.replace("git@github.com:", "https://github.com/");
635
- if (cleanUrl.startsWith("https://github.com/")) {
636
- return cleanUrl.replace("https://github.com/", `https://x-access-token:${token}@github.com/`);
736
+ try {
737
+ let cleanUrl = remoteUrl.trim();
738
+ if (cleanUrl.startsWith("git@github.com:")) {
739
+ cleanUrl = cleanUrl.replace("git@github.com:", "https://github.com/");
740
+ }
741
+ if (cleanUrl.includes("github.com")) {
742
+ const parsed = new URL(cleanUrl);
743
+ parsed.username = "x-access-token";
744
+ parsed.password = token;
745
+ return parsed.toString();
746
+ }
747
+ } catch (e) {
748
+ if (remoteUrl.includes("github.com")) {
749
+ return remoteUrl.replace(/https:\/\/(?:[^@]+@)?github\.com\//, `https://x-access-token:${token}@github.com/`);
637
750
  }
638
751
  }
639
752
  return remoteUrl;
@@ -1009,6 +1122,97 @@ async function executeRawCommand(sessionId, args) {
1009
1122
  const { git } = getGitSession(sessionId);
1010
1123
  return await git.raw(args);
1011
1124
  }
1125
+ async function cloneRepository(sessionId, remoteUrl, destPath) {
1126
+ let normalizedUrl = remoteUrl.trim();
1127
+ if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://") && !normalizedUrl.startsWith("git@")) {
1128
+ normalizedUrl = "https://github.com/" + normalizedUrl;
1129
+ }
1130
+ if (normalizedUrl.startsWith("git@github.com:")) {
1131
+ normalizedUrl = normalizedUrl.replace("git@github.com:", "https://github.com/");
1132
+ }
1133
+ if (!normalizedUrl.endsWith(".git") && normalizedUrl.startsWith("http")) {
1134
+ normalizedUrl += ".git";
1135
+ }
1136
+ const authUrl = injectAuthToken(sessionId, normalizedUrl);
1137
+ await fs__namespace.mkdir(path__namespace.dirname(destPath), { recursive: true });
1138
+ let destExists = false;
1139
+ let destIsEmpty = false;
1140
+ let destHasGit = false;
1141
+ try {
1142
+ const stats = await fs__namespace.stat(destPath);
1143
+ if (stats.isDirectory()) {
1144
+ destExists = true;
1145
+ const files = await fs__namespace.readdir(destPath);
1146
+ const realFiles = files.filter(
1147
+ (f) => ![".ds_store", "desktop.ini", "thumbs.db"].includes(f.toLowerCase())
1148
+ );
1149
+ destIsEmpty = realFiles.length === 0;
1150
+ destHasGit = files.includes(".git");
1151
+ }
1152
+ } catch {
1153
+ destExists = false;
1154
+ }
1155
+ try {
1156
+ if (destExists && destHasGit) {
1157
+ const existingGit = createSafeGit(destPath);
1158
+ try {
1159
+ await existingGit.raw(["config", "--global", "--add", "safe.directory", destPath]);
1160
+ const remotes = await existingGit.getRemotes();
1161
+ if (remotes.some((r) => r.name === "origin")) {
1162
+ await existingGit.raw(["remote", "set-url", "origin", authUrl]);
1163
+ } else {
1164
+ await existingGit.raw(["remote", "add", "origin", authUrl]);
1165
+ }
1166
+ await existingGit.raw(["-c", "credential.helper=", "fetch", "origin"]);
1167
+ const st = await existingGit.status();
1168
+ const branch = st.current || "main";
1169
+ try {
1170
+ await existingGit.raw(["-c", "credential.helper=", "pull", "origin", branch]);
1171
+ } catch (e) {
1172
+ }
1173
+ return destPath;
1174
+ } catch (err) {
1175
+ console.warn("Re-using existing repo failed, falling through to clone attempt:", err);
1176
+ }
1177
+ }
1178
+ if (destExists && destIsEmpty) {
1179
+ const emptyDirGit = createSafeGit(destPath);
1180
+ await emptyDirGit.raw(["-c", "credential.helper=", "clone", authUrl, "."]);
1181
+ try {
1182
+ await createSafeGit().raw(["config", "--global", "--add", "safe.directory", destPath]);
1183
+ } catch (e) {
1184
+ }
1185
+ return destPath;
1186
+ }
1187
+ if (destExists && !destIsEmpty && !destHasGit) {
1188
+ throw new Error(
1189
+ `The folder '${path__namespace.basename(destPath)}' already contains files. Please select an empty folder or a different location to clone.`
1190
+ );
1191
+ }
1192
+ const baseDir = path__namespace.dirname(destPath);
1193
+ const targetDirName = path__namespace.basename(destPath);
1194
+ const parentGit = createSafeGit(baseDir);
1195
+ await parentGit.raw(["-c", "credential.helper=", "clone", authUrl, targetDirName]);
1196
+ try {
1197
+ await createSafeGit().raw(["config", "--global", "--add", "safe.directory", destPath]);
1198
+ } catch (e) {
1199
+ }
1200
+ return destPath;
1201
+ } catch (err) {
1202
+ const token = getToken(sessionId);
1203
+ let cleanMsg = err?.message || String(err);
1204
+ if (token) {
1205
+ cleanMsg = cleanMsg.split(token).join("***");
1206
+ }
1207
+ cleanMsg = cleanMsg.replace(/https:\/\/[^@]+@github\.com/g, "https://github.com");
1208
+ if (cleanMsg.includes("terminal prompts disabled") || cleanMsg.includes("Authentication failed") || cleanMsg.includes("could not read Username")) {
1209
+ throw new Error(
1210
+ `Authentication failed for GitHub repository. Please verify your account is logged in and has access to this repository.`
1211
+ );
1212
+ }
1213
+ throw new Error(cleanMsg);
1214
+ }
1215
+ }
1012
1216
  const SYSTEM_PROMPT = `You are an autonomous Git Healer Agent for the "GitElectron" app.
1013
1217
  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.
1014
1218
  You will receive the Git Status and the Raw Error Message.
@@ -1114,6 +1318,32 @@ ${errorMsg}
1114
1318
  `AI Analysis failed after trying all provided keys. Last error: ${lastError?.message}`
1115
1319
  );
1116
1320
  }
1321
+ const getConfigPath = () => path__namespace.join(electron.app.getPath("userData"), "gitx-config.json");
1322
+ const saveLastWorkspace = async (workspacePath) => {
1323
+ try {
1324
+ const configPath = getConfigPath();
1325
+ let config = {};
1326
+ try {
1327
+ const data = await fs__namespace.readFile(configPath, "utf8");
1328
+ config = JSON.parse(data);
1329
+ } catch (e) {
1330
+ }
1331
+ config.lastWorkspace = workspacePath;
1332
+ await fs__namespace.writeFile(configPath, JSON.stringify(config, null, 2));
1333
+ } catch (e) {
1334
+ console.error("Failed to save workspace", e);
1335
+ }
1336
+ };
1337
+ const getLastWorkspace = async () => {
1338
+ try {
1339
+ const configPath = getConfigPath();
1340
+ const data = await fs__namespace.readFile(configPath, "utf8");
1341
+ const config = JSON.parse(data);
1342
+ return config.lastWorkspace || null;
1343
+ } catch (e) {
1344
+ return null;
1345
+ }
1346
+ };
1117
1347
  let currentWatcher = null;
1118
1348
  let syncTimeout = null;
1119
1349
  let isAutoSyncEnabled = false;
@@ -1288,6 +1518,7 @@ electron.app.whenReady().then(async () => {
1288
1518
  if (isRepo) {
1289
1519
  initGit(sessionId, path2);
1290
1520
  }
1521
+ await saveLastWorkspace(path2);
1291
1522
  return { path: path2, isRepo };
1292
1523
  });
1293
1524
  electron.ipcMain.handle("set-repo-path", async (event, path2) => {
@@ -1296,6 +1527,7 @@ electron.app.whenReady().then(async () => {
1296
1527
  if (isRepo) {
1297
1528
  initGit(sessionId, path2);
1298
1529
  }
1530
+ await saveLastWorkspace(path2);
1299
1531
  return { path: path2, isRepo };
1300
1532
  });
1301
1533
  electron.ipcMain.handle("init-repo", async (event, path2) => {
@@ -1378,6 +1610,9 @@ electron.app.whenReady().then(async () => {
1378
1610
  electron.ipcMain.handle("get-user-repositories", async (event) => {
1379
1611
  return await getUserRepositories(event.sender.id);
1380
1612
  });
1613
+ electron.ipcMain.handle("get-collaborated-repos", async (event) => {
1614
+ return await getCollaboratedRepos(event.sender.id);
1615
+ });
1381
1616
  electron.ipcMain.handle(
1382
1617
  "create-github-repository",
1383
1618
  async (event, name, description, isPrivate, autoInit, gitignoreTemplate, licenseTemplate) => {
@@ -1497,6 +1732,37 @@ electron.app.whenReady().then(async () => {
1497
1732
  electron.ipcMain.handle("execute-raw-git", async (event, args) => {
1498
1733
  return await executeRawCommand(event.sender.id, args);
1499
1734
  });
1735
+ electron.ipcMain.handle("clone-repository", async (event, remoteUrl, destPath) => {
1736
+ return await cloneRepository(event.sender.id, remoteUrl, destPath);
1737
+ });
1738
+ electron.ipcMain.handle("check-path-exists", async (_event, targetPath) => {
1739
+ if (!targetPath) return false;
1740
+ try {
1741
+ const stats = await fs__namespace.stat(targetPath);
1742
+ return stats.isDirectory();
1743
+ } catch {
1744
+ return false;
1745
+ }
1746
+ });
1747
+ electron.ipcMain.handle("upload-files-to-repo", async (event, repoPath) => {
1748
+ const window = electron.BrowserWindow.fromWebContents(event.sender);
1749
+ if (!repoPath) throw new Error("No repository path specified");
1750
+ const result = await electron.dialog.showOpenDialog(window || void 0, {
1751
+ title: "Select Files to Upload to Repository",
1752
+ properties: ["openFile", "multiSelections"]
1753
+ });
1754
+ if (result.canceled || !result.filePaths.length) {
1755
+ return { success: false, count: 0, files: [] };
1756
+ }
1757
+ const copiedFiles = [];
1758
+ for (const filePath of result.filePaths) {
1759
+ const fileName = path__namespace.basename(filePath);
1760
+ const destPath = path__namespace.join(repoPath, fileName);
1761
+ await fs__namespace.copyFile(filePath, destPath);
1762
+ copiedFiles.push(fileName);
1763
+ }
1764
+ return { success: true, count: copiedFiles.length, files: copiedFiles };
1765
+ });
1500
1766
  electron.ipcMain.on("toggle-auto-sync", (event, repoPath, enabled) => {
1501
1767
  toggleAutoSync(event.sender.id, repoPath, enabled, (msg) => {
1502
1768
  event.sender.send("auto-sync-log", msg);
@@ -1549,7 +1815,10 @@ electron.app.whenReady().then(async () => {
1549
1815
  }, 500);
1550
1816
  });
1551
1817
  });
1552
- createWindow();
1818
+ if (!initialRepo) {
1819
+ initialRepo = await getLastWorkspace();
1820
+ }
1821
+ createWindow(initialRepo || void 0);
1553
1822
  electron.app.on("activate", function() {
1554
1823
  if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
1555
1824
  });
@@ -40,6 +40,7 @@ const api = {
40
40
  switchAccount: (accountId) => electron.ipcRenderer.invoke("switch-account", accountId),
41
41
  removeAccount: (accountId) => electron.ipcRenderer.invoke("remove-account", accountId),
42
42
  getUserRepositories: () => electron.ipcRenderer.invoke("get-user-repositories"),
43
+ getCollaboratedRepos: () => electron.ipcRenderer.invoke("get-collaborated-repos"),
43
44
  createGitHubRepository: (name, description, isPrivate, autoInit = false, gitignoreTemplate = "", licenseTemplate = "") => electron.ipcRenderer.invoke(
44
45
  "create-github-repository",
45
46
  name,
@@ -68,6 +69,9 @@ const api = {
68
69
  setupUniversalWorkspace: (remoteUrl, branch, baseBranch, dest) => electron.ipcRenderer.invoke("setup-universal-workspace", remoteUrl, branch, baseBranch, dest),
69
70
  checkUniversalCollisions: (workspacePath, sourceFiles, dest) => electron.ipcRenderer.invoke("check-universal-collisions", workspacePath, sourceFiles, dest),
70
71
  getUniversalDiff: (workspacePath, file) => electron.ipcRenderer.invoke("get-universal-diff", workspacePath, file),
72
+ cloneRepository: (remoteUrl, destPath) => electron.ipcRenderer.invoke("clone-repository", remoteUrl, destPath),
73
+ checkPathExists: (targetPath) => electron.ipcRenderer.invoke("check-path-exists", targetPath),
74
+ uploadFilesToRepo: (repoPath) => electron.ipcRenderer.invoke("upload-files-to-repo", repoPath),
71
75
  commitUniversalWorkspace: (workspacePath, msg, branch) => electron.ipcRenderer.invoke("commit-universal-workspace", workspacePath, msg, branch),
72
76
  cleanupUniversalWorkspace: (workspacePath) => electron.ipcRenderer.invoke("cleanup-universal-workspace", workspacePath),
73
77
  toggleAutoSync: (repoPath, enabled) => electron.ipcRenderer.send("toggle-auto-sync", repoPath, enabled),