2026-03-28 15:42:44 -05:00
# ! / u s r / b i n / e n v - S n o d e - - i m p o r t t s x
import { spawn } from "node:child_process" ;
import { existsSync , mkdirSync , readdirSync , rmSync , statSync , writeFileSync } from "node:fs" ;
import path from "node:path" ;
import { createInterface } from "node:readline/promises" ;
import { stdin , stdout } from "node:process" ;
2026-04-06 20:17:47 -05:00
import { createCapturedOutputBuffer , parseJsonResponseWithLimit } from "./dev-runner-output.mjs" ;
2026-03-28 15:42:44 -05:00
import { shouldTrackDevServerPath } from "./dev-runner-paths.mjs" ;
import { createDevServiceIdentity , repoRoot } from "./dev-service-profile.ts" ;
2026-04-11 08:17:16 -05:00
import { bootstrapDevRunnerWorktreeEnv } from "../server/src/dev-runner-worktree.ts" ;
2026-03-28 15:42:44 -05:00
import {
findAdoptableLocalService ,
removeLocalServiceRegistryRecord ,
touchLocalServiceRegistryRecord ,
writeLocalServiceRegistryRecord ,
} from "../server/src/services/local-service-supervisor.ts" ;
2026-04-10 07:48:42 -05:00
// Keep these values local so the dev runner can boot from the server package's
// tsx context without requiring workspace package resolution first.
const BIND_MODES = [ "loopback" , "lan" , "tailnet" , "custom" ] as const ;
type BindMode = ( typeof BIND_MODES ) [ number ] ;
2026-04-11 08:17:16 -05:00
const worktreeEnvBootstrap = bootstrapDevRunnerWorktreeEnv ( repoRoot , process . env ) ;
if ( worktreeEnvBootstrap . missingEnv ) {
console . error (
` [paperclip] linked git worktree at ${ repoRoot } is missing ${ path . relative ( repoRoot , worktreeEnvBootstrap . envPath ? ? "" ) } . Run \` paperclipai worktree init \` in this worktree before \` pnpm dev \` . ` ,
) ;
process . exit ( 1 ) ;
}
2026-03-28 15:42:44 -05:00
const mode = process . argv [ 2 ] === "watch" ? "watch" : "dev" ;
const cliArgs = process . argv . slice ( 3 ) ;
const scanIntervalMs = 1500 ;
const autoRestartPollIntervalMs = 2500 ;
const gracefulShutdownTimeoutMs = 10 _000 ;
const changedPathSampleLimit = 5 ;
const devServerStatusFilePath = path . join ( repoRoot , ".paperclip" , "dev-server-status.json" ) ;
const watchedDirectories = [
"cli" ,
"scripts" ,
"server" ,
"packages/adapter-utils" ,
"packages/adapters" ,
"packages/db" ,
"packages/plugins/sdk" ,
"packages/shared" ,
] . map ( ( relativePath ) = > path . join ( repoRoot , relativePath ) ) ;
const watchedFiles = [
".env" ,
"package.json" ,
"pnpm-workspace.yaml" ,
"tsconfig.base.json" ,
"tsconfig.json" ,
"vitest.config.ts" ,
] . map ( ( relativePath ) = > path . join ( repoRoot , relativePath ) ) ;
const ignoredDirectoryNames = new Set ( [
".git" ,
".turbo" ,
".vite" ,
"coverage" ,
"dist" ,
"node_modules" ,
"ui-dist" ,
] ) ;
const ignoredRelativePaths = new Set ( [
".paperclip/dev-server-status.json" ,
] ) ;
const tailscaleAuthFlagNames = new Set ( [
"--tailscale-auth" ,
"--authenticated-private" ,
] ) ;
let tailscaleAuth = false ;
2026-04-10 07:32:16 -05:00
let bindMode : BindMode | null = null ;
let bindHost : string | null = null ;
2026-03-28 15:42:44 -05:00
const forwardedArgs : string [ ] = [ ] ;
2026-04-10 07:32:16 -05:00
for ( let index = 0 ; index < cliArgs . length ; index += 1 ) {
const arg = cliArgs [ index ] ;
2026-03-28 15:42:44 -05:00
if ( tailscaleAuthFlagNames . has ( arg ) ) {
tailscaleAuth = true ;
continue ;
}
2026-04-10 07:32:16 -05:00
if ( arg === "--bind" ) {
const value = cliArgs [ index + 1 ] ;
if ( ! value || value . startsWith ( "--" ) || ! BIND_MODES . includes ( value as BindMode ) ) {
console . error ( ` [paperclip] invalid --bind value. Use one of: ${ BIND_MODES . join ( ", " ) } ` ) ;
process . exit ( 1 ) ;
}
bindMode = value as BindMode ;
index += 1 ;
continue ;
}
if ( arg === "--bind-host" ) {
const value = cliArgs [ index + 1 ] ;
if ( ! value || value . startsWith ( "--" ) ) {
console . error ( "[paperclip] --bind-host requires a value" ) ;
process . exit ( 1 ) ;
}
bindHost = value ;
index += 1 ;
continue ;
}
2026-03-28 15:42:44 -05:00
forwardedArgs . push ( arg ) ;
}
if ( process . env . npm_config_tailscale_auth === "true" ) {
tailscaleAuth = true ;
}
if ( process . env . npm_config_authenticated_private === "true" ) {
tailscaleAuth = true ;
}
2026-04-10 07:32:16 -05:00
if ( ! bindMode && process . env . npm_config_bind && BIND_MODES . includes ( process . env . npm_config_bind as BindMode ) ) {
bindMode = process . env . npm_config_bind as BindMode ;
}
if ( ! bindHost && process . env . npm_config_bind_host ) {
bindHost = process . env . npm_config_bind_host ;
}
if ( bindMode === "custom" && ! bindHost ) {
console . error ( "[paperclip] --bind custom requires --bind-host <host>" ) ;
process . exit ( 1 ) ;
}
2026-03-28 15:42:44 -05:00
const env : NodeJS.ProcessEnv = {
. . . process . env ,
PAPERCLIP_UI_DEV_MIDDLEWARE : "true" ,
} ;
if ( mode === "dev" ) {
env . PAPERCLIP_DEV_SERVER_STATUS_FILE = devServerStatusFilePath ;
2026-03-28 15:52:55 -05:00
env . PAPERCLIP_MIGRATION_AUTO_APPLY ? ? = "true" ;
2026-03-28 15:42:44 -05:00
}
if ( mode === "watch" ) {
env . PAPERCLIP_MIGRATION_PROMPT ? ? = "never" ;
env . PAPERCLIP_MIGRATION_AUTO_APPLY ? ? = "true" ;
}
2026-04-10 07:32:16 -05:00
if ( tailscaleAuth || bindMode ) {
const effectiveBind = bindMode ? ? "lan" ;
if ( tailscaleAuth ) {
console . log ( "[paperclip] note: --tailscale-auth/--authenticated-private are legacy aliases for --bind lan" ) ;
}
env . PAPERCLIP_BIND = effectiveBind ;
if ( bindHost ) {
env . PAPERCLIP_BIND_HOST = bindHost ;
} else {
delete env . PAPERCLIP_BIND_HOST ;
}
if ( effectiveBind === "loopback" && ! tailscaleAuth ) {
delete env . PAPERCLIP_DEPLOYMENT_MODE ;
delete env . PAPERCLIP_DEPLOYMENT_EXPOSURE ;
delete env . PAPERCLIP_AUTH_BASE_URL_MODE ;
console . log ( "[paperclip] dev mode: local_trusted (bind=loopback)" ) ;
} else {
env . PAPERCLIP_DEPLOYMENT_MODE = "authenticated" ;
env . PAPERCLIP_DEPLOYMENT_EXPOSURE = "private" ;
env . PAPERCLIP_AUTH_BASE_URL_MODE = "auto" ;
console . log (
` [paperclip] dev mode: authenticated/private (bind= ${ effectiveBind } ${ bindHost ? ` : ${ bindHost } ` : "" } ) ` ,
) ;
}
2026-03-28 15:42:44 -05:00
} else {
2026-04-10 07:32:16 -05:00
delete env . PAPERCLIP_BIND ;
delete env . PAPERCLIP_BIND_HOST ;
delete env . PAPERCLIP_DEPLOYMENT_MODE ;
delete env . PAPERCLIP_DEPLOYMENT_EXPOSURE ;
delete env . PAPERCLIP_AUTH_BASE_URL_MODE ;
2026-03-28 15:42:44 -05:00
console . log ( "[paperclip] dev mode: local_trusted (default)" ) ;
}
const serverPort = Number . parseInt ( env . PORT ? ? process . env . PORT ? ? "3100" , 10 ) || 3100 ;
const devService = createDevServiceIdentity ( {
mode ,
forwardedArgs ,
2026-04-10 07:32:16 -05:00
networkProfile : tailscaleAuth ? ` legacy: ${ bindMode ? ? "lan" } ` : ( bindMode ? ? "default" ) ,
2026-03-28 15:42:44 -05:00
port : serverPort ,
} ) ;
const existingRunner = await findAdoptableLocalService ( {
serviceKey : devService.serviceKey ,
cwd : repoRoot ,
envFingerprint : devService.envFingerprint ,
port : serverPort ,
} ) ;
if ( existingRunner ) {
console . log (
` [paperclip] ${ devService . serviceName } already running (pid ${ existingRunner . pid } ${ typeof existingRunner . metadata ? . childPid === "number" ? ` , child ${ existingRunner . metadata . childPid } ` : "" } ) ` ,
) ;
process . exit ( 0 ) ;
}
const pnpmBin = process . platform === "win32" ? "pnpm.cmd" : "pnpm" ;
let previousSnapshot = collectWatchedSnapshot ( ) ;
let dirtyPaths = new Set < string > ( ) ;
let pendingMigrations : string [ ] = [ ] ;
let lastChangedAt : string | null = null ;
let lastRestartAt : string | null = null ;
let scanInFlight = false ;
let restartInFlight = false ;
let shuttingDown = false ;
let childExitWasExpected = false ;
let child : ReturnType < typeof spawn > | null = null ;
let childExitPromise : Promise < { code : number ; signal : NodeJS.Signals | null } > | null = null ;
let scanTimer : ReturnType < typeof setInterval > | null = null ;
let autoRestartTimer : ReturnType < typeof setInterval > | null = null ;
function toError ( error : unknown , context = "Dev runner command failed" ) {
if ( error instanceof Error ) return error ;
if ( error === undefined ) return new Error ( context ) ;
if ( typeof error === "string" ) return new Error ( ` ${ context } : ${ error } ` ) ;
try {
return new Error ( ` ${ context } : ${ JSON . stringify ( error ) } ` ) ;
} catch {
return new Error ( ` ${ context } : ${ String ( error ) } ` ) ;
}
}
process . on ( "uncaughtException" , async ( error ) = > {
await removeLocalServiceRegistryRecord ( devService . serviceKey ) ;
const err = toError ( error , "Uncaught exception in dev runner" ) ;
process . stderr . write ( ` ${ err . stack ? ? err . message } \ n ` ) ;
process . exit ( 1 ) ;
} ) ;
process . on ( "unhandledRejection" , async ( reason ) = > {
await removeLocalServiceRegistryRecord ( devService . serviceKey ) ;
const err = toError ( reason , "Unhandled promise rejection in dev runner" ) ;
process . stderr . write ( ` ${ err . stack ? ? err . message } \ n ` ) ;
process . exit ( 1 ) ;
} ) ;
function formatPendingMigrationSummary ( migrations : string [ ] ) {
if ( migrations . length === 0 ) return "none" ;
return migrations . length > 3
? ` ${ migrations . slice ( 0 , 3 ) . join ( ", " ) } (+ ${ migrations . length - 3 } more) `
: migrations . join ( ", " ) ;
}
function exitForSignal ( signal : NodeJS.Signals ) {
if ( signal === "SIGINT" ) {
process . exit ( 130 ) ;
}
if ( signal === "SIGTERM" ) {
process . exit ( 143 ) ;
}
process . exit ( 1 ) ;
}
function toRelativePath ( absolutePath : string ) {
return path . relative ( repoRoot , absolutePath ) . split ( path . sep ) . join ( "/" ) ;
}
function readSignature ( absolutePath : string ) {
const stats = statSync ( absolutePath ) ;
return ` ${ Math . trunc ( stats . mtimeMs ) } : ${ stats . size } ` ;
}
function addFileToSnapshot ( snapshot : Map < string , string > , absolutePath : string ) {
const relativePath = toRelativePath ( absolutePath ) ;
if ( ignoredRelativePaths . has ( relativePath ) ) return ;
if ( ! shouldTrackDevServerPath ( relativePath ) ) return ;
snapshot . set ( relativePath , readSignature ( absolutePath ) ) ;
}
function walkDirectory ( snapshot : Map < string , string > , absoluteDirectory : string ) {
if ( ! existsSync ( absoluteDirectory ) ) return ;
for ( const entry of readdirSync ( absoluteDirectory , { withFileTypes : true } ) ) {
if ( ignoredDirectoryNames . has ( entry . name ) ) continue ;
const absolutePath = path . join ( absoluteDirectory , entry . name ) ;
if ( entry . isDirectory ( ) ) {
walkDirectory ( snapshot , absolutePath ) ;
continue ;
}
if ( entry . isFile ( ) || entry . isSymbolicLink ( ) ) {
addFileToSnapshot ( snapshot , absolutePath ) ;
}
}
}
function collectWatchedSnapshot() {
const snapshot = new Map < string , string > ( ) ;
for ( const absoluteDirectory of watchedDirectories ) {
walkDirectory ( snapshot , absoluteDirectory ) ;
}
for ( const absoluteFile of watchedFiles ) {
if ( ! existsSync ( absoluteFile ) ) continue ;
addFileToSnapshot ( snapshot , absoluteFile ) ;
}
return snapshot ;
}
function diffSnapshots ( previous : Map < string , string > , next : Map < string , string > ) {
const changed = new Set < string > ( ) ;
for ( const [ relativePath , signature ] of next ) {
if ( previous . get ( relativePath ) !== signature ) {
changed . add ( relativePath ) ;
}
}
for ( const relativePath of previous . keys ( ) ) {
if ( ! next . has ( relativePath ) ) {
changed . add ( relativePath ) ;
}
}
return [ . . . changed ] . sort ( ) ;
}
function ensureDevStatusDirectory() {
mkdirSync ( path . dirname ( devServerStatusFilePath ) , { recursive : true } ) ;
}
function writeDevServerStatus() {
if ( mode !== "dev" ) return ;
ensureDevStatusDirectory ( ) ;
const changedPaths = [ . . . dirtyPaths ] . sort ( ) ;
writeFileSync (
devServerStatusFilePath ,
` ${ JSON . stringify ( {
dirty : changedPaths.length > 0 || pendingMigrations . length > 0 ,
lastChangedAt ,
changedPathCount : changedPaths.length ,
changedPathsSample : changedPaths.slice ( 0 , changedPathSampleLimit ) ,
pendingMigrations ,
lastRestartAt ,
} , null , 2 ) } \ n ` ,
"utf8" ,
) ;
}
function clearDevServerStatus() {
if ( mode !== "dev" ) return ;
rmSync ( devServerStatusFilePath , { force : true } ) ;
}
async function updateDevServiceRecord ( extra? : Record < string , unknown > ) {
await writeLocalServiceRegistryRecord ( {
version : 1 ,
serviceKey : devService.serviceKey ,
profileKind : "paperclip-dev" ,
serviceName : devService.serviceName ,
command : "dev-runner.ts" ,
cwd : repoRoot ,
envFingerprint : devService.envFingerprint ,
port : serverPort ,
url : ` http://127.0.0.1: ${ serverPort } ` ,
pid : process.pid ,
processGroupId : null ,
provider : "local_process" ,
runtimeServiceId : null ,
reuseKey : null ,
startedAt : lastRestartAt ? ? new Date ( ) . toISOString ( ) ,
lastSeenAt : new Date ( ) . toISOString ( ) ,
metadata : {
repoRoot ,
mode ,
childPid : child?.pid ? ? null ,
url : ` http://127.0.0.1: ${ serverPort } ` ,
. . . extra ,
} ,
} ) ;
}
async function runPnpm ( args : string [ ] , options : {
stdio ? : "inherit" | [ "ignore" , "pipe" , "pipe" ] ;
env? : NodeJS.ProcessEnv ;
2026-03-28 15:52:55 -05:00
cwd? : string ;
2026-03-28 15:42:44 -05:00
} = { } ) {
return await new Promise < { code : number ; signal : NodeJS.Signals | null ; stdout : string ; stderr : string } > ( ( resolve , reject ) = > {
const spawned = spawn ( pnpmBin , args , {
stdio : options.stdio ? ? [ "ignore" , "pipe" , "pipe" ] ,
env : options.env ? ? process . env ,
2026-03-28 15:52:55 -05:00
cwd : options.cwd ,
2026-03-28 15:42:44 -05:00
shell : process.platform === "win32" ,
} ) ;
2026-04-06 19:24:51 -05:00
const stdoutBuffer = createCapturedOutputBuffer ( ) ;
const stderrBuffer = createCapturedOutputBuffer ( ) ;
2026-03-28 15:42:44 -05:00
if ( spawned . stdout ) {
spawned . stdout . on ( "data" , ( chunk ) = > {
2026-04-06 19:24:51 -05:00
stdoutBuffer . append ( chunk ) ;
2026-03-28 15:42:44 -05:00
} ) ;
}
if ( spawned . stderr ) {
spawned . stderr . on ( "data" , ( chunk ) = > {
2026-04-06 19:24:51 -05:00
stderrBuffer . append ( chunk ) ;
2026-03-28 15:42:44 -05:00
} ) ;
}
spawned . on ( "error" , reject ) ;
spawned . on ( "exit" , ( code , signal ) = > {
2026-04-06 19:24:51 -05:00
const stdout = stdoutBuffer . finish ( ) ;
const stderr = stderrBuffer . finish ( ) ;
2026-03-28 15:42:44 -05:00
resolve ( {
code : code ? ? 0 ,
signal ,
2026-04-06 19:24:51 -05:00
stdout : stdout.text ,
stderr : stderr.text ,
2026-03-28 15:42:44 -05:00
} ) ;
} ) ;
} ) ;
}
async function getMigrationStatusPayload() {
const status = await runPnpm (
[ "--filter" , "@paperclipai/db" , "exec" , "tsx" , "src/migration-status.ts" , "--json" ] ,
{ env } ,
) ;
if ( status . code !== 0 ) {
process . stderr . write (
status . stderr ||
status . stdout ||
` [paperclip] Command failed with code ${ status . code } : pnpm --filter @paperclipai/db exec tsx src/migration-status.ts --json \ n ` ,
) ;
process . exit ( status . code ) ;
}
try {
return JSON . parse ( status . stdout . trim ( ) ) as { status? : string ; pendingMigrations? : string [ ] } ;
} catch ( error ) {
process . stderr . write (
status . stderr ||
status . stdout ||
"[paperclip] migration-status returned invalid JSON payload\n" ,
) ;
throw toError ( error , "Unable to parse migration-status JSON output" ) ;
}
}
async function refreshPendingMigrations() {
const payload = await getMigrationStatusPayload ( ) ;
pendingMigrations =
payload . status === "needsMigrations" && Array . isArray ( payload . pendingMigrations )
? payload . pendingMigrations . filter ( ( entry ) = > typeof entry === "string" && entry . trim ( ) . length > 0 )
: [ ] ;
writeDevServerStatus ( ) ;
return payload ;
}
async function maybePreflightMigrations ( options : { interactive? : boolean ; autoApply? : boolean ; exitOnDecline? : boolean } = { } ) {
const interactive = options . interactive ? ? mode === "watch" ;
const autoApply = options . autoApply ? ? env . PAPERCLIP_MIGRATION_AUTO_APPLY === "true" ;
const exitOnDecline = options . exitOnDecline ? ? mode === "watch" ;
const payload = await refreshPendingMigrations ( ) ;
if ( payload . status !== "needsMigrations" || pendingMigrations . length === 0 ) {
return ;
}
let shouldApply = autoApply ;
if ( ! autoApply && interactive ) {
if ( ! stdin . isTTY || ! stdout . isTTY ) {
shouldApply = true ;
} else {
const prompt = createInterface ( { input : stdin , output : stdout } ) ;
try {
const answer = (
await prompt . question (
` Apply pending migrations ( ${ formatPendingMigrationSummary ( pendingMigrations ) } ) now? (y/N): ` ,
)
)
. trim ( )
. toLowerCase ( ) ;
shouldApply = answer === "y" || answer === "yes" ;
} finally {
prompt . close ( ) ;
}
}
}
if ( ! shouldApply ) {
if ( exitOnDecline ) {
process . stderr . write (
` [paperclip] Pending migrations detected ( ${ formatPendingMigrationSummary ( pendingMigrations ) } ). Refusing to start watch mode against a stale schema. \ n ` ,
) ;
process . exit ( 1 ) ;
}
return ;
}
2026-03-28 15:52:55 -05:00
const exit = await runPnpm ( [ "db:migrate" ] , {
2026-03-28 15:42:44 -05:00
stdio : "inherit" ,
env ,
2026-03-28 15:52:55 -05:00
cwd : repoRoot ,
2026-03-28 15:42:44 -05:00
} ) ;
if ( exit . signal ) {
exitForSignal ( exit . signal ) ;
return ;
}
if ( exit . code !== 0 ) {
process . exit ( exit . code ) ;
}
await refreshPendingMigrations ( ) ;
}
async function buildPluginSdk() {
console . log ( "[paperclip] building plugin sdk..." ) ;
const result = await runPnpm (
[ "--filter" , "@paperclipai/plugin-sdk" , "build" ] ,
{ stdio : "inherit" } ,
) ;
if ( result . signal ) {
exitForSignal ( result . signal ) ;
return ;
}
if ( result . code !== 0 ) {
console . error ( "[paperclip] plugin sdk build failed" ) ;
process . exit ( result . code ) ;
}
}
async function markChildAsCurrent() {
previousSnapshot = collectWatchedSnapshot ( ) ;
dirtyPaths = new Set ( ) ;
lastChangedAt = null ;
lastRestartAt = new Date ( ) . toISOString ( ) ;
await refreshPendingMigrations ( ) ;
await updateDevServiceRecord ( ) ;
}
async function scanForBackendChanges() {
if ( mode !== "dev" || scanInFlight || restartInFlight ) return ;
scanInFlight = true ;
try {
const nextSnapshot = collectWatchedSnapshot ( ) ;
const changed = diffSnapshots ( previousSnapshot , nextSnapshot ) ;
previousSnapshot = nextSnapshot ;
if ( changed . length === 0 ) return ;
for ( const relativePath of changed ) {
dirtyPaths . add ( relativePath ) ;
}
lastChangedAt = new Date ( ) . toISOString ( ) ;
await refreshPendingMigrations ( ) ;
} finally {
scanInFlight = false ;
}
}
async function getDevHealthPayload() {
const response = await fetch ( ` http://127.0.0.1: ${ serverPort } /api/health ` ) ;
if ( ! response . ok ) {
throw new Error ( ` Health request failed ( ${ response . status } ) ` ) ;
}
2026-04-06 20:17:47 -05:00
return await parseJsonResponseWithLimit < { devServer ? : { enabled? : boolean ; autoRestartEnabled? : boolean ; activeRunCount? : number } } > ( response ) ;
2026-03-28 15:42:44 -05:00
}
async function waitForChildExit() {
if ( ! childExitPromise ) {
return { code : 0 , signal : null } ;
}
return await childExitPromise ;
}
async function stopChildForRestart() {
if ( ! child ) return { code : 0 , signal : null } ;
childExitWasExpected = true ;
child . kill ( "SIGTERM" ) ;
const killTimer = setTimeout ( ( ) = > {
if ( child ) {
child . kill ( "SIGKILL" ) ;
}
} , gracefulShutdownTimeoutMs ) ;
try {
return await waitForChildExit ( ) ;
} finally {
clearTimeout ( killTimer ) ;
}
}
async function startServerChild() {
await buildPluginSdk ( ) ;
const serverScript = mode === "watch" ? "dev:watch" : "dev" ;
child = spawn (
pnpmBin ,
[ "--filter" , "@paperclipai/server" , serverScript , . . . forwardedArgs ] ,
{ stdio : "inherit" , env , shell : process.platform === "win32" } ,
) ;
childExitPromise = new Promise ( ( resolve , reject ) = > {
child ? . on ( "error" , reject ) ;
child ? . on ( "exit" , ( code , signal ) = > {
const expected = childExitWasExpected ;
childExitWasExpected = false ;
child = null ;
childExitPromise = null ;
void touchLocalServiceRegistryRecord ( devService . serviceKey , {
metadata : {
repoRoot ,
mode ,
childPid : null ,
url : ` http://127.0.0.1: ${ serverPort } ` ,
} ,
} ) ;
resolve ( { code : code ? ? 0 , signal } ) ;
if ( restartInFlight || expected || shuttingDown ) {
return ;
}
if ( signal ) {
exitForSignal ( signal ) ;
return ;
}
process . exit ( code ? ? 0 ) ;
} ) ;
} ) ;
await markChildAsCurrent ( ) ;
}
async function maybeAutoRestartChild() {
if ( mode !== "dev" || restartInFlight || ! child ) return ;
if ( dirtyPaths . size === 0 && pendingMigrations . length === 0 ) return ;
restartInFlight = true ;
let health : { devServer ? : { enabled? : boolean ; autoRestartEnabled? : boolean ; activeRunCount? : number } } | null = null ;
try {
health = await getDevHealthPayload ( ) ;
} catch {
restartInFlight = false ;
return ;
}
const devServer = health ? . devServer ;
if ( ! devServer ? . enabled || devServer . autoRestartEnabled !== true ) {
restartInFlight = false ;
return ;
}
if ( ( devServer . activeRunCount ? ? 0 ) > 0 ) {
restartInFlight = false ;
return ;
}
try {
await maybePreflightMigrations ( {
autoApply : true ,
interactive : false ,
exitOnDecline : false ,
} ) ;
await stopChildForRestart ( ) ;
await startServerChild ( ) ;
} catch ( error ) {
const err = toError ( error , "Auto-restart failed" ) ;
process . stderr . write ( ` ${ err . stack ? ? err . message } \ n ` ) ;
process . exit ( 1 ) ;
} finally {
restartInFlight = false ;
}
}
function installDevIntervals() {
if ( mode !== "dev" ) return ;
scanTimer = setInterval ( ( ) = > {
void scanForBackendChanges ( ) ;
} , scanIntervalMs ) ;
autoRestartTimer = setInterval ( ( ) = > {
void maybeAutoRestartChild ( ) ;
} , autoRestartPollIntervalMs ) ;
}
function clearDevIntervals() {
if ( scanTimer ) {
clearInterval ( scanTimer ) ;
scanTimer = null ;
}
if ( autoRestartTimer ) {
clearInterval ( autoRestartTimer ) ;
autoRestartTimer = null ;
}
}
async function shutdown ( signal : NodeJS.Signals ) {
if ( shuttingDown ) return ;
shuttingDown = true ;
clearDevIntervals ( ) ;
clearDevServerStatus ( ) ;
await removeLocalServiceRegistryRecord ( devService . serviceKey ) ;
if ( ! child ) {
exitForSignal ( signal ) ;
return ;
}
childExitWasExpected = true ;
child . kill ( signal ) ;
const exit = await waitForChildExit ( ) ;
if ( exit . signal ) {
exitForSignal ( exit . signal ) ;
return ;
}
process . exit ( exit . code ? ? 0 ) ;
}
process . on ( "SIGINT" , ( ) = > {
void shutdown ( "SIGINT" ) ;
} ) ;
process . on ( "SIGTERM" , ( ) = > {
void shutdown ( "SIGTERM" ) ;
} ) ;
await maybePreflightMigrations ( ) ;
await startServerChild ( ) ;
installDevIntervals ( ) ;
if ( mode === "watch" ) {
const exit = await waitForChildExit ( ) ;
await removeLocalServiceRegistryRecord ( devService . serviceKey ) ;
if ( exit . signal ) {
exitForSignal ( exit . signal ) ;
}
process . exit ( exit . code ? ? 0 ) ;
}