gitxelectron 1.3.7 → 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.
- package/README.md +247 -70
- package/bin/auto-push.js +526 -0
- package/bin/cli.js +41 -7
- package/out/main/index.js +6 -2
- package/out/renderer/assets/index-D_wJ7hR7.css +2989 -0
- package/out/renderer/assets/{index-DyLS9VOZ.js → index-FVTFdxtU.js} +772 -205
- package/out/renderer/index.html +2 -2
- package/package.json +3 -1
- package/out/renderer/assets/index-Vre34D_W.css +0 -2913
package/bin/auto-push.js
ADDED
|
@@ -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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
@@ -408,7 +408,10 @@ async function getCollaborators(sessionId, remoteUrl) {
|
|
|
408
408
|
return await fetch(url, { headers });
|
|
409
409
|
} catch (e) {
|
|
410
410
|
console.error(`Fetch failed for ${url}:`, e.message);
|
|
411
|
-
return {
|
|
411
|
+
return {
|
|
412
|
+
ok: false,
|
|
413
|
+
json: async () => ({ message: e.message || "Network error" })
|
|
414
|
+
};
|
|
412
415
|
}
|
|
413
416
|
};
|
|
414
417
|
const [collabRes, invRes, contribRes] = await Promise.all([
|
|
@@ -2063,7 +2066,8 @@ async function masterPush(sessionId, remote, branch) {
|
|
|
2063
2066
|
return;
|
|
2064
2067
|
}
|
|
2065
2068
|
for (const entry of entries) {
|
|
2066
|
-
if (entry.name === ".git" || entry.name === "node_modules" || entry.name.endsWith(".__lfs_ptr_bak__"))
|
|
2069
|
+
if (entry.name === ".git" || entry.name === "node_modules" || entry.name.endsWith(".__lfs_ptr_bak__"))
|
|
2070
|
+
continue;
|
|
2067
2071
|
const fullPath = path__namespace.join(dir, entry.name);
|
|
2068
2072
|
if (entry.isDirectory()) {
|
|
2069
2073
|
await processRepo(fullPath);
|