1
0
Files
auto-git-gui/main.js
Victor Giers ff9ec031bf auto-git:
[change] main.js
2025-06-01 10:46:45 +02:00

2204 lines
65 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Function>
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);
}
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;
}
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 }
});
let debounceTimer = null;
let pendingNames = new Set();
function handleDebounced() {
enqueueTask(folderPath, async () => {
const namesArr = Array.from(pendingNames);
pendingNames.clear();
// .gitignore sofort synchron aktualisieren (im Event-Handler)
let gitignoreChanged = false;
for (const fileOrDirName of namesArr) {
for (const name of IGNORED_NAMES) {
if (name.includes('*')) {
if (micromatch.isMatch(fileOrDirName, name)) {
gitignoreChanged = ensureInGitignore(folderPath, name) || gitignoreChanged;
}
} else {
if (fileOrDirName === name.replace(/\/$/, '')) {
gitignoreChanged = ensureInGitignore(folderPath, name) || gitignoreChanged;
}
}
}
}
// Danach wie gehabt:
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);
}
});
}
['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') {
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;
hasMaster = false;
}
debug(`[autoCommit] HEAD: ${headCommit}`);
debug(`[autoCommit] master: ${masterCommit}`);
if (hasMaster && headCommit === masterCommit) {
await git.checkout('master');
debug('[autoCommit] HEAD war detached, zeigte aber exakt auf master jetzt zurück auf master.');
currentBranch = (await git.revparse(['--abbrev-ref', 'HEAD'])).trim();
debug(`[autoCommit] Nach checkout: Aktueller Branch: ${currentBranch}`);
} else if (hasMaster) {
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 {
await git.checkout(['-b', 'master']);
debug('[autoCommit] Kein master-Branch vorhanden, neuer master erstellt.');
currentBranch = 'master';
}
}
// Zeilen zählen
let diffOutput = await git.diff(['--numstat']);
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) {
let folderObj = folders[idx];
// Wenn gerade ein Rebase läuft, committen wir nicht direkt in llmCandidates,
// sondern in pendingLLMCandidates. So hüten wir uns, das laufende Rebase zu stören.
if (folderObj.rebasing) {
await git.add(['-A']);
debug('[autoCommit] Alle Änderungen gestaged (während Rebase).');
await git.commit(message || '[auto]');
debug('[autoCommit] Commit erfolgreich erstellt (während Rebase).');
const newHead = (await git.revparse(['HEAD'])).trim();
folderObj.pendingLLMCandidates.push(newHead);
folderObj.linesChanged = (folderObj.linesChanged || 0) + changedLines;
folders[idx] = folderObj;
store.set('folders', folders);
return true;
}
// Standard-Fall: kein laufender Rebase, normal committen
await git.add(['-A']);
debug('[autoCommit] Alle Änderungen gestaged.');
await git.commit(message || '[auto]');
debug('[autoCommit] Commit erfolgreich erstellt.');
const newHead = (await git.revparse(['HEAD'])).trim();
folderObj.linesChanged = (folderObj.linesChanged || 0) + changedLines;
folderObj.llmCandidates = folderObj.llmCandidates || [];
folderObj.llmCandidates.push(newHead);
if (folderObj.llmCandidates.length === 1) {
folderObj.firstCandidateBirthday = Date.now();
debug('[autoCommit] Erster Commit aufgenommen.');
}
folderObj.lastHeadHash = newHead;
const threshold = store.get('intelligentCommitThreshold') || 10;
if (folderObj.linesChanged >= threshold) {
debug('Threshold erreicht starte LLM-Rebase!');
await runLLMCommitRewrite(folderObj, win);
}
folders[idx] = folderObj;
store.set('folders', folders);
return true;
} else {
debug(`[autoCommit] Warning: Folder ${folderPath} nicht gefunden im Store`);
return false;
}
}
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;
});