From f8c68a034b596d7b3c928b5794c6d64ab7375d25 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sun, 1 Jun 2025 08:10:28 +0200 Subject: [PATCH] auto-git: [change] main.js --- main.js | 2201 ------------------------------------------------------- 1 file changed, 2201 deletions(-) diff --git a/main.js b/main.js index 7e72fa7..e69de29 100644 --- a/main.js +++ b/main.js @@ -1,2201 +0,0 @@ -const { app, BrowserWindow, ipcMain, dialog, Tray, Menu, shell, clipboard, nativeImage } = require('electron'); -app.name = 'Auto-Git'; -const { exec } = require('child_process'); -const { execSync } = require('child_process'); //just for hack -const http = require('http'); //just for hack -const { spawn } = require('child_process'); -const { spawnSync } = require('child_process'); -const path = require('path'); -const fs = require('fs'); -const os = require('os'); -const Store = require('electron-store'); -const simpleGit = require('simple-git'); -const chokidar = require('chokidar'); -const micromatch = require('micromatch'); -const ignore = require('ignore'); - -const store = new Store({ - defaults: { - folders: [], - selected: null, - skymode: true, - skipGitPrompt: true, - intelligentCommitThreshold: 20, - minutesCommitThreshold: 5, - autostart: false, - closeToTray: true, - needsRelocation: false, - dailyCommitStats: {} - } -}); - -let folders = store.get('folders') || []; -folders = folders.map(f => ({ - ...f, - needsRelocation: !fs.existsSync(f.path) -})); -store.set('folders', folders); -console.log("Startup-Folders:", store.get('folders')); - -let tray = null; -let isQuiting = false; - - - -function createTray(win) { - const iconPath = path.join(__dirname, 'assets/icon/trayicon.png'); - const icon = nativeImage.createFromPath(iconPath); - - // Standard-Größen je nach OS - let size; - switch (process.platform) { - case 'darwin': // macOS - size = { width: 22, height: 22 }; - break; - case 'win32': // Windows - size = { width: 16, height: 16 }; - break; - default: // Linux / other - size = { width: 24, height: 24 }; - } - - const trayImage = icon.resize(size); - const tray = new Tray(trayImage); - - tray.setToolTip('Auto-Git läuft im Hintergrund'); - tray.on('double-click', () => { - win.show(); - win.focus(); - }); - - return tray; -} - -if (Array.isArray(folders)) { - folders = folders.map(f => ({ - ...f, - //linesChanged: 0, // zurück auf 0 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - //llmCandidates: [] // leeres Array - })); - store.set('folders', folders); -} -// Map zum Speichern der Watcher pro Ordner -const repoWatchers = new Map(); - -// Debug Helper -function debug(msg) { - console.log(`[DEBUG ${new Date().toISOString()}] ${msg}`); -} - -/** - * Erstellt das BrowserWindow und lädt index.html. - * Gibt das Window-Objekt zurück. - */ -function createWindow() { - const win = new BrowserWindow({ - width: 900, - height: 600, - minWidth: 600, - minHeight: 500, - title: 'Auto-Git', - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - contextIsolation: true - } - }); - win.loadFile('index.html'); - return win; -} - -// Settings-Fenster -let settingsWin; -function openSettings(win) { - if (settingsWin) { - settingsWin.focus(); - return; - } - settingsWin = new BrowserWindow({ - parent: win, - modal: true, - width: 450, - height: 450, - resizable: false, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - contextIsolation: true - } - }); - settingsWin.removeMenu(); - settingsWin.loadFile('settings.html'); - settingsWin.webContents.openDevTools({ mode: 'detach' }); - settingsWin.on('closed', () => settingsWin = null); -} - - - -// ************HACK -// Hilfsfunktion: killt alle Prozesse auf Port 11434 (nur Unix/macOS) -function killOllamaOnPort() { - try { - // Finde Zeilen wie: "ollama 1234 ..." - const stdout = execSync("lsof -i :11434 -t || true").toString(); - const pids = stdout.split('\n').map(l => l.trim()).filter(Boolean); - if (pids.length) { - console.log(`[AutoGit] Kille Prozess(e) auf Port 11434: ${pids.join(', ')}`); - pids.forEach(pid => { - try { - process.kill(parseInt(pid), 'SIGKILL'); - } catch (e) { /* ignore */ } - }); - return true; - } - } catch (err) { - // ignore - } - return false; -} - -async function ensureOllamaRunning() { - function pingOllama() { - return new Promise((resolve, reject) => { - const req = http.request({ hostname: 'localhost', port: 11434, path: '/', method: 'GET', timeout: 500 }, res => { - res.destroy(); resolve(true); - }); - req.on('error', reject); - req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); }); - req.end(); - }); - } - - // Probieren, ob Ollama erreichbar ist - try { - await pingOllama(); - return true; // Bereits gestartet - } catch (err) { - // Port könnte blockiert sein. Versuch zu killen! - killOllamaOnPort(); - await new Promise(res => setTimeout(res, 500)); // Kurz warten - - // Noch einmal testen, ob der Port jetzt frei ist - try { await pingOllama(); return true; } catch {} - - // Startversuch - console.log('[AutoGit] Versuche ollama serve zu starten ...'); - try { - const proc = spawn('ollama', ['serve'], { detached: true, stdio: 'ignore' }); - proc.unref(); - } catch (e) { - console.error('[AutoGit] ollama serve konnte nicht gestartet werden:', e.message); - throw e; - } - - // Warte bis zu 10x 500ms (max. 5 Sekunden), ob Port aufgeht - for (let i = 0; i < 10; i++) { - await new Promise(res => setTimeout(res, 500)); - try { - await pingOllama(); - console.log('[AutoGit] Ollama läuft jetzt!'); - return true; - } catch (_) {/*noch nicht da*/} - } - throw new Error('[AutoGit] ollama serve konnte nach 5 Sekunden nicht erreicht werden!'); - } -} -// ************ENDOFHACK - - -/** - * Startet einen File-Watcher auf .git/refs/heads/master, - * sendet bei Änderungen 'repo-updated' an den Renderer. - */ -/*) -function watchRepo(folder, win) { - const gitHead = path.join(folder, '.git', 'refs', 'heads', 'master'); - const watcher = chokidar.watch(gitHead, { ignoreInitial: true }); - watcher.on('change', () => { - win.webContents.send('repo-updated', folder); - }); - repoWatchers.set(folder, watcher); -}*/ - -/** - * Initiiert ein Git-Repo in `folder`, falls noch nicht vorhanden, - * und erzeugt einen Initial-Commit mit Timestamp. - */ -async function initGitRepo(folder) { - const git = simpleGit(folder); - const gitDir = path.join(folder, '.git'); - if (!fs.existsSync(gitDir)) { - await git.init(); - const message = `Initial commit (generated by auto-git)`; - const readmePath = path.join(folder, 'README.md'); - fs.writeFileSync(readmePath, `# Projekt in ${path.basename(folder)}\n`); - await git.add('./*'); - await git.commit(message); - } -} - -// Map für Monitoring-Watcher (nicht repoWatchers!) -const monitoringWatchers = new Map(); - -// Helper: Baut eine Commit-Message aus git.status() -function buildCommitMessageFromStatus(status, prefix = '[auto]') { - const changes = []; - status.not_added.forEach(f => changes.push(`[add] ${f}`)); - status.created.forEach(f => changes.push(`[add] ${f}`)); - status.modified.forEach(f => changes.push(`[change] ${f}`)); - status.deleted.forEach(f => changes.push(`[unlink] ${f}`)); - status.renamed.forEach(r => changes.push(`[rename] ${r.from} → ${r.to}`)); - return prefix + '\n' + changes.map(l => ` ${l}`).join('\n'); -} - - - -function ensureInGitignore(folderPath, name) { - const gitignorePath = path.join(folderPath, '.gitignore'); - let lines = []; - if (fs.existsSync(gitignorePath)) { - lines = fs.readFileSync(gitignorePath, 'utf-8').split(/\r?\n/); - } - // Schon drin? - if (lines.some(line => line.trim() === name)) return; - // Anfügen - fs.appendFileSync(gitignorePath, name + '\n'); -} - -const IGNORED_NAMES = [ - // Betriebssystem-spezifische Dateien - '.DS_Store', // macOS - 'Thumbs.db', // Windows - 'desktop.ini', // Windows - '.AppleDouble', // macOS - '.LSOverride', // macOS - 'ehthumbs.db', // Windows - 'Icon\r', // macOS (Icon-Datei) - - // Git- und Versionskontrolle - '.git', // Git-Repository selbst - '.gitattributes', - - // Node.js / JavaScript / TypeScript - 'node_modules', - 'npm-debug.log*', - 'yarn-error.log', - 'yarn-debug.log*', - 'pnpm-debug.log*', - 'package-lock.json', // falls man es nicht committen möchte (normalerweise aber doch) - 'yarn.lock', // falls man es nicht committen möchte (normalerweise aber doch) - 'tsconfig.tsbuildinfo', - 'dist', - 'build', - '.cache', - 'out', - '.next', // Next.js-Build-Verzeichnis - '.turbo', // Turborepo Cache - - // Python - '.venv', - 'venv/', - '__pycache__/', - '*.py[cod]', - '*$py.class', - '.mypy_cache/', - '.pytest_cache/', - '.tox/', - 'dist/', - 'build/', - '*.egg-info/', - 'eggs/', - 'parts/', - 'var/', - 'sdist/', - 'develop-eggs/', - 'lib/', - 'lib64/', - 'wheelhouse/', - '*.egg', - '*.egg-info', - '.coverage', - 'htmlcov/', - '.cache/', - '.env', - '.env.*', - - // Java - 'target/', // Maven/Gradle Build-Ordner - '*.class', - '*.jar', - '*.war', - '*.ear', - '*.nar', - '*.zip', - '*.tar.gz', - '*.rar', - '*.log', - '*.iml', - '.idea/', - '.project', - '.classpath', - '.settings/', - '*.launch', - 'hs_err_pid*', - '*.hprof', - '*.log', - '*.jks', - 'out/', - 'build/', - - // C / C++ / Objective-C - '*.o', - '*.obj', - '*.so', - '*.dylib', - '*.dll', - '*.exe', - '*.out', - '*.app', - '*.ilk', - '*.pch', - '*.pdb', - '*.lib', - '*.a', - '*.lo', - '*.la', - 'CMakeFiles/', - 'CMakeCache.txt', - 'cmake_install.cmake', - 'Makefile', - '*.mk', - 'Debug/', - 'Release/', - 'build/', - 'xcodebuild/', - '*.xcworkspace', - '*.xcuserstate', - '*.xcuserdatad', - - // Go - 'bin/', - 'pkg/', - 'vendor/', - - // Rust - 'target/', - 'Cargo.lock', // in Bibliotheksprojekten oft ignoriert, in Binaries meistens nicht - - // Ruby - '*.gem', - '*.rbc', - '.bundle/', - 'vendor/bundle/', - 'log/', - 'tmp/', - 'coverage/', - 'byebug_history', - - // PHP / Composer - 'vendor/', - 'composer.lock', // in Bibliotheken oft ignoriert, in Projekten meist committed - '*.cache', - '*.log', - '*.session', - - // .NET / Visual Studio - '*.user', - '*.rsuser', - '*.suo', - '*.userosscache', - '*.sln.docstates', - '*.pdb', - '*.cache', - '*.ilk', - '*.log', - 'bin/', - 'obj/', - 'Debug/', - 'Release/', - 'TestResults/', - '.vs/', - '*.exe', - '*.dll', - '*.nupkg', - '*.snk', - - // Java IDEs (IntelliJ / Eclipse / NetBeans) - '.idea/', - '*.iml', - '*.ipr', - '*.iws', - '.classpath', - '.project', - '.settings/', - 'nbproject/', - 'build/', - - // Editors - '.vscode/', - '.history/', // VSCode-Erweiterung „Local History“ - '*.code-workspace', - '*.sublime-project', - '*.sublime-workspace', - '*.komodoproject', - '.ropeproject/', // Python-Rope - '.jupyter/', // Jupyter Notebooks - - // Vim / Emacs / Editor-Temp - '*.swp', - '*.swo', - '*.tmp', - '*.bak', - '*~', - '.netrwhist', - '.session', - '.emacs.desktop', - '.emacs.desktop.lock', - - // Logs / Reports / Coverage - '*.log', - 'logs/', - 'log/', - '*.trace', - 'coverage/', - 'test-results/', - 'lcov-report/', - - // Database-Dateien - '*.sqlite3', - '*.sqlite3-journal', - '*.db', - '*.db-journal', - - // Docker / Container - 'docker-compose.override.yml', - '.docker/', - 'docker-compose.*.yml', - 'docker-compose.*.env', - '*.pid', - '*.seed', - '*.pid.lock', - - // Terraform - '.terraform/', - '*.tfstate', - '*.tfstate.backup', - '.terraform.lock.hcl', - - // Kubernetes / Helm - 'helm-debug.log', - '.helm/', - 'kustomization.yaml~', - - // Ansible - 'ansible.cfg~', - 'inventory.ini', - - // Allgemein temporäre/versteckte Dateien - '*.backup', - '*.swp', - '*.swo', - '*.old', - '*.orig', - '*.rej', - '*.~', - '*.tmp', - '.*~', - '#*#', - '.#*', - '*.kate-swp', - '*.directory', - '.Trash-*', - '.fseventsd', - - // Paketmanager / Lockfiles (falls man sie ignorieren will) - 'Pipfile.lock', - 'yarn.lock', - 'pnpm-lock.yaml', - 'composer.lock', - 'package-lock.json', - 'Gemfile.lock', - 'Gopkg.lock', - - // Cloud-spezifisch - '.terraform/', - '.serverless/', // Serverless Framework - '.aws-sam/', - - // Editor- und IDE-Cache - '.cache/', - '.gradle/', - '.meteor/local/', - '.expo/', - '.next/', - '.nuxt/', - '.parcel-cache/', - '.fusebox/', - '.web-types/', - '.stryker-tmp/', - - // Sonstige generierte Artefakte - 'dist/', - 'build/', - 'public/dist/', - 'public/build/', - 'out/', - 'reports/', - 'coverage/', - - // Beispiel: JetBrains Rider - '*.sln.iml', - '.idea/', - '*.DotSettings.user', - - // Beispiel: Android / Flutter - '*.apk', - '*.ap_', - '*.aab', - 'android/.gradle', - 'android/gradle/', - 'android/local.properties', - '*.keystore', - 'build/', - '.android/', - '.flutter-plugins', - '.flutter-plugins-dependencies', - '.packages', - - // Swift / Xcode - '*.xcworkspace', - 'xcuserdata/', - '*.xcuserdatad', - '*.xcuserstate', - 'DerivedData/', - 'build/', - '*.hmap', - '*.ipa', - '*.dSYM.zip', - '*.dSYM', - - // Unity - 'Library/', - 'Temp/', - 'Obj/', - 'Build/', - 'Builds/', - 'Logs/', - 'MemoryCaptures/', - '*.csproj', - '*.unityproj', - '*.sln', - '*.userprefs', - '*.pidb', - '*.booproj', - '*.svd', - '*.user', - '*.pidb.meta', - '*.pdb', - '*.mdb', - '*.opendb', - '*.VC.db', - - // Unreal Engine - 'Binaries/', - 'DerivedDataCache/', - 'Intermediate/', - 'Saved/', - 'Build/', - '*.sln', - '*.vcxproj*', - - // Maven Wrapper - 'mvnw.cmd', - 'mvnw', - '.mvn/wrapper/maven-wrapper.jar', - - // Allgemeine Lock-Dateien - '*.lock', - - // Temporäre Archive / komprimierte Dateien - '*.zip', - '*.tar.gz', - '*.rar', - '*.7z' -]; - - - -const monitoringQueues = new Map(); // Map: folderPath -> Array -const monitoringActive = new Map(); // Map: folderPath -> Boolean (ob Task aktiv) - -function enqueueTask(folderPath, fn) { - if (!monitoringQueues.has(folderPath)) monitoringQueues.set(folderPath, []); - monitoringQueues.get(folderPath).push(fn); - processQueue(folderPath); -} - -async function processQueue(folderPath) { - if (monitoringActive.get(folderPath)) return; - monitoringActive.set(folderPath, true); - - const queue = monitoringQueues.get(folderPath) || []; - while (queue.length > 0) { - const task = queue.shift(); - try { await task(); } catch (e) { console.error(e); } - } - monitoringActive.set(folderPath, false); -} // <---- HIER FÜGEN! - -function startMonitoringWatcher(folderPath, win) { - if (monitoringWatchers.has(folderPath)) return; - const watcher = chokidar.watch(folderPath, { - ignored: /(^|[\/\\])\..|node_modules|\.git/, - ignoreInitial: false, // wichtig: ruft add-Events für vorhandene Dateien auf! - persistent: true, - depth: 99, - awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 } - }); - - // .gitignore-Update als eigene Funktion (inkl. Wildcards!) - function updateGitignoreIfNeeded(fileOrDirName) { - let changed = false; - for (const name of IGNORED_NAMES) { - if (name.includes('*')) { - if (micromatch.isMatch(fileOrDirName, name)) { - changed = ensureInGitignore(folderPath, name) || changed; - } - } else { - if (fileOrDirName === name.replace(/\/$/, '')) { - changed = ensureInGitignore(folderPath, name) || changed; - } - } - } - return changed; - } - - // Modifiziert: ensureInGitignore gibt true zurück, wenn wirklich geschrieben wurde! - function ensureInGitignore(folderPath, name) { - const gitignorePath = path.join(folderPath, '.gitignore'); - let lines = []; - if (fs.existsSync(gitignorePath)) { - lines = fs.readFileSync(gitignorePath, 'utf-8').split(/\r?\n/); - } - if (lines.some(line => line.trim() === name)) return false; - fs.appendFileSync(gitignorePath, name + '\n'); - return true; - } - - // Debounce für File-Events (mehrere in kurzer Zeit bündeln) - let debounceTimer = null; - let dirty = false; - let pendingNames = new Set(); - - function handleDebounced() { - enqueueTask(folderPath, async () => { - const namesArr = Array.from(pendingNames); - pendingNames.clear(); - let gitignoreChanged = false; - - for (const fileOrDirName of namesArr) { - gitignoreChanged = updateGitignoreIfNeeded(fileOrDirName) || gitignoreChanged; - } - // Optional: .gitignore direkt committen (uncomment falls gewünscht) - // if (gitignoreChanged) await autoCommit(folderPath, '[auto] Update .gitignore', win); - - // Danach: Eigentliche Repo-Änderung checken und ggf. Committen - const git = simpleGit(folderPath); - const status = await git.status(); - if ( - status.not_added.length > 0 || - status.created.length > 0 || - status.modified.length > 0 || - status.deleted.length > 0 || - status.renamed.length > 0 - ) { - const msg = buildCommitMessageFromStatus(status, 'auto-git: '); - await autoCommit(folderPath, msg, win); - win.webContents.send('repo-updated', folderPath); - } - }); - } - - // File-Events bündeln & debouncen - ['add', 'change', 'unlink'].forEach(ev => { - watcher.on(ev, filePath => { - const fileOrDirName = path.basename(filePath); - pendingNames.add(fileOrDirName); - if (debounceTimer) clearTimeout(debounceTimer); - debounceTimer = setTimeout(handleDebounced, 500); - }); - }); - - // Initialer Commit (direkt in Queue, wie ein Event) - enqueueTask(folderPath, async () => { - debug(`[MONITOR] Starte initialen Commit-Check für ${folderPath}`); - const git = simpleGit(folderPath); - const status = await git.status(); - if ( - status.not_added.length > 0 || - status.created.length > 0 || - status.modified.length > 0 || - status.deleted.length > 0 || - status.renamed.length > 0 - ) { - const msg = buildCommitMessageFromStatus(status, 'auto-git: '); - const did = await autoCommit(folderPath, msg, win); - if (did) { - win.webContents.send('repo-updated', folderPath); - debug(`[MONITOR] Initialer Auto-Commit für ${folderPath} durchgeführt:\n${msg}`); - } - } - }); - - monitoringWatchers.set(folderPath, watcher); - debug(`[MONITOR] Watcher aktiv für ${folderPath}`); -} - -function stopMonitoringWatcher(folderPath) { - const watcher = monitoringWatchers.get(folderPath); - if (watcher) { - watcher.close(); - monitoringWatchers.delete(folderPath); - debug(`[MONITOR] Watcher gestoppt für ${folderPath}`); - } -} - -// ---- Rewrite Git Messages with LLM generated messages ---- - -// ---- 1. Commits & Diffs für LLM sammeln ---- -async function getCommitsForLLM(folderPath, hashes) { - const git = simpleGit(folderPath); - const commits = []; - for (const hash of hashes) { - const shortHash = hash.substring(0, 7); - const diff = await git.diff([`${hash}^!`]); - const msg = (await git.show(['-s', '--format=%B', hash])).trim(); - commits.push({ hash: shortHash, message: msg, diff }); - } - return commits; -} - -// ---- 2. Prompt für LLM bauen ---- -async function generateLLMCommitMessages(folderPath, hashes) { - const commits = await getCommitsForLLM(folderPath, hashes); - const prompt = ` -Analyze the following git commits. For each commit, generate a concise commit message summarizing the actual change. -- ONLY output a JSON object mapping each commit hash to its new message. -- Do NOT add any explanations, greetings, or extra text. - -Example Output: -{ - "1a2b3c4": "Fix bug in user registration", - "2b3c4d5": "Refactor login logic" -} - -COMMITS (as JSON): - -${JSON.stringify(commits, null, 2)} - `; - return prompt; -} - -// ---- 3. LLM Streaming Call ---- -async function streamLLMCommitMessages(prompt, onDataChunk, win) { - await ensureOllamaRunning(); - const selectedModel = store.get('commitModel') || 'qwen2.5-coder:7b'; - const response = await fetch('http://localhost:11434/api/generate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: selectedModel, - prompt: prompt, - stream: true, - options: { temperature: 0.3 } - }) - }); - - if (!response.body) throw new Error('No stream returned'); - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - - let fullOutput = ''; - let done = false; - - // ⭐️ Starte den Stream für die Katze! - win.webContents.send('cat-begin'); - - while (!done) { - const { value, done: streamDone } = await reader.read(); - done = streamDone; - if (value) { - const chunk = decoder.decode(value, { stream: true }); - for (const line of chunk.split('\n')) { - if (!line.trim()) continue; - try { - const obj = JSON.parse(line); - if (obj.response) { - fullOutput += obj.response; - // Sende Chunk an Renderer/Katze: - win.webContents.send('cat-chunk', obj.response); - if (onDataChunk) onDataChunk(obj.response); - } - if (obj.done) break; - } catch (e) { - // ignore malformed chunk - } - } - } - } - - // ⭐️ Stream ist zu Ende - win.webContents.send('cat-end'); - - return fullOutput; -} - -async function streamLLMREADME(prompt, onDataChunk, win) { - await ensureOllamaRunning(); - const selectedModel = store.get('readmeModel') || 'qwen2.5-coder:32b'; - const response = await fetch('http://localhost:11434/api/generate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: selectedModel, - prompt: prompt, - stream: true, - options: { temperature: 0.4 } - }) - }); - - if (!response.body) throw new Error('No stream returned'); - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - - let fullOutput = ''; - let done = false; - - // ⭐️ Starte den Stream für die Katze! - win.webContents.send('cat-begin'); - - while (!done) { - const { value, done: streamDone } = await reader.read(); - done = streamDone; - if (value) { - const chunk = decoder.decode(value, { stream: true }); - for (const line of chunk.split('\n')) { - if (!line.trim()) continue; - try { - const obj = JSON.parse(line); - if (obj.response) { - fullOutput += obj.response; - // Sende Chunk an Renderer/Katze: - win.webContents.send('cat-chunk', obj.response); - if (onDataChunk) onDataChunk(obj.response); - } - if (obj.done) break; - } catch (e) { - // ignore malformed chunk - } - } - } - } - - // ⭐️ Stream ist zu Ende - win.webContents.send('cat-end'); - - return fullOutput; -} - - - -// ---- 4. JSON Output robust parsen ---- -function parseLLMCommitMessages(rawOutput) { - let cleaned = rawOutput.trim(); - cleaned = cleaned.replace(/^```(?:json)?|```$/gmi, ''); - - try { - if (cleaned.trim().startsWith('[')) return JSON.parse(cleaned); - if (cleaned.trim().startsWith('{')) { - const obj = JSON.parse(cleaned); - return Object.entries(obj).map(([commit, newMessage]) => ({ commit, newMessage })); - } - // Zeilenweise Objekte (fuzzy) - if (cleaned.includes('\n')) { - let lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean); - let objs = []; - for (let line of lines) { - try { let o = JSON.parse(line); objs.push(o); } catch {} - } - if (objs.length) return objs; - } - } catch (err) { - // Fallback repair - try { - cleaned = cleaned.replace(/}\s*{/g, '},\n{'); - if (!cleaned.startsWith('[')) cleaned = '[' + cleaned; - if (!cleaned.endsWith(']')) cleaned = cleaned + ']'; - return JSON.parse(cleaned); - } catch (e) { - throw new Error('Could not parse LLM output:\n' + rawOutput); - } - } - throw new Error('Could not parse LLM output:\n' + rawOutput); -} - -// --- Hauptfunktion --- -/** - * Rewords commit messages for each hash (oldest to newest) using git rebase -i in a loop. - * @param {string} repoPath - Path to your repo - * @param {object} commitMessageMap - { fullHash: newMessage } - * @param {string[]} hashes - array of full commit hashes (oldest first!) - */ -async function rewordCommitsSequentially(repoPath, commitMessageMap, hashes) { - const git = simpleGit(repoPath); - - // Sort hashes... - const allCommits = (await git.log()).all; - const hashesOrdered = hashes - .map(h => allCommits.find(c => c.hash.startsWith(h))) - .filter(Boolean) - .sort((a, b) => - allCommits.findIndex(c => c.hash === a.hash) - allCommits.findIndex(c => c.hash === b.hash) - ) - .map(c => c.hash); - - // EIN Loop! - for (const hash of hashesOrdered) { - // --- Lookup: full hash oder short hash - let msg = commitMessageMap[hash]; - if (!msg) msg = commitMessageMap[hash.substring(0, 7)]; - if (!msg) { - console.warn('No commit message found for hash', hash, 'or', hash.substring(0, 7)); - continue; - } - const commitMsg = msg.replace(/(["$`\\])/g, '\\$1'); - const sequenceEditor = `sed -i '' '1s/pick/reword/'`; - - // await auf Promise – aber OHNE zweiten for! - await new Promise((resolve, reject) => { - const proc = spawn('git', [ - 'rebase', '-i', `${hash}^` - ], { - cwd: repoPath, - env: { - ...process.env, - GIT_SEQUENCE_EDITOR: sequenceEditor, - GIT_EDITOR: `echo "${commitMsg}" >` - }, - stdio: 'inherit' - }); - proc.on('exit', code => code === 0 ? resolve() : reject(new Error(`Failed to reword ${hash}`))); - }); - console.log(`[AutoGit] Reworded commit ${hash} ✔`); - } - console.log('[AutoGit] All specified commit messages updated!'); -} -/* - - - - - - - - - - - - - - -*/ - -//---- 6. Workflow ---- -async function runLLMCommitRewrite(folderObj, win) { - if(!folderObj.needsRelocation){ - const hashes = folderObj.llmCandidates; - const birthday = folderObj.firstCandidateBirthday; - const folderPath = folderObj.path; - folderObj.llmCandidates = []; - folderObj.firstCandidateBirthday = null; - folderObj.linesChanged = 0; - const folders = store.get('folders') || []; - const idx = folders.findIndex(f => f.path === folderObj.path); - if (idx !== -1) { - folders[idx] = folderObj; - store.set('folders', folders); - } - const prompt = await generateLLMCommitMessages(folderPath, hashes); - const llmRaw = await streamLLMCommitMessages(prompt, chunk => process.stdout.write(chunk), win); - const commitList = parseLLMCommitMessages(llmRaw); - const messageMap = {}; - for (const entry of commitList) messageMap[entry.commit] = entry.newMessage; - await rewordCommitsSequentially(folderPath, messageMap, hashes); - win.webContents.send('repo-updated', folderObj.path); - } -} - - - - - - - - - - -/* - - -// ---- 6. Komplett-Workflow (Random instant messages für debugging) ---- -async function runLLMCommitRewrite(folderPath, hashes) { - // Generate a mapping { hash: message } - const messageMap = hashes.reduce((map, hash) => { - map[hash] = getRandomMessage(); - return map; - }, {}); - console.log(messageMap) - - // Call your existing rewrite step with the fake messages - //await cherryPickCommitRewrite(folderPath, messageMap, hashes); - await rewordCommitsSequentially(folderPath, messageMap, hashes); -} - -// Helper: returns a “random” placeholder commit message -function getRandomMessage() { - const verbs = [ - 'Update', 'Refactor', 'Fix', 'Add', 'Remove', 'Improve', 'Optimize', - 'Document', 'Cleanup', 'Configure', 'Upgrade', 'Revert' - ]; - const objects = [ - 'authentication flow', 'API endpoint', 'styling', 'logging', - 'error handling', 'data model', 'build script', 'test suite', - 'configuration', 'dependencies', 'README', 'README.md' - ]; - const details = [ - 'for better performance', - 'to meet new requirements', - 'after feedback', - 'as per spec', - 'to fix typo', - 'to improve readability', - 'to avoid regressions', - 'for consistency' - ]; - - const pick = arr => arr[Math.floor(Math.random() * arr.length)]; - return `${pick(verbs)} ${pick(objects)} ${pick(details)}`; -} -*/ - -// Nutze das Template aus dem Projektordner: -const TEMPLATE_PATH = path.join(__dirname, 'rewrite-commit-msg.js.template'); - -function createRewriteScript(mapping) { - // Lies das Template - let content = fs.readFileSync(TEMPLATE_PATH, 'utf-8'); - // Ersetze __MESSAGE_MAP__ durch dein Mapping - content = content.replace('__MESSAGE_MAP__', JSON.stringify(mapping)); - // Speichere als temporäre Datei - const scriptPath = path.join(__dirname, `rewrite-commit-msg.${Date.now()}.js`); - fs.writeFileSync(scriptPath, content, 'utf-8'); - return scriptPath; -} - -function addMatchingFilesToGitignore(folderPath, pattern) { - const files = fs.readdirSync(folderPath); - const matches = micromatch(files, pattern); - for (const file of matches) { - ensureInGitignore(folderPath, pattern); - break; // Nur einmal pro Pattern eintragen - } -} - - -async function autoCommit(folderPath, message, win) { - - const git = simpleGit(folderPath); - const status = await git.status(); - if ( - status.not_added.length === 0 && - status.created.length === 0 && - status.deleted.length === 0 && - status.modified.length === 0 && - status.renamed.length === 0 - ) { - debug('Auto-Commit: Keine Änderungen zum committen.'); - return false; - } - - let currentBranch = null; - try { - currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim(); - debug(`[autoCommit] Aktueller Branch: ${currentBranch}`); - } catch { - debug('[autoCommit] HEAD ist detached.'); - currentBranch = null; - } - - if (!currentBranch || currentBranch === 'HEAD') { - // === Erweiterte Logs === - const headCommit = (await git.revparse(['HEAD'])).trim(); - let masterCommit = null; - let hasMaster = false; - try { - masterCommit = (await git.revparse(['refs/heads/master'])).trim(); - hasMaster = true; - } catch (e) { - debug('[autoCommit] master branch existiert nicht.'); - masterCommit = null; - hasMaster = false; - } - debug(`[autoCommit] HEAD: ${headCommit}`); - debug(`[autoCommit] master: ${masterCommit}`); - - if (hasMaster && headCommit === masterCommit) { - // HEAD ist detached, zeigt aber exakt auf master-Tip → einfach auf master auschecken. - await git.checkout('master'); - debug('[autoCommit] HEAD war detached, zeigte aber exakt auf master – jetzt zurück auf master.'); - // Nach dem Checkout nochmal aktuellen Branch loggen: - currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim(); - debug(`[autoCommit] Nach checkout: Aktueller Branch: ${currentBranch}`); - // Jetzt **NICHT** weiter zur Umbenenn-Logik! - } else if (hasMaster) { - // HEAD ist detached, zeigt auf einen anderen Commit → backup master und neuen master-Branch - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const backupBranch = `backup-master-${timestamp}`; - await git.branch(['-m', 'master', backupBranch]); - debug(`[autoCommit] Alter master in ${backupBranch} umbenannt.`); - await git.checkout(['-b', 'master']); - debug('[autoCommit] Neuer master-Branch erstellt und ausgecheckt.'); - currentBranch = 'master'; - } else { - // Kein master vorhanden (erstmalig) - await git.checkout(['-b', 'master']); - debug('[autoCommit] Kein master-Branch vorhanden, neuer master erstellt.'); - currentBranch = 'master'; - } - } - - // --- Zeilenzählung --- - let diffOutput = await git.diff(['--numstat']); - // Zeilensumme berechnen: - let changedLines = 0; - for (let line of diffOutput.split('\n')) { - const match = line.match(/^(\d+|\-)\s+(\d+|\-)\s+(.*)$/); - if (match) { - const added = match[1] === '-' ? 0 : parseInt(match[1], 10); - const deleted = match[2] === '-' ? 0 : parseInt(match[2], 10); - changedLines += added + deleted; - } - } - - // Folders aus Store holen - let folders = store.get('folders') || []; - let idx = folders.findIndex(f => f.path === folderPath); - if (idx !== -1) { - folders[idx].linesChanged = (folders[idx].linesChanged || 0) + changedLines; - folders[idx].llmCandidates = folders[idx].llmCandidates || []; - - // ===> WICHTIG: Wir commiten ja jetzt, daher merken wir uns gleich die neue Commit-Hash - // Vor git.commit: Merke alten HEAD - const oldHead = (await git.revparse(['HEAD'])).trim(); - - // Stagen & Committen - await git.add(['-A']); - debug('[autoCommit] Alle Änderungen gestaged.'); - await git.commit(message || '[auto]'); - debug('[autoCommit] Commit erfolgreich erstellt.'); - - // --- Gamification: Commit-Statistik speichern --- - const today = new Date().toISOString().slice(0, 10); // Format: 'YYYY-MM-DD' - const stats = store.get('dailyCommitStats') || {}; - stats[today] = (stats[today] || 0) + 1; - store.set('dailyCommitStats', stats); - - // Nach Commit: neuen HEAD ermitteln und in llmCandidates speichern - const newHead = (await git.revparse(['HEAD'])).trim(); - folders[idx].llmCandidates = folders[idx].llmCandidates || []; - folders[idx].llmCandidates.push(newHead); - if(folders[idx].llmCandidates.length == 1){ - folders[idx].firstCandidateBirthday = Date.now(); - debug('[autoCommit] Erster Commit aufgenommen.'); - } - folders[idx].lastHeadHash = newHead; - console.log(folders[idx].llmCandidates) - - // Threshold holen - const threshold = store.get('intelligentCommitThreshold') || 10; - if (folders[idx].linesChanged >= threshold) { - debug('Congratulations! You changed enough lines of code :)'); - await runLLMCommitRewrite(folders[idx], win); - //folders[idx].linesChanged = 0; // !!!!!!!!!!!!!!!!!!!! needs logic to handle several llm runs called at the same time test - //folders[idx].llmCandidates = []; - //folders.[idx].firstCandidateBirthday = null - } - store.set('folders', folders); - } else { - // Folder not found! (Debug) - debug(`[autoCommit] Warning: Folder ${folderPath} not found in store`); - } -} - -app.whenReady().then(main); -async function main() { - - const win = createWindow(); - - async function updateFoldersListener(win) { - let folders = store.get('folders') || []; - - /* NEXT BLOCK: MONITOR FOLDERS INITIAL CANDIDATE-COMMITS BIRTHDAY FOR AUTOCOMMIT */ - /* ─── NEXT BLOCK: MONITOR “BIRTHDAY” FOR AUTOCOMMIT ─── */ - const minutesThreshold = store.get('minutesCommitThreshold'); - const now = Date.now(); - - folders.forEach(folderObj => { - if (folderObj.firstCandidateBirthday != null) { - const elapsedMin = (now - folderObj.firstCandidateBirthday) / 1000 / 60; - if (elapsedMin >= minutesThreshold) { - runLLMCommitRewrite(folderObj, win); - } - } - }); - - - /* ──────────────────────────────────────────────────── */ - - /* NEXT BLOCK: MONITOR FOLDER MISSING / RELOCATED */ - let updatedFolders = []; - let anyChanged = false; - - // Wir müssen auf asynchrone Checks warten (wegen simple-git) - folders = await Promise.all(folders.map(async f => { - const wasRelocated = f.needsRelocation || false; - const nowExists = fs.existsSync(f.path); - - // EdgeCase: Ordner taucht "wieder" auf - if (wasRelocated && nowExists) { - let hashFound = false; - if (f.lastHeadHash) { - try { - const git = simpleGit(f.path); - // Prüfe, ob Commit irgendwo im Repo existiert - const result = await git.raw(['branch', '--contains', f.lastHeadHash]); - hashFound = result.trim().length > 0; - } catch (err) { - hashFound = false; - } - } - if (hashFound) { - // Repo validiert → needsRelocation zurücknehmen, Monitoring bleibt wie es war - anyChanged = true; - updatedFolders.push({ ...f, needsRelocation: false }); - return { ...f, needsRelocation: false }; - } else { - // Commit-Hash nicht gefunden → Ordner bleibt in Relocation - return { ...f, needsRelocation: true }; - } - } - - // EdgeCase: Ordner verschwindet - if (!nowExists && !wasRelocated) { - anyChanged = true; - updatedFolders.push({ ...f, needsRelocation: true, monitoring: false }); - // Monitoring sofort beenden! - stopMonitoringWatcher(f.path); - return { ...f, needsRelocation: true, monitoring: false }; - } - // Keine Änderung an needsRelocation - return f; - })); - - if (anyChanged) { - store.set('folders', folders); - // Benachrichtige Renderer für alle betroffenen Folder - updatedFolders.forEach(folderObj => { - win.webContents.send('folders-location-updated', folderObj); - }); - } -} - - - await updateFoldersListener(win); - setInterval(() => { updateFoldersListener(win); }, 3000); - - - // Menubar - const menu = Menu.buildFromTemplate([ - { - role: 'appMenu', - submenu: [ - { label: 'Settings', click: () => openSettings(win) }, - { label: 'Quit', role: 'quit', click: () => { isQuiting = true; app.quit(); } } - ] - }, - { role: 'editMenu' } - ]); - Menu.setApplicationMenu(menu); - - - const tray = createTray(win); - - - - - // --- Context Menu bauen --- -function buildTrayMenu() { - const folders = store.get('folders') || []; - const monitoringActive = folders.some(f => f.monitoring); - - return Menu.buildFromTemplate([ - { label: 'Auto-Git öffnen', click: () => { win.show(); win.focus(); } }, - { type: 'separator' }, - ...folders.map(f => ({ - label: `${f.monitoring ? '🟢' : '🔴'} ${path.basename(f.path)}`, - submenu: [ - { - label: f.monitoring ? 'Monitoring stoppen' : 'Monitoring starten', - enabled: !f.needsRelocation, // <--- HIER! - click: () => { - if (!f.needsRelocation) { - win.webContents.send('tray-toggle-monitoring', f.path); - } - // Optional: Feedback anzeigen, falls doch geklickt wird. - } - }, - { - label: 'Ordner entfernen', - click: () => { - win.webContents.send('tray-remove-folder', f.path); - } - } - ] - })), - { type: 'separator' }, - { - label: 'Neuen Ordner hinzufügen', - click: () => { win.webContents.send('tray-add-folder'); } - }, - { - label: 'Alle Monitorings starten', - click: () => { - folders.forEach(f => { - if (!f.monitoring && !f.needsRelocation) { - win.webContents.send('tray-toggle-monitoring', f.path); - } - }); - } - }, - { - label: 'Alle Monitorings stoppen', - click: () => { - folders.forEach(f => { - if (f.monitoring) win.webContents.send('tray-toggle-monitoring', f.path); - }); - } - }, - { type: 'separator' }, - { label: 'Beenden', click: () => { isQuiting = true; app.quit(); } } - ]); -} - - tray.setToolTip('Auto-Git läuft im Hintergrund'); - tray.setContextMenu(buildTrayMenu()); - - // Menu immer wieder aktualisieren, wenn sich Ordner ändern: - store.onDidChange('folders', () => { - tray.setContextMenu(buildTrayMenu()); - }); - - // Optional: Minimieren auf Tray bei Fenster-Schließen - win.on('close', (e) => { - if (!app.isQuiting) { - e.preventDefault(); - win.hide(); - } - }); - - // Doppelklick aufs Tray: Fenster zeigen - tray.on('double-click', () => { - win.show(); - }); - - - - // 1) Beim Start bereits gespeicherte Ordner überwachen und monitoren - const folders = store.get('folders') || []; - folders.forEach(folderObj => { - //if (fs.existsSync(path.join(folderObj.path, '.git', 'refs', 'heads', 'master'))) { - //watchRepo(folderObj.path, win); - //} - if (folderObj.monitoring) { - startMonitoringWatcher(folderObj.path, win); - } - }); - - // 2) IPC-Handler - ipcMain.handle('get-selected', () => { - const folders = store.get('folders') || []; - const selectedPath = store.get('selected'); - return folders.find(f => f.path === selectedPath) || null; - }); - - ipcMain.handle('set-selected', (_e, folderObjOrPath) => { - // Akzeptiert sowohl String (legacy) als auch Objekt: - const folderPath = typeof folderObjOrPath === 'string' - ? folderObjOrPath - : folderObjOrPath.path; - store.set('selected', folderPath); - const folders = store.get('folders') || []; - return folders.find(f => f.path === folderPath) || null; - }); - - // Liste aller Folders - ipcMain.handle('get-folders', () => store.get('folders')); - - - - // (1) Die Kernfunktion - async function addFolderByPath(newFolder) { - await initGitRepo(newFolder); - - // HEAD-Hash holen - let lastHeadHash = null; - try { - const git = simpleGit(newFolder); - lastHeadHash = (await git.revparse(['HEAD'])).trim(); - } catch {} - - let folders = store.get('folders') || []; - let folderObj = folders.find(f => f.path === newFolder); - if (!folderObj) { - folderObj = { path: newFolder, monitoring: true, linesChanged: 0, llmCandidates: [], firstCandidateBirthday: null, lastHeadHash }; - folders.push(folderObj); - store.set('folders', folders); - } else { - folderObj.lastHeadHash = lastHeadHash; - store.set('folders', folders); - } - store.set('selected', newFolder); - //watchRepo(newFolder, win); - startMonitoringWatcher(newFolder, win); - return store.get('folders'); - } - - - // (2) Die IPC-Handler anpassen: - ipcMain.handle('add-folder', async () => { - const { canceled, filePaths } = await dialog.showOpenDialog({ - properties: ['openDirectory'] - }); - if (canceled || !filePaths[0]) { - return store.get('folders'); - } - return await addFolderByPath(filePaths[0]); - }); - - ipcMain.handle('add-folder-by-path', async (_e, folderPath) => { - return await addFolderByPath(folderPath); - }); - - // Ordner entfernen: Watcher schließen, Store-Update - ipcMain.handle('remove-folder', (_e, folderObj) => { - const folders = store.get('folders') || []; - const updated = folders.filter(f => f.path !== folderObj.path); - store.set('folders', updated); - if (store.get('selected') === folderObj.path) store.set('selected', null); - stopMonitoringWatcher(folderObj.path); - const watcher = repoWatchers.get(folderObj.path); - if (watcher) watcher.close(), repoWatchers.delete(folderObj.path); - return updated; - }); - - // Zähle Commits - ipcMain.handle('get-commit-count', async (_e, folderObj) => { - try { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return 0; - } - const git = simpleGit(folderObj.path); - const log = await git.log(); - return log.total; - } catch (err) { - return 0; - } - }); - - // Prüfe, ob es ungestagte Änderungen gibt - /*ipcMain.handle('has-diffs', async (_e, folderObj) => { - const git = simpleGit(folderObj.path); - const status = await git.status(); - // modified, not_added, deleted, etc. - return status.files.length > 0; - });*/ - ipcMain.handle('has-diffs', async (_e, folderObj) => { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return false; - } - const git = simpleGit(folderObj.path); - const status = await git.status(); - return status.files.length > 0; - }); - - // Entferne das .git-Verzeichnis - ipcMain.handle('remove-git-folder', async (_e, folderObj) => { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return; - } - const gitDir = path.join(folderObj.path, '.git'); - if (fs.existsSync(gitDir)) { - await fs.promises.rm(gitDir, { recursive: true, force: true }); - } - return; - }); - - // Commits holen (paginiert) - ipcMain.handle('get-commits', async (_e, folderObj, page = 1, pageSize = 50) => { - try { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return { head: null, commits: [], total: 0, page: 1, pageSize: 50, pages: 1 }; - } - const git = simpleGit(folderObj.path); - // Offset berechnen (0-basiert!) - const skip = (page - 1) * pageSize; - const log = await git.log({ - '--all': null, - '--skip': skip, - '--max-count': pageSize - }); - // Gesamte Anzahl Commits für die Pagination - const totalLog = await git.log({ '--all': null }); - const total = totalLog.total || totalLog.all.length; - - const fullHead = (await git.revparse(['--verify', 'HEAD'])).trim(); - const head = fullHead.substring(0, 7); - const pages = Math.max(1, Math.ceil(total / pageSize)); - - return { - head, - commits: log.all.map(c => ({ - hash: c.hash.substring(0, 7), - date: c.date, - message: c.message - })), - total, - page, - pageSize, - pages - }; - } catch (err) { - return { head: null, commits: [], total: 0, page: 1, pageSize: 50, pages: 1 }; - } - }); - // Diff - ipcMain.handle('diff-commit', async (_e, folderObj, hash) => { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return null; - } - const git = simpleGit(folderObj.path); - return git.diff([`${hash}^!`]); - }); - - // Revert - ipcMain.handle('revert-commit', async (_e, folderObj, hash) => { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return; - } - const git = simpleGit(folderObj.path); - await git.revert(hash, ['--no-edit']); - }); -//yo - /** - * Checkt das Arbeitsverzeichnis auf exakt den Zustand von `hash` aus. - */ - ipcMain.handle('checkout-commit', async (_e, folderObj, hash) => { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return; - } - const git = simpleGit(folderObj.path); - // clean mode: alle lokalen Veränderungen verwerfen - await git.checkout([hash, '--force']); - }); - - - // Snapshot - ipcMain.handle('snapshot-commit', async (_e, folderObj, hash) => { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return null; - } - const { canceled, filePaths } = await dialog.showOpenDialog({ - title: 'Ordner auswählen zum Speichern des Snapshots', - properties: ['openDirectory'] - }); - if (canceled || !filePaths[0]) return; - const outDir = filePaths[0]; - const baseName = path.basename(folderObj.path); - const filePath = path.join(outDir, `${baseName}-${hash}.zip`); - return new Promise((resolve, reject) => { - exec( - `git -C "${folderObj.path}" archive --format zip --output "${filePath}" ${hash}`, - err => err ? reject(err) : resolve(filePath) - ); - }); - }); - - // IPC für skymode - ipcMain.handle('get-skymode', () => store.get('skymode')); - ipcMain.handle('set-skymode', (_e, val) => { - store.set('skymode', val); - // sende an alle Fenster - BrowserWindow.getAllWindows().forEach(win => { - win.webContents.send('skymode-changed', val); - }); - }); - ipcMain.handle('get-skip-git-prompt', () => store.get('skipGitPrompt')); - ipcMain.handle('set-skip-git-prompt', (_e,val) => store.set('skipGitPrompt', val)); - - // Auto-Verzeichnisstruktur - const IGNORED_NAMES = [ - '.DS_Store', 'node_modules', '.git', 'dist', 'build', - '.cache', 'out', '.venv', '.mypy_cache', '__pycache__', 'package-lock.json' - ]; - - function isIgnored(name) { - return IGNORED_NAMES.includes(name); - } - - function walkDir(base, rel = '.') { - const full = path.join(base, rel); - let list = []; - try { - fs.readdirSync(full, { withFileTypes: true }).forEach(dirent => { - if (isIgnored(dirent.name)) return; - const entry = path.join(rel, dirent.name); - if (dirent.isDirectory()) { - list.push({ name: dirent.name, type: 'dir', children: walkDir(base, entry) }); - } else { - list.push({ name: dirent.name, type: 'file' }); - } - }); - } catch (e) {} - return list; - } - - ipcMain.handle('get-folder-tree', async (_e, folderPath) => { - try { - return walkDir(folderPath, '.'); - } catch { - return []; - } - }); - - ipcMain.handle('commit-current-folder', async (_e, folderObj, message) => { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return {}; - } - folder = folderObj.path; - try { - debug(`Commit-Vorgang für ${folder} gestartet…`); - const git = simpleGit(folder); - - // Prüfe: Gibt es was zu committen? - const status = await git.status(); - if ( - status.not_added.length === 0 && - status.created.length === 0 && - status.deleted.length === 0 && - status.modified.length === 0 && - status.renamed.length === 0 - ) { - debug('Nichts zu committen.'); - return { success: false, error: 'Nichts zu committen.' }; - } - - // HEAD-Status prüfen - let currentBranch = null; - try { - currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim(); - debug(`Aktueller Branch: ${currentBranch}`); - } catch (err) { - debug('HEAD ist detached.'); - } - - // Falls detached, **jetzt erst** alten Branch umbenennen und neuen master erzeugen - if (!currentBranch || currentBranch === 'HEAD') { - // HEAD ist detached, prüfe ob HEAD auf dem Tip von master ist! - const headCommit = (await git.revparse(['HEAD'])).trim(); - let masterCommit = null; - let hasMaster = false; - try { - masterCommit = (await git.revparse(['refs/heads/master'])).trim(); - hasMaster = true; - } catch (e) { - masterCommit = null; //wawa - hasMaster = false; - } - - if (hasMaster && headCommit === masterCommit) { - // HEAD ist detached, aber zeigt exakt auf master! - await git.checkout('master'); - debug('[autoCommit] HEAD war detached, zeigte aber exakt auf master – jetzt zurück auf master.'); - // **Return nicht vergessen, sonst geht der Branch-Move weiter** - // ----> Das ist die Zeile die du wahrscheinlich vergessen hast! - // Beende hier die Branch-Logik - return { success: true }; - } - - // Ansonsten wie gehabt: - if (hasMaster) { - // HEAD ist detached, zeigt nicht auf master -> backup + neuer master - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const backupBranch = `backup-master-${timestamp}`; - await git.branch(['-m', 'master', backupBranch]); - debug(`[autoCommit] Alter master in ${backupBranch} umbenannt.`); - await git.checkout(['-b', 'master']); - debug('[autoCommit] Neuer master-Branch erstellt und ausgecheckt.'); - } else { - // Kein master vorhanden (erstmalig) - await git.checkout(['-b', 'master']); - debug('[autoCommit] Kein master-Branch vorhanden, neuer master erstellt.'); - } - } - - await git.add(['-A']); - debug('Alle Änderungen gestaged.'); - await git.commit(message || 'test'); - debug('Commit erfolgreich erstellt.'); - // Push hier ggf. noch auskommentiert lassen - - return { success: true }; - } catch (err) { - debug(`FEHLER beim Commit: ${err.message}`); - return { success: false, error: err.message }; - } - }); - - ipcMain.handle('set-monitoring', async (_e, folderPath, monitoring) => { - let folders = store.get('folders') || []; - const folderObj = folders.find(f => f.path === folderPath); - if (!folderObj || folderObj.needsRelocation) { - // Monitoring-Start für fehlenden Ordner: Ignorieren! - return false; - } - folders = folders.map(f => - f.path === folderPath ? { ...f, monitoring } : f - ); - store.set('folders', folders); - debug(`[STORE] Monitoring für ${folderPath}: ${monitoring}`); - // Monitoring-Watcher starten/stoppen - if (monitoring) { - startMonitoringWatcher(folderPath, win); - } else { - stopMonitoringWatcher(folderPath); - } - return monitoring; - }); - - ipcMain.handle('ollama-list', async () => { - // Versuche erst JSON-Ausgabe - return new Promise(resolve => { - exec('ollama list --json', (err, stdout, stderr) => { - if (err) { - // ENOENT → ollama CLI fehlt - if (err.code === 'ENOENT') { - return resolve({ status: 'no-cli' }); - } - // JSON-Modus nicht unterstützt? Dann Fallback auf plain text - return parsePlain(); - } - try { - const lines = stdout - .split('\n') - .filter(l => l.trim()) - .map(l => JSON.parse(l)); - return resolve({ status: 'ok', models: lines }); - } catch (_) { - return parsePlain(); - } - }); - - // Fallback-Funktion: parst die tabellarische Ausgabe - function parsePlain() { - exec('ollama list', (err2, out2, stderr2) => { - if (err2) { - if (err2.code === 'ENOENT') return resolve({ status: 'no-cli' }); - return resolve({ status: 'error', msg: stderr2 || err2.message }); - } - const models = []; - // Jede Zeile nach HEADER ignorieren, Spalten splitten - out2.split('\n').slice(1).forEach(line => { - const cols = line.trim().split(/\s{2,}/); - if (cols[0]) models.push({ name: cols[0] }); - }); - resolve({ status: 'ok', models }); - }); - } - }); - }); - - ipcMain.handle('ollama-pull', async (_e, model) => { - return new Promise(resolve => { - exec(`ollama pull ${model}`, (err, stdout, stderr) => { - if (err) return resolve({ status: 'error', msg: stderr || err.message }); - resolve({ status: 'ok', msg: stdout }); - }); - }); - }); - - ipcMain.handle('get-commit-model', () => store.get('commitModel') || 'qwen2.5-coder:7b'); - ipcMain.handle('set-commit-model', (_e, val) => store.set('commitModel', val)); - - ipcMain.handle('get-readme-model', () => store.get('readmeModel') || 'qwen2.5-coder:32b'); - ipcMain.handle('set-readme-model', (_e, val) => store.set('readmeModel', val)); - - ipcMain.handle('get-intelligent-commit-threshold', () => store.get('intelligentCommitThreshold')); - ipcMain.handle('set-intelligent-commit-threshold', (_e, value) => { - store.set('intelligentCommitThreshold', value); - }); - - ipcMain.handle('get-minutes-commit-threshold', () => store.get('minutesCommitThreshold')); - ipcMain.handle('set-minutes-commit-threshold', (_e, value) => { - store.set('minutesCommitThreshold', value); - }); - - ipcMain.handle('get-autostart', () => store.get('autostart')); - ipcMain.handle('set-autostart', (_e, enabled) => { - store.set('autostart', enabled); - app.setLoginItemSettings({ - openAtLogin: !!enabled - }); - }); - ipcMain.handle('get-close-to-tray', () => store.get('closeToTray')); - ipcMain.handle('set-close-to-tray', (_e, val) => store.set('closeToTray', val)); - - - ipcMain.on('close-settings', () => { - if (settingsWin) settingsWin.close(); - }); - - ipcMain.handle('is-git-repo', async (_e, folderPath) => { - const gitFolder = path.join(folderPath, '.git'); - return fs.existsSync(gitFolder); - }); - - // Setzt für ein Folder-Objekt den neuen Pfad, needsRelocation => false - ipcMain.handle('relocate-folder', async (_e, oldPath, newPath) => { - let folders = store.get('folders') || []; - folders = folders.map(f => - f.path === oldPath - ? { ...f, path: newPath, needsRelocation: false } - : f - ); - store.set('folders', folders); - return folders.find(f => f.path === newPath); - }); - - ipcMain.handle('pick-folder', async () => { - const result = await dialog.showOpenDialog({ - properties: ['openDirectory'] - }); - return result.canceled ? null : result.filePaths; - }); - - ipcMain.handle('repo-has-commit', async (_e, repoPath, commitHash) => { - try { - const git = simpleGit(repoPath); - // Perform a git log to check if the commit exists anywhere in the history - const result = await git.raw(['branch', '--contains', commitHash]); - // Falls irgendein Branch den Commit enthält, ist das unser Repo! - return result.trim().length > 0; - } catch { - return false; - } - }); - - - - ipcMain.handle('get-daily-commit-stats', () => store.get('dailyCommitStats') || {}); - - - ipcMain.handle('get-all-commit-hashes', async (_e, folderObj) => { - try { - if (folderObj.needsRelocation || !fs.existsSync(folderObj.path)) { - return []; - } - const git = simpleGit(folderObj.path); - // Wir holen ALLE Commits, HEAD → root - const log = await git.log(['--all']); - // Rückgabe: Array mit vollständigen Hashes (du kannst auch .substring(0, 7) nehmen, falls du überall Short-Hashes verwendest) - return log.all.map(c => c.hash); - } catch (err) { - return []; - } - }); - - - - -ipcMain.on('show-folder-context-menu', (event, folderPath) => { - const win = BrowserWindow.fromWebContents(event.sender); - const template = [ - { - label: 'Open Folder', - click: () => { - // öffnet den Ordner in der nativen Dateiansicht - shell.openPath(folderPath); - } - }, - { - label: 'Copy Folder Path', - click: () => { - clipboard.writeText(folderPath); - } - } - ]; - const menu = Menu.buildFromTemplate(template); - menu.popup({ window: win }); -}); - -ipcMain.on('show-tree-context-menu', (event, { absPath, relPath, root, type }) => { - const win = BrowserWindow.fromWebContents(event.sender); - - const template = [ - { - label: 'Open File', - click: () => shell.openPath(absPath), - visible: type === 'file' // Nur für Dateien anzeigen - }, - { - label: 'Open Folder', - click: () => shell.openPath(absPath), - visible: type === 'dir' // Nur für Dateien anzeigen - }, - { - label: 'Copy File Path', - click: () => clipboard.writeText(absPath), - visible: type === 'file' // Nur für Dateien anzeigen - }, - { - label: 'Copy Folder Path', - click: () => clipboard.writeText(absPath), - visible: type === 'dir' // Nur für Dateien anzeigen - }, - { - label: 'Add to .gitignore', - click: () => { - const gitignore = path.join(root, '.gitignore'); - fs.appendFile(gitignore, `\n${relPath}\n`, (err) => { - if (err) { - dialog.showErrorBox('Error', 'Konnte nicht zu .gitignore hinzufügen:\n' + err.message); - } - }); - } - } - ]; - - const menu = Menu.buildFromTemplate(template); - menu.popup({ window: win }); -}); - - - - - // README STUFF - - const MAX_TOTAL_SIZE = 100 * 1024; // 100 KB - const CODE_EXTS = [ - '.js','.jsx','.ts','.tsx','.py','.sh','.rb','.pl','.php','.java','.c','.cpp','.h','.cs','.go','.rs','.json','.yml','.yaml','.toml','.md','.html','.css','.txt' - ]; - - function isTextFile(filePath) { - // Optional: Mehr Intelligenz! - const ext = path.extname(filePath).toLowerCase(); - if (CODE_EXTS.includes(ext)) return true; - // Ignoriere node_modules und große Binaries! - const stat = fs.statSync(filePath); - if (stat.size > 200*1024) return false; - const buffer = fs.readFileSync(filePath, {encoding: null, flag: 'r'}); - for (let i = 0; i < Math.min(buffer.length, 400); i++) { - if (buffer[i] === 0) return false; - } - return true; - } - - // --- Dateien sammeln --- - function getGitignoreFilter(folderPath) { - const gitignorePath = path.join(folderPath, '.gitignore'); - if (!fs.existsSync(gitignorePath)) return null; - const content = fs.readFileSync(gitignorePath, 'utf8'); - return ignore().add(content); - } - - function getFileRelevanceScore(filename, relPath, content) { - const base = path.basename(filename).toLowerCase(); - let score = 0; - // 1. Dateiname und Entry-Patterns - if (/^(main|index|app|server)\.(js|py|ts|go|rb|php|java|c|cpp)$/.test(base)) score += 20; - if (/package\.json|requirements\.txt|pyproject\.toml|makefile|cargo\.toml/.test(base)) score += 20; - if (path.dirname(relPath) === '.') score += 10; - if (/test|mock|example|spec|demo/.test(relPath)) score -= 30; - if (/\.min\./.test(base)) score -= 20; - // 2. Exports / Functions / Klassen - if (content) { - // Viele Exports - const exportCount = (content.match(/export\s+(function|class|const|let|var)/g) || []).length; - const moduleExportCount = (content.match(/module\.exports/g) || []).length; - score += (exportCount + moduleExportCount) * 2; - // Viele Funktionen/Klassen - const functionCount = (content.match(/function\s+/g) || []).length; - const classCount = (content.match(/class\s+/g) || []).length; - score += (functionCount + classCount); - // Für Python - const pyDefCount = (content.match(/^def\s+/gm) || []).length; - const pyClassCount = (content.match(/^class\s+/gm) || []).length; - score += (pyDefCount + pyClassCount); - // Typische Utility-Signaturen - if (content.includes('main(') || content.includes('if __name__ == "__main__":')) score += 5; - } - // 3. Reduziere Score für zu kurze Dateien (<20 Zeilen) - if (content && content.split('\n').length < 20) score -= 5; - // 4. Ein bisschen Bonus für große Dateien (viel Logik, solange keine Data-Files) - if (content && content.length > 1500) score += 2; - return score; - } - - function getRelevantFiles(dir, maxSize = 100*1024, ig = null, base = null) { - base = base || dir; - if (!ig) ig = getGitignoreFilter(base); - let files = []; - function walk(current) { - for (const f of fs.readdirSync(current)) { - const full = path.join(current, f); - const rel = path.relative(base, full); - if (ig && ig.ignores(rel)) continue; - if (fs.statSync(full).isDirectory()) { - if (f.startsWith('.')) continue; - walk(full); - } else if (isTextFile(full)) { - const content = fs.readFileSync(full, 'utf8').slice(0, 3000); // reicht für scoring - files.push({ f: full, rel, s: fs.statSync(full).size, score: getFileRelevanceScore(full, rel, content) }); - } - } - } - walk(dir); - files.sort((a, b) => b.score - a.score || a.s - b.s); - // Limit by maxSize - let sum = 0, selected = []; - for (let {f,s} of files) { - if (sum + s > maxSize) break; - selected.push(f); - sum += s; - } - return selected; - } - - - - - ipcMain.handle('has-readme', async (_evt, folderPath) => { - const readmePath = path.join(folderPath, 'README.md'); - return fs.existsSync(readmePath); - }); - - - ipcMain.handle('generate-readme', async (evt, folderPath) => { - // Hole Author aus Settings oder Default - // const store = require('./yourStore'); // oder wie auch immer... - const authorName = store.get('author') || 'Unknown'; - const licenseType = store.get('license') || 'MIT'; - const repoName = path.basename(folderPath); - - // Finde alle Code/Textdateien - const codeFiles = getRelevantFiles(folderPath); - let prompt = ` -You are a tool that generates README.md files in markdown format. -Do not review, suggest, or improve the code. -Your only job is to create a clear and concise README in markdown, suitable for immediate use on GitHub. - -The project source code is below. - -Example README.md: ---- -# Example Project Name - -**Author:** Alice - -A simple script for downloading and processing web pages. - -## Features -- Downloads pages from a list of URLs -- Extracts and saves the text content -- Generates a summary report - -## Usage - -\`\`\`bash -python main.py input.txt -\`\`\` ---- -NEVER add Contact Details. -IMPORTANT: The LICENSE is ${licenseType}! -So, write something like: -"## License -This project is licensed under the ${licenseType} License." - -Now write a similar README.md for the following project (think of a good name and use the provided author): - -**Author:** ${authorName} - -Source Code: - `; - for (const f of codeFiles) { - let rel = path.relative(folderPath, f); - prompt += `\n---\nFile: ${rel}\n${fs.readFileSync(f, 'utf-8')}\n`; - } - prompt += `\n---\nWrite ONLY the complete README.md in markdown format. Do NOT add extra explanations, commentary, or code reviews. And remember, the license is ${licenseType}!`; - - console.log(prompt); - // LLM call - //const win = BrowserWindow.fromWebContents(evt.sender); // für Cat-Stream - await ensureOllamaRunning(); - const selectedModel = store.get('readmeModel') || 'qwen2.5-coder:32b'; - //let result = await streamLLMCommitMessages(prompt, null, win); - - let result = await streamLLMREADME(prompt, chunk => process.stdout.write(chunk), win); - - // Output fixen: Entferne eventuelle Codeblocks - result = result.replace(/^```markdown|^```md|^```/gmi, '').replace(/```$/gmi, '').trim(); - - // Disclaimer einbauen - const disclaimer = `> ⚠️ **This README.md has been automatically generated using AI and might contain hallucinations or inaccuracies. Please proceed with caution!**\n\n`; - // Falls nötig, Name/Author oben einbauen - let final = `# ${repoName}\n\n**Author:** ${authorName}\n\n${disclaimer}${result}`; - // Schreibe/Überschreibe README.md (wenn du willst, oder Preview) - fs.writeFileSync(path.join(folderPath, 'README.md'), final, 'utf-8'); - return final; - }); - - - - // … Ende der IPC-Handler … - - - win.webContents.openDevTools({ mode: 'detach' }); - // clean up on exit - win.on('close', (e) => { - if (!isQuiting && store.get('closeToTray')) { - e.preventDefault(); - win.hide(); - } - }); -}; - - - - - - -ipcMain.on('get-selected-sync', (event) => { - const folders = store.get('folders') || []; - const selectedPath = store.get('selected'); - event.returnValue = folders.find(f => f.path === selectedPath) || null; -}); \ No newline at end of file