import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync, } from 'node:fs'; import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; const normalize = value => value.split(sep).join('/').replace(/^\.\//, ''); const allowedEnvTemplates = new Set(['.env.example', '.env.docker.example']); const skippedTopLevelDirs = new Set([ 'node_modules', 'runtime', 'docker-data', 'uploads', 'backups', 'logs', 'sessions', 'tmp', 'temp', '__pycache__', 'release-output', 'operations', 'updates', 'modules', 'archiv', 'archive', 'archives', ]); const skippedLocalAiDirs = new Set(['local-ai/comfyui', 'local-ai/downloads', 'local-ai/extract-temp']); const skippedExactFiles = new Set([ '.vendoo-install-mode', '.vendoo-github-state.json', 'pending-operation.json', 'last-operation.json', 'first-admin-code.txt', ]); const skippedExtensions = new Set([ '.db', '.sqlite', '.sqlite3', '.log', '.key', '.pem', '.pfx', '.p12', ]); export function shouldSkipStagingEntry(relativePath, stat) { const rel = normalize(relativePath); const parts = rel.split('/'); const name = basename(rel); const lower = rel.toLowerCase(); if (!rel || rel === '.git' || lower.startsWith('.git/')) return true; if (skippedTopLevelDirs.has(parts[0].toLowerCase())) return true; if (parts.some(part => part.toLowerCase() === '__pycache__') || /\.pyc$/i.test(rel)) return true; if ([...skippedLocalAiDirs].some(dir => lower === dir || lower.startsWith(`${dir}/`))) return true; if (/^\.env(?:\..+)?$/i.test(name) && !allowedEnvTemplates.has(rel)) return true; if (skippedExactFiles.has(name.toLowerCase())) return true; if (/\.(?:db|sqlite|sqlite3)-(?:wal|shm)$/i.test(name)) return true; if (skippedExtensions.has(extname(name).toLowerCase())) return true; if (stat.isSymbolicLink()) return true; return false; } function cleanDestination(destination) { if (!existsSync(join(destination, '.git'))) { throw new Error(`Ziel ist kein geklontes Git-Repository: ${destination}`); } for (const name of readdirSync(destination)) { if (name === '.git') continue; rmSync(join(destination, name), { recursive: true, force: true }); } } export function prepareGitStaging(sourceRoot, destinationRoot) { const source = resolve(sourceRoot); const destination = resolve(destinationRoot); if (source === destination) throw new Error('Quell- und Zielordner dürfen nicht identisch sein.'); if (!existsSync(source)) throw new Error(`Quellordner fehlt: ${source}`); if (!existsSync(destination)) throw new Error(`Zielordner fehlt: ${destination}`); cleanDestination(destination); let copied = 0; let skipped = 0; function walk(current) { for (const name of readdirSync(current)) { const absolute = join(current, name); const rel = normalize(relative(source, absolute)); const stat = lstatSync(absolute); if (shouldSkipStagingEntry(rel, stat)) { skipped += 1; continue; } const target = join(destination, rel); if (stat.isDirectory()) { mkdirSync(target, { recursive: true }); walk(absolute); } else if (stat.isFile()) { mkdirSync(dirname(target), { recursive: true }); copyFileSync(absolute, target); try { chmodSync(target, stat.mode & 0o777); } catch {} copied += 1; } } } walk(source); return { source, destination, copied, skipped }; } const invokedPath = process.argv[1] ? resolve(process.argv[1]) : ''; if (invokedPath === fileURLToPath(import.meta.url)) { const [, , source, destination] = process.argv; if (!source || !destination) { console.error('Aufruf: node tools/prepare-git-staging.mjs '); process.exit(64); } try { const result = prepareGitStaging(source, destination); console.log(`Git-Staging vorbereitet: ${result.copied} Quelldateien kopiert; ${result.skipped} lokale Einträge ausgeschlossen.`); } catch (error) { console.error(`[FEHLER] Git-Staging konnte nicht vorbereitet werden: ${error.message}`); process.exit(1); } }