gitxelectron 1.3.6 → 1.3.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.
@@ -0,0 +1,526 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GitQuickPush / GitElectron Auto-Push & Big File Splitting Engine
5
+ *
6
+ * Features:
7
+ * - Safety guards (prevents accidental staging of system root / home dir)
8
+ * - Secret protection (blocks .env, private keys, certificates, credentials)
9
+ * - Git LFS auto-configuration for AI/ML file formats
10
+ * - Big file detector (>95MB) with ultra-fast streaming chunking & live progress
11
+ * - Companion 1-click reassembly (auto-restore) with SHA-256 verification
12
+ * - High-speed Git push with live transfer progress & network buffering
13
+ */
14
+
15
+ const fs = require('fs')
16
+ const path = require('path')
17
+ const crypto = require('crypto')
18
+ const { spawnSync, spawn } = require('child_process')
19
+
20
+ const CHUNK_SIZE_BYTES = 45 * 1024 * 1024 // 45 MB chunks (safely under GitHub's 100MB limit)
21
+ const BIG_FILE_THRESHOLD = 95 * 1024 * 1024 // 95 MB trigger threshold
22
+
23
+ const SENSITIVE_PATTERNS = [
24
+ /^\.env(\..+)?$/i,
25
+ /\.pem$/i,
26
+ /\.key$/i,
27
+ /\.pkcs12$/i,
28
+ /\.pfx$/i,
29
+ /\.p12$/i,
30
+ /^id_rsa(\..+)?$/i,
31
+ /^id_ed25519(\..+)?$/i,
32
+ /^id_ecdsa(\..+)?$/i,
33
+ /credentials\.json$/i,
34
+ /service-account.*\.json$/i
35
+ ]
36
+
37
+ const AIML_EXTENSIONS = [
38
+ '*.csv',
39
+ '*.jsonl',
40
+ '*.parquet',
41
+ '*.zip',
42
+ '*.tar',
43
+ '*.pt',
44
+ '*.pth',
45
+ '*.onnx',
46
+ '*.h5',
47
+ '*.pkl',
48
+ '*.joblib',
49
+ '*.bin'
50
+ ]
51
+
52
+ // Terminal styling helpers
53
+ const colors = {
54
+ reset: '\x1b[0m',
55
+ bold: '\x1b[1m',
56
+ green: '\x1b[32m',
57
+ yellow: '\x1b[33m',
58
+ red: '\x1b[31m',
59
+ cyan: '\x1b[36m',
60
+ magenta: '\x1b[35m',
61
+ gray: '\x1b[90m'
62
+ }
63
+
64
+ function log(msg, color = colors.reset) {
65
+ console.log(`${color}${msg}${colors.reset}`)
66
+ }
67
+
68
+ function runGit(args, options = {}) {
69
+ const result = spawnSync('git', args, {
70
+ cwd: options.cwd || process.cwd(),
71
+ encoding: 'utf-8',
72
+ stdio: options.stdio || 'pipe',
73
+ shell: false
74
+ })
75
+ return result
76
+ }
77
+
78
+ function runGitLive(args, options = {}) {
79
+ return new Promise((resolve) => {
80
+ const child = spawn('git', args, {
81
+ cwd: options.cwd || process.cwd(),
82
+ stdio: 'inherit',
83
+ shell: false
84
+ })
85
+ child.on('close', (code) => resolve(code))
86
+ child.on('error', (err) => {
87
+ log(`Git execution error: ${err.message}`, colors.red)
88
+ resolve(1)
89
+ })
90
+ })
91
+ }
92
+
93
+ // Check directory safety
94
+ function verifySafety(cwd) {
95
+ const normalized = path.resolve(cwd)
96
+ const home = path.resolve(process.env.HOME || process.env.USERPROFILE || '')
97
+ const root = path.parse(normalized).root
98
+
99
+ if (normalized === root) {
100
+ log('❌ SAFETY BLOCK: Running auto-push in the system drive root is blocked.', colors.red)
101
+ log('Run this script inside your specific project directory.', colors.yellow)
102
+ process.exit(1)
103
+ }
104
+
105
+ if (home && normalized === home) {
106
+ log(
107
+ '❌ SAFETY BLOCK: Running auto-push directly in your User Home directory is blocked.',
108
+ colors.red
109
+ )
110
+ log(
111
+ 'This prevents accidental upload of SSH keys, browser data, and private files.',
112
+ colors.yellow
113
+ )
114
+ process.exit(1)
115
+ }
116
+ }
117
+
118
+ // Configure Git LFS and .gitattributes
119
+ function configureGitLfs(cwd) {
120
+ const gitattrPath = path.join(cwd, '.gitattributes')
121
+ let currentContent = ''
122
+ if (fs.existsSync(gitattrPath)) {
123
+ try {
124
+ currentContent = fs.readFileSync(gitattrPath, 'utf-8')
125
+ } catch {}
126
+ }
127
+
128
+ const missingRules = []
129
+ for (const ext of AIML_EXTENSIONS) {
130
+ const rule = `${ext} filter=lfs diff=lfs merge=lfs -text`
131
+ if (!currentContent.includes(ext)) {
132
+ missingRules.push(rule)
133
+ }
134
+ }
135
+
136
+ if (missingRules.length > 0) {
137
+ log(`📦 Updating .gitattributes with Git LFS rules for AI/ML formats...`, colors.cyan)
138
+ const newContent =
139
+ (currentContent ? currentContent.trim() + '\n' : '') +
140
+ '# Git LFS tracking for AI/ML and large binary assets\n' +
141
+ missingRules.join('\n') +
142
+ '\n'
143
+ fs.writeFileSync(gitattrPath, newContent, 'utf-8')
144
+ }
145
+
146
+ // Ensure git lfs install is invoked
147
+ runGit(['lfs', 'install'], { cwd })
148
+ }
149
+
150
+ // Scan for sensitive files
151
+ function scanSecrets(cwd) {
152
+ const gitStatus = runGit(['status', '--porcelain', '-uall'], { cwd })
153
+ if (gitStatus.status !== 0) return
154
+
155
+ const lines = gitStatus.stdout.split('\n')
156
+ const detected = []
157
+
158
+ for (const line of lines) {
159
+ const trimmed = line.trim()
160
+ if (!trimmed) continue
161
+ const filePath = trimmed.substring(3).trim().replace(/^"|"$/g, '')
162
+ const baseName = path.basename(filePath)
163
+
164
+ if (baseName === '.env.example' || baseName === '.env.template') continue
165
+
166
+ for (const pat of SENSITIVE_PATTERNS) {
167
+ if (pat.test(baseName)) {
168
+ detected.push(filePath)
169
+ break
170
+ }
171
+ }
172
+ }
173
+
174
+ if (detected.length > 0) {
175
+ log('\n⚠️ SECURITY WARNING: Potential sensitive files detected:', colors.yellow)
176
+ for (const file of detected) {
177
+ log(` ⛔ ${file}`, colors.red)
178
+ }
179
+ log('Secrets should never be committed to Git repositories.', colors.yellow)
180
+ log('Adding sensitive files to .git/info/exclude to prevent leak...', colors.cyan)
181
+
182
+ const excludePath = path.join(cwd, '.git', 'info', 'exclude')
183
+ try {
184
+ fs.mkdirSync(path.dirname(excludePath), { recursive: true })
185
+ let excludeContent = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, 'utf-8') : ''
186
+ for (const file of detected) {
187
+ if (!excludeContent.includes(file)) {
188
+ excludeContent += `\n${file}`
189
+ }
190
+ }
191
+ fs.writeFileSync(excludePath, excludeContent, 'utf-8')
192
+ runGit(['reset', 'HEAD', ...detected], { cwd })
193
+ log('✅ Excluded sensitive files safely.', colors.green)
194
+ } catch (e) {
195
+ log(`Failed to update exclude: ${e.message}`, colors.red)
196
+ }
197
+ }
198
+ }
199
+
200
+ // Format bytes
201
+ function formatBytes(bytes) {
202
+ if (bytes === 0) return '0 B'
203
+ const k = 1024
204
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
205
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
206
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
207
+ }
208
+
209
+ // Split a large file with streaming and live status
210
+ async function splitLargeFile(filePath, cwd) {
211
+ const stat = fs.statSync(filePath)
212
+ const totalSize = stat.size
213
+ const relPath = path.relative(cwd, filePath).replace(/\\/g, '/')
214
+ const totalChunks = Math.ceil(totalSize / CHUNK_SIZE_BYTES)
215
+
216
+ log(`\n⚡ [BIG FILE DETECTED] ${relPath} (${formatBytes(totalSize)})`, colors.yellow)
217
+ log(` Splitting into ${totalChunks} parts (each ≤ 45 MB) with live status...`, colors.cyan)
218
+
219
+ const hash = crypto.createHash('sha256')
220
+ const manifest = {
221
+ originalFile: relPath,
222
+ fileSize: totalSize,
223
+ sha256: '',
224
+ chunkSize: CHUNK_SIZE_BYTES,
225
+ totalChunks,
226
+ chunks: [],
227
+ splitTimestamp: new Date().toISOString()
228
+ }
229
+
230
+ const readStream = fs.createReadStream(filePath, { highWaterMark: 4 * 1024 * 1024 })
231
+ let currentChunkIndex = 0
232
+ let currentChunkBytes = 0
233
+ let currentChunkPath = `${filePath}.part${String(currentChunkIndex).padStart(2, '0')}`
234
+ let currentWriteStream = fs.createWriteStream(currentChunkPath)
235
+ manifest.chunks.push(path.basename(currentChunkPath))
236
+
237
+ let processedBytes = 0
238
+ const startTime = Date.now()
239
+
240
+ return new Promise((resolve, reject) => {
241
+ readStream.on('data', (buffer) => {
242
+ hash.update(buffer)
243
+ let offset = 0
244
+
245
+ while (offset < buffer.length) {
246
+ const remainingInChunk = CHUNK_SIZE_BYTES - currentChunkBytes
247
+ const toWrite = Math.min(buffer.length - offset, remainingInChunk)
248
+
249
+ currentWriteStream.write(buffer.slice(offset, offset + toWrite))
250
+ currentChunkBytes += toWrite
251
+ offset += toWrite
252
+ processedBytes += toWrite
253
+
254
+ // Render live progress
255
+ const percent = Math.min(100, Math.floor((processedBytes / totalSize) * 100))
256
+ const elapsedSec = (Date.now() - startTime) / 1000
257
+ const speedMBs =
258
+ elapsedSec > 0 ? (processedBytes / (1024 * 1024) / elapsedSec).toFixed(1) : '0'
259
+ const barWidth = 25
260
+ const filled = Math.floor((percent / 100) * barWidth)
261
+ const bar =
262
+ '='.repeat(filled) +
263
+ (filled < barWidth ? '>' : '') +
264
+ ' '.repeat(Math.max(0, barWidth - filled - 1))
265
+ process.stdout.write(
266
+ `\r [${bar}] ${percent}% | ${formatBytes(processedBytes)} / ${formatBytes(totalSize)} | ${speedMBs} MB/s`
267
+ )
268
+
269
+ if (currentChunkBytes >= CHUNK_SIZE_BYTES && processedBytes < totalSize) {
270
+ currentWriteStream.end()
271
+ currentChunkIndex++
272
+ currentChunkBytes = 0
273
+ currentChunkPath = `${filePath}.part${String(currentChunkIndex).padStart(2, '0')}`
274
+ currentWriteStream = fs.createWriteStream(currentChunkPath)
275
+ manifest.chunks.push(path.basename(currentChunkPath))
276
+ }
277
+ }
278
+ })
279
+
280
+ readStream.on('end', () => {
281
+ currentWriteStream.end(() => {
282
+ manifest.sha256 = hash.digest('hex')
283
+ process.stdout.write('\n')
284
+
285
+ // Write manifest
286
+ const manifestPath = `${filePath}.split-manifest.json`
287
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8')
288
+
289
+ // Exclude the original big file from git so only chunks & manifest are tracked
290
+ const excludePath = path.join(cwd, '.git', 'info', 'exclude')
291
+ try {
292
+ fs.mkdirSync(path.dirname(excludePath), { recursive: true })
293
+ let excludeContent = fs.existsSync(excludePath)
294
+ ? fs.readFileSync(excludePath, 'utf-8')
295
+ : ''
296
+ if (!excludeContent.includes(relPath)) {
297
+ excludeContent += `\n${relPath}\n`
298
+ fs.writeFileSync(excludePath, excludeContent, 'utf-8')
299
+ }
300
+ // Untrack original if it was previously staged
301
+ runGit(['rm', '--cached', '-f', relPath], { cwd })
302
+ } catch {}
303
+
304
+ log(
305
+ ` ✅ Finished splitting: ${manifest.chunks.length} parts created. SHA-256: ${manifest.sha256.substring(0, 16)}...`,
306
+ colors.green
307
+ )
308
+ resolve()
309
+ })
310
+ })
311
+
312
+ readStream.on('error', reject)
313
+ })
314
+ }
315
+
316
+ // Find files exceeding BIG_FILE_THRESHOLD
317
+ function findBigFiles(dir, cwd, list = []) {
318
+ const ignoredDirs = new Set([
319
+ '.git',
320
+ 'node_modules',
321
+ 'dist',
322
+ 'out',
323
+ 'build',
324
+ '.venv',
325
+ 'venv',
326
+ '__pycache__',
327
+ 'target'
328
+ ])
329
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
330
+
331
+ for (const entry of entries) {
332
+ if (entry.isDirectory()) {
333
+ if (ignoredDirs.has(entry.name)) continue
334
+ findBigFiles(path.join(dir, entry.name), cwd, list)
335
+ } else if (entry.isFile()) {
336
+ if (entry.name.includes('.part') || entry.name.endsWith('.split-manifest.json')) continue
337
+ const fullPath = path.join(dir, entry.name)
338
+ try {
339
+ const stat = fs.statSync(fullPath)
340
+ if (stat.size >= BIG_FILE_THRESHOLD) {
341
+ list.push(fullPath)
342
+ }
343
+ } catch {}
344
+ }
345
+ }
346
+ return list
347
+ }
348
+
349
+ // Restore all split files in the workspace
350
+ async function restoreAllSplitFiles(cwd) {
351
+ log('\n🔍 Scanning for split file manifests...', colors.cyan)
352
+ const manifests = []
353
+
354
+ function findManifests(dir) {
355
+ const ignoredDirs = new Set(['.git', 'node_modules', 'dist', 'out', 'build', '.venv', 'venv'])
356
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
357
+ for (const entry of entries) {
358
+ if (entry.isDirectory()) {
359
+ if (ignoredDirs.has(entry.name)) continue
360
+ findManifests(path.join(dir, entry.name))
361
+ } else if (entry.isFile() && entry.name.endsWith('.split-manifest.json')) {
362
+ manifests.push(path.join(dir, entry.name))
363
+ }
364
+ }
365
+ }
366
+
367
+ findManifests(cwd)
368
+
369
+ if (manifests.length === 0) {
370
+ log('No split manifests found. All files are intact!', colors.green)
371
+ return
372
+ }
373
+
374
+ log(`Found ${manifests.length} split file manifest(s). Starting reassembly...`, colors.yellow)
375
+
376
+ for (const mPath of manifests) {
377
+ const manifest = JSON.parse(fs.readFileSync(mPath, 'utf-8'))
378
+ const dir = path.dirname(mPath)
379
+ const targetFile = path.join(cwd, manifest.originalFile)
380
+
381
+ log(
382
+ `\nReassembling: ${manifest.originalFile} (${formatBytes(manifest.fileSize)})...`,
383
+ colors.cyan
384
+ )
385
+ const writeStream = fs.createWriteStream(targetFile)
386
+ const hash = crypto.createHash('sha256')
387
+
388
+ for (let i = 0; i < manifest.chunks.length; i++) {
389
+ const chunkName = manifest.chunks[i]
390
+ const chunkPath = path.join(dir, chunkName)
391
+ if (!fs.existsSync(chunkPath)) {
392
+ log(`❌ Error: Chunk ${chunkName} is missing!`, colors.red)
393
+ writeStream.end()
394
+ return
395
+ }
396
+
397
+ const chunkBuffer = fs.readFileSync(chunkPath)
398
+ hash.update(chunkBuffer)
399
+ writeStream.write(chunkBuffer)
400
+ process.stdout.write(`\r Reassembling part ${i + 1}/${manifest.chunks.length}...`)
401
+ }
402
+
403
+ await new Promise((res) => writeStream.end(res))
404
+ process.stdout.write('\n')
405
+
406
+ const computedHash = hash.digest('hex')
407
+ if (computedHash === manifest.sha256) {
408
+ log(`✅ Integrity verified! SHA-256 matches.`, colors.green)
409
+ } else {
410
+ log(`⚠️ Warning: Checksum mismatch for ${manifest.originalFile}!`, colors.red)
411
+ }
412
+ }
413
+ }
414
+
415
+ // Main auto-push routine
416
+ async function main() {
417
+ const cwd = process.cwd()
418
+ verifySafety(cwd)
419
+
420
+ const args = process.argv.slice(2)
421
+ if (args.includes('restore')) {
422
+ await restoreAllSplitFiles(cwd)
423
+ return
424
+ }
425
+
426
+ log('====================================================', colors.cyan)
427
+ log('⚡ GitQuickPush Auto-Push & Big File Engine ⚡', colors.bold + colors.green)
428
+ log('====================================================', colors.cyan)
429
+
430
+ // 1. Ensure Git repo exists
431
+ const isRepo = runGit(['rev-parse', '--is-inside-work-tree'], { cwd })
432
+ if (isRepo.status !== 0) {
433
+ log('📦 Initializing Git repository in current directory...', colors.cyan)
434
+ runGit(['init'], { cwd })
435
+ }
436
+
437
+ // 2. Configure Git LFS and AI/ML tracking
438
+ configureGitLfs(cwd)
439
+
440
+ // 3. Scan for sensitive files to prevent leaks
441
+ scanSecrets(cwd)
442
+
443
+ // 4. Detect and split big files exceeding GitHub's 100MB limit
444
+ const bigFiles = findBigFiles(cwd, cwd)
445
+ if (bigFiles.length > 0) {
446
+ log(
447
+ `\nDetected ${bigFiles.length} file(s) exceeding 95MB. Initiating fast split...`,
448
+ colors.yellow
449
+ )
450
+ for (const bFile of bigFiles) {
451
+ await splitLargeFile(bFile, cwd)
452
+ }
453
+ }
454
+
455
+ // 5. High-speed Git network optimizations
456
+ runGit(['config', 'http.postBuffer', '1048576000'], { cwd })
457
+ runGit(['config', 'core.safecrlf', 'false'], { cwd })
458
+ runGit(['config', 'pack.windowMemory', '256m'], { cwd })
459
+ runGit(['config', 'pack.packSizeLimit', '500m'], { cwd })
460
+
461
+ // 6. Stage all project files (git add -A)
462
+ log('\n[1/3] 📂 Staging all project files (git add -A)...', colors.cyan)
463
+ const addResult = runGit(['add', '-A'], { cwd })
464
+ if (addResult.status !== 0) {
465
+ log(`Git add warning: ${addResult.stderr}`, colors.yellow)
466
+ }
467
+
468
+ // 7. Check status & commit
469
+ const statusCheck = runGit(['status', '--porcelain'], { cwd })
470
+ const hasChanges = statusCheck.stdout.trim().length > 0
471
+
472
+ const timestamp = new Date().toISOString()
473
+ const customMsg = args.find((a) => !a.startsWith('-')) || `Auto-save: ${timestamp}`
474
+
475
+ if (hasChanges) {
476
+ log(`[2/3] 💾 Committing: "${customMsg}"...`, colors.cyan)
477
+ runGit(['commit', '-m', customMsg], { cwd })
478
+ } else {
479
+ log('[2/3] ℹ️ Working tree clean, no new changes to commit.', colors.gray)
480
+ }
481
+
482
+ // 8. Determine remote and branch
483
+ const remotesResult = runGit(['remote'], { cwd })
484
+ const remotes = remotesResult.stdout
485
+ .trim()
486
+ .split('\n')
487
+ .filter((r) => r.trim())
488
+
489
+ if (remotes.length === 0) {
490
+ log('\n⚠️ No remote configured yet. Changes are committed locally!', colors.yellow)
491
+ log('To link to GitHub, run: git remote add origin <your-github-repo-url>', colors.gray)
492
+ log('Then run auto-push again!', colors.green)
493
+ return
494
+ }
495
+
496
+ const branchResult = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd })
497
+ let branch = branchResult.stdout.trim() || 'main'
498
+ if (branch === 'HEAD') branch = 'main'
499
+
500
+ log(`[3/3] 🚀 Pushing to remote (${remotes[0]}/${branch}) with live progress...`, colors.cyan)
501
+
502
+ let pushCode = await runGitLive(['push', '--progress', remotes[0], `HEAD:${branch}`], { cwd })
503
+
504
+ // If rejected because remote has commits, rebase and retry
505
+ if (pushCode !== 0) {
506
+ log('\n🔄 Remote has newer changes. Running fast rebase pull...', colors.yellow)
507
+ const pullCode = await runGitLive(['pull', '--rebase', remotes[0], branch], { cwd })
508
+ if (pullCode === 0) {
509
+ log('🚀 Retrying push with live progress...', colors.cyan)
510
+ pushCode = await runGitLive(['push', '--progress', remotes[0], `HEAD:${branch}`], { cwd })
511
+ }
512
+ }
513
+
514
+ if (pushCode === 0) {
515
+ log('\n====================================================', colors.green)
516
+ log('✨ Auto-Push Completed Successfully! 🚀', colors.bold + colors.green)
517
+ log('====================================================\n', colors.green)
518
+ } else {
519
+ log('\n❌ Push did not complete cleanly. Check network or remote permissions.', colors.red)
520
+ }
521
+ }
522
+
523
+ main().catch((err) => {
524
+ console.error('\nFatal error in auto-push:', err)
525
+ process.exit(1)
526
+ })
package/bin/cli.js CHANGED
@@ -101,13 +101,12 @@ Return ONLY valid JSON (no markdown): {"explanation":"...", "commandsToRun":["gi
101
101
 
102
102
  const program = new Command()
103
103
 
104
- // The Electron executable is provided by the 'electron' package
105
- let electronPath = ''
106
- try {
107
- electronPath = require('electron')
108
- } catch (e) {
109
- console.error('GitElectron is missing its electron dependency. Run npm install.')
110
- process.exit(1)
104
+ function getElectronPath() {
105
+ try {
106
+ return require('electron')
107
+ } catch (e) {
108
+ return null
109
+ }
111
110
  }
112
111
 
113
112
  // Path to the application root (where package.json and out/ exist)
@@ -122,6 +121,14 @@ program
122
121
  .command('open')
123
122
  .description('Open GitElectron desktop application')
124
123
  .action(() => {
124
+ const electronPath = getElectronPath()
125
+ if (!electronPath) {
126
+ console.error(
127
+ 'GitElectron desktop dependency (electron) is not installed. Run npm install or use npx.'
128
+ )
129
+ process.exit(1)
130
+ }
131
+
125
132
  console.log('Opening GitElectron...')
126
133
 
127
134
  // Spawn the electron process and pass the current working directory
@@ -210,4 +217,31 @@ program
210
217
  }
211
218
  })
212
219
 
220
+ program
221
+ .command('auto-push')
222
+ .description('Auto-stage, commit, split big files (>95MB), and push with live progress')
223
+ .argument('[message]', 'Optional custom commit message')
224
+ .action((message) => {
225
+ const autoPushPath = path.resolve(__dirname, 'auto-push.js')
226
+ if (fs.existsSync(autoPushPath)) {
227
+ if (message) {
228
+ process.argv = [process.argv[0], autoPushPath, message]
229
+ } else {
230
+ process.argv = [process.argv[0], autoPushPath]
231
+ }
232
+ require(autoPushPath)
233
+ }
234
+ })
235
+
236
+ program
237
+ .command('auto-restore')
238
+ .description('Reassemble all split big file parts into original files with SHA-256 verification')
239
+ .action(() => {
240
+ const autoPushPath = path.resolve(__dirname, 'auto-push.js')
241
+ if (fs.existsSync(autoPushPath)) {
242
+ process.argv = [process.argv[0], autoPushPath, 'restore']
243
+ require(autoPushPath)
244
+ }
245
+ })
246
+
213
247
  program.parse()
package/out/main/index.js CHANGED
@@ -384,12 +384,13 @@ async function createGitHubRepository(sessionId, name, description, isPrivate, a
384
384
  return await res.json();
385
385
  }
386
386
  function parseGithubUrl(remoteUrl) {
387
- if (!remoteUrl.includes("github.com")) throw new Error("Not a GitHub repository");
388
- let parts = remoteUrl.split("github.com");
387
+ const url = remoteUrl.trim();
388
+ if (!url.includes("github.com")) throw new Error("Not a GitHub repository");
389
+ let parts = url.split("github.com");
389
390
  let pathPart = parts[1].replace(/^[:\/]/, "").replace(/\.git$/, "");
390
391
  const pathSplits = pathPart.split("/");
391
392
  if (pathSplits.length >= 2) {
392
- return { owner: pathSplits[0], repo: pathSplits[1] };
393
+ return { owner: pathSplits[0].trim(), repo: pathSplits[1].trim() };
393
394
  }
394
395
  throw new Error("Invalid GitHub repository URL");
395
396
  }
@@ -402,10 +403,21 @@ async function getCollaborators(sessionId, remoteUrl) {
402
403
  Accept: "application/vnd.github.v3+json",
403
404
  "User-Agent": "gitelectron-App"
404
405
  };
406
+ const safeFetch = async (url) => {
407
+ try {
408
+ return await fetch(url, { headers });
409
+ } catch (e) {
410
+ console.error(`Fetch failed for ${url}:`, e.message);
411
+ return {
412
+ ok: false,
413
+ json: async () => ({ message: e.message || "Network error" })
414
+ };
415
+ }
416
+ };
405
417
  const [collabRes, invRes, contribRes] = await Promise.all([
406
- fetch(`https://api.github.com/repos/${owner}/${repo}/collaborators`, { headers }),
407
- fetch(`https://api.github.com/repos/${owner}/${repo}/invitations`, { headers }),
408
- fetch(`https://api.github.com/repos/${owner}/${repo}/contributors`, { headers })
418
+ safeFetch(`https://api.github.com/repos/${owner}/${repo}/collaborators`),
419
+ safeFetch(`https://api.github.com/repos/${owner}/${repo}/invitations`),
420
+ safeFetch(`https://api.github.com/repos/${owner}/${repo}/contributors`)
409
421
  ]);
410
422
  if (!collabRes.ok) {
411
423
  const errorData = await collabRes.json().catch(() => ({}));
@@ -2054,7 +2066,8 @@ async function masterPush(sessionId, remote, branch) {
2054
2066
  return;
2055
2067
  }
2056
2068
  for (const entry of entries) {
2057
- if (entry.name === ".git" || entry.name === "node_modules" || entry.name.endsWith(".__lfs_ptr_bak__")) continue;
2069
+ if (entry.name === ".git" || entry.name === "node_modules" || entry.name.endsWith(".__lfs_ptr_bak__"))
2070
+ continue;
2058
2071
  const fullPath = path__namespace.join(dir, entry.name);
2059
2072
  if (entry.isDirectory()) {
2060
2073
  await processRepo(fullPath);