import { existsSync, readFileSync } from 'node:fs'; import { dirname, join, normalize, resolve, win32 } from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')); const command = packageJson?.scripts?.['verify:installer-shell'] || ''; const isWindowsHost = process.platform === 'win32' || process.env.VENDOO_TEST_WINDOWS_SHELL === '1'; function fail(message) { console.error(`Installer-Shell-Portabilitätsgate fehlgeschlagen: ${message}`); process.exit(1); } if (/^\s*sh\s+/i.test(command)) { fail('package.json darf den POSIX-Befehl sh nicht direkt voraussetzen.'); } if (command !== 'node tools/verify-installer-shell.mjs') { fail(`Unerwarteter npm-Vertrag: ${command}`); } const shellContract = readFileSync(join(root, 'tools', 'verify-installer-shell.sh'), 'utf8'); for (const marker of [ 'VENDOO_INSTALLER_SHELL_HOST', 'MINGW*|MSYS*|CYGWIN*', 'POSIX-Modus 0600', 'show_file', 'assert_env_permissions', '--exclude=.git', ]) { if (!shellContract.includes(marker)) fail(`Shell-Testvertrag fehlt: ${marker}`); } function run(file, args, options = {}) { return spawnSync(file, args, { cwd: root, encoding: 'utf8', windowsHide: true, maxBuffer: 16 * 1024 * 1024, ...options, }); } function addCandidate(set, value) { if (!value) return; const candidate = normalize(value.replace(/^"|"$/g, '').trim()); if (candidate) set.add(candidate); } function addGitRootCandidates(set, gitRoot) { if (!gitRoot) return; addCandidate(set, join(gitRoot, 'bin', 'sh.exe')); addCandidate(set, join(gitRoot, 'usr', 'bin', 'sh.exe')); } function pathApiFor(value) { return /^[a-zA-Z]:[\\/]/.test(value) ? win32 : { normalize, dirname }; } function deriveGitRootFromExecutable(executable) { const raw = executable.replace(/^"|"$/g, '').trim(); const api = pathApiFor(raw); const clean = api.normalize(raw); const parent = api.dirname(clean); const leaf = parent.toLowerCase().split(/[\\/]/).pop(); if (['cmd', 'bin', 'usr'].includes(leaf)) { return api.dirname(parent); } return api.dirname(clean); } function deriveGitRootFromExecPath(execPath) { const raw = execPath.replace(/^"|"$/g, '').trim(); const api = pathApiFor(raw); const clean = api.normalize(raw); const lowered = clean.toLowerCase().replace(/\\/g, '/'); for (const marker of ['/mingw64/', '/mingw32/', '/usr/libexec/', '/libexec/']) { const index = lowered.indexOf(marker); if (index > 0) return clean.slice(0, index); } return null; } function findWindowsGitBash() { const candidates = new Set(); addCandidate(candidates, process.env.VENDOO_SH); addCandidate(candidates, process.env.SHELL); for (const base of [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA]) { if (!base) continue; addGitRootCandidates(candidates, join(base, base === process.env.LOCALAPPDATA ? 'Programs' : '', 'Git')); } const whereGit = run('where.exe', ['git.exe']); if (whereGit.status === 0) { for (const line of whereGit.stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) { addGitRootCandidates(candidates, deriveGitRootFromExecutable(line)); } } const execPath = run('git.exe', ['--exec-path']); if (execPath.status === 0) { const gitRoot = deriveGitRootFromExecPath(execPath.stdout); if (gitRoot) addGitRootCandidates(candidates, gitRoot); } const whereSh = run('where.exe', ['sh.exe']); if (whereSh.status === 0) { for (const line of whereSh.stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) { addCandidate(candidates, line); } } return [...candidates].find((candidate) => existsSync(candidate)) || null; } // Regression contracts for common Git-for-Windows layouts. const standardRoot = deriveGitRootFromExecutable('C:\\Program Files\\Git\\cmd\\git.exe'); if (standardRoot.toLowerCase() !== win32.normalize('C:\\Program Files\\Git').toLowerCase()) { fail(`Git-Root aus cmd\\git.exe falsch abgeleitet: ${standardRoot}`); } const execRoot = deriveGitRootFromExecPath('C:\\Program Files\\Git\\mingw64\\libexec\\git-core'); if (!execRoot || execRoot.toLowerCase() !== win32.normalize('C:\\Program Files\\Git').toLowerCase()) { fail(`Git-Root aus git --exec-path falsch abgeleitet: ${execRoot}`); } let shell; if (isWindowsHost) { shell = process.platform === 'win32' ? findWindowsGitBash() : 'sh'; if (!shell) { fail('Git Bash wurde trotz installiertem Git nicht gefunden. Erwartet wird bin\\sh.exe oder usr\\bin\\sh.exe innerhalb der Git-for-Windows-Installation.'); } } else { const probe = run('sh', ['-c', 'printf vendoo-shell-ok']); if (probe.status !== 0 || probe.stdout !== 'vendoo-shell-ok') { fail('Eine funktionsfähige POSIX-Shell wurde nicht gefunden.'); } shell = 'sh'; } if (isWindowsHost) { for (const script of ['setup.sh', 'tools/verify-installer-shell.sh']) { const syntax = run(shell, ['-n', script], { env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0', MSYS2_ARG_CONV_EXCL: '*', }, timeout: 60_000, }); if (syntax.stdout) process.stdout.write(syntax.stdout); if (syntax.stderr) process.stderr.write(syntax.stderr); if (syntax.error) fail(`Shell-Syntaxprüfung konnte nicht gestartet werden (${script}): ${syntax.error.message}`); if (syntax.signal) fail(`Shell-Syntaxprüfung wurde durch Signal ${syntax.signal} beendet (${script}).`); if (syntax.status !== 0) { fail(`Shell-Syntaxprüfung fehlgeschlagen (${script}, Exit-Code ${syntax.status}). Verwendete Shell: ${shell}`); } } console.log('Installer-Shell-Portabilitätsgate erfolgreich: Git Bash hat setup.sh und den Linux-Funktionstest syntaktisch geprüft; die funktionale Docker-/NAS-/VPS-Simulation läuft im verpflichtenden Linux-CI-Gate.'); process.exit(0); } const result = run(shell, ['tools/verify-installer-shell.sh'], { env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0', VENDOO_INSTALLER_SHELL_HOST: 'posix', }, timeout: 180_000, }); if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stderr.write(result.stderr); if (result.error) fail(`Shell-Gate konnte nicht gestartet werden: ${result.error.message}`); if (result.signal) fail(`Shell-Gate wurde durch Signal ${result.signal} beendet.`); if (result.status !== 0) { const stdout = String(result.stdout || '').trim(); const stderr = String(result.stderr || '').trim(); const diagnostics = [ stderr ? `Shell-Fehlerausgabe:\n${stderr}` : '', stdout ? `Shell-Standardausgabe:\n${stdout}` : '', ].filter(Boolean).join('\n\n'); fail(`Shell-Gate endete mit Exit-Code ${result.status}. Verwendete Shell: ${shell}${diagnostics ? `\n${diagnostics}` : ''}`); } console.log(`Installer-Shell-Portabilitätsgate erfolgreich: ${shell} führte die vollständige Linux-/NAS-/VPS-Simulation mit strikter POSIX-Rechteprüfung aus.`);