AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: clean shutdown on Ctrl+C for hatk dev and hatk start

Replace execSync with spawn-based helper that forwards SIGINT/SIGTERM
to child processes, eliminating the need to Ctrl+C twice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+33 -24
+33 -24
packages/hatk/src/cli.ts
··· 1 1 #!/usr/bin/env node 2 2 import { mkdirSync, writeFileSync, existsSync, unlinkSync, readdirSync, readFileSync } from 'node:fs' 3 3 import { resolve, join } from 'node:path' 4 - import { execSync } from 'node:child_process' 4 + import { execSync, spawn } from 'node:child_process' 5 5 import { loadLexicons } from './schema.ts' 6 6 import { loadConfig } from './config.ts' 7 7 ··· 31 31 } 32 32 console.error('[dev] PDS failed to start') 33 33 process.exit(1) 34 + } 35 + 36 + /** Spawn a long-running process and forward SIGINT/SIGTERM for clean shutdown. */ 37 + function spawnForward(cmd: string, args: string[], env?: Record<string, string | undefined>): Promise<void> { 38 + return new Promise((resolve, reject) => { 39 + const child = spawn(cmd, args, { 40 + stdio: 'inherit', 41 + cwd: process.cwd(), 42 + env: { ...process.env, ...env }, 43 + }) 44 + const onSignal = (sig: NodeJS.Signals) => { 45 + child.kill(sig) 46 + } 47 + process.on('SIGINT', onSignal) 48 + process.on('SIGTERM', onSignal) 49 + child.on('close', (code, signal) => { 50 + process.removeListener('SIGINT', onSignal) 51 + process.removeListener('SIGTERM', onSignal) 52 + if (signal === 'SIGINT' || signal === 'SIGTERM') process.exit(0) 53 + if (code === 0 || code === null) resolve() 54 + else reject(new Error(`Process exited with code ${code}`)) 55 + }) 56 + }) 34 57 } 35 58 36 59 function runSeed() { ··· 1734 1757 await ensurePds() 1735 1758 runSeed() 1736 1759 1737 - try { 1738 - if (existsSync(resolve('svelte.config.js')) && existsSync(resolve('src/app.html'))) { 1739 - // SvelteKit project — vite dev starts the hatk server via the plugin 1740 - execSync('npx vite dev', { stdio: 'inherit', cwd: process.cwd() }) 1741 - } else { 1742 - // No frontend — just run the hatk server directly 1743 - const mainPath = resolve(import.meta.dirname!, 'main.js') 1744 - execSync(`npx tsx ${mainPath} hatk.config.ts`, { 1745 - stdio: 'inherit', 1746 - cwd: process.cwd(), 1747 - env: { ...process.env, DEV_MODE: '1' }, 1748 - }) 1749 - } 1750 - } catch (e: any) { 1751 - if (e.signal === 'SIGINT' || e.signal === 'SIGTERM') process.exit(0) 1752 - throw e 1760 + if (existsSync(resolve('svelte.config.js')) && existsSync(resolve('src/app.html'))) { 1761 + // SvelteKit project — vite dev starts the hatk server via the plugin 1762 + await spawnForward('npx', ['vite', 'dev']) 1763 + } else { 1764 + // No frontend — just run the hatk server directly 1765 + const mainPath = resolve(import.meta.dirname!, 'main.js') 1766 + await spawnForward('npx', ['tsx', mainPath, 'hatk.config.ts'], { DEV_MODE: '1' }) 1753 1767 } 1754 1768 } else if (command === 'format' || command === 'fmt') { 1755 1769 try { ··· 1962 1976 console.log() 1963 1977 } 1964 1978 } else if (command === 'start') { 1965 - try { 1966 - const mainPath = resolve(import.meta.dirname!, 'main.js') 1967 - execSync(`npx tsx ${mainPath} hatk.config.ts`, { stdio: 'inherit', cwd: process.cwd() }) 1968 - } catch (e: any) { 1969 - if (e.signal === 'SIGINT' || e.signal === 'SIGTERM') process.exit(0) 1970 - throw e 1971 - } 1979 + const mainPath = resolve(import.meta.dirname!, 'main.js') 1980 + await spawnForward('npx', ['tsx', mainPath, 'hatk.config.ts']) 1972 1981 } else { 1973 1982 usage() 1974 1983 }