import { spawnSync } from 'node:child_process' import { promises as fs } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const uiRoot = path.resolve(__dirname, '..') const repoRoot = path.resolve(uiRoot, '..') const lockPath = path.join(uiRoot, 'node_modules', '.cache', 'familiaros-website-dev-server.lock.json') const dryRun = process.argv.includes('--dry-run') const actionVerb = dryRun ? 'matched' : 'stopped' function processIsAlive(pid) { if (!Number.isInteger(pid) || pid <= 0) return false try { process.kill(pid, 0) return true } catch { return false } } function normalizeForSearch(value) { return String(value ?? '').replaceAll('\\', '/').toLowerCase() } async function readLock() { try { const raw = await fs.readFile(lockPath, 'utf8') return JSON.parse(raw) } catch { return null } } async function clearLock() { try { await fs.unlink(lockPath) } catch { // Ignore missing lock file. } } function killPid(pid) { if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false if (dryRun) { console.log(`[dev:clean] dry-run: would stop PID ${pid}`) return true } if (process.platform === 'win32') { const result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', }) return result.status === 0 } try { process.kill(pid, 'SIGTERM') return true } catch { return false } } function parsePowerShellJson(stdout) { const text = String(stdout ?? '').trim() if (!text) return [] try { const parsed = JSON.parse(text) return Array.isArray(parsed) ? parsed : [parsed] } catch { return [] } } function queryProcessMetadata(pid) { if (!Number.isInteger(pid) || pid <= 0) return null if (process.platform === 'win32') { const script = [ "$ErrorActionPreference = 'SilentlyContinue'", `$proc = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object ProcessId, Name, CommandLine, ExecutablePath`, '$proc | ConvertTo-Json -Compress', ].join('; ') const result = spawnSync( 'powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], { encoding: 'utf8' } ) return parsePowerShellJson(result.stdout)[0] ?? null } const result = spawnSync('ps', ['-p', String(pid), '-o', 'comm=', '-o', 'args='], { encoding: 'utf8', }) const output = String(result.stdout ?? '').trim() if (!output) return null const [firstLine] = output.split(/\r?\n/) const trimmed = firstLine.trim() if (!trimmed) return null const [name = '', ...rest] = trimmed.split(/\s+/) return { ProcessId: pid, Name: path.basename(name), CommandLine: rest.join(' '), ExecutablePath: name, } } function isExpectedDevProcessName(name) { const normalized = String(name ?? '').trim().toLowerCase() return [ 'node', 'node.exe', 'cmd.exe', 'npm', 'npm.cmd', 'vite', 'vite.cmd', 'esbuild', 'esbuild.exe', 'sh', 'bash', 'powershell', 'powershell.exe', ].includes(normalized) } function isOwnedDevProcess(pid) { if (!processIsAlive(pid)) return false const metadata = queryProcessMetadata(pid) if (!metadata) return false const name = String(metadata.Name ?? '') if (!isExpectedDevProcessName(name)) return false const haystack = normalizeForSearch([ metadata.CommandLine, metadata.ExecutablePath, name, ].filter(Boolean).join(' ')) const uiRootToken = normalizeForSearch(uiRoot) const repoRootToken = normalizeForSearch(repoRoot) const pathMatches = haystack.includes(uiRootToken) || haystack.includes(repoRootToken) const commandMatches = haystack.includes('vite') || haystack.includes('dev:raw') || haystack.includes('dev-singleton.mjs') || haystack.includes('familiaros-website') return pathMatches || commandMatches } function queryWindowsDevProcesses() { const escapedUiRoot = uiRoot.replace(/'/g, "''") const escapedRepoRoot = repoRoot.replace(/'/g, "''") const script = [ "$ErrorActionPreference = 'SilentlyContinue'", `$targets = @('${escapedUiRoot}', '${escapedRepoRoot}') | ForEach-Object { $_.ToLowerInvariant() }`, '$procs = Get-CimInstance Win32_Process | Where-Object {', ' if (-not $_.CommandLine) { return $false }', " if ($_.Name -ne 'node.exe' -and $_.Name -ne 'esbuild.exe') { return $false }", ' $cmd = $_.CommandLine.ToLowerInvariant()', ' return ($targets | Where-Object { $_.Length -gt 0 -and $cmd.Contains($_) }).Count -gt 0', '} | Select-Object ProcessId, Name, CommandLine', '$procs | ConvertTo-Json -Compress', ].join('; ') const result = spawnSync( 'powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], { encoding: 'utf8' } ) return parsePowerShellJson(result.stdout) } async function main() { const lock = await readLock() const lockPidCandidates = [lock?.childPid, lock?.wrapperPid].filter((pid) => Number.isInteger(pid) ) const reported = new Set() let stoppedCount = 0 for (const pid of lockPidCandidates) { if (!isOwnedDevProcess(pid) || reported.has(pid)) continue reported.add(pid) const stopped = killPid(pid) if (stopped) { console.log(`[dev:clean] ${actionVerb} lock PID ${pid}`) stoppedCount += 1 } } if (process.platform === 'win32') { const processes = queryWindowsDevProcesses() for (const proc of processes) { const pid = Number(proc?.ProcessId) if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid || reported.has(pid)) continue reported.add(pid) const name = String(proc?.Name ?? 'process') const stopped = killPid(pid) if (stopped) { console.log(`[dev:clean] ${actionVerb} ${name} PID ${pid}`) stoppedCount += 1 } } } else { const escapedUi = uiRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const escapedRepo = repoRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const pattern = `(${escapedUi}|${escapedRepo}).*(vite|esbuild|node)` const pkill = spawnSync('pkill', ['-f', pattern], { stdio: 'ignore', }) if (pkill.status === 0) { stoppedCount += 1 if (dryRun) { console.log('[dev:clean] dry-run: would stop matching vite/esbuild processes') } else { console.log('[dev:clean] stopped matching vite/esbuild processes') } } } await clearLock() if (stoppedCount === 0) { console.log('[dev:clean] no matching stale dev processes found.') } else { console.log(`[dev:clean] completed. Processes handled: ${stoppedCount}`) } } main().catch(async (error) => { console.error(`[dev:clean] ${error.message}`) await clearLock() process.exit(1) })