gitxelectron 1.1.6 → 1.1.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
@@ -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");
@@ -176,9 +176,37 @@ async function pollDeviceFlow(sessionId, deviceCode, intervalMs) {
176
176
  activeSessions.set(sessionId, accountId);
177
177
  try {
178
178
  const { setGitConfig: setGitConfig2 } = require("./gitService");
179
- const email = `${userData.id}+${username}@users.noreply.github.com`;
180
- await setGitConfig2(sessionId, "user.name", username);
181
- await setGitConfig2(sessionId, "user.email", email);
179
+ let email = userData.email;
180
+ try {
181
+ const emailsRes = await fetch("https://api.github.com/user/emails", {
182
+ headers: {
183
+ Authorization: `Bearer ${data.access_token}`,
184
+ Accept: "application/vnd.github.v3+json",
185
+ "User-Agent": "gitelectron-App"
186
+ }
187
+ });
188
+ if (emailsRes.ok) {
189
+ const emails = await emailsRes.json();
190
+ const primary = emails.find((e) => e.primary);
191
+ if (primary) email = primary.email;
192
+ if (!email) {
193
+ const noreply = emails.find((e) => e.email.includes("noreply.github.com"));
194
+ if (noreply) email = noreply.email;
195
+ }
196
+ }
197
+ } catch (e) {
198
+ }
199
+ if (!email) {
200
+ email = `${userData.id}+${username}@users.noreply.github.com`;
201
+ }
202
+ await setGitConfig2(sessionId, "user.name", username, true).catch(() => {
203
+ });
204
+ await setGitConfig2(sessionId, "user.email", email, true).catch(() => {
205
+ });
206
+ await setGitConfig2(sessionId, "user.name", username).catch(() => {
207
+ });
208
+ await setGitConfig2(sessionId, "user.email", email).catch(() => {
209
+ });
182
210
  } catch (e) {
183
211
  console.error("Failed to configure git user", e);
184
212
  }
@@ -260,6 +288,68 @@ async function getUserRepositories(sessionId) {
260
288
  }
261
289
  return await res.json();
262
290
  }
291
+ async function getCollaboratedRepos(sessionId) {
292
+ const token = getToken(sessionId);
293
+ if (!token) throw new Error("Not authenticated in this session");
294
+ const headers = {
295
+ Authorization: `Bearer ${token}`,
296
+ Accept: "application/vnd.github.v3+json",
297
+ "User-Agent": "gitelectron-App"
298
+ };
299
+ const res = await fetch(
300
+ "https://api.github.com/user/repos?affiliation=owner,collaborator,organization_member&per_page=100&sort=updated",
301
+ { headers }
302
+ );
303
+ if (!res.ok) {
304
+ const errorData = await res.json().catch(() => ({}));
305
+ throw new Error(errorData.message || "Failed to fetch collaborated repositories");
306
+ }
307
+ const allRepos = await res.json();
308
+ const account = getActiveAccount(sessionId);
309
+ const currentUsername = account?.username?.toLowerCase() || "";
310
+ const externalCollabRepos = [];
311
+ const ownedRepos = [];
312
+ for (const repo of allRepos) {
313
+ const ownerLogin = repo.owner?.login?.toLowerCase() || "";
314
+ if (ownerLogin !== currentUsername) {
315
+ externalCollabRepos.push(repo);
316
+ } else {
317
+ ownedRepos.push(repo);
318
+ }
319
+ }
320
+ const ownedCollabChecks = await Promise.allSettled(
321
+ ownedRepos.map(async (repo) => {
322
+ try {
323
+ const [collabRes, invRes] = await Promise.all([
324
+ fetch(`https://api.github.com/repos/${repo.owner.login}/${repo.name}/collaborators`, { headers }),
325
+ fetch(`https://api.github.com/repos/${repo.owner.login}/${repo.name}/invitations`, { headers })
326
+ ]);
327
+ const collabs = collabRes.ok ? await collabRes.json() : [];
328
+ const invs = invRes.ok ? await invRes.json() : [];
329
+ const hasOtherCollabs = Array.isArray(collabs) && collabs.some(
330
+ (c) => c.login && c.login.toLowerCase() !== currentUsername
331
+ );
332
+ const hasPendingInvs = Array.isArray(invs) && invs.length > 0;
333
+ if (hasOtherCollabs || hasPendingInvs) {
334
+ return {
335
+ ...repo,
336
+ collaboratorsList: collabs,
337
+ hasPendingInvitations: hasPendingInvs
338
+ };
339
+ }
340
+ } catch (err) {
341
+ }
342
+ return null;
343
+ })
344
+ );
345
+ const ownedCollabRepos = [];
346
+ for (const result of ownedCollabChecks) {
347
+ if (result.status === "fulfilled" && result.value) {
348
+ ownedCollabRepos.push(result.value);
349
+ }
350
+ }
351
+ return [...externalCollabRepos, ...ownedCollabRepos];
352
+ }
263
353
  async function createGitHubRepository(sessionId, name, description, isPrivate, autoInit = false, gitignoreTemplate = "", licenseTemplate = "") {
264
354
  const token = getToken(sessionId);
265
355
  if (!token) throw new Error("Not authenticated in this session");
@@ -301,9 +391,10 @@ async function getCollaborators(sessionId, remoteUrl) {
301
391
  Accept: "application/vnd.github.v3+json",
302
392
  "User-Agent": "gitelectron-App"
303
393
  };
304
- const [collabRes, invRes] = await Promise.all([
394
+ const [collabRes, invRes, contribRes] = await Promise.all([
305
395
  fetch(`https://api.github.com/repos/${owner}/${repo}/collaborators`, { headers }),
306
- fetch(`https://api.github.com/repos/${owner}/${repo}/invitations`, { headers })
396
+ fetch(`https://api.github.com/repos/${owner}/${repo}/invitations`, { headers }),
397
+ fetch(`https://api.github.com/repos/${owner}/${repo}/contributors`, { headers })
307
398
  ]);
308
399
  if (!collabRes.ok) {
309
400
  const errorData = await collabRes.json().catch(() => ({}));
@@ -313,7 +404,8 @@ async function getCollaborators(sessionId, remoteUrl) {
313
404
  }
314
405
  const collaborators = await collabRes.json();
315
406
  const invitations = invRes.ok ? await invRes.json() : [];
316
- return { collaborators, invitations };
407
+ const contributors = contribRes.ok ? await contribRes.json() : [];
408
+ return { collaborators, invitations, contributors };
317
409
  }
318
410
  async function addCollaborator(sessionId, remoteUrl, username) {
319
411
  const token = getToken(sessionId);
@@ -360,7 +452,9 @@ async function removeCollaborator(sessionId, remoteUrl, usernameOrId, isInvitati
360
452
  async function searchGitHubUser(sessionId, username) {
361
453
  const token = getToken(sessionId);
362
454
  if (!token) throw new Error("Not authenticated");
363
- const res = await fetch(`https://api.github.com/users/${username}`, {
455
+ const trimmed = username.trim();
456
+ if (!trimmed) return null;
457
+ const res = await fetch(`https://api.github.com/users/${encodeURIComponent(trimmed)}`, {
364
458
  headers: {
365
459
  Authorization: `Bearer ${token}`,
366
460
  Accept: "application/vnd.github.v3+json",
@@ -374,6 +468,36 @@ async function searchGitHubUser(sessionId, username) {
374
468
  }
375
469
  return await res.json();
376
470
  }
471
+ async function searchGitHubUsers(sessionId, query) {
472
+ const token = getToken(sessionId);
473
+ if (!token) throw new Error("Not authenticated");
474
+ const trimmed = query.trim();
475
+ if (!trimmed) return [];
476
+ const searchPromise = fetch(
477
+ `https://api.github.com/search/users?q=${encodeURIComponent(trimmed)}+type:user&per_page=15`,
478
+ {
479
+ headers: {
480
+ Authorization: `Bearer ${token}`,
481
+ Accept: "application/vnd.github.v3+json",
482
+ "User-Agent": "gitelectron-App"
483
+ }
484
+ }
485
+ ).then((r) => r.ok ? r.json() : { items: [] }).catch(() => ({ items: [] }));
486
+ const exactPromise = /^[a-zA-Z0-9-]+$/.test(trimmed) ? fetch(`https://api.github.com/users/${encodeURIComponent(trimmed)}`, {
487
+ headers: {
488
+ Authorization: `Bearer ${token}`,
489
+ Accept: "application/vnd.github.v3+json",
490
+ "User-Agent": "gitelectron-App"
491
+ }
492
+ }).then((r) => r.ok ? r.json() : null).catch(() => null) : Promise.resolve(null);
493
+ const [searchData, exactUser] = await Promise.all([searchPromise, exactPromise]);
494
+ let items = searchData.items || [];
495
+ if (exactUser && exactUser.login) {
496
+ items = items.filter((u) => u.login.toLowerCase() !== exactUser.login.toLowerCase());
497
+ items.unshift(exactUser);
498
+ }
499
+ return items;
500
+ }
377
501
  async function getRepositoryBranches(sessionId, owner, repo) {
378
502
  const token = getToken(sessionId);
379
503
  if (!token) throw new Error("Not authenticated");
@@ -463,6 +587,13 @@ function getGitSession(sessionId) {
463
587
  return session;
464
588
  }
465
589
  async function isGitRepo(dir) {
590
+ let hasGitFolder = false;
591
+ try {
592
+ await fs__namespace.access(path__namespace.join(dir, ".git"));
593
+ hasGitFolder = true;
594
+ } catch {
595
+ hasGitFolder = false;
596
+ }
466
597
  try {
467
598
  const tempGit = simpleGit.simpleGit(dir).env({
468
599
  ...process.env,
@@ -470,8 +601,28 @@ async function isGitRepo(dir) {
470
601
  GIT_TERMINAL_PROMPT: "0",
471
602
  GIT_ASKPASS: "echo"
472
603
  });
473
- return await tempGit.checkIsRepo();
604
+ const isRepo = await tempGit.checkIsRepo();
605
+ if (isRepo) return true;
606
+ if (hasGitFolder) {
607
+ await simpleGit.simpleGit().raw(["config", "--global", "--add", "safe.directory", dir]);
608
+ return await tempGit.checkIsRepo();
609
+ }
610
+ return false;
474
611
  } catch {
612
+ if (hasGitFolder) {
613
+ try {
614
+ await simpleGit.simpleGit().raw(["config", "--global", "--add", "safe.directory", dir]);
615
+ const tempGit = simpleGit.simpleGit(dir).env({
616
+ ...process.env,
617
+ GCM_INTERACTIVE: "false",
618
+ GIT_TERMINAL_PROMPT: "0",
619
+ GIT_ASKPASS: "echo"
620
+ });
621
+ return await tempGit.checkIsRepo();
622
+ } catch {
623
+ return true;
624
+ }
625
+ }
475
626
  return false;
476
627
  }
477
628
  }
@@ -778,11 +929,20 @@ async function getGitConfig(sessionId, key, global = false) {
778
929
  }
779
930
  }
780
931
  async function setGitConfig(sessionId, key, value, global = false) {
781
- const { git } = getGitSession(sessionId);
932
+ let gitInstance;
933
+ try {
934
+ gitInstance = getGitSession(sessionId).git;
935
+ } catch (e) {
936
+ if (global) {
937
+ gitInstance = simpleGit.simpleGit();
938
+ } else {
939
+ throw e;
940
+ }
941
+ }
782
942
  const args = ["config"];
783
943
  if (global) args.push("--global");
784
944
  args.push(key, value);
785
- await git.raw(args);
945
+ await gitInstance.raw(args);
786
946
  }
787
947
  async function readFile(sessionId, file) {
788
948
  const { repoPath } = getGitSession(sessionId);
@@ -928,6 +1088,22 @@ async function executeRawCommand(sessionId, args) {
928
1088
  const { git } = getGitSession(sessionId);
929
1089
  return await git.raw(args);
930
1090
  }
1091
+ async function cloneRepository(sessionId, remoteUrl, destPath) {
1092
+ const authUrl = injectAuthToken(sessionId, remoteUrl);
1093
+ await fs__namespace.mkdir(path__namespace.dirname(destPath), { recursive: true });
1094
+ const git = simpleGit.simpleGit().env({
1095
+ ...process.env,
1096
+ GCM_INTERACTIVE: "false",
1097
+ GIT_TERMINAL_PROMPT: "0",
1098
+ GIT_ASKPASS: "echo"
1099
+ });
1100
+ await git.clone(authUrl, destPath);
1101
+ try {
1102
+ await simpleGit.simpleGit().raw(["config", "--global", "--add", "safe.directory", destPath]);
1103
+ } catch (e) {
1104
+ }
1105
+ return destPath;
1106
+ }
931
1107
  const SYSTEM_PROMPT = `You are an autonomous Git Healer Agent for the "GitElectron" app.
932
1108
  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.
933
1109
  You will receive the Git Status and the Raw Error Message.
@@ -1097,7 +1273,7 @@ async function triggerSyncSequence(sessionId, _repoPath, onLog) {
1097
1273
  }
1098
1274
  }
1099
1275
  const icon = path.join(__dirname, "../../resources/icon.png");
1100
- function createWindow() {
1276
+ function createWindow(initialPath) {
1101
1277
  const mainWindow = new electron.BrowserWindow({
1102
1278
  width: 1100,
1103
1279
  height: 800,
@@ -1130,6 +1306,11 @@ function createWindow() {
1130
1306
  } else {
1131
1307
  mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
1132
1308
  }
1309
+ mainWindow.webContents.on("did-finish-load", () => {
1310
+ if (initialPath) {
1311
+ mainWindow.webContents.send("set-cli-repo", initialPath);
1312
+ }
1313
+ });
1133
1314
  }
1134
1315
  const gotTheLock = electron.app.requestSingleInstanceLock();
1135
1316
  if (!gotTheLock) {
@@ -1174,8 +1355,8 @@ electron.app.whenReady().then(async () => {
1174
1355
  utils.optimizer.watchWindowShortcuts(window);
1175
1356
  });
1176
1357
  electron.ipcMain.on("ping", () => console.log("pong"));
1177
- electron.ipcMain.handle("new-window", () => {
1178
- createWindow();
1358
+ electron.ipcMain.handle("new-window", (_, repoPath) => {
1359
+ createWindow(repoPath);
1179
1360
  });
1180
1361
  electron.ipcMain.handle("get-accounts", async (event) => {
1181
1362
  return await getAccounts(event.sender.id);
@@ -1292,6 +1473,9 @@ electron.app.whenReady().then(async () => {
1292
1473
  electron.ipcMain.handle("get-user-repositories", async (event) => {
1293
1474
  return await getUserRepositories(event.sender.id);
1294
1475
  });
1476
+ electron.ipcMain.handle("get-collaborated-repos", async (event) => {
1477
+ return await getCollaboratedRepos(event.sender.id);
1478
+ });
1295
1479
  electron.ipcMain.handle(
1296
1480
  "create-github-repository",
1297
1481
  async (event, name, description, isPrivate, autoInit, gitignoreTemplate, licenseTemplate) => {
@@ -1344,6 +1528,9 @@ electron.app.whenReady().then(async () => {
1344
1528
  electron.ipcMain.handle("search-github-user", async (event, username) => {
1345
1529
  return await searchGitHubUser(event.sender.id, username);
1346
1530
  });
1531
+ electron.ipcMain.handle("search-github-users", async (event, query) => {
1532
+ return await searchGitHubUsers(event.sender.id, query);
1533
+ });
1347
1534
  electron.ipcMain.handle("get-user-invitations", async (event) => {
1348
1535
  return await getUserInvitations(event.sender.id);
1349
1536
  });
@@ -1408,6 +1595,37 @@ electron.app.whenReady().then(async () => {
1408
1595
  electron.ipcMain.handle("execute-raw-git", async (event, args) => {
1409
1596
  return await executeRawCommand(event.sender.id, args);
1410
1597
  });
1598
+ electron.ipcMain.handle("clone-repository", async (event, remoteUrl, destPath) => {
1599
+ return await cloneRepository(event.sender.id, remoteUrl, destPath);
1600
+ });
1601
+ electron.ipcMain.handle("check-path-exists", async (_event, targetPath) => {
1602
+ if (!targetPath) return false;
1603
+ try {
1604
+ const stats = await fs__namespace.stat(targetPath);
1605
+ return stats.isDirectory();
1606
+ } catch {
1607
+ return false;
1608
+ }
1609
+ });
1610
+ electron.ipcMain.handle("upload-files-to-repo", async (event, repoPath) => {
1611
+ const window = electron.BrowserWindow.fromWebContents(event.sender);
1612
+ if (!repoPath) throw new Error("No repository path specified");
1613
+ const result = await electron.dialog.showOpenDialog(window || void 0, {
1614
+ title: "Select Files to Upload to Repository",
1615
+ properties: ["openFile", "multiSelections"]
1616
+ });
1617
+ if (result.canceled || !result.filePaths.length) {
1618
+ return { success: false, count: 0, files: [] };
1619
+ }
1620
+ const copiedFiles = [];
1621
+ for (const filePath of result.filePaths) {
1622
+ const fileName = path__namespace.basename(filePath);
1623
+ const destPath = path__namespace.join(repoPath, fileName);
1624
+ await fs__namespace.copyFile(filePath, destPath);
1625
+ copiedFiles.push(fileName);
1626
+ }
1627
+ return { success: true, count: copiedFiles.length, files: copiedFiles };
1628
+ });
1411
1629
  electron.ipcMain.on("toggle-auto-sync", (event, repoPath, enabled) => {
1412
1630
  toggleAutoSync(event.sender.id, repoPath, enabled, (msg) => {
1413
1631
  event.sender.send("auto-sync-log", msg);
@@ -35,11 +35,12 @@ const api = {
35
35
  writeFile: (file, content) => electron.ipcRenderer.invoke("write-file", file, content),
36
36
  executeRawGit: (args) => electron.ipcRenderer.invoke("execute-raw-git", args),
37
37
  // NEW: Multi-Account & Window
38
- newWindow: () => electron.ipcRenderer.invoke("new-window"),
38
+ newWindow: (repoPath) => electron.ipcRenderer.invoke("new-window", repoPath),
39
39
  getAccounts: () => electron.ipcRenderer.invoke("get-accounts"),
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,
@@ -58,6 +59,7 @@ const api = {
58
59
  addCollaborator: (remoteUrl, username) => electron.ipcRenderer.invoke("add-collaborator", remoteUrl, username),
59
60
  removeCollaborator: (remoteUrl, usernameOrId, isInvitation) => electron.ipcRenderer.invoke("remove-collaborator", remoteUrl, usernameOrId, isInvitation),
60
61
  searchGitHubUser: (username) => electron.ipcRenderer.invoke("search-github-user", username),
62
+ searchGitHubUsers: (query) => electron.ipcRenderer.invoke("search-github-users", query),
61
63
  getUserInvitations: () => electron.ipcRenderer.invoke("get-user-invitations"),
62
64
  acceptInvitation: (invitationId) => electron.ipcRenderer.invoke("accept-invitation", invitationId),
63
65
  declineInvitation: (invitationId) => electron.ipcRenderer.invoke("decline-invitation", invitationId),
@@ -67,6 +69,9 @@ const api = {
67
69
  setupUniversalWorkspace: (remoteUrl, branch, baseBranch, dest) => electron.ipcRenderer.invoke("setup-universal-workspace", remoteUrl, branch, baseBranch, dest),
68
70
  checkUniversalCollisions: (workspacePath, sourceFiles, dest) => electron.ipcRenderer.invoke("check-universal-collisions", workspacePath, sourceFiles, dest),
69
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),
70
75
  commitUniversalWorkspace: (workspacePath, msg, branch) => electron.ipcRenderer.invoke("commit-universal-workspace", workspacePath, msg, branch),
71
76
  cleanupUniversalWorkspace: (workspacePath) => electron.ipcRenderer.invoke("cleanup-universal-workspace", workspacePath),
72
77
  toggleAutoSync: (repoPath, enabled) => electron.ipcRenderer.send("toggle-auto-sync", repoPath, enabled),