pid1 0.0.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +119 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-Bdt88RU-.d.mts +217 -0
- package/dist/client-Bdt88RU-.d.mts.map +1 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.mjs +12 -0
- package/dist/client.mjs.map +1 -0
- package/dist/config-BgRb4pSG.mjs +186 -0
- package/dist/config-BgRb4pSG.mjs.map +1 -0
- package/dist/config.d.mts +84 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +3 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +5 -0
- package/dist/process-manager-gO56266Q.mjs +643 -0
- package/dist/process-manager-gO56266Q.mjs.map +1 -0
- package/package.json +52 -0
- package/src/api/client.ts +20 -0
- package/src/api/router.ts +150 -0
- package/src/api/server.ts +66 -0
- package/src/cli.ts +168 -0
- package/src/config.ts +89 -0
- package/src/env.ts +74 -0
- package/src/exec.ts +85 -0
- package/src/index.ts +14 -0
- package/src/logger.ts +155 -0
- package/src/process-manager.ts +632 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-manager-gO56266Q.mjs","names":["logger","logger","honoLogger"],"sources":["../src/env.ts","../src/exec.ts","../src/api/router.ts","../src/api/server.ts","../src/process-manager.ts"],"sourcesContent":["import { readFileSync, existsSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { parse } from \"dotenv\";\nimport { globalLogger } from \"./logger\";\n\nexport type EnvFileConfig = {\n envFile?: string | false;\n};\n\nconst logger = globalLogger.child(\"env\");\n\nfunction loadEnvFile(path: string): Record<string, string> {\n if (!existsSync(path)) {\n return {};\n }\n\n try {\n const content = readFileSync(path, \"utf-8\");\n const parsed = parse(content);\n logger.debug(`Loaded env file: ${path}`);\n return parsed;\n } catch (error) {\n logger.warn(`Failed to parse env file ${path}: ${error}`);\n return {};\n }\n}\n\nexport type LoadEnvOptions = {\n configDir: string;\n processName: string;\n globalEnvFile?: string | false;\n processEnvFile?: string | false;\n configEnv?: Record<string, string>;\n};\n\nexport function loadEnvForProcess(\n options: LoadEnvOptions,\n): Record<string, string> {\n const {\n configDir,\n processName,\n globalEnvFile,\n processEnvFile,\n configEnv = {},\n } = options;\n\n const baseEnv = Object.fromEntries(\n Object.entries(process.env).filter(([, value]) => value !== undefined),\n ) as Record<string, string>;\n\n let globalEnv: Record<string, string> = {};\n if (globalEnvFile !== false) {\n const globalPath = globalEnvFile ?? join(configDir, \".env\");\n globalEnv = loadEnvFile(globalPath);\n }\n\n let processEnv: Record<string, string> = {};\n if (processEnvFile !== false) {\n const processPath =\n processEnvFile ?? join(configDir, `.env.${processName}`);\n processEnv = loadEnvFile(processPath);\n }\n\n return {\n ...baseEnv,\n ...globalEnv,\n ...configEnv,\n ...processEnv,\n };\n}\n\nexport function getConfigDir(configPath: string): string {\n return dirname(configPath);\n}\n","import { processLogger } from \"./logger\";\nimport { x } from \"tinyexec\";\n\ntype ExecType = \"task\" | \"process\";\n\ntype ExecWithLoggingParams = {\n name: string;\n command: string;\n args: string[];\n env: Record<string, string>;\n cwd: string;\n logFile: string;\n abortSignal?: AbortSignal;\n type: ExecType;\n};\n\ntype TinyExecProcess = ReturnType<typeof x>;\n\nexport type SpawnedProcess = {\n proc: TinyExecProcess;\n done: Promise<number | null>;\n kill: (signal?: NodeJS.Signals) => boolean;\n};\n\nexport function spawnWithLogging({\n name,\n command,\n args,\n env,\n cwd,\n logFile,\n abortSignal,\n type,\n}: ExecWithLoggingParams): SpawnedProcess {\n const proc = x(command, args, {\n nodeOptions: { cwd, env },\n signal: abortSignal,\n throwOnError: true,\n });\n\n const kill = (signal: NodeJS.Signals = \"SIGTERM\"): boolean => {\n if (proc.pid) {\n try {\n process.kill(proc.pid, signal);\n return true;\n } catch {\n return false;\n }\n }\n return false;\n };\n\n const done = (async () => {\n const logger = processLogger({ name: `${type}:${name}`, logFile });\n\n logger.info(\n `Starting: command=${command}, args=[${args.join(\", \")}], cwd=${cwd}`,\n );\n\n for await (const line of proc) {\n logger.info(line);\n }\n\n await proc;\n\n const exitCode = proc.exitCode;\n\n if (\n proc.aborted ||\n proc.killed ||\n exitCode === null ||\n exitCode === undefined\n ) {\n logger.info(`${name} was terminated`);\n } else if (exitCode === 0) {\n logger.info(`${name} completed successfully`);\n } else {\n logger.info(`${name} exited with code ${exitCode}`);\n }\n\n return proc.exitCode ?? null;\n })();\n\n return { proc, done, kill };\n}\n","import { os, ORPCError } from \"@orpc/server\";\nimport * as v from \"valibot\";\nimport type { ProcessManager, ProcessInfo } from \"../process-manager.ts\";\n\nconst ProcessInfoSchema = v.object({\n id: v.number(),\n name: v.string(),\n status: v.picklist([\"stopped\", \"running\", \"errored\"]),\n pid: v.optional(v.number()),\n exitCode: v.nullable(v.number()),\n restarts: v.number(),\n startedAt: v.nullable(v.date()),\n command: v.string(),\n args: v.array(v.string()),\n});\n\nconst ProcessNameSchema = v.object({\n name: v.string(),\n});\n\ntype RouterContext = {\n pm: ProcessManager;\n};\n\nconst base = os.$context<RouterContext>();\n\nexport const list = base\n .output(v.array(ProcessInfoSchema))\n .handler(async ({ context }) => {\n return context.pm.list();\n });\n\nexport const get = base\n .input(ProcessNameSchema)\n .output(v.nullable(ProcessInfoSchema))\n .handler(async ({ input, context }) => {\n return context.pm.get(input.name) ?? null;\n });\n\nexport const start = base\n .input(\n v.object({\n name: v.optional(v.string()),\n }),\n )\n .output(v.object({ success: v.boolean(), message: v.string() }))\n .handler(async ({ input, context }) => {\n try {\n if (input.name) {\n context.pm.start(input.name);\n return { success: true, message: `Started process: ${input.name}` };\n } else {\n context.pm.startAll();\n return { success: true, message: \"Started all processes\" };\n }\n } catch (error) {\n throw new ORPCError(\"BAD_REQUEST\", {\n message: error instanceof Error ? error.message : \"Failed to start\",\n });\n }\n });\n\nexport const stop = base\n .input(\n v.object({\n name: v.optional(v.string()),\n }),\n )\n .output(v.object({ success: v.boolean(), message: v.string() }))\n .handler(async ({ input, context }) => {\n try {\n if (input.name) {\n await context.pm.stop(input.name);\n return { success: true, message: `Stopped process: ${input.name}` };\n } else {\n await context.pm.stopAll();\n return { success: true, message: \"Stopped all processes\" };\n }\n } catch (error) {\n throw new ORPCError(\"BAD_REQUEST\", {\n message: error instanceof Error ? error.message : \"Failed to stop\",\n });\n }\n });\n\nexport const restart = base\n .input(\n v.object({\n name: v.optional(v.string()),\n }),\n )\n .output(v.object({ success: v.boolean(), message: v.string() }))\n .handler(async ({ input, context }) => {\n try {\n if (input.name) {\n await context.pm.restart(input.name);\n return { success: true, message: `Restarted process: ${input.name}` };\n } else {\n await context.pm.restartAll();\n return { success: true, message: \"Restarted all processes\" };\n }\n } catch (error) {\n throw new ORPCError(\"BAD_REQUEST\", {\n message: error instanceof Error ? error.message : \"Failed to restart\",\n });\n }\n });\n\nexport const deleteProcess = base\n .input(\n v.object({\n name: v.optional(v.string()),\n }),\n )\n .output(v.object({ success: v.boolean(), message: v.string() }))\n .handler(async ({ input, context }) => {\n try {\n if (input.name) {\n await context.pm.delete(input.name);\n return { success: true, message: `Deleted process: ${input.name}` };\n } else {\n await context.pm.deleteAll();\n return { success: true, message: \"Deleted all processes\" };\n }\n } catch (error) {\n throw new ORPCError(\"BAD_REQUEST\", {\n message: error instanceof Error ? error.message : \"Failed to delete\",\n });\n }\n });\n\nexport const health = os\n .output(v.object({ status: v.literal(\"ok\") }))\n .handler(async () => {\n return { status: \"ok\" as const };\n });\n\nexport const router = {\n health,\n process: {\n list,\n get,\n start,\n stop,\n restart,\n delete: deleteProcess,\n },\n};\n\nexport type Router = typeof router;\n","import { Hono } from \"hono\";\nimport { serve } from \"@hono/node-server\";\nimport { RPCHandler } from \"@orpc/server/fetch\";\nimport { onError } from \"@orpc/server\";\nimport { router } from \"./router.ts\";\nimport type { ProcessManager } from \"../process-manager.ts\";\nimport { globalLogger } from \"../logger.ts\";\nimport { logger as honoLogger } from \"hono/logger\";\n\nexport type ServerOptions = {\n port?: number;\n host?: string;\n};\n\nexport function createServer(pm: ProcessManager, options: ServerOptions = {}) {\n const { port = 3000, host = \"127.0.0.1\" } = options;\n const logger = globalLogger.child(\"server\");\n\n const app = new Hono();\n\n const handler = new RPCHandler(router, {\n interceptors: [\n onError((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n logger.error(`RPC Error: ${message}`);\n }),\n ],\n });\n\n app.use(\n honoLogger((msg, ...args) => logger.debug(`${msg} ${args.join(\" \")}`)),\n );\n\n app.get(\"/health\", (c) => c.json({ status: \"ok\" }));\n\n app.use(\"/rpc/*\", async (c, next) => {\n const { matched, response } = await handler.handle(c.req.raw, {\n prefix: \"/rpc\",\n context: { pm },\n });\n\n if (matched) {\n return c.newResponse(response.body, response);\n }\n\n await next();\n });\n\n app.notFound((c) => c.json({ error: \"Not found\" }, 404));\n\n return {\n app,\n start: () => {\n const server = serve({\n fetch: app.fetch,\n port,\n hostname: host,\n });\n\n logger.info(`HTTP server listening on http://${host}:${port}`);\n logger.info(`RPC endpoint: http://${host}:${port}/rpc`);\n\n return server;\n },\n };\n}\n\nexport type { Router } from \"./router.ts\";\n","import { dirname, join } from \"node:path\";\nimport { watch, type FSWatcher } from \"node:fs\";\nimport { loadConfigFile } from \"./config.ts\";\nimport { loadEnvForProcess } from \"./env.ts\";\nimport { spawnWithLogging, type SpawnedProcess } from \"./exec.ts\";\nimport { globalLogger } from \"./logger.ts\";\nimport { createServer, type ServerOptions } from \"./api/server.ts\";\n\nexport type ProcessManagerOptions = {\n cwd?: string;\n configPath?: string;\n server?: ServerOptions & { enabled?: boolean };\n};\n\nexport type ProcessStatus = \"stopped\" | \"running\" | \"errored\";\nexport type RestartPolicy = \"never\" | \"always\" | \"on-failure\";\n\nexport type ProcessConfig = {\n name: string;\n command: string;\n args: string[];\n env: Record<string, string>;\n cwd?: string;\n envFile?: string | false;\n restartOnEnvChange?: boolean;\n restartPolicy?: RestartPolicy;\n maxRestarts?: number;\n restartMinDelayMs?: number;\n restartMaxDelayMs?: number;\n restartResetSeconds?: number;\n};\n\nexport type ManagedProcess = {\n id: number;\n config: ProcessConfig;\n status: ProcessStatus;\n spawned: SpawnedProcess | null;\n exitCode: number | null;\n restarts: number;\n restartAttempts: number;\n restartTimer: NodeJS.Timeout | null;\n stopRequested: boolean;\n startedAt: Date | null;\n};\n\nconst DEFAULT_STOP_TIMEOUT_MS = 10000;\nconst DEFAULT_RESTART_RESET_SECONDS = 300;\nconst DEFAULT_RESTART_MAX = 10;\nconst DEFAULT_RESTART_MIN_DELAY_MS = 5000;\nconst DEFAULT_RESTART_MAX_DELAY_MS = 60000;\n\nexport type ProcessInfo = {\n id: number;\n name: string;\n status: ProcessStatus;\n pid: number | undefined;\n exitCode: number | null;\n restarts: number;\n startedAt: Date | null;\n command: string;\n args: string[];\n};\n\nexport class ProcessManager {\n private logger = globalLogger.child(\"pm\");\n private cwd: string;\n private configPath?: string;\n private configDir?: string;\n private globalEnvFile?: string | false;\n private processes: Map<string, ManagedProcess>;\n private processIds: Map<number, string>;\n private nextProcessId = 0;\n private signalHandlersSetup = false;\n private serverOptions?: ServerOptions & { enabled?: boolean };\n private httpServer?: ReturnType<ReturnType<typeof createServer>[\"start\"]>;\n private envWatcher: FSWatcher | null = null;\n private envChangeTimers = new Map<string, NodeJS.Timeout>();\n private envRestartTimers = new Map<string, NodeJS.Timeout>();\n private restartOnEnvChangeGlobal = true;\n private restartMinDelayMsGlobal = DEFAULT_RESTART_MIN_DELAY_MS;\n private restartMaxDelayMsGlobal = DEFAULT_RESTART_MAX_DELAY_MS;\n private currentTask: SpawnedProcess | null = null;\n private currentTaskName: string | null = null;\n private taskAbortController: AbortController | null = null;\n\n constructor(options: ProcessManagerOptions = {}) {\n this.cwd = options.cwd ?? process.cwd();\n this.configPath = options.configPath;\n this.processes = new Map();\n this.processIds = new Map();\n this.serverOptions = options.server;\n }\n\n async init(): Promise<void> {\n const config = await loadConfigFile(this.configPath, this.cwd);\n\n this.configDir = dirname(config.configPath);\n this.globalEnvFile = config.envFile;\n this.restartOnEnvChangeGlobal = config.restartOnEnvChange ?? true;\n this.restartMinDelayMsGlobal =\n config.restartMinDelayMs ?? DEFAULT_RESTART_MIN_DELAY_MS;\n this.restartMaxDelayMsGlobal =\n config.restartMaxDelayMs ?? DEFAULT_RESTART_MAX_DELAY_MS;\n\n this.logger.info(\n `Executing ${config.tasks.length} tasks and registering ${config.processes.length} processes`,\n );\n\n this.setupSignalHandlers();\n this.setupEnvWatchers();\n await this.runTasks(config.tasks);\n\n for (const p of config.processes) {\n this.register(p);\n }\n }\n\n register(config: ProcessConfig): void {\n if (this.processes.has(config.name)) {\n throw new Error(`Process ${config.name} already registered`);\n }\n\n const id = this.nextProcessId++;\n const managed: ManagedProcess = {\n id,\n config,\n status: \"stopped\",\n spawned: null,\n exitCode: null,\n restarts: 0,\n restartAttempts: 0,\n restartTimer: null,\n stopRequested: false,\n startedAt: null,\n };\n\n this.processes.set(config.name, managed);\n this.processIds.set(id, config.name);\n this.logger.info(`Registered process: ${config.name} (id: ${id})`);\n }\n\n list(): ProcessInfo[] {\n return [...this.processes.values()].map((m) => ({\n id: m.id,\n name: m.config.name,\n status: m.status,\n pid: m.spawned?.proc.pid,\n exitCode: m.exitCode,\n restarts: m.restarts,\n startedAt: m.startedAt,\n command: m.config.command,\n args: m.config.args,\n }));\n }\n\n start(name: string): void {\n const { name: resolvedName, managed } = this.resolveProcessIdentifier(name);\n\n if (managed.status === \"running\") {\n this.logger.warn(`Process ${resolvedName} is already running`);\n return;\n }\n\n this.spawnProcess(managed);\n }\n\n startAll(): void {\n this.setupSignalHandlers();\n\n for (const [_, managed] of this.processes) {\n if (managed.status !== \"running\") {\n this.spawnProcess(managed);\n }\n }\n }\n\n serve(options?: ServerOptions & { enabled?: boolean }): void {\n const serverOpts = { ...this.serverOptions, ...options };\n if (serverOpts.enabled === false) return;\n const { enabled: _enabled, ...serverOptions } = serverOpts;\n const server = createServer(this, serverOptions);\n this.httpServer = server.start();\n }\n\n async stop(name: string, timeoutMs = DEFAULT_STOP_TIMEOUT_MS): Promise<void> {\n const { name: resolvedName, managed } = this.resolveProcessIdentifier(name);\n\n managed.stopRequested = true;\n this.clearRestartTimer(managed);\n\n if (managed.status !== \"running\" || !managed.spawned) {\n this.logger.warn(`Process ${resolvedName} is not running`);\n return;\n }\n\n const { spawned } = managed;\n\n this.logger.info(`Stopping process: ${resolvedName} (SIGTERM)`);\n spawned.kill(\"SIGTERM\");\n\n const exitedGracefully = await Promise.race([\n spawned.done.then(() => true).catch(() => true),\n new Promise<false>((resolve) =>\n setTimeout(() => resolve(false), timeoutMs),\n ),\n ]);\n\n if (!exitedGracefully && managed.status === \"running\") {\n this.logger.warn(\n `Process ${resolvedName} did not stop gracefully, sending SIGKILL`,\n );\n spawned.kill(\"SIGKILL\");\n await spawned.done.catch(() => {});\n }\n }\n\n async stopAll(): Promise<void> {\n this.logger.info(\"Stopping all processes...\");\n\n for (const managed of this.processes.values()) {\n managed.stopRequested = true;\n this.clearRestartTimer(managed);\n }\n\n const stopPromises = [...this.processes.entries()]\n .filter(([, m]) => m.status === \"running\")\n .map(([name]) => this.stop(name));\n\n await Promise.allSettled(stopPromises);\n }\n\n async restart(name: string): Promise<void> {\n const { name: resolvedName, managed } = this.resolveProcessIdentifier(name);\n\n this.logger.info(`Restarting process: ${resolvedName}`);\n\n if (managed.status === \"running\") {\n await this.stop(resolvedName);\n }\n\n managed.restarts++;\n this.spawnProcess(managed);\n }\n\n async restartAll(): Promise<void> {\n this.logger.info(\"Restarting all processes...\");\n\n for (const [name] of this.processes) {\n await this.restart(name);\n }\n }\n\n async delete(name: string): Promise<void> {\n const { name: resolvedName, managed } = this.resolveProcessIdentifier(name);\n\n managed.stopRequested = true;\n this.clearRestartTimer(managed);\n\n if (managed.status === \"running\") {\n await this.stop(resolvedName);\n }\n\n this.processes.delete(resolvedName);\n this.processIds.delete(managed.id);\n this.logger.info(`Deleted process: ${resolvedName}`);\n }\n\n async deleteAll(): Promise<void> {\n await this.stopAll();\n this.processes.clear();\n this.logger.info(\"Deleted all processes\");\n }\n\n get(name: string): ProcessInfo | undefined {\n let resolved: { name: string; managed: ManagedProcess };\n try {\n resolved = this.resolveProcessIdentifier(name);\n } catch {\n return undefined;\n }\n\n return {\n id: resolved.managed.id,\n name: resolved.managed.config.name,\n status: resolved.managed.status,\n pid: resolved.managed.spawned?.proc.pid,\n exitCode: resolved.managed.exitCode,\n restarts: resolved.managed.restarts,\n startedAt: resolved.managed.startedAt,\n command: resolved.managed.config.command,\n args: resolved.managed.config.args,\n };\n }\n\n async wait(): Promise<void> {\n const donePromises = [...this.processes.values()]\n .filter((m) => m.spawned)\n .map((m) => m.spawned!.done);\n\n await Promise.allSettled(donePromises);\n this.logger.info(\"All processes exited\");\n }\n\n async shutdown(): Promise<void> {\n await this.stopActiveTask();\n await this.stopAll();\n this.closeEnvWatcher();\n if (this.httpServer) {\n this.httpServer.close();\n this.logger.info(\"HTTP server closed\");\n }\n }\n\n private spawnProcess(managed: ManagedProcess): void {\n const { config } = managed;\n\n managed.stopRequested = false;\n this.clearRestartTimer(managed);\n\n const env = loadEnvForProcess({\n configDir: this.configDir ?? this.cwd,\n processName: config.name,\n globalEnvFile: this.globalEnvFile,\n processEnvFile: config.envFile,\n configEnv: config.env,\n });\n\n const spawned = spawnWithLogging({\n cwd: config.cwd ?? this.cwd,\n logFile: join(this.cwd, \"logs\", \"processes\", `${config.name}.log`),\n type: \"process\",\n name: config.name,\n command: config.command,\n args: config.args,\n env,\n });\n\n managed.spawned = spawned;\n managed.status = \"running\";\n managed.startedAt = new Date();\n managed.exitCode = null;\n\n this.logger.info(\n `Started process: ${config.name} (pid: ${spawned.proc.pid})`,\n );\n\n const handleExit = (exitCode: number | null, error?: unknown) => {\n managed.exitCode = exitCode;\n managed.status =\n exitCode === null || exitCode === undefined || exitCode === 0\n ? \"stopped\"\n : \"errored\";\n const now = Date.now();\n const startedAt = managed.startedAt?.getTime() ?? now;\n const uptimeMs = Math.max(0, now - startedAt);\n const resetMs =\n (config.restartResetSeconds ?? DEFAULT_RESTART_RESET_SECONDS) * 1000;\n\n if (uptimeMs >= resetMs) {\n managed.restartAttempts = 0;\n }\n\n if (exitCode === null || exitCode === undefined) {\n this.logger.info(`Process ${config.name} stopped`);\n } else if (exitCode !== 0) {\n this.logger.info(\n `Process ${config.name} exited with code ${exitCode}`,\n );\n } else {\n this.logger.info(`Process ${config.name} exited successfully`);\n }\n\n if (error) {\n this.logger.error(`Process ${config.name} failed with error: ${error}`);\n }\n\n const policy = config.restartPolicy ?? \"on-failure\";\n const isFailure =\n exitCode === null || exitCode === undefined || exitCode !== 0;\n const shouldRestart =\n !managed.stopRequested &&\n (policy === \"always\" || (policy === \"on-failure\" && isFailure));\n\n if (!shouldRestart) {\n return;\n }\n\n const maxRestarts = config.maxRestarts ?? DEFAULT_RESTART_MAX;\n if (managed.restartAttempts >= maxRestarts) {\n managed.status = \"errored\";\n this.logger.warn(\n `Process ${config.name} reached restart limit (${maxRestarts}), not restarting`,\n );\n return;\n }\n\n managed.restartAttempts += 1;\n managed.restarts += 1;\n const minDelay =\n config.restartMinDelayMs ?? this.restartMinDelayMsGlobal;\n const maxDelay =\n config.restartMaxDelayMs ?? this.restartMaxDelayMsGlobal;\n const delay = Math.min(\n maxDelay,\n minDelay * Math.pow(2, managed.restartAttempts - 1),\n );\n\n this.logger.warn(\n `Restarting process ${config.name} in ${delay}ms (attempt ${managed.restartAttempts}/${maxRestarts})`,\n );\n\n managed.restartTimer = setTimeout(() => {\n managed.restartTimer = null;\n if (!managed.stopRequested) {\n this.spawnProcess(managed);\n }\n }, delay);\n };\n\n spawned.done\n .then((exitCode) => handleExit(exitCode))\n .catch((error) => {\n const exitCode = spawned.proc.exitCode ?? 1;\n handleExit(exitCode, error);\n });\n }\n\n private async runTasks(tasks: Array<ProcessConfig>): Promise<void> {\n for (const task of tasks) {\n const env = loadEnvForProcess({\n configDir: this.configDir ?? this.cwd,\n processName: task.name,\n globalEnvFile: this.globalEnvFile,\n processEnvFile: task.envFile,\n configEnv: task.env,\n });\n\n const taskAbortController = new AbortController();\n this.taskAbortController = taskAbortController;\n\n const spawned = spawnWithLogging({\n cwd: task.cwd ?? this.cwd,\n logFile: join(this.cwd, \"logs\", \"tasks\", `${task.name}.log`),\n type: \"task\",\n name: task.name,\n command: task.command,\n args: task.args,\n env,\n abortSignal: taskAbortController.signal,\n });\n\n this.currentTask = spawned;\n this.currentTaskName = task.name;\n\n let exitCode: number | null;\n try {\n exitCode = await spawned.done;\n } finally {\n this.currentTask = null;\n this.currentTaskName = null;\n this.taskAbortController = null;\n }\n\n if (exitCode !== 0) {\n this.logger.error(\n `Task ${task.name} failed with exit code ${exitCode}`,\n );\n throw new Error(`Task ${task.name} failed`);\n }\n }\n\n if (tasks.length > 0) {\n this.logger.info(\"All tasks completed\");\n }\n }\n\n private clearRestartTimer(managed: ManagedProcess): void {\n if (managed.restartTimer) {\n clearTimeout(managed.restartTimer);\n managed.restartTimer = null;\n }\n }\n\n private async stopActiveTask(\n timeoutMs = DEFAULT_STOP_TIMEOUT_MS,\n ): Promise<void> {\n if (!this.currentTask) return;\n\n const taskName = this.currentTaskName ?? \"unknown\";\n this.logger.info(`Stopping task: ${taskName} (SIGTERM)`);\n this.currentTask.kill(\"SIGTERM\");\n\n const exitedGracefully = await Promise.race([\n this.currentTask.done.then(() => true).catch(() => true),\n new Promise<false>((resolve) =>\n setTimeout(() => resolve(false), timeoutMs),\n ),\n ]);\n\n if (!exitedGracefully) {\n this.logger.warn(\n `Task ${taskName} did not stop gracefully, sending SIGKILL`,\n );\n this.currentTask.kill(\"SIGKILL\");\n await this.currentTask.done.catch(() => {});\n }\n }\n\n private abortActiveTask(signal: NodeJS.Signals): void {\n if (this.taskAbortController && !this.taskAbortController.signal.aborted) {\n this.taskAbortController.abort();\n }\n if (this.currentTask) {\n this.currentTask.kill(signal);\n }\n }\n\n private setupSignalHandlers(): void {\n if (this.signalHandlersSetup) return;\n this.signalHandlersSetup = true;\n\n const signals = [\"SIGINT\", \"SIGTERM\"] as const;\n for (const signal of signals) {\n process.on(signal, () => {\n process.stdout.write(\"\\r\\x1b[K\");\n this.logger.info(`Received ${signal}, shutting down...`);\n this.abortActiveTask(signal);\n this.shutdown().then(() => process.exit(0));\n });\n }\n }\n\n private setupEnvWatchers(): void {\n if (this.envWatcher) return;\n if (!this.restartOnEnvChangeGlobal) return;\n\n const watchDir = this.configDir ?? this.cwd;\n this.envWatcher = watch(watchDir, (event, filename) => {\n if (!filename) return;\n const envFile = filename.toString();\n if (!envFile.startsWith(\".env\")) return;\n if (event !== \"change\" && event !== \"rename\") return;\n\n if (envFile === \".env\") {\n this.scheduleEnvRestart(\"global\", () => {\n this.logger.info(\"Detected .env change, scheduling restarts\");\n for (const managed of this.processes.values()) {\n this.scheduleEnvRestartForProcess(managed);\n }\n });\n return;\n }\n\n if (!envFile.startsWith(\".env.\")) return;\n const processName = envFile.slice(\".env.\".length);\n if (!processName) return;\n\n this.scheduleEnvRestart(processName, () => {\n const managed = this.processes.get(processName);\n if (!managed) return;\n this.logger.info(\n `Detected ${envFile} change, scheduling restart for ${processName}`,\n );\n this.scheduleEnvRestartForProcess(managed);\n });\n });\n }\n\n private closeEnvWatcher(): void {\n if (this.envWatcher) {\n this.envWatcher.close();\n this.envWatcher = null;\n }\n for (const timer of this.envChangeTimers.values()) {\n clearTimeout(timer);\n }\n this.envChangeTimers.clear();\n for (const timer of this.envRestartTimers.values()) {\n clearTimeout(timer);\n }\n this.envRestartTimers.clear();\n }\n\n private scheduleEnvRestart(key: string, restart: () => void): void {\n const existing = this.envChangeTimers.get(key);\n if (existing) clearTimeout(existing);\n const timer = setTimeout(() => {\n this.envChangeTimers.delete(key);\n restart();\n }, 250);\n this.envChangeTimers.set(key, timer);\n }\n\n private scheduleEnvRestartForProcess(managed: ManagedProcess): void {\n if (!this.restartOnEnvChangeGlobal) return;\n if (managed.config.restartOnEnvChange === false) return;\n if (managed.status !== \"running\") return;\n const name = managed.config.name;\n const existing = this.envRestartTimers.get(name);\n if (existing) clearTimeout(existing);\n\n const delayMs =\n managed.config.restartMinDelayMs ?? this.restartMinDelayMsGlobal;\n\n const timer = setTimeout(() => {\n this.envRestartTimers.delete(name);\n this.restart(name).catch((error) => {\n this.logger.error(\n `Failed to restart ${name} after env change: ${error}`,\n );\n });\n }, Math.max(0, delayMs));\n\n this.envRestartTimers.set(name, timer);\n this.logger.info(\n `Restarting ${name} after env change in ${Math.max(0, delayMs)}ms`,\n );\n }\n\n private resolveProcessIdentifier(nameOrId: string): {\n name: string;\n managed: ManagedProcess;\n } {\n const byName = this.processes.get(nameOrId);\n if (byName) {\n return { name: nameOrId, managed: byName };\n }\n\n if (/^\\d+$/.test(nameOrId)) {\n const id = Number(nameOrId);\n const name = this.processIds.get(id);\n if (name) {\n const managed = this.processes.get(name);\n if (managed) {\n return { name, managed };\n }\n }\n }\n\n throw new Error(`Process ${nameOrId} not found`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AASA,MAAMA,WAAS,aAAa,MAAM,MAAM;AAExC,SAAS,YAAY,MAAsC;AACzD,KAAI,CAAC,WAAW,KAAK,CACnB,QAAO,EAAE;AAGX,KAAI;EAEF,MAAM,SAAS,MADC,aAAa,MAAM,QAAQ,CACd;AAC7B,WAAO,MAAM,oBAAoB,OAAO;AACxC,SAAO;UACA,OAAO;AACd,WAAO,KAAK,4BAA4B,KAAK,IAAI,QAAQ;AACzD,SAAO,EAAE;;;AAYb,SAAgB,kBACd,SACwB;CACxB,MAAM,EACJ,WACA,aACA,eACA,gBACA,YAAY,EAAE,KACZ;CAEJ,MAAM,UAAU,OAAO,YACrB,OAAO,QAAQ,QAAQ,IAAI,CAAC,QAAQ,GAAG,WAAW,UAAU,OAAU,CACvE;CAED,IAAI,YAAoC,EAAE;AAC1C,KAAI,kBAAkB,MAEpB,aAAY,YADO,iBAAiB,KAAK,WAAW,OAAO,CACxB;CAGrC,IAAI,aAAqC,EAAE;AAC3C,KAAI,mBAAmB,MAGrB,cAAa,YADX,kBAAkB,KAAK,WAAW,QAAQ,cAAc,CACrB;AAGvC,QAAO;EACL,GAAG;EACH,GAAG;EACH,GAAG;EACH,GAAG;EACJ;;;;;AC5CH,SAAgB,iBAAiB,EAC/B,MACA,SACA,MACA,KACA,KACA,SACA,aACA,QACwC;CACxC,MAAM,OAAO,EAAE,SAAS,MAAM;EAC5B,aAAa;GAAE;GAAK;GAAK;EACzB,QAAQ;EACR,cAAc;EACf,CAAC;CAEF,MAAM,QAAQ,SAAyB,cAAuB;AAC5D,MAAI,KAAK,IACP,KAAI;AACF,WAAQ,KAAK,KAAK,KAAK,OAAO;AAC9B,UAAO;UACD;AACN,UAAO;;AAGX,SAAO;;AAkCT,QAAO;EAAE;EAAM,OA/BD,YAAY;GACxB,MAAM,SAAS,cAAc;IAAE,MAAM,GAAG,KAAK,GAAG;IAAQ;IAAS,CAAC;AAElE,UAAO,KACL,qBAAqB,QAAQ,UAAU,KAAK,KAAK,KAAK,CAAC,SAAS,MACjE;AAED,cAAW,MAAM,QAAQ,KACvB,QAAO,KAAK,KAAK;AAGnB,SAAM;GAEN,MAAM,WAAW,KAAK;AAEtB,OACE,KAAK,WACL,KAAK,UACL,aAAa,QACb,aAAa,OAEb,QAAO,KAAK,GAAG,KAAK,iBAAiB;YAC5B,aAAa,EACtB,QAAO,KAAK,GAAG,KAAK,yBAAyB;OAE7C,QAAO,KAAK,GAAG,KAAK,oBAAoB,WAAW;AAGrD,UAAO,KAAK,YAAY;MACtB;EAEiB;EAAM;;;;;AC/E7B,MAAM,oBAAoB,EAAE,OAAO;CACjC,IAAI,EAAE,QAAQ;CACd,MAAM,EAAE,QAAQ;CAChB,QAAQ,EAAE,SAAS;EAAC;EAAW;EAAW;EAAU,CAAC;CACrD,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC;CAC3B,UAAU,EAAE,SAAS,EAAE,QAAQ,CAAC;CAChC,UAAU,EAAE,QAAQ;CACpB,WAAW,EAAE,SAAS,EAAE,MAAM,CAAC;CAC/B,SAAS,EAAE,QAAQ;CACnB,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC;CAC1B,CAAC;AAEF,MAAM,oBAAoB,EAAE,OAAO,EACjC,MAAM,EAAE,QAAQ,EACjB,CAAC;AAMF,MAAM,OAAO,GAAG,UAAyB;AAEzC,MAAa,OAAO,KACjB,OAAO,EAAE,MAAM,kBAAkB,CAAC,CAClC,QAAQ,OAAO,EAAE,cAAc;AAC9B,QAAO,QAAQ,GAAG,MAAM;EACxB;AAEJ,MAAa,MAAM,KAChB,MAAM,kBAAkB,CACxB,OAAO,EAAE,SAAS,kBAAkB,CAAC,CACrC,QAAQ,OAAO,EAAE,OAAO,cAAc;AACrC,QAAO,QAAQ,GAAG,IAAI,MAAM,KAAK,IAAI;EACrC;AAEJ,MAAa,QAAQ,KAClB,MACC,EAAE,OAAO,EACP,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,EAC7B,CAAC,CACH,CACA,OAAO,EAAE,OAAO;CAAE,SAAS,EAAE,SAAS;CAAE,SAAS,EAAE,QAAQ;CAAE,CAAC,CAAC,CAC/D,QAAQ,OAAO,EAAE,OAAO,cAAc;AACrC,KAAI;AACF,MAAI,MAAM,MAAM;AACd,WAAQ,GAAG,MAAM,MAAM,KAAK;AAC5B,UAAO;IAAE,SAAS;IAAM,SAAS,oBAAoB,MAAM;IAAQ;SAC9D;AACL,WAAQ,GAAG,UAAU;AACrB,UAAO;IAAE,SAAS;IAAM,SAAS;IAAyB;;UAErD,OAAO;AACd,QAAM,IAAI,UAAU,eAAe,EACjC,SAAS,iBAAiB,QAAQ,MAAM,UAAU,mBACnD,CAAC;;EAEJ;AAEJ,MAAa,OAAO,KACjB,MACC,EAAE,OAAO,EACP,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,EAC7B,CAAC,CACH,CACA,OAAO,EAAE,OAAO;CAAE,SAAS,EAAE,SAAS;CAAE,SAAS,EAAE,QAAQ;CAAE,CAAC,CAAC,CAC/D,QAAQ,OAAO,EAAE,OAAO,cAAc;AACrC,KAAI;AACF,MAAI,MAAM,MAAM;AACd,SAAM,QAAQ,GAAG,KAAK,MAAM,KAAK;AACjC,UAAO;IAAE,SAAS;IAAM,SAAS,oBAAoB,MAAM;IAAQ;SAC9D;AACL,SAAM,QAAQ,GAAG,SAAS;AAC1B,UAAO;IAAE,SAAS;IAAM,SAAS;IAAyB;;UAErD,OAAO;AACd,QAAM,IAAI,UAAU,eAAe,EACjC,SAAS,iBAAiB,QAAQ,MAAM,UAAU,kBACnD,CAAC;;EAEJ;AAEJ,MAAa,UAAU,KACpB,MACC,EAAE,OAAO,EACP,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,EAC7B,CAAC,CACH,CACA,OAAO,EAAE,OAAO;CAAE,SAAS,EAAE,SAAS;CAAE,SAAS,EAAE,QAAQ;CAAE,CAAC,CAAC,CAC/D,QAAQ,OAAO,EAAE,OAAO,cAAc;AACrC,KAAI;AACF,MAAI,MAAM,MAAM;AACd,SAAM,QAAQ,GAAG,QAAQ,MAAM,KAAK;AACpC,UAAO;IAAE,SAAS;IAAM,SAAS,sBAAsB,MAAM;IAAQ;SAChE;AACL,SAAM,QAAQ,GAAG,YAAY;AAC7B,UAAO;IAAE,SAAS;IAAM,SAAS;IAA2B;;UAEvD,OAAO;AACd,QAAM,IAAI,UAAU,eAAe,EACjC,SAAS,iBAAiB,QAAQ,MAAM,UAAU,qBACnD,CAAC;;EAEJ;AAEJ,MAAa,gBAAgB,KAC1B,MACC,EAAE,OAAO,EACP,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,EAC7B,CAAC,CACH,CACA,OAAO,EAAE,OAAO;CAAE,SAAS,EAAE,SAAS;CAAE,SAAS,EAAE,QAAQ;CAAE,CAAC,CAAC,CAC/D,QAAQ,OAAO,EAAE,OAAO,cAAc;AACrC,KAAI;AACF,MAAI,MAAM,MAAM;AACd,SAAM,QAAQ,GAAG,OAAO,MAAM,KAAK;AACnC,UAAO;IAAE,SAAS;IAAM,SAAS,oBAAoB,MAAM;IAAQ;SAC9D;AACL,SAAM,QAAQ,GAAG,WAAW;AAC5B,UAAO;IAAE,SAAS;IAAM,SAAS;IAAyB;;UAErD,OAAO;AACd,QAAM,IAAI,UAAU,eAAe,EACjC,SAAS,iBAAiB,QAAQ,MAAM,UAAU,oBACnD,CAAC;;EAEJ;AAEJ,MAAa,SAAS,GACnB,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,KAAK,EAAE,CAAC,CAAC,CAC7C,QAAQ,YAAY;AACnB,QAAO,EAAE,QAAQ,MAAe;EAChC;AAEJ,MAAa,SAAS;CACpB;CACA,SAAS;EACP;EACA;EACA;EACA;EACA;EACA,QAAQ;EACT;CACF;;;;ACrID,SAAgB,aAAa,IAAoB,UAAyB,EAAE,EAAE;CAC5E,MAAM,EAAE,OAAO,KAAM,OAAO,gBAAgB;CAC5C,MAAMC,WAAS,aAAa,MAAM,SAAS;CAE3C,MAAM,MAAM,IAAI,MAAM;CAEtB,MAAM,UAAU,IAAI,WAAW,QAAQ,EACrC,cAAc,CACZ,SAAS,UAAmB;EAC1B,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,WAAO,MAAM,cAAc,UAAU;GACrC,CACH,EACF,CAAC;AAEF,KAAI,IACFC,QAAY,KAAK,GAAG,SAASD,SAAO,MAAM,GAAG,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,CAAC,CACvE;AAED,KAAI,IAAI,YAAY,MAAM,EAAE,KAAK,EAAE,QAAQ,MAAM,CAAC,CAAC;AAEnD,KAAI,IAAI,UAAU,OAAO,GAAG,SAAS;EACnC,MAAM,EAAE,SAAS,aAAa,MAAM,QAAQ,OAAO,EAAE,IAAI,KAAK;GAC5D,QAAQ;GACR,SAAS,EAAE,IAAI;GAChB,CAAC;AAEF,MAAI,QACF,QAAO,EAAE,YAAY,SAAS,MAAM,SAAS;AAG/C,QAAM,MAAM;GACZ;AAEF,KAAI,UAAU,MAAM,EAAE,KAAK,EAAE,OAAO,aAAa,EAAE,IAAI,CAAC;AAExD,QAAO;EACL;EACA,aAAa;GACX,MAAM,SAAS,MAAM;IACnB,OAAO,IAAI;IACX;IACA,UAAU;IACX,CAAC;AAEF,YAAO,KAAK,mCAAmC,KAAK,GAAG,OAAO;AAC9D,YAAO,KAAK,wBAAwB,KAAK,GAAG,KAAK,MAAM;AAEvD,UAAO;;EAEV;;;;;ACnBH,MAAM,0BAA0B;AAChC,MAAM,gCAAgC;AACtC,MAAM,sBAAsB;AAC5B,MAAM,+BAA+B;AACrC,MAAM,+BAA+B;AAcrC,IAAa,iBAAb,MAA4B;CAC1B,AAAQ,SAAS,aAAa,MAAM,KAAK;CACzC,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ,gBAAgB;CACxB,AAAQ,sBAAsB;CAC9B,AAAQ;CACR,AAAQ;CACR,AAAQ,aAA+B;CACvC,AAAQ,kCAAkB,IAAI,KAA6B;CAC3D,AAAQ,mCAAmB,IAAI,KAA6B;CAC5D,AAAQ,2BAA2B;CACnC,AAAQ,0BAA0B;CAClC,AAAQ,0BAA0B;CAClC,AAAQ,cAAqC;CAC7C,AAAQ,kBAAiC;CACzC,AAAQ,sBAA8C;CAEtD,YAAY,UAAiC,EAAE,EAAE;AAC/C,OAAK,MAAM,QAAQ,OAAO,QAAQ,KAAK;AACvC,OAAK,aAAa,QAAQ;AAC1B,OAAK,4BAAY,IAAI,KAAK;AAC1B,OAAK,6BAAa,IAAI,KAAK;AAC3B,OAAK,gBAAgB,QAAQ;;CAG/B,MAAM,OAAsB;EAC1B,MAAM,SAAS,MAAM,eAAe,KAAK,YAAY,KAAK,IAAI;AAE9D,OAAK,YAAY,QAAQ,OAAO,WAAW;AAC3C,OAAK,gBAAgB,OAAO;AAC5B,OAAK,2BAA2B,OAAO,sBAAsB;AAC7D,OAAK,0BACH,OAAO,qBAAqB;AAC9B,OAAK,0BACH,OAAO,qBAAqB;AAE9B,OAAK,OAAO,KACV,aAAa,OAAO,MAAM,OAAO,yBAAyB,OAAO,UAAU,OAAO,YACnF;AAED,OAAK,qBAAqB;AAC1B,OAAK,kBAAkB;AACvB,QAAM,KAAK,SAAS,OAAO,MAAM;AAEjC,OAAK,MAAM,KAAK,OAAO,UACrB,MAAK,SAAS,EAAE;;CAIpB,SAAS,QAA6B;AACpC,MAAI,KAAK,UAAU,IAAI,OAAO,KAAK,CACjC,OAAM,IAAI,MAAM,WAAW,OAAO,KAAK,qBAAqB;EAG9D,MAAM,KAAK,KAAK;EAChB,MAAM,UAA0B;GAC9B;GACA;GACA,QAAQ;GACR,SAAS;GACT,UAAU;GACV,UAAU;GACV,iBAAiB;GACjB,cAAc;GACd,eAAe;GACf,WAAW;GACZ;AAED,OAAK,UAAU,IAAI,OAAO,MAAM,QAAQ;AACxC,OAAK,WAAW,IAAI,IAAI,OAAO,KAAK;AACpC,OAAK,OAAO,KAAK,uBAAuB,OAAO,KAAK,QAAQ,GAAG,GAAG;;CAGpE,OAAsB;AACpB,SAAO,CAAC,GAAG,KAAK,UAAU,QAAQ,CAAC,CAAC,KAAK,OAAO;GAC9C,IAAI,EAAE;GACN,MAAM,EAAE,OAAO;GACf,QAAQ,EAAE;GACV,KAAK,EAAE,SAAS,KAAK;GACrB,UAAU,EAAE;GACZ,UAAU,EAAE;GACZ,WAAW,EAAE;GACb,SAAS,EAAE,OAAO;GAClB,MAAM,EAAE,OAAO;GAChB,EAAE;;CAGL,MAAM,MAAoB;EACxB,MAAM,EAAE,MAAM,cAAc,YAAY,KAAK,yBAAyB,KAAK;AAE3E,MAAI,QAAQ,WAAW,WAAW;AAChC,QAAK,OAAO,KAAK,WAAW,aAAa,qBAAqB;AAC9D;;AAGF,OAAK,aAAa,QAAQ;;CAG5B,WAAiB;AACf,OAAK,qBAAqB;AAE1B,OAAK,MAAM,CAAC,GAAG,YAAY,KAAK,UAC9B,KAAI,QAAQ,WAAW,UACrB,MAAK,aAAa,QAAQ;;CAKhC,MAAM,SAAuD;EAC3D,MAAM,aAAa;GAAE,GAAG,KAAK;GAAe,GAAG;GAAS;AACxD,MAAI,WAAW,YAAY,MAAO;EAClC,MAAM,EAAE,SAAS,UAAU,GAAG,kBAAkB;AAEhD,OAAK,aADU,aAAa,MAAM,cAAc,CACvB,OAAO;;CAGlC,MAAM,KAAK,MAAc,YAAY,yBAAwC;EAC3E,MAAM,EAAE,MAAM,cAAc,YAAY,KAAK,yBAAyB,KAAK;AAE3E,UAAQ,gBAAgB;AACxB,OAAK,kBAAkB,QAAQ;AAE/B,MAAI,QAAQ,WAAW,aAAa,CAAC,QAAQ,SAAS;AACpD,QAAK,OAAO,KAAK,WAAW,aAAa,iBAAiB;AAC1D;;EAGF,MAAM,EAAE,YAAY;AAEpB,OAAK,OAAO,KAAK,qBAAqB,aAAa,YAAY;AAC/D,UAAQ,KAAK,UAAU;AASvB,MAAI,CAPqB,MAAM,QAAQ,KAAK,CAC1C,QAAQ,KAAK,WAAW,KAAK,CAAC,YAAY,KAAK,EAC/C,IAAI,SAAgB,YAClB,iBAAiB,QAAQ,MAAM,EAAE,UAAU,CAC5C,CACF,CAAC,IAEuB,QAAQ,WAAW,WAAW;AACrD,QAAK,OAAO,KACV,WAAW,aAAa,2CACzB;AACD,WAAQ,KAAK,UAAU;AACvB,SAAM,QAAQ,KAAK,YAAY,GAAG;;;CAItC,MAAM,UAAyB;AAC7B,OAAK,OAAO,KAAK,4BAA4B;AAE7C,OAAK,MAAM,WAAW,KAAK,UAAU,QAAQ,EAAE;AAC7C,WAAQ,gBAAgB;AACxB,QAAK,kBAAkB,QAAQ;;EAGjC,MAAM,eAAe,CAAC,GAAG,KAAK,UAAU,SAAS,CAAC,CAC/C,QAAQ,GAAG,OAAO,EAAE,WAAW,UAAU,CACzC,KAAK,CAAC,UAAU,KAAK,KAAK,KAAK,CAAC;AAEnC,QAAM,QAAQ,WAAW,aAAa;;CAGxC,MAAM,QAAQ,MAA6B;EACzC,MAAM,EAAE,MAAM,cAAc,YAAY,KAAK,yBAAyB,KAAK;AAE3E,OAAK,OAAO,KAAK,uBAAuB,eAAe;AAEvD,MAAI,QAAQ,WAAW,UACrB,OAAM,KAAK,KAAK,aAAa;AAG/B,UAAQ;AACR,OAAK,aAAa,QAAQ;;CAG5B,MAAM,aAA4B;AAChC,OAAK,OAAO,KAAK,8BAA8B;AAE/C,OAAK,MAAM,CAAC,SAAS,KAAK,UACxB,OAAM,KAAK,QAAQ,KAAK;;CAI5B,MAAM,OAAO,MAA6B;EACxC,MAAM,EAAE,MAAM,cAAc,YAAY,KAAK,yBAAyB,KAAK;AAE3E,UAAQ,gBAAgB;AACxB,OAAK,kBAAkB,QAAQ;AAE/B,MAAI,QAAQ,WAAW,UACrB,OAAM,KAAK,KAAK,aAAa;AAG/B,OAAK,UAAU,OAAO,aAAa;AACnC,OAAK,WAAW,OAAO,QAAQ,GAAG;AAClC,OAAK,OAAO,KAAK,oBAAoB,eAAe;;CAGtD,MAAM,YAA2B;AAC/B,QAAM,KAAK,SAAS;AACpB,OAAK,UAAU,OAAO;AACtB,OAAK,OAAO,KAAK,wBAAwB;;CAG3C,IAAI,MAAuC;EACzC,IAAI;AACJ,MAAI;AACF,cAAW,KAAK,yBAAyB,KAAK;UACxC;AACN;;AAGF,SAAO;GACL,IAAI,SAAS,QAAQ;GACrB,MAAM,SAAS,QAAQ,OAAO;GAC9B,QAAQ,SAAS,QAAQ;GACzB,KAAK,SAAS,QAAQ,SAAS,KAAK;GACpC,UAAU,SAAS,QAAQ;GAC3B,UAAU,SAAS,QAAQ;GAC3B,WAAW,SAAS,QAAQ;GAC5B,SAAS,SAAS,QAAQ,OAAO;GACjC,MAAM,SAAS,QAAQ,OAAO;GAC/B;;CAGH,MAAM,OAAsB;EAC1B,MAAM,eAAe,CAAC,GAAG,KAAK,UAAU,QAAQ,CAAC,CAC9C,QAAQ,MAAM,EAAE,QAAQ,CACxB,KAAK,MAAM,EAAE,QAAS,KAAK;AAE9B,QAAM,QAAQ,WAAW,aAAa;AACtC,OAAK,OAAO,KAAK,uBAAuB;;CAG1C,MAAM,WAA0B;AAC9B,QAAM,KAAK,gBAAgB;AAC3B,QAAM,KAAK,SAAS;AACpB,OAAK,iBAAiB;AACtB,MAAI,KAAK,YAAY;AACnB,QAAK,WAAW,OAAO;AACvB,QAAK,OAAO,KAAK,qBAAqB;;;CAI1C,AAAQ,aAAa,SAA+B;EAClD,MAAM,EAAE,WAAW;AAEnB,UAAQ,gBAAgB;AACxB,OAAK,kBAAkB,QAAQ;EAE/B,MAAM,MAAM,kBAAkB;GAC5B,WAAW,KAAK,aAAa,KAAK;GAClC,aAAa,OAAO;GACpB,eAAe,KAAK;GACpB,gBAAgB,OAAO;GACvB,WAAW,OAAO;GACnB,CAAC;EAEF,MAAM,UAAU,iBAAiB;GAC/B,KAAK,OAAO,OAAO,KAAK;GACxB,SAAS,KAAK,KAAK,KAAK,QAAQ,aAAa,GAAG,OAAO,KAAK,MAAM;GAClE,MAAM;GACN,MAAM,OAAO;GACb,SAAS,OAAO;GAChB,MAAM,OAAO;GACb;GACD,CAAC;AAEF,UAAQ,UAAU;AAClB,UAAQ,SAAS;AACjB,UAAQ,4BAAY,IAAI,MAAM;AAC9B,UAAQ,WAAW;AAEnB,OAAK,OAAO,KACV,oBAAoB,OAAO,KAAK,SAAS,QAAQ,KAAK,IAAI,GAC3D;EAED,MAAM,cAAc,UAAyB,UAAoB;AAC/D,WAAQ,WAAW;AACnB,WAAQ,SACN,aAAa,QAAQ,aAAa,UAAa,aAAa,IACxD,YACA;GACN,MAAM,MAAM,KAAK,KAAK;GACtB,MAAM,YAAY,QAAQ,WAAW,SAAS,IAAI;AAKlD,OAJiB,KAAK,IAAI,GAAG,MAAM,UAAU,KAE1C,OAAO,uBAAuB,iCAAiC,IAGhE,SAAQ,kBAAkB;AAG5B,OAAI,aAAa,QAAQ,aAAa,OACpC,MAAK,OAAO,KAAK,WAAW,OAAO,KAAK,UAAU;YACzC,aAAa,EACtB,MAAK,OAAO,KACV,WAAW,OAAO,KAAK,oBAAoB,WAC5C;OAED,MAAK,OAAO,KAAK,WAAW,OAAO,KAAK,sBAAsB;AAGhE,OAAI,MACF,MAAK,OAAO,MAAM,WAAW,OAAO,KAAK,sBAAsB,QAAQ;GAGzE,MAAM,SAAS,OAAO,iBAAiB;GACvC,MAAM,YACJ,aAAa,QAAQ,aAAa,UAAa,aAAa;AAK9D,OAAI,EAHF,CAAC,QAAQ,kBACR,WAAW,YAAa,WAAW,gBAAgB,YAGpD;GAGF,MAAM,cAAc,OAAO,eAAe;AAC1C,OAAI,QAAQ,mBAAmB,aAAa;AAC1C,YAAQ,SAAS;AACjB,SAAK,OAAO,KACV,WAAW,OAAO,KAAK,0BAA0B,YAAY,mBAC9D;AACD;;AAGF,WAAQ,mBAAmB;AAC3B,WAAQ,YAAY;GACpB,MAAM,WACJ,OAAO,qBAAqB,KAAK;GACnC,MAAM,WACJ,OAAO,qBAAqB,KAAK;GACnC,MAAM,QAAQ,KAAK,IACjB,UACA,WAAW,KAAK,IAAI,GAAG,QAAQ,kBAAkB,EAAE,CACpD;AAED,QAAK,OAAO,KACV,sBAAsB,OAAO,KAAK,MAAM,MAAM,cAAc,QAAQ,gBAAgB,GAAG,YAAY,GACpG;AAED,WAAQ,eAAe,iBAAiB;AACtC,YAAQ,eAAe;AACvB,QAAI,CAAC,QAAQ,cACX,MAAK,aAAa,QAAQ;MAE3B,MAAM;;AAGX,UAAQ,KACL,MAAM,aAAa,WAAW,SAAS,CAAC,CACxC,OAAO,UAAU;AAEhB,cADiB,QAAQ,KAAK,YAAY,GACrB,MAAM;IAC3B;;CAGN,MAAc,SAAS,OAA4C;AACjE,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,MAAM,kBAAkB;IAC5B,WAAW,KAAK,aAAa,KAAK;IAClC,aAAa,KAAK;IAClB,eAAe,KAAK;IACpB,gBAAgB,KAAK;IACrB,WAAW,KAAK;IACjB,CAAC;GAEF,MAAM,sBAAsB,IAAI,iBAAiB;AACjD,QAAK,sBAAsB;GAE3B,MAAM,UAAU,iBAAiB;IAC/B,KAAK,KAAK,OAAO,KAAK;IACtB,SAAS,KAAK,KAAK,KAAK,QAAQ,SAAS,GAAG,KAAK,KAAK,MAAM;IAC5D,MAAM;IACN,MAAM,KAAK;IACX,SAAS,KAAK;IACd,MAAM,KAAK;IACX;IACA,aAAa,oBAAoB;IAClC,CAAC;AAEF,QAAK,cAAc;AACnB,QAAK,kBAAkB,KAAK;GAE5B,IAAI;AACJ,OAAI;AACF,eAAW,MAAM,QAAQ;aACjB;AACR,SAAK,cAAc;AACnB,SAAK,kBAAkB;AACvB,SAAK,sBAAsB;;AAG7B,OAAI,aAAa,GAAG;AAClB,SAAK,OAAO,MACV,QAAQ,KAAK,KAAK,yBAAyB,WAC5C;AACD,UAAM,IAAI,MAAM,QAAQ,KAAK,KAAK,SAAS;;;AAI/C,MAAI,MAAM,SAAS,EACjB,MAAK,OAAO,KAAK,sBAAsB;;CAI3C,AAAQ,kBAAkB,SAA+B;AACvD,MAAI,QAAQ,cAAc;AACxB,gBAAa,QAAQ,aAAa;AAClC,WAAQ,eAAe;;;CAI3B,MAAc,eACZ,YAAY,yBACG;AACf,MAAI,CAAC,KAAK,YAAa;EAEvB,MAAM,WAAW,KAAK,mBAAmB;AACzC,OAAK,OAAO,KAAK,kBAAkB,SAAS,YAAY;AACxD,OAAK,YAAY,KAAK,UAAU;AAShC,MAAI,CAPqB,MAAM,QAAQ,KAAK,CAC1C,KAAK,YAAY,KAAK,WAAW,KAAK,CAAC,YAAY,KAAK,EACxD,IAAI,SAAgB,YAClB,iBAAiB,QAAQ,MAAM,EAAE,UAAU,CAC5C,CACF,CAAC,EAEqB;AACrB,QAAK,OAAO,KACV,QAAQ,SAAS,2CAClB;AACD,QAAK,YAAY,KAAK,UAAU;AAChC,SAAM,KAAK,YAAY,KAAK,YAAY,GAAG;;;CAI/C,AAAQ,gBAAgB,QAA8B;AACpD,MAAI,KAAK,uBAAuB,CAAC,KAAK,oBAAoB,OAAO,QAC/D,MAAK,oBAAoB,OAAO;AAElC,MAAI,KAAK,YACP,MAAK,YAAY,KAAK,OAAO;;CAIjC,AAAQ,sBAA4B;AAClC,MAAI,KAAK,oBAAqB;AAC9B,OAAK,sBAAsB;AAG3B,OAAK,MAAM,UADK,CAAC,UAAU,UAAU,CAEnC,SAAQ,GAAG,cAAc;AACvB,WAAQ,OAAO,MAAM,WAAW;AAChC,QAAK,OAAO,KAAK,YAAY,OAAO,oBAAoB;AACxD,QAAK,gBAAgB,OAAO;AAC5B,QAAK,UAAU,CAAC,WAAW,QAAQ,KAAK,EAAE,CAAC;IAC3C;;CAIN,AAAQ,mBAAyB;AAC/B,MAAI,KAAK,WAAY;AACrB,MAAI,CAAC,KAAK,yBAA0B;AAGpC,OAAK,aAAa,MADD,KAAK,aAAa,KAAK,MACL,OAAO,aAAa;AACrD,OAAI,CAAC,SAAU;GACf,MAAM,UAAU,SAAS,UAAU;AACnC,OAAI,CAAC,QAAQ,WAAW,OAAO,CAAE;AACjC,OAAI,UAAU,YAAY,UAAU,SAAU;AAE9C,OAAI,YAAY,QAAQ;AACtB,SAAK,mBAAmB,gBAAgB;AACtC,UAAK,OAAO,KAAK,4CAA4C;AAC7D,UAAK,MAAM,WAAW,KAAK,UAAU,QAAQ,CAC3C,MAAK,6BAA6B,QAAQ;MAE5C;AACF;;AAGF,OAAI,CAAC,QAAQ,WAAW,QAAQ,CAAE;GAClC,MAAM,cAAc,QAAQ,MAAM,EAAe;AACjD,OAAI,CAAC,YAAa;AAElB,QAAK,mBAAmB,mBAAmB;IACzC,MAAM,UAAU,KAAK,UAAU,IAAI,YAAY;AAC/C,QAAI,CAAC,QAAS;AACd,SAAK,OAAO,KACV,YAAY,QAAQ,kCAAkC,cACvD;AACD,SAAK,6BAA6B,QAAQ;KAC1C;IACF;;CAGJ,AAAQ,kBAAwB;AAC9B,MAAI,KAAK,YAAY;AACnB,QAAK,WAAW,OAAO;AACvB,QAAK,aAAa;;AAEpB,OAAK,MAAM,SAAS,KAAK,gBAAgB,QAAQ,CAC/C,cAAa,MAAM;AAErB,OAAK,gBAAgB,OAAO;AAC5B,OAAK,MAAM,SAAS,KAAK,iBAAiB,QAAQ,CAChD,cAAa,MAAM;AAErB,OAAK,iBAAiB,OAAO;;CAG/B,AAAQ,mBAAmB,KAAa,SAA2B;EACjE,MAAM,WAAW,KAAK,gBAAgB,IAAI,IAAI;AAC9C,MAAI,SAAU,cAAa,SAAS;EACpC,MAAM,QAAQ,iBAAiB;AAC7B,QAAK,gBAAgB,OAAO,IAAI;AAChC,YAAS;KACR,IAAI;AACP,OAAK,gBAAgB,IAAI,KAAK,MAAM;;CAGtC,AAAQ,6BAA6B,SAA+B;AAClE,MAAI,CAAC,KAAK,yBAA0B;AACpC,MAAI,QAAQ,OAAO,uBAAuB,MAAO;AACjD,MAAI,QAAQ,WAAW,UAAW;EAClC,MAAM,OAAO,QAAQ,OAAO;EAC5B,MAAM,WAAW,KAAK,iBAAiB,IAAI,KAAK;AAChD,MAAI,SAAU,cAAa,SAAS;EAEpC,MAAM,UACJ,QAAQ,OAAO,qBAAqB,KAAK;EAE3C,MAAM,QAAQ,iBAAiB;AAC7B,QAAK,iBAAiB,OAAO,KAAK;AAClC,QAAK,QAAQ,KAAK,CAAC,OAAO,UAAU;AAClC,SAAK,OAAO,MACV,qBAAqB,KAAK,qBAAqB,QAChD;KACD;KACD,KAAK,IAAI,GAAG,QAAQ,CAAC;AAExB,OAAK,iBAAiB,IAAI,MAAM,MAAM;AACtC,OAAK,OAAO,KACV,cAAc,KAAK,uBAAuB,KAAK,IAAI,GAAG,QAAQ,CAAC,IAChE;;CAGH,AAAQ,yBAAyB,UAG/B;EACA,MAAM,SAAS,KAAK,UAAU,IAAI,SAAS;AAC3C,MAAI,OACF,QAAO;GAAE,MAAM;GAAU,SAAS;GAAQ;AAG5C,MAAI,QAAQ,KAAK,SAAS,EAAE;GAC1B,MAAM,KAAK,OAAO,SAAS;GAC3B,MAAM,OAAO,KAAK,WAAW,IAAI,GAAG;AACpC,OAAI,MAAM;IACR,MAAM,UAAU,KAAK,UAAU,IAAI,KAAK;AACxC,QAAI,QACF,QAAO;KAAE;KAAM;KAAS;;;AAK9B,QAAM,IAAI,MAAM,WAAW,SAAS,YAAY"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pid1",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0-dev.0",
|
|
5
|
+
"bin": "./dist/cli.mjs",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.mts"
|
|
10
|
+
},
|
|
11
|
+
"./config": {
|
|
12
|
+
"import": "./dist/config.mjs",
|
|
13
|
+
"types": "./dist/config.d.mts"
|
|
14
|
+
},
|
|
15
|
+
"./client": {
|
|
16
|
+
"import": "./dist/client.mjs",
|
|
17
|
+
"types": "./dist/client.d.mts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@hono/node-server": "^1.19.9",
|
|
26
|
+
"@orpc/client": "^1.13.4",
|
|
27
|
+
"@orpc/contract": "^1.13.4",
|
|
28
|
+
"@orpc/server": "^1.13.4",
|
|
29
|
+
"cli-table3": "^0.6.5",
|
|
30
|
+
"commander": "^14.0.2",
|
|
31
|
+
"dotenv": "^17.2.3",
|
|
32
|
+
"hono": "^4.11.5",
|
|
33
|
+
"tinyexec": "^1.0.2",
|
|
34
|
+
"tsx": "^4.21.0",
|
|
35
|
+
"valibot": "^1.2.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^25.0.10",
|
|
39
|
+
"oxfmt": "^0.26.0",
|
|
40
|
+
"tsdown": "^0.20.1",
|
|
41
|
+
"vitest": "^3.2.4",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsdown",
|
|
46
|
+
"dev": "tsx src/cli.ts init",
|
|
47
|
+
"dev:docker": "docker build -t pid1 . && docker run --rm pid1",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RouterClient } from "@orpc/server";
|
|
2
|
+
import { createORPCClient } from "@orpc/client";
|
|
3
|
+
import { RPCLink } from "@orpc/client/fetch";
|
|
4
|
+
import type { Router } from "./router.ts";
|
|
5
|
+
|
|
6
|
+
export type ClientOptions = {
|
|
7
|
+
url?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function createClient(
|
|
11
|
+
options: ClientOptions = {},
|
|
12
|
+
): RouterClient<Router> {
|
|
13
|
+
const { url = "http://127.0.0.1:3000/rpc" } = options;
|
|
14
|
+
|
|
15
|
+
const link = new RPCLink({ url });
|
|
16
|
+
|
|
17
|
+
return createORPCClient(link);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type Client = RouterClient<Router>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { os, ORPCError } from "@orpc/server";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
import type { ProcessManager, ProcessInfo } from "../process-manager.ts";
|
|
4
|
+
|
|
5
|
+
const ProcessInfoSchema = v.object({
|
|
6
|
+
id: v.number(),
|
|
7
|
+
name: v.string(),
|
|
8
|
+
status: v.picklist(["stopped", "running", "errored"]),
|
|
9
|
+
pid: v.optional(v.number()),
|
|
10
|
+
exitCode: v.nullable(v.number()),
|
|
11
|
+
restarts: v.number(),
|
|
12
|
+
startedAt: v.nullable(v.date()),
|
|
13
|
+
command: v.string(),
|
|
14
|
+
args: v.array(v.string()),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const ProcessNameSchema = v.object({
|
|
18
|
+
name: v.string(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
type RouterContext = {
|
|
22
|
+
pm: ProcessManager;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const base = os.$context<RouterContext>();
|
|
26
|
+
|
|
27
|
+
export const list = base
|
|
28
|
+
.output(v.array(ProcessInfoSchema))
|
|
29
|
+
.handler(async ({ context }) => {
|
|
30
|
+
return context.pm.list();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const get = base
|
|
34
|
+
.input(ProcessNameSchema)
|
|
35
|
+
.output(v.nullable(ProcessInfoSchema))
|
|
36
|
+
.handler(async ({ input, context }) => {
|
|
37
|
+
return context.pm.get(input.name) ?? null;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const start = base
|
|
41
|
+
.input(
|
|
42
|
+
v.object({
|
|
43
|
+
name: v.optional(v.string()),
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
.output(v.object({ success: v.boolean(), message: v.string() }))
|
|
47
|
+
.handler(async ({ input, context }) => {
|
|
48
|
+
try {
|
|
49
|
+
if (input.name) {
|
|
50
|
+
context.pm.start(input.name);
|
|
51
|
+
return { success: true, message: `Started process: ${input.name}` };
|
|
52
|
+
} else {
|
|
53
|
+
context.pm.startAll();
|
|
54
|
+
return { success: true, message: "Started all processes" };
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
58
|
+
message: error instanceof Error ? error.message : "Failed to start",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const stop = base
|
|
64
|
+
.input(
|
|
65
|
+
v.object({
|
|
66
|
+
name: v.optional(v.string()),
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
.output(v.object({ success: v.boolean(), message: v.string() }))
|
|
70
|
+
.handler(async ({ input, context }) => {
|
|
71
|
+
try {
|
|
72
|
+
if (input.name) {
|
|
73
|
+
await context.pm.stop(input.name);
|
|
74
|
+
return { success: true, message: `Stopped process: ${input.name}` };
|
|
75
|
+
} else {
|
|
76
|
+
await context.pm.stopAll();
|
|
77
|
+
return { success: true, message: "Stopped all processes" };
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
81
|
+
message: error instanceof Error ? error.message : "Failed to stop",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const restart = base
|
|
87
|
+
.input(
|
|
88
|
+
v.object({
|
|
89
|
+
name: v.optional(v.string()),
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
.output(v.object({ success: v.boolean(), message: v.string() }))
|
|
93
|
+
.handler(async ({ input, context }) => {
|
|
94
|
+
try {
|
|
95
|
+
if (input.name) {
|
|
96
|
+
await context.pm.restart(input.name);
|
|
97
|
+
return { success: true, message: `Restarted process: ${input.name}` };
|
|
98
|
+
} else {
|
|
99
|
+
await context.pm.restartAll();
|
|
100
|
+
return { success: true, message: "Restarted all processes" };
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
104
|
+
message: error instanceof Error ? error.message : "Failed to restart",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const deleteProcess = base
|
|
110
|
+
.input(
|
|
111
|
+
v.object({
|
|
112
|
+
name: v.optional(v.string()),
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
.output(v.object({ success: v.boolean(), message: v.string() }))
|
|
116
|
+
.handler(async ({ input, context }) => {
|
|
117
|
+
try {
|
|
118
|
+
if (input.name) {
|
|
119
|
+
await context.pm.delete(input.name);
|
|
120
|
+
return { success: true, message: `Deleted process: ${input.name}` };
|
|
121
|
+
} else {
|
|
122
|
+
await context.pm.deleteAll();
|
|
123
|
+
return { success: true, message: "Deleted all processes" };
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
127
|
+
message: error instanceof Error ? error.message : "Failed to delete",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export const health = os
|
|
133
|
+
.output(v.object({ status: v.literal("ok") }))
|
|
134
|
+
.handler(async () => {
|
|
135
|
+
return { status: "ok" as const };
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const router = {
|
|
139
|
+
health,
|
|
140
|
+
process: {
|
|
141
|
+
list,
|
|
142
|
+
get,
|
|
143
|
+
start,
|
|
144
|
+
stop,
|
|
145
|
+
restart,
|
|
146
|
+
delete: deleteProcess,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type Router = typeof router;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import { RPCHandler } from "@orpc/server/fetch";
|
|
4
|
+
import { onError } from "@orpc/server";
|
|
5
|
+
import { router } from "./router.ts";
|
|
6
|
+
import type { ProcessManager } from "../process-manager.ts";
|
|
7
|
+
import { globalLogger } from "../logger.ts";
|
|
8
|
+
import { logger as honoLogger } from "hono/logger";
|
|
9
|
+
|
|
10
|
+
export type ServerOptions = {
|
|
11
|
+
port?: number;
|
|
12
|
+
host?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createServer(pm: ProcessManager, options: ServerOptions = {}) {
|
|
16
|
+
const { port = 3000, host = "127.0.0.1" } = options;
|
|
17
|
+
const logger = globalLogger.child("server");
|
|
18
|
+
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
|
|
21
|
+
const handler = new RPCHandler(router, {
|
|
22
|
+
interceptors: [
|
|
23
|
+
onError((error: unknown) => {
|
|
24
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25
|
+
logger.error(`RPC Error: ${message}`);
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.use(
|
|
31
|
+
honoLogger((msg, ...args) => logger.debug(`${msg} ${args.join(" ")}`)),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
35
|
+
|
|
36
|
+
app.use("/rpc/*", async (c, next) => {
|
|
37
|
+
const { matched, response } = await handler.handle(c.req.raw, {
|
|
38
|
+
prefix: "/rpc",
|
|
39
|
+
context: { pm },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (matched) {
|
|
43
|
+
return c.newResponse(response.body, response);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await next();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
app.notFound((c) => c.json({ error: "Not found" }, 404));
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
app,
|
|
53
|
+
start: () => {
|
|
54
|
+
const server = serve({
|
|
55
|
+
fetch: app.fetch,
|
|
56
|
+
port,
|
|
57
|
+
hostname: host,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
logger.info(`HTTP server listening on http://${host}:${port}`);
|
|
61
|
+
return server;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type { Router } from "./router.ts";
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import Table from "cli-table3";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { ProcessManager } from "./process-manager.ts";
|
|
5
|
+
import { createClient } from "./api/client.ts";
|
|
6
|
+
import { globalLogger } from "./logger.ts";
|
|
7
|
+
|
|
8
|
+
const logger = globalLogger.child("cli");
|
|
9
|
+
const defaultPort = Number(process.env.PID1_PORT ?? process.env.PORT ?? 3000);
|
|
10
|
+
const envHost = process.env.PID1_HOST ?? process.env.HOST;
|
|
11
|
+
|
|
12
|
+
function formatTable(rows: Array<Record<string, unknown>>): void {
|
|
13
|
+
if (rows.length === 0) {
|
|
14
|
+
console.log("No processes registered");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const table = new Table({
|
|
19
|
+
head: ["ID", "NAME", "STATUS", "PID", "RESTARTS", "STARTED"],
|
|
20
|
+
style: { head: [], border: [] },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
table.push([
|
|
25
|
+
String(row.id),
|
|
26
|
+
String(row.name),
|
|
27
|
+
String(row.status),
|
|
28
|
+
row.pid ? String(row.pid) : "-",
|
|
29
|
+
String(row.restarts),
|
|
30
|
+
row.startedAt ? new Date(row.startedAt as string).toLocaleString() : "-",
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(table.toString());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type CommonArgs = {
|
|
38
|
+
port?: number;
|
|
39
|
+
host?: string;
|
|
40
|
+
};
|
|
41
|
+
type InitArgs = CommonArgs & {
|
|
42
|
+
cwd?: string;
|
|
43
|
+
config?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
async function runInit(argv: InitArgs) {
|
|
47
|
+
const host = argv.host ?? envHost ?? "0.0.0.0";
|
|
48
|
+
const pm = new ProcessManager({
|
|
49
|
+
cwd: argv.cwd,
|
|
50
|
+
configPath: argv.config,
|
|
51
|
+
server: { port: argv.port ?? defaultPort, host },
|
|
52
|
+
});
|
|
53
|
+
pm.serve();
|
|
54
|
+
await pm.init();
|
|
55
|
+
pm.startAll();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createRpcClient(argv: CommonArgs) {
|
|
59
|
+
const host = argv.host ?? envHost ?? "127.0.0.1";
|
|
60
|
+
const port = argv.port ?? defaultPort;
|
|
61
|
+
return createClient({ url: `http://${host}:${port}/rpc` });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
const program = new Command();
|
|
66
|
+
program
|
|
67
|
+
.name("pid1")
|
|
68
|
+
.description("Process manager daemon and controller")
|
|
69
|
+
.option("--port <number>", "Port for the HTTP server", (value) =>
|
|
70
|
+
Number(value),
|
|
71
|
+
)
|
|
72
|
+
.option("--host <host>", "Host for the HTTP server");
|
|
73
|
+
|
|
74
|
+
const commonOptions = (): CommonArgs => {
|
|
75
|
+
const opts = program.opts<CommonArgs>();
|
|
76
|
+
return {
|
|
77
|
+
port: opts.port,
|
|
78
|
+
host: opts.host,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
program
|
|
83
|
+
.command("init")
|
|
84
|
+
.description("Start the daemon, run tasks, and start all processes")
|
|
85
|
+
.option("--cwd <path>", "Working directory for config lookup")
|
|
86
|
+
.option("--config <path>", "Path to config file")
|
|
87
|
+
.action(async (options: InitArgs) => {
|
|
88
|
+
await runInit({ ...commonOptions(), ...options });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.command("start [name]")
|
|
93
|
+
.description("Start all processes, or a specific process by name or id")
|
|
94
|
+
.action(async (name?: string) => {
|
|
95
|
+
const client = createRpcClient(commonOptions());
|
|
96
|
+
const result = await client.process.start({ name });
|
|
97
|
+
console.log(result.message);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
program
|
|
101
|
+
.command("stop [name]")
|
|
102
|
+
.description("Stop all processes, or a specific process by name or id")
|
|
103
|
+
.action(async (name?: string) => {
|
|
104
|
+
const client = createRpcClient(commonOptions());
|
|
105
|
+
const result = await client.process.stop({ name });
|
|
106
|
+
console.log(result.message);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
program
|
|
110
|
+
.command("restart [name]")
|
|
111
|
+
.description("Restart all processes, or a specific process by name or id")
|
|
112
|
+
.action(async (name?: string) => {
|
|
113
|
+
const client = createRpcClient(commonOptions());
|
|
114
|
+
const result = await client.process.restart({ name });
|
|
115
|
+
console.log(result.message);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
program
|
|
119
|
+
.command("delete [name]")
|
|
120
|
+
.description("Delete all processes, or a specific process by name or id")
|
|
121
|
+
.action(async (name?: string) => {
|
|
122
|
+
const client = createRpcClient(commonOptions());
|
|
123
|
+
const result = await client.process.delete({ name });
|
|
124
|
+
console.log(result.message);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
program
|
|
128
|
+
.command("list")
|
|
129
|
+
.description("List all managed processes")
|
|
130
|
+
.action(async () => {
|
|
131
|
+
const client = createRpcClient(commonOptions());
|
|
132
|
+
const processes = await client.process.list({});
|
|
133
|
+
formatTable(processes);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
program
|
|
137
|
+
.command("get <name>")
|
|
138
|
+
.description("Get a process by name or id")
|
|
139
|
+
.action(async (name: string) => {
|
|
140
|
+
const client = createRpcClient(commonOptions());
|
|
141
|
+
const process = await client.process.get({ name });
|
|
142
|
+
if (!process) {
|
|
143
|
+
console.log(`Process not found: ${name}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
formatTable([process]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (process.argv.length <= 2) {
|
|
150
|
+
program.outputHelp();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await program.parseAsync(process.argv);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
main().catch((error) => {
|
|
158
|
+
if (error.cause?.code === "ECONNREFUSED") {
|
|
159
|
+
console.error(
|
|
160
|
+
"Error: Could not connect to pid1 daemon. Is it running? Start it with: pid1 init",
|
|
161
|
+
);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
logger.error(
|
|
165
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
166
|
+
);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { globSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tsImport } from "tsx/esm/api";
|
|
4
|
+
import * as v from "valibot";
|
|
5
|
+
import { globalLogger } from "./logger";
|
|
6
|
+
|
|
7
|
+
const logger = globalLogger.child("config");
|
|
8
|
+
const EnvFileOption = v.optional(v.union([v.string(), v.literal(false)]));
|
|
9
|
+
const RestartPolicySchema = v.picklist(["never", "always", "on-failure"]);
|
|
10
|
+
|
|
11
|
+
const ProcessConfigSchema = v.object({
|
|
12
|
+
name: v.string(),
|
|
13
|
+
command: v.string(),
|
|
14
|
+
args: v.optional(v.array(v.string()), []),
|
|
15
|
+
env: v.optional(v.record(v.string(), v.string()), {}),
|
|
16
|
+
cwd: v.optional(v.string()),
|
|
17
|
+
envFile: EnvFileOption,
|
|
18
|
+
restartOnEnvChange: v.optional(v.boolean(), true),
|
|
19
|
+
restartPolicy: v.optional(RestartPolicySchema, "on-failure"),
|
|
20
|
+
maxRestarts: v.optional(v.number(), 10),
|
|
21
|
+
restartMinDelayMs: v.optional(v.number(), 5000),
|
|
22
|
+
restartMaxDelayMs: v.optional(v.number(), 60000),
|
|
23
|
+
restartResetSeconds: v.optional(v.number(), 300),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const Config = v.object({
|
|
27
|
+
envFile: EnvFileOption,
|
|
28
|
+
restartOnEnvChange: v.optional(v.boolean(), true),
|
|
29
|
+
restartMinDelayMs: v.optional(v.number(), 5000),
|
|
30
|
+
restartMaxDelayMs: v.optional(v.number(), 60000),
|
|
31
|
+
tasks: v.optional(v.array(ProcessConfigSchema), []),
|
|
32
|
+
processes: v.optional(v.array(ProcessConfigSchema), []),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
type ConfigInput = v.InferInput<typeof Config>;
|
|
36
|
+
|
|
37
|
+
export type LoadedConfig = v.InferOutput<typeof Config> & {
|
|
38
|
+
configPath: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export async function loadConfigFile(
|
|
42
|
+
path?: string,
|
|
43
|
+
cwd?: string,
|
|
44
|
+
): Promise<LoadedConfig> {
|
|
45
|
+
const configFile = path ?? findConfigFile(cwd ?? process.cwd());
|
|
46
|
+
if (!configFile) {
|
|
47
|
+
logger.error(
|
|
48
|
+
`Failed to find a config file in ${cwd ?? process.cwd()} or any of its parent directories`,
|
|
49
|
+
);
|
|
50
|
+
logger.error("Exiting...");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
logger.info(`Trying to load config from ${configFile}`);
|
|
55
|
+
const config = await tsImport(configFile, {
|
|
56
|
+
parentURL: import.meta.url,
|
|
57
|
+
})
|
|
58
|
+
.then((m) =>
|
|
59
|
+
"default" in m.default ? m.default.default : m.default,
|
|
60
|
+
)
|
|
61
|
+
.catch((error) => {
|
|
62
|
+
logger.error(`Failed to load config from ${configFile}`);
|
|
63
|
+
logger.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
64
|
+
logger.error("Exiting...");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const res = await v.safeParseAsync(Config, config);
|
|
69
|
+
if (!res.success) {
|
|
70
|
+
logger.error(`Config validation failed`);
|
|
71
|
+
logger.error(v.summarize(res.issues));
|
|
72
|
+
logger.error("Exiting...");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { ...res.output, configPath: configFile };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function findConfigFile(cwd: string) {
|
|
80
|
+
const files = globSync("pid1.config.{ts,js,mts,mjs}", { cwd });
|
|
81
|
+
if (files.length === 0 && join(cwd) !== "/") {
|
|
82
|
+
return findConfigFile(join(cwd, ".."));
|
|
83
|
+
}
|
|
84
|
+
return files[0] ? join(cwd, files[0]) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function defineConfig(config: ConfigInput) {
|
|
88
|
+
return config;
|
|
89
|
+
}
|