pidnap 0.0.0-dev.3 → 0.0.0-dev.5

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger-BU2RmetS.mjs","names":["setTimeout"],"sources":["../src/lazy-process.ts","../src/restarting-process.ts","../src/cron-process.ts","../src/task-list.ts","../src/env-manager.ts","../src/logger.ts"],"sourcesContent":["import * as v from \"valibot\";\nimport type { Logger } from \"./logger.ts\";\nimport { setTimeout } from \"node:timers/promises\";\nimport { createInterface } from \"node:readline/promises\";\nimport { spawn, type ChildProcess } from \"node:child_process\";\n\nexport const ProcessDefinition = v.object({\n command: v.string(),\n args: v.optional(v.array(v.string())),\n cwd: v.optional(v.string()),\n env: v.optional(v.record(v.string(), v.string())),\n});\nexport type ProcessDefinition = v.InferOutput<typeof ProcessDefinition>;\n\nexport const ProcessState = v.picklist([\n \"idle\",\n \"starting\",\n \"running\",\n \"stopping\",\n \"stopped\",\n \"error\",\n]);\nexport type ProcessState = v.InferOutput<typeof ProcessState>;\n\n/**\n * Kill a process group (the process and all its descendants).\n * Uses negative PID to target the entire process group.\n */\nfunction killProcessGroup(pid: number, signal: NodeJS.Signals): boolean {\n try {\n // Negative PID kills the entire process group\n process.kill(-pid, signal);\n return true;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n // ESRCH = No such process (already dead)\n // EPERM = Permission denied (might happen if some children already exited)\n if (code === \"ESRCH\" || code === \"EPERM\") {\n return true;\n }\n return false;\n }\n}\n\nexport class LazyProcess {\n readonly name: string;\n definition: ProcessDefinition;\n private logger: Logger;\n private childProcess: ChildProcess | null = null;\n private _state: ProcessState = \"idle\";\n private processExit = Promise.withResolvers<void>();\n public exitCode: number | null = null;\n\n constructor(name: string, definition: ProcessDefinition, logger: Logger) {\n this.name = name;\n this.definition = definition;\n this.logger = logger.withPrefix(\"SYS\");\n }\n\n get state(): ProcessState {\n return this._state;\n }\n\n async start() {\n if (this._state === \"running\" || this._state === \"starting\") {\n throw new Error(`Process \"${this.name}\" is already ${this._state}`);\n }\n\n if (this._state === \"stopping\") {\n throw new Error(`Process \"${this.name}\" is currently stopping`);\n }\n\n this._state = \"starting\";\n this.processExit = Promise.withResolvers<void>();\n this.logger.debug(`Starting process: ${this.definition.command}`);\n\n try {\n const env = this.definition.env ? { ...process.env, ...this.definition.env } : process.env;\n\n this.childProcess = spawn(this.definition.command, this.definition.args ?? [], {\n cwd: this.definition.cwd,\n env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n detached: true,\n });\n\n this._state = \"running\";\n\n if (this.childProcess.stdout) {\n const rl = createInterface({ input: this.childProcess.stdout });\n rl.on(\"line\", (line) => this.logger.withPrefix(\"OUT\").info(line));\n this.processExit.promise.then(() => rl.close());\n }\n\n if (this.childProcess.stderr) {\n const rl = createInterface({ input: this.childProcess.stderr });\n rl.on(\"line\", (line) => this.logger.withPrefix(\"ERR\").info(line));\n this.processExit.promise.then(() => rl.close());\n }\n\n this.childProcess.on(\"exit\", (code, signal) => {\n this.exitCode = code;\n\n if (this._state === \"running\") {\n if (code === 0) {\n this._state = \"stopped\";\n this.logger.info(`Process exited with code ${code}`);\n } else if (signal) {\n this._state = \"stopped\";\n this.logger.info(`Process killed with signal ${signal}`);\n } else {\n this._state = \"error\";\n this.logger.error(`Process exited with code ${code}`);\n }\n }\n\n this.processExit.resolve();\n });\n\n this.childProcess.on(\"error\", (err) => {\n if (this._state !== \"stopping\" && this._state !== \"stopped\") {\n this._state = \"error\";\n this.logger.error(`Process error:`, err);\n }\n this.processExit.resolve();\n });\n } catch (err) {\n this._state = \"error\";\n this.logger.error(`Failed to start process:`, err);\n throw err;\n }\n }\n\n async stop(timeout?: number): Promise<void> {\n if (this._state === \"idle\" || this._state === \"stopped\" || this._state === \"error\") {\n return;\n }\n\n if (this._state === \"stopping\") {\n // Already stopping, wait for completion\n await this.processExit.promise;\n return;\n }\n\n if (!this.childProcess) {\n this._state = \"stopped\";\n return;\n }\n\n this._state = \"stopping\";\n const pid = this.childProcess.pid;\n\n if (pid === undefined) {\n this._state = \"stopped\";\n this.cleanup();\n return;\n }\n\n this.logger.debug(`Stopping process group (pid: ${pid}) with SIGTERM`);\n\n const timeoutMs = timeout ?? 5000;\n const resultRace = Promise.race([\n this.processExit.promise.then(() => \"exited\" as const),\n setTimeout(timeoutMs, \"timeout\"),\n ]);\n\n killProcessGroup(pid, \"SIGTERM\");\n\n this.logger.debug(`Waiting for process exit (timeout: ${timeoutMs}ms)`);\n const result = await resultRace;\n this.logger.debug(`process exit result: ${result}`);\n\n if (result === \"timeout\") {\n this.logger.warn(`Process did not exit within ${timeoutMs}ms, sending SIGKILL (pid: ${pid})`);\n const killed = killProcessGroup(pid, \"SIGKILL\");\n this.logger.info(`SIGKILL sent to process group: ${killed ? \"success\" : \"failed\"}`);\n\n const killTimeout = setTimeout(1000, \"timeout\");\n await Promise.race([this.processExit.promise, killTimeout]);\n\n if (this.childProcess && !this.childProcess.killed) {\n this.logger.error(`Process still alive after SIGKILL (pid: ${pid})`);\n }\n }\n\n this._state = \"stopped\";\n this.cleanup();\n this.logger.info(`Process stopped`);\n }\n\n async reset(): Promise<void> {\n if (this.childProcess?.pid !== undefined) {\n // Kill the entire process group\n killProcessGroup(this.childProcess.pid, \"SIGKILL\");\n await this.processExit.promise;\n this.cleanup();\n }\n\n this._state = \"idle\";\n // Create a fresh promise for the next process lifecycle\n this.processExit = Promise.withResolvers<void>();\n this.logger.info(`Process reset to idle`);\n }\n\n updateDefinition(definition: ProcessDefinition): void {\n this.definition = definition;\n }\n\n async waitForExit(): Promise<ProcessState> {\n if (!this.childProcess) return this._state;\n await this.processExit.promise;\n return this._state;\n }\n\n private cleanup(): void {\n if (this.childProcess) {\n this.childProcess.removeAllListeners();\n this.childProcess = null;\n }\n\n this.exitCode = null;\n }\n}\n","import * as v from \"valibot\";\nimport { LazyProcess, type ProcessDefinition, type ProcessState } from \"./lazy-process.ts\";\nimport type { Logger } from \"./logger.ts\";\n\nexport const RestartPolicy = v.picklist([\n \"always\",\n \"on-failure\",\n \"never\",\n \"unless-stopped\",\n \"on-success\",\n]);\nexport type RestartPolicy = v.InferOutput<typeof RestartPolicy>;\n\nexport const BackoffStrategy = v.union([\n v.object({\n type: v.literal(\"fixed\"),\n delayMs: v.number(),\n }),\n v.object({\n type: v.literal(\"exponential\"),\n initialDelayMs: v.number(),\n maxDelayMs: v.number(),\n multiplier: v.optional(v.number()),\n }),\n]);\nexport type BackoffStrategy = v.InferOutput<typeof BackoffStrategy>;\n\nexport const CrashLoopConfig = v.object({\n maxRestarts: v.number(),\n windowMs: v.number(),\n backoffMs: v.number(),\n});\nexport type CrashLoopConfig = v.InferOutput<typeof CrashLoopConfig>;\n\nexport const RestartingProcessOptions = v.object({\n restartPolicy: RestartPolicy,\n backoff: v.optional(BackoffStrategy),\n crashLoop: v.optional(CrashLoopConfig),\n minUptimeMs: v.optional(v.number()),\n maxTotalRestarts: v.optional(v.number()),\n});\nexport type RestartingProcessOptions = v.InferOutput<typeof RestartingProcessOptions>;\n\nexport const RestartingProcessState = v.picklist([\n \"idle\",\n \"running\",\n \"restarting\",\n \"stopping\",\n \"stopped\",\n \"crash-loop-backoff\",\n \"max-restarts-reached\",\n]);\nexport type RestartingProcessState = v.InferOutput<typeof RestartingProcessState>;\n\nconst DEFAULT_BACKOFF: BackoffStrategy = { type: \"fixed\", delayMs: 1000 };\nconst DEFAULT_CRASH_LOOP: CrashLoopConfig = { maxRestarts: 5, windowMs: 60000, backoffMs: 60000 };\n\nexport class RestartingProcess {\n readonly name: string;\n readonly lazyProcess: LazyProcess;\n private options: Required<Omit<RestartingProcessOptions, \"maxTotalRestarts\">> & {\n maxTotalRestarts?: number;\n };\n private logger: Logger;\n\n // State tracking\n private _state: RestartingProcessState = \"idle\";\n private _restartCount: number = 0;\n private restartTimestamps: number[] = []; // For crash loop detection\n private consecutiveFailures: number = 0; // For exponential backoff\n private lastStartTime: number | null = null;\n private stopRequested: boolean = false;\n private pendingDelayTimeout: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n name: string,\n definition: ProcessDefinition,\n options: RestartingProcessOptions,\n logger: Logger,\n ) {\n this.name = name;\n this.logger = logger;\n this.options = {\n restartPolicy: options.restartPolicy,\n backoff: options.backoff ?? DEFAULT_BACKOFF,\n crashLoop: options.crashLoop ?? DEFAULT_CRASH_LOOP,\n minUptimeMs: options.minUptimeMs ?? 0,\n maxTotalRestarts: options.maxTotalRestarts,\n };\n this.lazyProcess = new LazyProcess(name, definition, logger);\n }\n\n get state(): RestartingProcessState {\n return this._state;\n }\n\n get restarts(): number {\n return this._restartCount;\n }\n\n start(): void {\n if (this._state === \"running\" || this._state === \"restarting\") {\n throw new Error(`Process \"${this.name}\" is already ${this._state}`);\n }\n\n if (this._state === \"stopping\") {\n throw new Error(`Process \"${this.name}\" is currently stopping`);\n }\n\n // Fresh start from terminal states - reset counters\n if (\n this._state === \"stopped\" ||\n this._state === \"idle\" ||\n this._state === \"max-restarts-reached\"\n ) {\n this.resetCounters();\n }\n\n this.stopRequested = false;\n this.startProcess();\n }\n\n async stop(timeout?: number): Promise<void> {\n this.stopRequested = true;\n\n // Clear any pending delays\n if (this.pendingDelayTimeout) {\n clearTimeout(this.pendingDelayTimeout);\n this.pendingDelayTimeout = null;\n }\n\n if (\n this._state === \"idle\" ||\n this._state === \"stopped\" ||\n this._state === \"max-restarts-reached\"\n ) {\n this._state = \"stopped\";\n return;\n }\n\n this._state = \"stopping\";\n await this.lazyProcess.stop(timeout);\n this._state = \"stopped\";\n this.logger.info(`RestartingProcess stopped`);\n }\n\n async restart(force: boolean = false): Promise<void> {\n // Fresh start from terminal states - reset counters and no delay\n if (\n this._state === \"stopped\" ||\n this._state === \"idle\" ||\n this._state === \"max-restarts-reached\"\n ) {\n this.resetCounters();\n this.stopRequested = false;\n this.startProcess();\n return;\n }\n\n // Stop the current process first\n await this.stop();\n\n this.stopRequested = false;\n\n if (force) {\n // Force restart - no delay\n this.startProcess();\n } else {\n // Follow normal delay strategy\n const delay = this.calculateDelay();\n if (delay > 0) {\n this._state = \"restarting\";\n this.logger.info(`Restarting in ${delay}ms`);\n await this.delay(delay);\n if (this.stopRequested) return;\n }\n this.startProcess();\n }\n }\n\n /**\n * Update process definition and optionally restart with new config\n */\n async reload(\n newDefinition: ProcessDefinition,\n restartImmediately: boolean = true,\n ): Promise<void> {\n this.logger.info(`Reloading process with new definition`);\n this.lazyProcess.updateDefinition(newDefinition);\n\n if (restartImmediately) {\n // Restart with force=true to apply changes immediately\n await this.restart(true);\n }\n }\n\n /**\n * Update restart options\n */\n updateOptions(newOptions: Partial<RestartingProcessOptions>): void {\n this.logger.info(`Updating restart options`);\n this.options = {\n ...this.options,\n restartPolicy: newOptions.restartPolicy ?? this.options.restartPolicy,\n backoff: newOptions.backoff ?? this.options.backoff,\n crashLoop: newOptions.crashLoop ?? this.options.crashLoop,\n minUptimeMs: newOptions.minUptimeMs ?? this.options.minUptimeMs,\n maxTotalRestarts: newOptions.maxTotalRestarts ?? this.options.maxTotalRestarts,\n };\n }\n\n private resetCounters(): void {\n this._restartCount = 0;\n this.consecutiveFailures = 0;\n this.restartTimestamps = [];\n }\n\n private startProcess(): void {\n this.lastStartTime = Date.now();\n this._state = \"running\";\n\n this.lazyProcess\n .reset()\n .then(async () => {\n if (this.stopRequested) return;\n await this.lazyProcess.start();\n return this.lazyProcess.waitForExit();\n })\n .then((exitState) => {\n if (!exitState) return;\n if (this.stopRequested && exitState === \"error\") {\n this._state = \"stopped\";\n return;\n }\n if (exitState === \"stopped\" || exitState === \"error\") {\n this.handleProcessExit(exitState);\n }\n })\n .catch((err) => {\n if (this.stopRequested) return;\n this._state = \"stopped\";\n this.logger.error(`Failed to start process:`, err);\n });\n }\n\n private handleProcessExit(exitState: ProcessState): void {\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n\n const uptime = this.lastStartTime ? Date.now() - this.lastStartTime : 0;\n const wasHealthy = uptime >= this.options.minUptimeMs;\n const exitedWithError = exitState === \"error\";\n\n // Reset consecutive failures if the process ran long enough\n if (wasHealthy) {\n this.consecutiveFailures = 0;\n } else {\n this.consecutiveFailures++;\n }\n\n // Check if policy allows restart\n if (!this.shouldRestart(exitedWithError)) {\n this._state = \"stopped\";\n this.logger.info(\n `Process exited, policy \"${this.options.restartPolicy}\" does not allow restart`,\n );\n return;\n }\n\n // Check max total restarts\n if (\n this.options.maxTotalRestarts !== undefined &&\n this._restartCount >= this.options.maxTotalRestarts\n ) {\n this._state = \"max-restarts-reached\";\n this.logger.warn(`Max total restarts (${this.options.maxTotalRestarts}) reached`);\n return;\n }\n\n // Record restart timestamp for crash loop detection\n const now = Date.now();\n this.restartTimestamps.push(now);\n\n // Check for crash loop\n if (this.isInCrashLoop()) {\n this._state = \"crash-loop-backoff\";\n this.logger.warn(\n `Crash loop detected (${this.options.crashLoop.maxRestarts} restarts in ${this.options.crashLoop.windowMs}ms), backing off for ${this.options.crashLoop.backoffMs}ms`,\n );\n this.scheduleCrashLoopRecovery();\n return;\n }\n\n // Schedule restart with delay\n this._restartCount++;\n this.scheduleRestart();\n }\n\n private shouldRestart(exitedWithError: boolean): boolean {\n switch (this.options.restartPolicy) {\n case \"always\":\n return true;\n case \"never\":\n return false;\n case \"on-failure\":\n return exitedWithError;\n case \"on-success\":\n return !exitedWithError;\n case \"unless-stopped\":\n return !this.stopRequested;\n default:\n return false;\n }\n }\n\n private isInCrashLoop(): boolean {\n const { maxRestarts, windowMs } = this.options.crashLoop;\n const now = Date.now();\n const cutoff = now - windowMs;\n\n // Clean up old timestamps\n this.restartTimestamps = this.restartTimestamps.filter((ts) => ts > cutoff);\n\n return this.restartTimestamps.length >= maxRestarts;\n }\n\n private calculateDelay(): number {\n const { backoff } = this.options;\n\n if (backoff.type === \"fixed\") {\n return backoff.delayMs;\n }\n\n // Exponential backoff\n const multiplier = backoff.multiplier ?? 2;\n const delay = backoff.initialDelayMs * Math.pow(multiplier, this.consecutiveFailures);\n return Math.min(delay, backoff.maxDelayMs);\n }\n\n private scheduleRestart(): void {\n this._state = \"restarting\";\n const delay = this.calculateDelay();\n\n this.logger.info(`Restarting in ${delay}ms (restart #${this._restartCount})`);\n\n this.pendingDelayTimeout = setTimeout(() => {\n this.pendingDelayTimeout = null;\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n this.startProcess();\n }, delay);\n }\n\n private scheduleCrashLoopRecovery(): void {\n const { backoffMs } = this.options.crashLoop;\n\n this.pendingDelayTimeout = setTimeout(() => {\n this.pendingDelayTimeout = null;\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n\n // Reset crash loop timestamps after backoff\n this.restartTimestamps = [];\n this._restartCount++;\n this.logger.info(`Crash loop backoff complete, restarting (restart #${this._restartCount})`);\n this.startProcess();\n }, backoffMs);\n }\n\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => {\n this.pendingDelayTimeout = setTimeout(() => {\n this.pendingDelayTimeout = null;\n resolve();\n }, ms);\n });\n }\n}\n","import { Cron } from \"croner\";\nimport * as v from \"valibot\";\nimport { LazyProcess, type ProcessDefinition } from \"./lazy-process.ts\";\nimport type { Logger } from \"./logger.ts\";\n\nexport const RetryConfig = v.object({\n maxRetries: v.number(),\n delayMs: v.optional(v.number()),\n});\nexport type RetryConfig = v.InferOutput<typeof RetryConfig>;\n\nexport const CronProcessOptions = v.object({\n schedule: v.string(),\n retry: v.optional(RetryConfig),\n runOnStart: v.optional(v.boolean()),\n});\nexport type CronProcessOptions = v.InferOutput<typeof CronProcessOptions>;\n\nexport const CronProcessState = v.picklist([\n \"idle\",\n \"scheduled\",\n \"running\",\n \"retrying\",\n \"queued\",\n \"stopping\",\n \"stopped\",\n]);\nexport type CronProcessState = v.InferOutput<typeof CronProcessState>;\n\nconst DEFAULT_RETRY_DELAY = 1000;\n\nexport class CronProcess {\n readonly name: string;\n readonly lazyProcess: LazyProcess;\n private options: CronProcessOptions;\n private logger: Logger;\n private cronJob: Cron | null = null;\n\n private _state: CronProcessState = \"idle\";\n private _runCount: number = 0;\n private _failCount: number = 0;\n private currentRetryAttempt: number = 0;\n private queuedRun: boolean = false;\n private stopRequested: boolean = false;\n private retryTimeout: ReturnType<typeof setTimeout> | null = null;\n\n constructor(\n name: string,\n definition: ProcessDefinition,\n options: CronProcessOptions,\n logger: Logger,\n ) {\n this.name = name;\n this.options = options;\n this.logger = logger;\n this.lazyProcess = new LazyProcess(name, definition, logger);\n }\n\n get state(): CronProcessState {\n return this._state;\n }\n\n get runCount(): number {\n return this._runCount;\n }\n\n get failCount(): number {\n return this._failCount;\n }\n\n get nextRun(): Date | null {\n if (!this.cronJob) return null;\n const next = this.cronJob.nextRun();\n return next ?? null;\n }\n\n start(): void {\n if (this._state === \"scheduled\" || this._state === \"running\" || this._state === \"queued\") {\n throw new Error(`CronProcess \"${this.name}\" is already ${this._state}`);\n }\n\n if (this._state === \"stopping\") {\n throw new Error(`CronProcess \"${this.name}\" is currently stopping`);\n }\n\n this.stopRequested = false;\n this.logger.info(`Starting cron schedule: ${this.options.schedule}`);\n\n this.cronJob = new Cron(this.options.schedule, { timezone: \"UTC\" }, () => {\n this.onCronTick();\n });\n\n this._state = \"scheduled\";\n\n if (this.options.runOnStart) {\n this.executeJob();\n }\n }\n\n async stop(timeout?: number): Promise<void> {\n this.stopRequested = true;\n\n // Stop the cron job\n if (this.cronJob) {\n this.cronJob.stop();\n this.cronJob = null;\n }\n\n // Clear any pending retry timeout\n if (this.retryTimeout) {\n clearTimeout(this.retryTimeout);\n this.retryTimeout = null;\n }\n\n if (this._state === \"idle\" || this._state === \"stopped\") {\n this._state = \"stopped\";\n return;\n }\n\n // If running, stop the current job\n if (this._state === \"running\" || this._state === \"retrying\" || this._state === \"queued\") {\n this._state = \"stopping\";\n await this.lazyProcess.stop(timeout);\n }\n\n this._state = \"stopped\";\n this.queuedRun = false;\n this.logger.info(`CronProcess stopped`);\n }\n\n async trigger(): Promise<void> {\n if (this.stopRequested) {\n throw new Error(`CronProcess \"${this.name}\" is stopped`);\n }\n\n // If already queued, just return (already have a run pending)\n if (this._state === \"queued\") {\n return;\n }\n\n // If already running, queue this trigger\n if (this._state === \"running\" || this._state === \"retrying\") {\n this.queuedRun = true;\n this._state = \"queued\";\n this.logger.info(`Run queued (current job still running)`);\n return;\n }\n\n await this.executeJob();\n }\n\n private onCronTick(): void {\n if (this.stopRequested) return;\n\n // If already running, queue the next run\n if (this._state === \"running\" || this._state === \"retrying\" || this._state === \"queued\") {\n this.queuedRun = true;\n if (this._state !== \"queued\") {\n this._state = \"queued\";\n }\n this.logger.info(`Cron tick: run queued (current job still running)`);\n return;\n }\n\n this.executeJob();\n }\n\n private async executeJob(): Promise<void> {\n if (this.stopRequested) return;\n\n this._state = \"running\";\n this.currentRetryAttempt = 0;\n this.logger.info(`Executing job`);\n\n await this.runJobWithRetry();\n }\n\n private async runJobWithRetry(): Promise<void> {\n if (this.stopRequested) return;\n\n // Reset and start the process\n await this.lazyProcess.reset();\n await this.lazyProcess.start();\n\n const exitState = await this.lazyProcess.waitForExit();\n if (this.stopRequested && exitState === \"error\") {\n this._state = \"stopped\";\n return;\n }\n this.handleJobComplete(exitState === \"error\");\n }\n\n private handleJobComplete(failed: boolean): void {\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n\n if (failed) {\n const maxRetries = this.options.retry?.maxRetries ?? 0;\n\n if (this.currentRetryAttempt < maxRetries) {\n // Retry\n this.currentRetryAttempt++;\n this._state = \"retrying\";\n const delayMs = this.options.retry?.delayMs ?? DEFAULT_RETRY_DELAY;\n\n this.logger.warn(\n `Job failed, retrying in ${delayMs}ms (attempt ${this.currentRetryAttempt}/${maxRetries})`,\n );\n\n this.retryTimeout = setTimeout(() => {\n this.retryTimeout = null;\n if (this.stopRequested) {\n this._state = \"stopped\";\n return;\n }\n this.runJobWithRetry();\n }, delayMs);\n return;\n }\n\n // All retries exhausted\n this._failCount++;\n this.logger.error(`Job failed after ${this.currentRetryAttempt} retries`);\n } else {\n this._runCount++;\n this.logger.info(`Job completed successfully`);\n }\n\n // Check for queued run\n if (this.queuedRun) {\n this.queuedRun = false;\n this.logger.info(`Starting queued run`);\n this.executeJob();\n return;\n }\n\n // Back to scheduled state\n if (this.cronJob) {\n this._state = \"scheduled\";\n } else {\n this._state = \"stopped\";\n }\n }\n}\n","import * as v from \"valibot\";\nimport { LazyProcess, ProcessDefinition } from \"./lazy-process.ts\";\nimport type { Logger } from \"./logger.ts\";\n\n// Per-task state\nexport const TaskStateSchema = v.picklist([\"pending\", \"running\", \"completed\", \"failed\", \"skipped\"]);\n\nexport type TaskState = v.InferOutput<typeof TaskStateSchema>;\n\n// Schema for named process definition\nexport const NamedProcessDefinitionSchema = v.object({\n name: v.string(),\n process: ProcessDefinition,\n});\n\nexport type NamedProcessDefinition = v.InferOutput<typeof NamedProcessDefinitionSchema>;\n\n// A task entry (single or parallel processes) with its state\nexport interface TaskEntry {\n id: string; // Unique task ID\n processes: NamedProcessDefinition[]; // Array (length 1 = sequential, >1 = parallel)\n state: TaskState;\n}\n\n// Simple TaskList state (just running or not)\nexport type TaskListState = \"idle\" | \"running\" | \"stopped\";\n\nexport class TaskList {\n readonly name: string;\n private _tasks: TaskEntry[] = [];\n private _state: TaskListState = \"idle\";\n private logger: Logger;\n private logFileResolver?: (processName: string) => string | undefined;\n private taskIdCounter: number = 0;\n private runningProcesses: LazyProcess[] = [];\n private stopRequested: boolean = false;\n private runLoopPromise: Promise<void> | null = null;\n\n constructor(\n name: string,\n logger: Logger,\n initialTasks?: (NamedProcessDefinition | NamedProcessDefinition[])[],\n logFileResolver?: (processName: string) => string | undefined,\n ) {\n this.name = name;\n this.logger = logger;\n this.logFileResolver = logFileResolver;\n\n // Add initial tasks if provided\n if (initialTasks) {\n for (const task of initialTasks) {\n this.addTask(task);\n }\n }\n }\n\n get state(): TaskListState {\n return this._state;\n }\n\n get tasks(): ReadonlyArray<TaskEntry> {\n return this._tasks;\n }\n\n removeTaskByTarget(target: string | number): TaskEntry {\n const index =\n typeof target === \"number\" ? target : this._tasks.findIndex((t) => t.id === target);\n if (index < 0 || index >= this._tasks.length) {\n throw new Error(`Task not found: ${target}`);\n }\n\n const task = this._tasks[index];\n if (task.state === \"running\") {\n throw new Error(`Cannot remove running task: ${task.id}`);\n }\n\n this._tasks.splice(index, 1);\n this.logger.info(`Task \"${task.id}\" removed`);\n return task;\n }\n\n /**\n * Add a single process or parallel processes as a new task\n * @returns The unique task ID\n */\n addTask(task: NamedProcessDefinition | NamedProcessDefinition[]): string {\n const id = `task-${++this.taskIdCounter}`;\n const processes = Array.isArray(task) ? task : [task];\n\n const entry: TaskEntry = {\n id,\n processes,\n state: \"pending\",\n };\n\n this._tasks.push(entry);\n this.logger.info(`Task \"${id}\" added with ${processes.length} process(es)`);\n\n return id;\n }\n\n /**\n * Begin executing pending tasks\n */\n start(): void {\n if (this._state === \"running\") {\n throw new Error(`TaskList \"${this.name}\" is already running`);\n }\n\n this.stopRequested = false;\n this._state = \"running\";\n this.logger.info(`TaskList started`);\n\n // Start the run loop (non-blocking)\n this.runLoopPromise = this.runLoop();\n }\n\n /**\n * Wait until the TaskList becomes idle (all pending tasks completed)\n */\n async waitUntilIdle(): Promise<void> {\n if (this._state === \"idle\" || this._state === \"stopped\") {\n return;\n }\n\n // Wait for the run loop to complete\n if (this.runLoopPromise) {\n await this.runLoopPromise;\n }\n }\n\n /**\n * Stop execution and mark remaining tasks as skipped\n */\n async stop(timeout?: number): Promise<void> {\n if (this._state === \"idle\" || this._state === \"stopped\") {\n this._state = \"stopped\";\n return;\n }\n\n this.stopRequested = true;\n this.logger.info(`Stopping TaskList...`);\n\n // Stop all currently running processes\n const stopPromises = this.runningProcesses.map((p) => p.stop(timeout));\n await Promise.all(stopPromises);\n this.runningProcesses = [];\n\n // Mark all pending tasks as skipped\n for (const task of this._tasks) {\n if (task.state === \"pending\") {\n task.state = \"skipped\";\n }\n }\n\n // Wait for run loop to finish\n if (this.runLoopPromise) {\n await this.runLoopPromise;\n this.runLoopPromise = null;\n }\n\n this._state = \"stopped\";\n this.logger.info(`TaskList stopped`);\n }\n\n private async runLoop(): Promise<void> {\n while (this._state === \"running\" && !this.stopRequested) {\n // Find the next pending task\n const nextTask = this._tasks.find((t) => t.state === \"pending\");\n\n if (!nextTask) {\n // No more pending tasks, go back to idle\n this._state = \"idle\";\n this.logger.info(`All tasks completed, TaskList is idle`);\n break;\n }\n\n await this.executeTask(nextTask);\n }\n }\n\n private async executeTask(task: TaskEntry): Promise<void> {\n if (this.stopRequested) {\n task.state = \"skipped\";\n return;\n }\n\n task.state = \"running\";\n const taskNames = task.processes.map((p) => p.name).join(\", \");\n this.logger.info(`Executing task \"${task.id}\": [${taskNames}]`);\n\n // Create LazyProcess instances for each process in the task\n const lazyProcesses: LazyProcess[] = task.processes.map((p) => {\n const logFile = this.logFileResolver?.(p.name);\n const childLogger = logFile\n ? this.logger.child(p.name, { logFile })\n : this.logger.child(p.name);\n return new LazyProcess(p.name, p.process, childLogger);\n });\n\n this.runningProcesses = lazyProcesses;\n\n try {\n // Start all processes (parallel if multiple)\n await Promise.all(lazyProcesses.map((lp) => lp.start()));\n\n // Wait for all processes to complete\n const results = await Promise.all(lazyProcesses.map((lp) => this.waitForProcess(lp)));\n\n // Check if any failed\n const anyFailed = results.some((r) => r === \"error\");\n\n if (this.stopRequested) {\n task.state = \"skipped\";\n } else if (anyFailed) {\n task.state = \"failed\";\n this.logger.warn(`Task \"${task.id}\" failed`);\n } else {\n task.state = \"completed\";\n this.logger.info(`Task \"${task.id}\" completed`);\n }\n } catch (err) {\n task.state = \"failed\";\n this.logger.error(`Task \"${task.id}\" error:`, err);\n } finally {\n this.runningProcesses = [];\n }\n }\n\n private async waitForProcess(lp: LazyProcess): Promise<\"stopped\" | \"error\"> {\n const state = await lp.waitForExit();\n return state === \"error\" ? \"error\" : \"stopped\";\n }\n}\n","import { parse } from \"dotenv\";\nimport { existsSync, globSync, readFileSync } from \"node:fs\";\nimport { resolve, basename, isAbsolute } from \"node:path\";\nimport { watch } from \"chokidar\";\n\nexport type EnvChangeEvent =\n | {\n type: \"global\";\n }\n | {\n type: \"process\";\n key: string;\n };\n\nexport type EnvChangeCallback = (event: EnvChangeEvent) => void;\n\nexport interface EnvManagerConfig {\n cwd: string;\n globalEnvFile?: string;\n customEnvFiles?: Record<string, string>;\n}\n\nexport class EnvManager {\n private globalEnv: Record<string, string> = {};\n private globalEnvPath: string;\n private env: Map<string, Record<string, string>> = new Map();\n private watchers: Map<string, ReturnType<typeof watch>> = new Map();\n private fileToKey: Map<string, string> = new Map();\n private changeCallbacks: Set<EnvChangeCallback> = new Set();\n private cwdWatcher: ReturnType<typeof watch> | null = null;\n private customKeys: Set<string> = new Set();\n\n constructor(private config: EnvManagerConfig) {\n this.globalEnvPath = config.globalEnvFile\n ? isAbsolute(config.globalEnvFile)\n ? config.globalEnvFile\n : resolve(config.cwd, config.globalEnvFile)\n : resolve(config.cwd, \".env\");\n\n // Load custom env files first (before auto-discovery)\n this.loadCustomEnvFiles();\n this.loadEnvFilesFromCwd();\n this.watchCwdForNewFiles();\n }\n\n /**\n * Register a custom env file for a key.\n * Once registered, auto-discovered .env.{key} files will be ignored for this key.\n */\n public registerFile(key: string, filePath: string): void {\n const absolutePath = isAbsolute(filePath) ? filePath : resolve(this.config.cwd, filePath);\n this.customKeys.add(key);\n this.loadEnvFile(key, absolutePath);\n }\n\n /**\n * Check if a key has a custom env file registered\n */\n public hasCustomFile(key: string): boolean {\n return this.customKeys.has(key);\n }\n\n /**\n * Load custom env files from config\n */\n private loadCustomEnvFiles(): void {\n if (!this.config.customEnvFiles) return;\n\n for (const [key, filePath] of Object.entries(this.config.customEnvFiles)) {\n this.registerFile(key, filePath);\n }\n }\n\n public getEnvVars(key: string): Record<string, string> {\n const specificEnv = this.env.get(key);\n return {\n ...this.globalEnv,\n ...specificEnv,\n };\n }\n\n private loadEnvFilesFromCwd(): void {\n if (existsSync(this.globalEnvPath)) this.loadGlobalEnv(this.globalEnvPath);\n\n try {\n const envFiles = globSync(\".env.*\", { cwd: this.config.cwd });\n for (const filePath of envFiles) {\n const key = this.getEnvKeySuffix(basename(filePath));\n // Skip if key has a custom file registered\n if (key && !this.customKeys.has(key)) {\n this.loadEnvFile(key, resolve(this.config.cwd, filePath));\n }\n }\n } catch (err) {\n console.warn(\"Failed to scan env files:\", err);\n }\n }\n\n private parseEnvFile(absolutePath: string): Record<string, string> | null {\n try {\n const content = readFileSync(absolutePath, \"utf-8\");\n return parse(content) ?? {};\n } catch (err) {\n console.warn(`Failed to parse env file: ${absolutePath}`, err);\n return null;\n }\n }\n\n private getEnvKeySuffix(fileName: string): string | null {\n const match = fileName.match(/^\\.env\\.(.+)$/);\n return match ? match[1] : null;\n }\n\n private loadGlobalEnv(absolutePath: string) {\n if (!existsSync(absolutePath)) return;\n const parsed = this.parseEnvFile(absolutePath);\n if (parsed) {\n this.globalEnv = parsed;\n this.watchFile(absolutePath);\n }\n }\n\n private loadEnvFile(key: string, absolutePath: string) {\n const parsed = this.parseEnvFile(absolutePath);\n if (!parsed) return;\n\n this.env.set(key, parsed);\n this.fileToKey.set(absolutePath, key);\n this.watchFile(absolutePath);\n }\n\n private watchFile(absolutePath: string): void {\n if (this.watchers.has(absolutePath)) return;\n try {\n const watcher = watch(absolutePath, { ignoreInitial: true })\n .on(\"change\", () => {\n this.handleFileChange(absolutePath);\n })\n .on(\"unlink\", () => {\n this.handleFileDelete(absolutePath);\n });\n\n this.watchers.set(absolutePath, watcher);\n } catch (err) {\n console.warn(`Failed to watch env file: ${absolutePath}`, err);\n }\n }\n\n /**\n * Watch cwd for new .env.* files\n */\n private watchCwdForNewFiles(): void {\n try {\n this.cwdWatcher = watch(this.config.cwd, {\n ignoreInitial: true,\n depth: 0,\n }).on(\"add\", (filePath) => {\n this.handleNewFile(filePath);\n });\n } catch (err) {\n console.warn(`Failed to watch cwd for new env files:`, err);\n }\n }\n\n private handleFileChange(absolutePath: string): void {\n if (absolutePath === this.globalEnvPath) {\n this.loadGlobalEnv(absolutePath);\n this.notifyCallbacks({ type: \"global\" });\n return;\n }\n\n const key = this.fileToKey.get(absolutePath);\n if (!key) return;\n\n const parsed = this.parseEnvFile(absolutePath);\n if (!parsed) return;\n\n this.env.set(key, parsed);\n this.notifyCallbacks({ type: \"process\", key });\n }\n\n private handleFileDelete(absolutePath: string): void {\n const watcher = this.watchers.get(absolutePath);\n if (watcher) {\n watcher.close();\n this.watchers.delete(absolutePath);\n }\n\n if (absolutePath === this.globalEnvPath) {\n this.globalEnv = {};\n this.notifyCallbacks({ type: \"global\" });\n return;\n }\n\n const key = this.fileToKey.get(absolutePath);\n if (key) {\n this.env.delete(key);\n this.fileToKey.delete(absolutePath);\n this.notifyCallbacks({ type: \"process\", key });\n }\n }\n\n private handleNewFile(filePath: string): void {\n const absolutePath = isAbsolute(filePath) ? filePath : resolve(this.config.cwd, filePath);\n\n if (absolutePath === this.globalEnvPath) {\n this.loadGlobalEnv(absolutePath);\n this.notifyCallbacks({ type: \"global\" });\n return;\n }\n\n const key = this.getEnvKeySuffix(basename(filePath));\n // Skip if key has a custom file registered or already loaded\n if (key && !this.customKeys.has(key) && !this.env.has(key)) {\n this.loadEnvFile(key, absolutePath);\n this.notifyCallbacks({ type: \"process\", key });\n }\n }\n\n private notifyCallbacks(event: EnvChangeEvent): void {\n for (const callback of this.changeCallbacks) callback(event);\n }\n\n onChange(callback: EnvChangeCallback): () => void {\n this.changeCallbacks.add(callback);\n return () => {\n this.changeCallbacks.delete(callback);\n };\n }\n\n close(): void {\n for (const watcher of this.watchers.values()) {\n watcher.close();\n }\n this.watchers.clear();\n\n if (this.cwdWatcher) {\n this.cwdWatcher.close();\n this.cwdWatcher = null;\n }\n\n this.changeCallbacks.clear();\n }\n}\n","import { appendFileSync } from \"node:fs\";\nimport { format } from \"node:util\";\n\nconst colors = {\n reset: \"\\x1b[0m\",\n gray: \"\\x1b[90m\",\n white: \"\\x1b[37m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n red: \"\\x1b[31m\",\n bold: \"\\x1b[1m\",\n} as const;\n\nconst levelColors = {\n debug: colors.gray,\n info: colors.green,\n warn: colors.yellow,\n error: colors.red,\n} as const;\n\nconst formatTime = (date: Date) =>\n Intl.DateTimeFormat(\"en-US\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n fractionalSecondDigits: 3,\n hourCycle: \"h23\",\n }).format(date);\n\nconst formatPrefixNoColor = (level: string, name: string, time: Date) => {\n const levelFormatted = level.toUpperCase().padStart(5);\n const timestamp = formatTime(time);\n return `[${timestamp}] ${levelFormatted} (${name})`;\n};\n\nconst formatPrefixWithColor = (level: string, name: string, time: Date) => {\n const levelFormatted = level.toUpperCase().padStart(5);\n const timestamp = formatTime(time);\n const levelTint = levelColors[level as keyof typeof levelColors] ?? \"\";\n return `${colors.gray}[${timestamp}]${colors.reset} ${levelTint}${levelFormatted}${colors.reset} (${name})`;\n};\n\ntype LoggerConfig = {\n name: string;\n stdout?: boolean;\n logFile?: string;\n prefix?: string;\n};\n\nconst formatMessagePrefix = (prefix: string | undefined, colored: boolean) => {\n if (!prefix) return \"\";\n if (colored) return `${colors.bold}${colors.white}[${prefix}]${colors.reset} `;\n return `[${prefix}]`;\n};\n\nconst logLine = (config: LoggerConfig, level: \"debug\" | \"info\" | \"warn\" | \"error\", args: any[]) => {\n const message = format(...args);\n const time = new Date();\n\n if (config.logFile) {\n const plainPrefix = formatPrefixNoColor(level, config.name, time);\n const plainMessagePrefix = formatMessagePrefix(config.prefix, false);\n const plainLine = [plainPrefix, plainMessagePrefix, message].filter(Boolean).join(\" \");\n try {\n appendFileSync(config.logFile, `${plainLine}\\n`);\n } catch {\n // Ignore errors\n }\n }\n\n if (config.stdout) {\n const coloredPrefix = formatPrefixWithColor(level, config.name, time);\n const coloredMessagePrefix = formatMessagePrefix(config.prefix, true);\n const coloredLine = [coloredPrefix, coloredMessagePrefix, message].filter(Boolean).join(\" \");\n console[level](coloredLine);\n }\n};\n\nexport const logger = (_config: LoggerConfig) => {\n const config = { stdout: true, ..._config };\n return {\n info: (...args: any[]) => logLine(config, \"info\", args),\n error: (...args: any[]) => logLine(config, \"error\", args),\n warn: (...args: any[]) => logLine(config, \"warn\", args),\n debug: (...args: any[]) => logLine(config, \"debug\", args),\n child: (suffix: string, overrides: Partial<Omit<LoggerConfig, \"name\">> = {}) =>\n logger({\n ...config,\n ...overrides,\n name: `${config.name}:${suffix}`,\n }),\n withPrefix: (prefix: string) =>\n logger({\n ...config,\n prefix,\n }),\n };\n};\n\nexport type Logger = ReturnType<typeof logger>;\n"],"mappings":";;;;;;;;;;;;AAMA,MAAa,oBAAoB,EAAE,OAAO;CACxC,SAAS,EAAE,QAAQ;CACnB,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;CACrC,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC;CAC3B,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC;CAClD,CAAC;AAGF,MAAa,eAAe,EAAE,SAAS;CACrC;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;AAOF,SAAS,iBAAiB,KAAa,QAAiC;AACtE,KAAI;AAEF,UAAQ,KAAK,CAAC,KAAK,OAAO;AAC1B,SAAO;UACA,KAAK;EACZ,MAAM,OAAQ,IAA8B;AAG5C,MAAI,SAAS,WAAW,SAAS,QAC/B,QAAO;AAET,SAAO;;;AAIX,IAAa,cAAb,MAAyB;CACvB,AAAS;CACT;CACA,AAAQ;CACR,AAAQ,eAAoC;CAC5C,AAAQ,SAAuB;CAC/B,AAAQ,cAAc,QAAQ,eAAqB;CACnD,AAAO,WAA0B;CAEjC,YAAY,MAAc,YAA+B,QAAgB;AACvE,OAAK,OAAO;AACZ,OAAK,aAAa;AAClB,OAAK,SAAS,OAAO,WAAW,MAAM;;CAGxC,IAAI,QAAsB;AACxB,SAAO,KAAK;;CAGd,MAAM,QAAQ;AACZ,MAAI,KAAK,WAAW,aAAa,KAAK,WAAW,WAC/C,OAAM,IAAI,MAAM,YAAY,KAAK,KAAK,eAAe,KAAK,SAAS;AAGrE,MAAI,KAAK,WAAW,WAClB,OAAM,IAAI,MAAM,YAAY,KAAK,KAAK,yBAAyB;AAGjE,OAAK,SAAS;AACd,OAAK,cAAc,QAAQ,eAAqB;AAChD,OAAK,OAAO,MAAM,qBAAqB,KAAK,WAAW,UAAU;AAEjE,MAAI;GACF,MAAM,MAAM,KAAK,WAAW,MAAM;IAAE,GAAG,QAAQ;IAAK,GAAG,KAAK,WAAW;IAAK,GAAG,QAAQ;AAEvF,QAAK,eAAe,MAAM,KAAK,WAAW,SAAS,KAAK,WAAW,QAAQ,EAAE,EAAE;IAC7E,KAAK,KAAK,WAAW;IACrB;IACA,OAAO;KAAC;KAAU;KAAQ;KAAO;IACjC,UAAU;IACX,CAAC;AAEF,QAAK,SAAS;AAEd,OAAI,KAAK,aAAa,QAAQ;IAC5B,MAAM,KAAK,gBAAgB,EAAE,OAAO,KAAK,aAAa,QAAQ,CAAC;AAC/D,OAAG,GAAG,SAAS,SAAS,KAAK,OAAO,WAAW,MAAM,CAAC,KAAK,KAAK,CAAC;AACjE,SAAK,YAAY,QAAQ,WAAW,GAAG,OAAO,CAAC;;AAGjD,OAAI,KAAK,aAAa,QAAQ;IAC5B,MAAM,KAAK,gBAAgB,EAAE,OAAO,KAAK,aAAa,QAAQ,CAAC;AAC/D,OAAG,GAAG,SAAS,SAAS,KAAK,OAAO,WAAW,MAAM,CAAC,KAAK,KAAK,CAAC;AACjE,SAAK,YAAY,QAAQ,WAAW,GAAG,OAAO,CAAC;;AAGjD,QAAK,aAAa,GAAG,SAAS,MAAM,WAAW;AAC7C,SAAK,WAAW;AAEhB,QAAI,KAAK,WAAW,UAClB,KAAI,SAAS,GAAG;AACd,UAAK,SAAS;AACd,UAAK,OAAO,KAAK,4BAA4B,OAAO;eAC3C,QAAQ;AACjB,UAAK,SAAS;AACd,UAAK,OAAO,KAAK,8BAA8B,SAAS;WACnD;AACL,UAAK,SAAS;AACd,UAAK,OAAO,MAAM,4BAA4B,OAAO;;AAIzD,SAAK,YAAY,SAAS;KAC1B;AAEF,QAAK,aAAa,GAAG,UAAU,QAAQ;AACrC,QAAI,KAAK,WAAW,cAAc,KAAK,WAAW,WAAW;AAC3D,UAAK,SAAS;AACd,UAAK,OAAO,MAAM,kBAAkB,IAAI;;AAE1C,SAAK,YAAY,SAAS;KAC1B;WACK,KAAK;AACZ,QAAK,SAAS;AACd,QAAK,OAAO,MAAM,4BAA4B,IAAI;AAClD,SAAM;;;CAIV,MAAM,KAAK,SAAiC;AAC1C,MAAI,KAAK,WAAW,UAAU,KAAK,WAAW,aAAa,KAAK,WAAW,QACzE;AAGF,MAAI,KAAK,WAAW,YAAY;AAE9B,SAAM,KAAK,YAAY;AACvB;;AAGF,MAAI,CAAC,KAAK,cAAc;AACtB,QAAK,SAAS;AACd;;AAGF,OAAK,SAAS;EACd,MAAM,MAAM,KAAK,aAAa;AAE9B,MAAI,QAAQ,QAAW;AACrB,QAAK,SAAS;AACd,QAAK,SAAS;AACd;;AAGF,OAAK,OAAO,MAAM,gCAAgC,IAAI,gBAAgB;EAEtE,MAAM,YAAY,WAAW;EAC7B,MAAM,aAAa,QAAQ,KAAK,CAC9B,KAAK,YAAY,QAAQ,WAAW,SAAkB,EACtDA,aAAW,WAAW,UAAU,CACjC,CAAC;AAEF,mBAAiB,KAAK,UAAU;AAEhC,OAAK,OAAO,MAAM,sCAAsC,UAAU,KAAK;EACvE,MAAM,SAAS,MAAM;AACrB,OAAK,OAAO,MAAM,wBAAwB,SAAS;AAEnD,MAAI,WAAW,WAAW;AACxB,QAAK,OAAO,KAAK,+BAA+B,UAAU,4BAA4B,IAAI,GAAG;GAC7F,MAAM,SAAS,iBAAiB,KAAK,UAAU;AAC/C,QAAK,OAAO,KAAK,kCAAkC,SAAS,YAAY,WAAW;GAEnF,MAAM,cAAcA,aAAW,KAAM,UAAU;AAC/C,SAAM,QAAQ,KAAK,CAAC,KAAK,YAAY,SAAS,YAAY,CAAC;AAE3D,OAAI,KAAK,gBAAgB,CAAC,KAAK,aAAa,OAC1C,MAAK,OAAO,MAAM,2CAA2C,IAAI,GAAG;;AAIxE,OAAK,SAAS;AACd,OAAK,SAAS;AACd,OAAK,OAAO,KAAK,kBAAkB;;CAGrC,MAAM,QAAuB;AAC3B,MAAI,KAAK,cAAc,QAAQ,QAAW;AAExC,oBAAiB,KAAK,aAAa,KAAK,UAAU;AAClD,SAAM,KAAK,YAAY;AACvB,QAAK,SAAS;;AAGhB,OAAK,SAAS;AAEd,OAAK,cAAc,QAAQ,eAAqB;AAChD,OAAK,OAAO,KAAK,wBAAwB;;CAG3C,iBAAiB,YAAqC;AACpD,OAAK,aAAa;;CAGpB,MAAM,cAAqC;AACzC,MAAI,CAAC,KAAK,aAAc,QAAO,KAAK;AACpC,QAAM,KAAK,YAAY;AACvB,SAAO,KAAK;;CAGd,AAAQ,UAAgB;AACtB,MAAI,KAAK,cAAc;AACrB,QAAK,aAAa,oBAAoB;AACtC,QAAK,eAAe;;AAGtB,OAAK,WAAW;;;;;;ACxNpB,MAAa,gBAAgB,EAAE,SAAS;CACtC;CACA;CACA;CACA;CACA;CACD,CAAC;AAGF,MAAa,kBAAkB,EAAE,MAAM,CACrC,EAAE,OAAO;CACP,MAAM,EAAE,QAAQ,QAAQ;CACxB,SAAS,EAAE,QAAQ;CACpB,CAAC,EACF,EAAE,OAAO;CACP,MAAM,EAAE,QAAQ,cAAc;CAC9B,gBAAgB,EAAE,QAAQ;CAC1B,YAAY,EAAE,QAAQ;CACtB,YAAY,EAAE,SAAS,EAAE,QAAQ,CAAC;CACnC,CAAC,CACH,CAAC;AAGF,MAAa,kBAAkB,EAAE,OAAO;CACtC,aAAa,EAAE,QAAQ;CACvB,UAAU,EAAE,QAAQ;CACpB,WAAW,EAAE,QAAQ;CACtB,CAAC;AAGF,MAAa,2BAA2B,EAAE,OAAO;CAC/C,eAAe;CACf,SAAS,EAAE,SAAS,gBAAgB;CACpC,WAAW,EAAE,SAAS,gBAAgB;CACtC,aAAa,EAAE,SAAS,EAAE,QAAQ,CAAC;CACnC,kBAAkB,EAAE,SAAS,EAAE,QAAQ,CAAC;CACzC,CAAC;AAGF,MAAa,yBAAyB,EAAE,SAAS;CAC/C;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAGF,MAAM,kBAAmC;CAAE,MAAM;CAAS,SAAS;CAAM;AACzE,MAAM,qBAAsC;CAAE,aAAa;CAAG,UAAU;CAAO,WAAW;CAAO;AAEjG,IAAa,oBAAb,MAA+B;CAC7B,AAAS;CACT,AAAS;CACT,AAAQ;CAGR,AAAQ;CAGR,AAAQ,SAAiC;CACzC,AAAQ,gBAAwB;CAChC,AAAQ,oBAA8B,EAAE;CACxC,AAAQ,sBAA8B;CACtC,AAAQ,gBAA+B;CACvC,AAAQ,gBAAyB;CACjC,AAAQ,sBAA4D;CAEpE,YACE,MACA,YACA,SACA,QACA;AACA,OAAK,OAAO;AACZ,OAAK,SAAS;AACd,OAAK,UAAU;GACb,eAAe,QAAQ;GACvB,SAAS,QAAQ,WAAW;GAC5B,WAAW,QAAQ,aAAa;GAChC,aAAa,QAAQ,eAAe;GACpC,kBAAkB,QAAQ;GAC3B;AACD,OAAK,cAAc,IAAI,YAAY,MAAM,YAAY,OAAO;;CAG9D,IAAI,QAAgC;AAClC,SAAO,KAAK;;CAGd,IAAI,WAAmB;AACrB,SAAO,KAAK;;CAGd,QAAc;AACZ,MAAI,KAAK,WAAW,aAAa,KAAK,WAAW,aAC/C,OAAM,IAAI,MAAM,YAAY,KAAK,KAAK,eAAe,KAAK,SAAS;AAGrE,MAAI,KAAK,WAAW,WAClB,OAAM,IAAI,MAAM,YAAY,KAAK,KAAK,yBAAyB;AAIjE,MACE,KAAK,WAAW,aAChB,KAAK,WAAW,UAChB,KAAK,WAAW,uBAEhB,MAAK,eAAe;AAGtB,OAAK,gBAAgB;AACrB,OAAK,cAAc;;CAGrB,MAAM,KAAK,SAAiC;AAC1C,OAAK,gBAAgB;AAGrB,MAAI,KAAK,qBAAqB;AAC5B,gBAAa,KAAK,oBAAoB;AACtC,QAAK,sBAAsB;;AAG7B,MACE,KAAK,WAAW,UAChB,KAAK,WAAW,aAChB,KAAK,WAAW,wBAChB;AACA,QAAK,SAAS;AACd;;AAGF,OAAK,SAAS;AACd,QAAM,KAAK,YAAY,KAAK,QAAQ;AACpC,OAAK,SAAS;AACd,OAAK,OAAO,KAAK,4BAA4B;;CAG/C,MAAM,QAAQ,QAAiB,OAAsB;AAEnD,MACE,KAAK,WAAW,aAChB,KAAK,WAAW,UAChB,KAAK,WAAW,wBAChB;AACA,QAAK,eAAe;AACpB,QAAK,gBAAgB;AACrB,QAAK,cAAc;AACnB;;AAIF,QAAM,KAAK,MAAM;AAEjB,OAAK,gBAAgB;AAErB,MAAI,MAEF,MAAK,cAAc;OACd;GAEL,MAAM,QAAQ,KAAK,gBAAgB;AACnC,OAAI,QAAQ,GAAG;AACb,SAAK,SAAS;AACd,SAAK,OAAO,KAAK,iBAAiB,MAAM,IAAI;AAC5C,UAAM,KAAK,MAAM,MAAM;AACvB,QAAI,KAAK,cAAe;;AAE1B,QAAK,cAAc;;;;;;CAOvB,MAAM,OACJ,eACA,qBAA8B,MACf;AACf,OAAK,OAAO,KAAK,wCAAwC;AACzD,OAAK,YAAY,iBAAiB,cAAc;AAEhD,MAAI,mBAEF,OAAM,KAAK,QAAQ,KAAK;;;;;CAO5B,cAAc,YAAqD;AACjE,OAAK,OAAO,KAAK,2BAA2B;AAC5C,OAAK,UAAU;GACb,GAAG,KAAK;GACR,eAAe,WAAW,iBAAiB,KAAK,QAAQ;GACxD,SAAS,WAAW,WAAW,KAAK,QAAQ;GAC5C,WAAW,WAAW,aAAa,KAAK,QAAQ;GAChD,aAAa,WAAW,eAAe,KAAK,QAAQ;GACpD,kBAAkB,WAAW,oBAAoB,KAAK,QAAQ;GAC/D;;CAGH,AAAQ,gBAAsB;AAC5B,OAAK,gBAAgB;AACrB,OAAK,sBAAsB;AAC3B,OAAK,oBAAoB,EAAE;;CAG7B,AAAQ,eAAqB;AAC3B,OAAK,gBAAgB,KAAK,KAAK;AAC/B,OAAK,SAAS;AAEd,OAAK,YACF,OAAO,CACP,KAAK,YAAY;AAChB,OAAI,KAAK,cAAe;AACxB,SAAM,KAAK,YAAY,OAAO;AAC9B,UAAO,KAAK,YAAY,aAAa;IACrC,CACD,MAAM,cAAc;AACnB,OAAI,CAAC,UAAW;AAChB,OAAI,KAAK,iBAAiB,cAAc,SAAS;AAC/C,SAAK,SAAS;AACd;;AAEF,OAAI,cAAc,aAAa,cAAc,QAC3C,MAAK,kBAAkB,UAAU;IAEnC,CACD,OAAO,QAAQ;AACd,OAAI,KAAK,cAAe;AACxB,QAAK,SAAS;AACd,QAAK,OAAO,MAAM,4BAA4B,IAAI;IAClD;;CAGN,AAAQ,kBAAkB,WAA+B;AACvD,MAAI,KAAK,eAAe;AACtB,QAAK,SAAS;AACd;;EAIF,MAAM,cADS,KAAK,gBAAgB,KAAK,KAAK,GAAG,KAAK,gBAAgB,MACzC,KAAK,QAAQ;EAC1C,MAAM,kBAAkB,cAAc;AAGtC,MAAI,WACF,MAAK,sBAAsB;MAE3B,MAAK;AAIP,MAAI,CAAC,KAAK,cAAc,gBAAgB,EAAE;AACxC,QAAK,SAAS;AACd,QAAK,OAAO,KACV,2BAA2B,KAAK,QAAQ,cAAc,0BACvD;AACD;;AAIF,MACE,KAAK,QAAQ,qBAAqB,UAClC,KAAK,iBAAiB,KAAK,QAAQ,kBACnC;AACA,QAAK,SAAS;AACd,QAAK,OAAO,KAAK,uBAAuB,KAAK,QAAQ,iBAAiB,WAAW;AACjF;;EAIF,MAAM,MAAM,KAAK,KAAK;AACtB,OAAK,kBAAkB,KAAK,IAAI;AAGhC,MAAI,KAAK,eAAe,EAAE;AACxB,QAAK,SAAS;AACd,QAAK,OAAO,KACV,wBAAwB,KAAK,QAAQ,UAAU,YAAY,eAAe,KAAK,QAAQ,UAAU,SAAS,uBAAuB,KAAK,QAAQ,UAAU,UAAU,IACnK;AACD,QAAK,2BAA2B;AAChC;;AAIF,OAAK;AACL,OAAK,iBAAiB;;CAGxB,AAAQ,cAAc,iBAAmC;AACvD,UAAQ,KAAK,QAAQ,eAArB;GACE,KAAK,SACH,QAAO;GACT,KAAK,QACH,QAAO;GACT,KAAK,aACH,QAAO;GACT,KAAK,aACH,QAAO,CAAC;GACV,KAAK,iBACH,QAAO,CAAC,KAAK;GACf,QACE,QAAO;;;CAIb,AAAQ,gBAAyB;EAC/B,MAAM,EAAE,aAAa,aAAa,KAAK,QAAQ;EAE/C,MAAM,SADM,KAAK,KAAK,GACD;AAGrB,OAAK,oBAAoB,KAAK,kBAAkB,QAAQ,OAAO,KAAK,OAAO;AAE3E,SAAO,KAAK,kBAAkB,UAAU;;CAG1C,AAAQ,iBAAyB;EAC/B,MAAM,EAAE,YAAY,KAAK;AAEzB,MAAI,QAAQ,SAAS,QACnB,QAAO,QAAQ;EAIjB,MAAM,aAAa,QAAQ,cAAc;EACzC,MAAM,QAAQ,QAAQ,iBAAiB,KAAK,IAAI,YAAY,KAAK,oBAAoB;AACrF,SAAO,KAAK,IAAI,OAAO,QAAQ,WAAW;;CAG5C,AAAQ,kBAAwB;AAC9B,OAAK,SAAS;EACd,MAAM,QAAQ,KAAK,gBAAgB;AAEnC,OAAK,OAAO,KAAK,iBAAiB,MAAM,eAAe,KAAK,cAAc,GAAG;AAE7E,OAAK,sBAAsB,iBAAiB;AAC1C,QAAK,sBAAsB;AAC3B,OAAI,KAAK,eAAe;AACtB,SAAK,SAAS;AACd;;AAEF,QAAK,cAAc;KAClB,MAAM;;CAGX,AAAQ,4BAAkC;EACxC,MAAM,EAAE,cAAc,KAAK,QAAQ;AAEnC,OAAK,sBAAsB,iBAAiB;AAC1C,QAAK,sBAAsB;AAC3B,OAAI,KAAK,eAAe;AACtB,SAAK,SAAS;AACd;;AAIF,QAAK,oBAAoB,EAAE;AAC3B,QAAK;AACL,QAAK,OAAO,KAAK,qDAAqD,KAAK,cAAc,GAAG;AAC5F,QAAK,cAAc;KAClB,UAAU;;CAGf,AAAQ,MAAM,IAA2B;AACvC,SAAO,IAAI,SAAS,YAAY;AAC9B,QAAK,sBAAsB,iBAAiB;AAC1C,SAAK,sBAAsB;AAC3B,aAAS;MACR,GAAG;IACN;;;;;;ACxXN,MAAa,cAAc,EAAE,OAAO;CAClC,YAAY,EAAE,QAAQ;CACtB,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC;CAChC,CAAC;AAGF,MAAa,qBAAqB,EAAE,OAAO;CACzC,UAAU,EAAE,QAAQ;CACpB,OAAO,EAAE,SAAS,YAAY;CAC9B,YAAY,EAAE,SAAS,EAAE,SAAS,CAAC;CACpC,CAAC;AAGF,MAAa,mBAAmB,EAAE,SAAS;CACzC;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAGF,MAAM,sBAAsB;AAE5B,IAAa,cAAb,MAAyB;CACvB,AAAS;CACT,AAAS;CACT,AAAQ;CACR,AAAQ;CACR,AAAQ,UAAuB;CAE/B,AAAQ,SAA2B;CACnC,AAAQ,YAAoB;CAC5B,AAAQ,aAAqB;CAC7B,AAAQ,sBAA8B;CACtC,AAAQ,YAAqB;CAC7B,AAAQ,gBAAyB;CACjC,AAAQ,eAAqD;CAE7D,YACE,MACA,YACA,SACA,QACA;AACA,OAAK,OAAO;AACZ,OAAK,UAAU;AACf,OAAK,SAAS;AACd,OAAK,cAAc,IAAI,YAAY,MAAM,YAAY,OAAO;;CAG9D,IAAI,QAA0B;AAC5B,SAAO,KAAK;;CAGd,IAAI,WAAmB;AACrB,SAAO,KAAK;;CAGd,IAAI,YAAoB;AACtB,SAAO,KAAK;;CAGd,IAAI,UAAuB;AACzB,MAAI,CAAC,KAAK,QAAS,QAAO;AAE1B,SADa,KAAK,QAAQ,SAAS,IACpB;;CAGjB,QAAc;AACZ,MAAI,KAAK,WAAW,eAAe,KAAK,WAAW,aAAa,KAAK,WAAW,SAC9E,OAAM,IAAI,MAAM,gBAAgB,KAAK,KAAK,eAAe,KAAK,SAAS;AAGzE,MAAI,KAAK,WAAW,WAClB,OAAM,IAAI,MAAM,gBAAgB,KAAK,KAAK,yBAAyB;AAGrE,OAAK,gBAAgB;AACrB,OAAK,OAAO,KAAK,2BAA2B,KAAK,QAAQ,WAAW;AAEpE,OAAK,UAAU,IAAI,KAAK,KAAK,QAAQ,UAAU,EAAE,UAAU,OAAO,QAAQ;AACxE,QAAK,YAAY;IACjB;AAEF,OAAK,SAAS;AAEd,MAAI,KAAK,QAAQ,WACf,MAAK,YAAY;;CAIrB,MAAM,KAAK,SAAiC;AAC1C,OAAK,gBAAgB;AAGrB,MAAI,KAAK,SAAS;AAChB,QAAK,QAAQ,MAAM;AACnB,QAAK,UAAU;;AAIjB,MAAI,KAAK,cAAc;AACrB,gBAAa,KAAK,aAAa;AAC/B,QAAK,eAAe;;AAGtB,MAAI,KAAK,WAAW,UAAU,KAAK,WAAW,WAAW;AACvD,QAAK,SAAS;AACd;;AAIF,MAAI,KAAK,WAAW,aAAa,KAAK,WAAW,cAAc,KAAK,WAAW,UAAU;AACvF,QAAK,SAAS;AACd,SAAM,KAAK,YAAY,KAAK,QAAQ;;AAGtC,OAAK,SAAS;AACd,OAAK,YAAY;AACjB,OAAK,OAAO,KAAK,sBAAsB;;CAGzC,MAAM,UAAyB;AAC7B,MAAI,KAAK,cACP,OAAM,IAAI,MAAM,gBAAgB,KAAK,KAAK,cAAc;AAI1D,MAAI,KAAK,WAAW,SAClB;AAIF,MAAI,KAAK,WAAW,aAAa,KAAK,WAAW,YAAY;AAC3D,QAAK,YAAY;AACjB,QAAK,SAAS;AACd,QAAK,OAAO,KAAK,yCAAyC;AAC1D;;AAGF,QAAM,KAAK,YAAY;;CAGzB,AAAQ,aAAmB;AACzB,MAAI,KAAK,cAAe;AAGxB,MAAI,KAAK,WAAW,aAAa,KAAK,WAAW,cAAc,KAAK,WAAW,UAAU;AACvF,QAAK,YAAY;AACjB,OAAI,KAAK,WAAW,SAClB,MAAK,SAAS;AAEhB,QAAK,OAAO,KAAK,oDAAoD;AACrE;;AAGF,OAAK,YAAY;;CAGnB,MAAc,aAA4B;AACxC,MAAI,KAAK,cAAe;AAExB,OAAK,SAAS;AACd,OAAK,sBAAsB;AAC3B,OAAK,OAAO,KAAK,gBAAgB;AAEjC,QAAM,KAAK,iBAAiB;;CAG9B,MAAc,kBAAiC;AAC7C,MAAI,KAAK,cAAe;AAGxB,QAAM,KAAK,YAAY,OAAO;AAC9B,QAAM,KAAK,YAAY,OAAO;EAE9B,MAAM,YAAY,MAAM,KAAK,YAAY,aAAa;AACtD,MAAI,KAAK,iBAAiB,cAAc,SAAS;AAC/C,QAAK,SAAS;AACd;;AAEF,OAAK,kBAAkB,cAAc,QAAQ;;CAG/C,AAAQ,kBAAkB,QAAuB;AAC/C,MAAI,KAAK,eAAe;AACtB,QAAK,SAAS;AACd;;AAGF,MAAI,QAAQ;GACV,MAAM,aAAa,KAAK,QAAQ,OAAO,cAAc;AAErD,OAAI,KAAK,sBAAsB,YAAY;AAEzC,SAAK;AACL,SAAK,SAAS;IACd,MAAM,UAAU,KAAK,QAAQ,OAAO,WAAW;AAE/C,SAAK,OAAO,KACV,2BAA2B,QAAQ,cAAc,KAAK,oBAAoB,GAAG,WAAW,GACzF;AAED,SAAK,eAAe,iBAAiB;AACnC,UAAK,eAAe;AACpB,SAAI,KAAK,eAAe;AACtB,WAAK,SAAS;AACd;;AAEF,UAAK,iBAAiB;OACrB,QAAQ;AACX;;AAIF,QAAK;AACL,QAAK,OAAO,MAAM,oBAAoB,KAAK,oBAAoB,UAAU;SACpE;AACL,QAAK;AACL,QAAK,OAAO,KAAK,6BAA6B;;AAIhD,MAAI,KAAK,WAAW;AAClB,QAAK,YAAY;AACjB,QAAK,OAAO,KAAK,sBAAsB;AACvC,QAAK,YAAY;AACjB;;AAIF,MAAI,KAAK,QACP,MAAK,SAAS;MAEd,MAAK,SAAS;;;;;;AC7OpB,MAAa,kBAAkB,EAAE,SAAS;CAAC;CAAW;CAAW;CAAa;CAAU;CAAU,CAAC;AAKnG,MAAa,+BAA+B,EAAE,OAAO;CACnD,MAAM,EAAE,QAAQ;CAChB,SAAS;CACV,CAAC;AAcF,IAAa,WAAb,MAAsB;CACpB,AAAS;CACT,AAAQ,SAAsB,EAAE;CAChC,AAAQ,SAAwB;CAChC,AAAQ;CACR,AAAQ;CACR,AAAQ,gBAAwB;CAChC,AAAQ,mBAAkC,EAAE;CAC5C,AAAQ,gBAAyB;CACjC,AAAQ,iBAAuC;CAE/C,YACE,MACA,QACA,cACA,iBACA;AACA,OAAK,OAAO;AACZ,OAAK,SAAS;AACd,OAAK,kBAAkB;AAGvB,MAAI,aACF,MAAK,MAAM,QAAQ,aACjB,MAAK,QAAQ,KAAK;;CAKxB,IAAI,QAAuB;AACzB,SAAO,KAAK;;CAGd,IAAI,QAAkC;AACpC,SAAO,KAAK;;CAGd,mBAAmB,QAAoC;EACrD,MAAM,QACJ,OAAO,WAAW,WAAW,SAAS,KAAK,OAAO,WAAW,MAAM,EAAE,OAAO,OAAO;AACrF,MAAI,QAAQ,KAAK,SAAS,KAAK,OAAO,OACpC,OAAM,IAAI,MAAM,mBAAmB,SAAS;EAG9C,MAAM,OAAO,KAAK,OAAO;AACzB,MAAI,KAAK,UAAU,UACjB,OAAM,IAAI,MAAM,+BAA+B,KAAK,KAAK;AAG3D,OAAK,OAAO,OAAO,OAAO,EAAE;AAC5B,OAAK,OAAO,KAAK,SAAS,KAAK,GAAG,WAAW;AAC7C,SAAO;;;;;;CAOT,QAAQ,MAAiE;EACvE,MAAM,KAAK,QAAQ,EAAE,KAAK;EAC1B,MAAM,YAAY,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,KAAK;EAErD,MAAM,QAAmB;GACvB;GACA;GACA,OAAO;GACR;AAED,OAAK,OAAO,KAAK,MAAM;AACvB,OAAK,OAAO,KAAK,SAAS,GAAG,eAAe,UAAU,OAAO,cAAc;AAE3E,SAAO;;;;;CAMT,QAAc;AACZ,MAAI,KAAK,WAAW,UAClB,OAAM,IAAI,MAAM,aAAa,KAAK,KAAK,sBAAsB;AAG/D,OAAK,gBAAgB;AACrB,OAAK,SAAS;AACd,OAAK,OAAO,KAAK,mBAAmB;AAGpC,OAAK,iBAAiB,KAAK,SAAS;;;;;CAMtC,MAAM,gBAA+B;AACnC,MAAI,KAAK,WAAW,UAAU,KAAK,WAAW,UAC5C;AAIF,MAAI,KAAK,eACP,OAAM,KAAK;;;;;CAOf,MAAM,KAAK,SAAiC;AAC1C,MAAI,KAAK,WAAW,UAAU,KAAK,WAAW,WAAW;AACvD,QAAK,SAAS;AACd;;AAGF,OAAK,gBAAgB;AACrB,OAAK,OAAO,KAAK,uBAAuB;EAGxC,MAAM,eAAe,KAAK,iBAAiB,KAAK,MAAM,EAAE,KAAK,QAAQ,CAAC;AACtE,QAAM,QAAQ,IAAI,aAAa;AAC/B,OAAK,mBAAmB,EAAE;AAG1B,OAAK,MAAM,QAAQ,KAAK,OACtB,KAAI,KAAK,UAAU,UACjB,MAAK,QAAQ;AAKjB,MAAI,KAAK,gBAAgB;AACvB,SAAM,KAAK;AACX,QAAK,iBAAiB;;AAGxB,OAAK,SAAS;AACd,OAAK,OAAO,KAAK,mBAAmB;;CAGtC,MAAc,UAAyB;AACrC,SAAO,KAAK,WAAW,aAAa,CAAC,KAAK,eAAe;GAEvD,MAAM,WAAW,KAAK,OAAO,MAAM,MAAM,EAAE,UAAU,UAAU;AAE/D,OAAI,CAAC,UAAU;AAEb,SAAK,SAAS;AACd,SAAK,OAAO,KAAK,wCAAwC;AACzD;;AAGF,SAAM,KAAK,YAAY,SAAS;;;CAIpC,MAAc,YAAY,MAAgC;AACxD,MAAI,KAAK,eAAe;AACtB,QAAK,QAAQ;AACb;;AAGF,OAAK,QAAQ;EACb,MAAM,YAAY,KAAK,UAAU,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;AAC9D,OAAK,OAAO,KAAK,mBAAmB,KAAK,GAAG,MAAM,UAAU,GAAG;EAG/D,MAAM,gBAA+B,KAAK,UAAU,KAAK,MAAM;GAC7D,MAAM,UAAU,KAAK,kBAAkB,EAAE,KAAK;GAC9C,MAAM,cAAc,UAChB,KAAK,OAAO,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,GACtC,KAAK,OAAO,MAAM,EAAE,KAAK;AAC7B,UAAO,IAAI,YAAY,EAAE,MAAM,EAAE,SAAS,YAAY;IACtD;AAEF,OAAK,mBAAmB;AAExB,MAAI;AAEF,SAAM,QAAQ,IAAI,cAAc,KAAK,OAAO,GAAG,OAAO,CAAC,CAAC;GAMxD,MAAM,aAHU,MAAM,QAAQ,IAAI,cAAc,KAAK,OAAO,KAAK,eAAe,GAAG,CAAC,CAAC,EAG3D,MAAM,MAAM,MAAM,QAAQ;AAEpD,OAAI,KAAK,cACP,MAAK,QAAQ;YACJ,WAAW;AACpB,SAAK,QAAQ;AACb,SAAK,OAAO,KAAK,SAAS,KAAK,GAAG,UAAU;UACvC;AACL,SAAK,QAAQ;AACb,SAAK,OAAO,KAAK,SAAS,KAAK,GAAG,aAAa;;WAE1C,KAAK;AACZ,QAAK,QAAQ;AACb,QAAK,OAAO,MAAM,SAAS,KAAK,GAAG,WAAW,IAAI;YAC1C;AACR,QAAK,mBAAmB,EAAE;;;CAI9B,MAAc,eAAe,IAA+C;AAE1E,SADc,MAAM,GAAG,aAAa,KACnB,UAAU,UAAU;;;;;;ACjNzC,IAAa,aAAb,MAAwB;CACtB,AAAQ,YAAoC,EAAE;CAC9C,AAAQ;CACR,AAAQ,sBAA2C,IAAI,KAAK;CAC5D,AAAQ,2BAAkD,IAAI,KAAK;CACnE,AAAQ,4BAAiC,IAAI,KAAK;CAClD,AAAQ,kCAA0C,IAAI,KAAK;CAC3D,AAAQ,aAA8C;CACtD,AAAQ,6BAA0B,IAAI,KAAK;CAE3C,YAAY,AAAQ,QAA0B;EAA1B;AAClB,OAAK,gBAAgB,OAAO,gBACxB,WAAW,OAAO,cAAc,GAC9B,OAAO,gBACP,QAAQ,OAAO,KAAK,OAAO,cAAc,GAC3C,QAAQ,OAAO,KAAK,OAAO;AAG/B,OAAK,oBAAoB;AACzB,OAAK,qBAAqB;AAC1B,OAAK,qBAAqB;;;;;;CAO5B,AAAO,aAAa,KAAa,UAAwB;EACvD,MAAM,eAAe,WAAW,SAAS,GAAG,WAAW,QAAQ,KAAK,OAAO,KAAK,SAAS;AACzF,OAAK,WAAW,IAAI,IAAI;AACxB,OAAK,YAAY,KAAK,aAAa;;;;;CAMrC,AAAO,cAAc,KAAsB;AACzC,SAAO,KAAK,WAAW,IAAI,IAAI;;;;;CAMjC,AAAQ,qBAA2B;AACjC,MAAI,CAAC,KAAK,OAAO,eAAgB;AAEjC,OAAK,MAAM,CAAC,KAAK,aAAa,OAAO,QAAQ,KAAK,OAAO,eAAe,CACtE,MAAK,aAAa,KAAK,SAAS;;CAIpC,AAAO,WAAW,KAAqC;EACrD,MAAM,cAAc,KAAK,IAAI,IAAI,IAAI;AACrC,SAAO;GACL,GAAG,KAAK;GACR,GAAG;GACJ;;CAGH,AAAQ,sBAA4B;AAClC,MAAI,WAAW,KAAK,cAAc,CAAE,MAAK,cAAc,KAAK,cAAc;AAE1E,MAAI;GACF,MAAM,WAAW,SAAS,UAAU,EAAE,KAAK,KAAK,OAAO,KAAK,CAAC;AAC7D,QAAK,MAAM,YAAY,UAAU;IAC/B,MAAM,MAAM,KAAK,gBAAgB,SAAS,SAAS,CAAC;AAEpD,QAAI,OAAO,CAAC,KAAK,WAAW,IAAI,IAAI,CAClC,MAAK,YAAY,KAAK,QAAQ,KAAK,OAAO,KAAK,SAAS,CAAC;;WAGtD,KAAK;AACZ,WAAQ,KAAK,6BAA6B,IAAI;;;CAIlD,AAAQ,aAAa,cAAqD;AACxE,MAAI;AAEF,UAAO,MADS,aAAa,cAAc,QAAQ,CAC9B,IAAI,EAAE;WACpB,KAAK;AACZ,WAAQ,KAAK,6BAA6B,gBAAgB,IAAI;AAC9D,UAAO;;;CAIX,AAAQ,gBAAgB,UAAiC;EACvD,MAAM,QAAQ,SAAS,MAAM,gBAAgB;AAC7C,SAAO,QAAQ,MAAM,KAAK;;CAG5B,AAAQ,cAAc,cAAsB;AAC1C,MAAI,CAAC,WAAW,aAAa,CAAE;EAC/B,MAAM,SAAS,KAAK,aAAa,aAAa;AAC9C,MAAI,QAAQ;AACV,QAAK,YAAY;AACjB,QAAK,UAAU,aAAa;;;CAIhC,AAAQ,YAAY,KAAa,cAAsB;EACrD,MAAM,SAAS,KAAK,aAAa,aAAa;AAC9C,MAAI,CAAC,OAAQ;AAEb,OAAK,IAAI,IAAI,KAAK,OAAO;AACzB,OAAK,UAAU,IAAI,cAAc,IAAI;AACrC,OAAK,UAAU,aAAa;;CAG9B,AAAQ,UAAU,cAA4B;AAC5C,MAAI,KAAK,SAAS,IAAI,aAAa,CAAE;AACrC,MAAI;GACF,MAAM,UAAU,MAAM,cAAc,EAAE,eAAe,MAAM,CAAC,CACzD,GAAG,gBAAgB;AAClB,SAAK,iBAAiB,aAAa;KACnC,CACD,GAAG,gBAAgB;AAClB,SAAK,iBAAiB,aAAa;KACnC;AAEJ,QAAK,SAAS,IAAI,cAAc,QAAQ;WACjC,KAAK;AACZ,WAAQ,KAAK,6BAA6B,gBAAgB,IAAI;;;;;;CAOlE,AAAQ,sBAA4B;AAClC,MAAI;AACF,QAAK,aAAa,MAAM,KAAK,OAAO,KAAK;IACvC,eAAe;IACf,OAAO;IACR,CAAC,CAAC,GAAG,QAAQ,aAAa;AACzB,SAAK,cAAc,SAAS;KAC5B;WACK,KAAK;AACZ,WAAQ,KAAK,0CAA0C,IAAI;;;CAI/D,AAAQ,iBAAiB,cAA4B;AACnD,MAAI,iBAAiB,KAAK,eAAe;AACvC,QAAK,cAAc,aAAa;AAChC,QAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACxC;;EAGF,MAAM,MAAM,KAAK,UAAU,IAAI,aAAa;AAC5C,MAAI,CAAC,IAAK;EAEV,MAAM,SAAS,KAAK,aAAa,aAAa;AAC9C,MAAI,CAAC,OAAQ;AAEb,OAAK,IAAI,IAAI,KAAK,OAAO;AACzB,OAAK,gBAAgB;GAAE,MAAM;GAAW;GAAK,CAAC;;CAGhD,AAAQ,iBAAiB,cAA4B;EACnD,MAAM,UAAU,KAAK,SAAS,IAAI,aAAa;AAC/C,MAAI,SAAS;AACX,WAAQ,OAAO;AACf,QAAK,SAAS,OAAO,aAAa;;AAGpC,MAAI,iBAAiB,KAAK,eAAe;AACvC,QAAK,YAAY,EAAE;AACnB,QAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACxC;;EAGF,MAAM,MAAM,KAAK,UAAU,IAAI,aAAa;AAC5C,MAAI,KAAK;AACP,QAAK,IAAI,OAAO,IAAI;AACpB,QAAK,UAAU,OAAO,aAAa;AACnC,QAAK,gBAAgB;IAAE,MAAM;IAAW;IAAK,CAAC;;;CAIlD,AAAQ,cAAc,UAAwB;EAC5C,MAAM,eAAe,WAAW,SAAS,GAAG,WAAW,QAAQ,KAAK,OAAO,KAAK,SAAS;AAEzF,MAAI,iBAAiB,KAAK,eAAe;AACvC,QAAK,cAAc,aAAa;AAChC,QAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACxC;;EAGF,MAAM,MAAM,KAAK,gBAAgB,SAAS,SAAS,CAAC;AAEpD,MAAI,OAAO,CAAC,KAAK,WAAW,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,IAAI,EAAE;AAC1D,QAAK,YAAY,KAAK,aAAa;AACnC,QAAK,gBAAgB;IAAE,MAAM;IAAW;IAAK,CAAC;;;CAIlD,AAAQ,gBAAgB,OAA6B;AACnD,OAAK,MAAM,YAAY,KAAK,gBAAiB,UAAS,MAAM;;CAG9D,SAAS,UAAyC;AAChD,OAAK,gBAAgB,IAAI,SAAS;AAClC,eAAa;AACX,QAAK,gBAAgB,OAAO,SAAS;;;CAIzC,QAAc;AACZ,OAAK,MAAM,WAAW,KAAK,SAAS,QAAQ,CAC1C,SAAQ,OAAO;AAEjB,OAAK,SAAS,OAAO;AAErB,MAAI,KAAK,YAAY;AACnB,QAAK,WAAW,OAAO;AACvB,QAAK,aAAa;;AAGpB,OAAK,gBAAgB,OAAO;;;;;;AC9OhC,MAAM,SAAS;CACb,OAAO;CACP,MAAM;CACN,OAAO;CACP,OAAO;CACP,QAAQ;CACR,KAAK;CACL,MAAM;CACP;AAED,MAAM,cAAc;CAClB,OAAO,OAAO;CACd,MAAM,OAAO;CACb,MAAM,OAAO;CACb,OAAO,OAAO;CACf;AAED,MAAM,cAAc,SAClB,KAAK,eAAe,SAAS;CAC3B,MAAM;CACN,QAAQ;CACR,QAAQ;CACR,wBAAwB;CACxB,WAAW;CACZ,CAAC,CAAC,OAAO,KAAK;AAEjB,MAAM,uBAAuB,OAAe,MAAc,SAAe;CACvE,MAAM,iBAAiB,MAAM,aAAa,CAAC,SAAS,EAAE;AAEtD,QAAO,IADW,WAAW,KAAK,CACb,IAAI,eAAe,IAAI,KAAK;;AAGnD,MAAM,yBAAyB,OAAe,MAAc,SAAe;CACzE,MAAM,iBAAiB,MAAM,aAAa,CAAC,SAAS,EAAE;CACtD,MAAM,YAAY,WAAW,KAAK;CAClC,MAAM,YAAY,YAAY,UAAsC;AACpE,QAAO,GAAG,OAAO,KAAK,GAAG,UAAU,GAAG,OAAO,MAAM,GAAG,YAAY,iBAAiB,OAAO,MAAM,IAAI,KAAK;;AAU3G,MAAM,uBAAuB,QAA4B,YAAqB;AAC5E,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI,QAAS,QAAO,GAAG,OAAO,OAAO,OAAO,MAAM,GAAG,OAAO,GAAG,OAAO,MAAM;AAC5E,QAAO,IAAI,OAAO;;AAGpB,MAAM,WAAW,QAAsB,OAA4C,SAAgB;CACjG,MAAM,UAAU,OAAO,GAAG,KAAK;CAC/B,MAAM,uBAAO,IAAI,MAAM;AAEvB,KAAI,OAAO,SAAS;EAGlB,MAAM,YAAY;GAFE,oBAAoB,OAAO,OAAO,MAAM,KAAK;GACtC,oBAAoB,OAAO,QAAQ,MAAM;GAChB;GAAQ,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;AACtF,MAAI;AACF,kBAAe,OAAO,SAAS,GAAG,UAAU,IAAI;UAC1C;;AAKV,KAAI,OAAO,QAAQ;EAGjB,MAAM,cAAc;GAFE,sBAAsB,OAAO,OAAO,MAAM,KAAK;GACxC,oBAAoB,OAAO,QAAQ,KAAK;GACX;GAAQ,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;AAC5F,UAAQ,OAAO,YAAY;;;AAI/B,MAAa,UAAU,YAA0B;CAC/C,MAAM,SAAS;EAAE,QAAQ;EAAM,GAAG;EAAS;AAC3C,QAAO;EACL,OAAO,GAAG,SAAgB,QAAQ,QAAQ,QAAQ,KAAK;EACvD,QAAQ,GAAG,SAAgB,QAAQ,QAAQ,SAAS,KAAK;EACzD,OAAO,GAAG,SAAgB,QAAQ,QAAQ,QAAQ,KAAK;EACvD,QAAQ,GAAG,SAAgB,QAAQ,QAAQ,SAAS,KAAK;EACzD,QAAQ,QAAgB,YAAiD,EAAE,KACzE,OAAO;GACL,GAAG;GACH,GAAG;GACH,MAAM,GAAG,OAAO,KAAK,GAAG;GACzB,CAAC;EACJ,aAAa,WACX,OAAO;GACL,GAAG;GACH;GACD,CAAC;EACL"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pidnap",
3
- "version": "0.0.0-dev.3",
3
+ "version": "0.0.0-dev.5",
4
4
  "bin": {
5
5
  "pidnap": "./dist/cli.mjs"
6
6
  },
@@ -23,6 +23,7 @@
23
23
  "@orpc/client": "^1.13.4",
24
24
  "@orpc/contract": "^1.13.4",
25
25
  "@orpc/server": "^1.13.4",
26
+ "chokidar": "^5.0.0",
26
27
  "cli-table3": "^0.6.5",
27
28
  "commander": "^14.0.2",
28
29
  "croner": "^9.1.0",
@@ -45,6 +46,7 @@
45
46
  "format": "oxfmt",
46
47
  "lint": "oxlint",
47
48
  "test": "vitest",
48
- "docker:dev": "docker build -t pidnap -f Dockerfile.example . && docker run --rm pidnap"
49
+ "docker:dev": "docker build -t pidnap -f Dockerfile.example . && docker run --rm --name pidnap-dev pidnap",
50
+ "docker:shell": "docker exec -it pidnap-dev bash"
49
51
  }
50
52
  }
package/src/api/client.ts CHANGED
@@ -4,9 +4,9 @@ import type { ContractRouterClient } from "@orpc/contract";
4
4
  import type { api } from "./contract.ts";
5
5
 
6
6
  export function createClient(
7
- url: string = "http://localhost:3000/rpc",
7
+ url = process.env.PIDNAP_RPC_URL ?? "http://localhost:9876/rpc",
8
8
  ): ContractRouterClient<typeof api> {
9
- const authToken = process.env.PID1_AUTH_TOKEN;
9
+ const authToken = process.env.PIDNAP_AUTH_TOKEN;
10
10
  const headers = authToken ? { Authorization: `Bearer ${authToken}` } : undefined;
11
11
  const link = new RPCLink({ url, headers });
12
12
  return createORPCClient(link);
@@ -1,16 +1,10 @@
1
1
  import { oc as ocBase } from "@orpc/contract";
2
2
  import * as v from "valibot";
3
- import { ProcessDefinitionSchema } from "../lazy-process.ts";
4
- import { RestartingProcessStateSchema } from "../restarting-process.ts";
5
- import { CronProcessStateSchema } from "../cron-process.ts";
3
+ import { ProcessDefinition } from "../lazy-process.ts";
4
+ import { RestartingProcessState } from "../restarting-process.ts";
5
+ import { CronProcessState } from "../cron-process.ts";
6
6
  import { TaskStateSchema } from "../task-list.ts";
7
7
 
8
- // Re-export schemas for use in other modules
9
- export { ProcessDefinitionSchema } from "../lazy-process.ts";
10
- export { RestartingProcessStateSchema } from "../restarting-process.ts";
11
- export { CronProcessStateSchema } from "../cron-process.ts";
12
- export { TaskStateSchema } from "../task-list.ts";
13
-
14
8
  const oc = ocBase.$input(v.void());
15
9
 
16
10
  // Resource target (name or index)
@@ -40,7 +34,7 @@ export type ManagerStatus = v.InferOutput<typeof ManagerStatusSchema>;
40
34
  // API response schemas
41
35
  export const RestartingProcessInfoSchema = v.object({
42
36
  name: v.string(),
43
- state: RestartingProcessStateSchema,
37
+ state: RestartingProcessState,
44
38
  restarts: v.number(),
45
39
  });
46
40
 
@@ -48,7 +42,7 @@ export type RestartingProcessInfo = v.InferOutput<typeof RestartingProcessInfoSc
48
42
 
49
43
  export const CronProcessInfoSchema = v.object({
50
44
  name: v.string(),
51
- state: CronProcessStateSchema,
45
+ state: CronProcessState,
52
46
  runCount: v.number(),
53
47
  failCount: v.number(),
54
48
  nextRun: v.nullable(v.string()), // ISO date string
@@ -73,7 +67,7 @@ export const processes = {
73
67
  get: oc.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
74
68
  list: oc.output(v.array(RestartingProcessInfoSchema)),
75
69
  add: oc
76
- .input(v.object({ name: v.string(), definition: ProcessDefinitionSchema }))
70
+ .input(v.object({ name: v.string(), definition: ProcessDefinition }))
77
71
  .output(RestartingProcessInfoSchema),
78
72
  start: oc.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
79
73
  stop: oc.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
@@ -84,7 +78,7 @@ export const processes = {
84
78
  .input(
85
79
  v.object({
86
80
  target: ResourceTarget,
87
- definition: ProcessDefinitionSchema,
81
+ definition: ProcessDefinition,
88
82
  restartImmediately: v.optional(v.boolean()),
89
83
  }),
90
84
  )
@@ -96,7 +90,7 @@ export const tasks = {
96
90
  get: oc.input(v.object({ target: ResourceTarget })).output(TaskEntryInfoSchema),
97
91
  list: oc.output(v.array(TaskEntryInfoSchema)),
98
92
  add: oc
99
- .input(v.object({ name: v.string(), definition: ProcessDefinitionSchema }))
93
+ .input(v.object({ name: v.string(), definition: ProcessDefinition }))
100
94
  .output(TaskEntryInfoSchema),
101
95
  remove: oc.input(v.object({ target: ResourceTarget })).output(TaskEntryInfoSchema),
102
96
  };
package/src/cli.ts CHANGED
@@ -7,25 +7,26 @@ import { RPCHandler } from "@orpc/server/node";
7
7
  import { onError } from "@orpc/server";
8
8
  import * as v from "valibot";
9
9
  import { router } from "./api/server.ts";
10
- import { Manager, ManagerConfigSchema } from "./manager.ts";
10
+ import { Manager, ManagerConfig } from "./manager.ts";
11
11
  import { logger } from "./logger.ts";
12
- import { tsImport } from "tsx/esm/api";
13
12
  import Table from "cli-table3";
14
13
  import { createClient } from "./api/client.ts";
15
- import { ProcessDefinitionSchema } from "./api/contract.ts";
14
+ import { ProcessDefinition } from "./lazy-process.ts";
15
+ import { tImport } from "./utils.ts";
16
16
 
17
17
  const program = new Command();
18
18
 
19
19
  program
20
20
  .name("pidnap")
21
21
  .description("Process manager with init system capabilities")
22
- .version("0.0.0-dev.1");
22
+ .version("0.0.0-dev.5");
23
23
 
24
24
  program
25
25
  .command("init")
26
26
  .description("Initialize and run the process manager with config file")
27
27
  .option("-c, --config [path]", "Path to config file", "pidnap.config.ts")
28
28
  .action(async (options) => {
29
+ process.title = "pidnap";
29
30
  const initLogger = logger({ name: "pidnap" });
30
31
  try {
31
32
  // Resolve config file path
@@ -33,16 +34,16 @@ program
33
34
  const configUrl = pathToFileURL(configPath).href;
34
35
 
35
36
  // Import the config file
36
- const configModule = await tsImport(configUrl, { parentURL: import.meta.url });
37
+ const configModule = await tImport(configUrl);
37
38
  const rawConfig =
38
39
  configModule.default.default || configModule.default || configModule.config || {};
39
40
 
40
41
  // Parse and validate the config with Valibot
41
- const config = v.parse(ManagerConfigSchema, rawConfig);
42
+ const config = v.parse(ManagerConfig, rawConfig);
42
43
 
43
44
  // Extract HTTP config with defaults
44
- const host = config.http?.host ?? "localhost";
45
- const port = config.http?.port ?? 3000;
45
+ const host = config.http?.host ?? "127.0.0.1";
46
+ const port = config.http?.port ?? 9876;
46
47
  const authToken = config.http?.authToken;
47
48
 
48
49
  // Create manager with config
@@ -76,7 +77,7 @@ program
76
77
  });
77
78
  if (matched) return;
78
79
  res.statusCode = 404;
79
- res.end("Not found");
80
+ res.end("Not found\n");
80
81
  });
81
82
 
82
83
  server.listen(port, host, async () => {
@@ -108,7 +109,7 @@ program
108
109
  program
109
110
  .command("status")
110
111
  .description("Show manager status")
111
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
112
+ .option("-u, --url <url>", "RPC server URL")
112
113
  .action(async (options) => {
113
114
  try {
114
115
  const client = createClient(options.url);
@@ -123,12 +124,12 @@ program
123
124
  }
124
125
  });
125
126
 
126
- const processes = program.command("processes").description("Manage restarting processes");
127
+ const processGroup = program.command("process").description("Manage restarting processes");
127
128
 
128
- processes
129
+ processGroup
129
130
  .command("list")
130
131
  .description("List restarting processes")
131
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
132
+ .option("-u, --url <url>", "RPC server URL")
132
133
  .action(async (options) => {
133
134
  try {
134
135
  const client = createClient(options.url);
@@ -145,11 +146,11 @@ processes
145
146
  }
146
147
  });
147
148
 
148
- processes
149
+ processGroup
149
150
  .command("get")
150
151
  .description("Get a restarting process by name or index")
151
152
  .argument("<target>", "Process name or index")
152
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
153
+ .option("-u, --url <url>", "RPC server URL")
153
154
  .action(async (target, options) => {
154
155
  try {
155
156
  const client = createClient(options.url);
@@ -164,12 +165,12 @@ processes
164
165
  }
165
166
  });
166
167
 
167
- processes
168
+ processGroup
168
169
  .command("add")
169
170
  .description("Add a restarting process")
170
171
  .requiredOption("-n, --name <name>", "Process name")
171
172
  .requiredOption("-d, --definition <json>", "Process definition JSON")
172
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
173
+ .option("-u, --url <url>", "RPC server URL")
173
174
  .action(async (options) => {
174
175
  try {
175
176
  const client = createClient(options.url);
@@ -185,11 +186,11 @@ processes
185
186
  }
186
187
  });
187
188
 
188
- processes
189
+ processGroup
189
190
  .command("start")
190
191
  .description("Start a restarting process")
191
192
  .argument("<target>", "Process name or index")
192
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
193
+ .option("-u, --url <url>", "RPC server URL")
193
194
  .action(async (target, options) => {
194
195
  try {
195
196
  const client = createClient(options.url);
@@ -204,11 +205,11 @@ processes
204
205
  }
205
206
  });
206
207
 
207
- processes
208
+ processGroup
208
209
  .command("stop")
209
210
  .description("Stop a restarting process")
210
211
  .argument("<target>", "Process name or index")
211
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
212
+ .option("-u, --url <url>", "RPC server URL")
212
213
  .action(async (target, options) => {
213
214
  try {
214
215
  const client = createClient(options.url);
@@ -223,12 +224,12 @@ processes
223
224
  }
224
225
  });
225
226
 
226
- processes
227
+ processGroup
227
228
  .command("restart")
228
229
  .description("Restart a restarting process")
229
230
  .argument("<target>", "Process name or index")
230
231
  .option("-f, --force", "Force restart")
231
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
232
+ .option("-u, --url <url>", "RPC server URL")
232
233
  .action(async (target, options) => {
233
234
  try {
234
235
  const client = createClient(options.url);
@@ -246,11 +247,11 @@ processes
246
247
  }
247
248
  });
248
249
 
249
- processes
250
+ processGroup
250
251
  .command("remove")
251
252
  .description("Remove a restarting process")
252
253
  .argument("<target>", "Process name or index")
253
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
254
+ .option("-u, --url <url>", "RPC server URL")
254
255
  .action(async (target, options) => {
255
256
  try {
256
257
  const client = createClient(options.url);
@@ -267,7 +268,7 @@ const crons = program.command("crons").description("Manage cron processes");
267
268
  crons
268
269
  .command("list")
269
270
  .description("List cron processes")
270
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
271
+ .option("-u, --url <url>", "RPC server URL")
271
272
  .action(async (options) => {
272
273
  try {
273
274
  const client = createClient(options.url);
@@ -288,7 +289,7 @@ crons
288
289
  .command("get")
289
290
  .description("Get a cron process by name or index")
290
291
  .argument("<target>", "Cron name or index")
291
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
292
+ .option("-u, --url <url>", "RPC server URL")
292
293
  .action(async (target, options) => {
293
294
  try {
294
295
  const client = createClient(options.url);
@@ -307,7 +308,7 @@ crons
307
308
  .command("trigger")
308
309
  .description("Trigger a cron process")
309
310
  .argument("<target>", "Cron name or index")
310
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
311
+ .option("-u, --url <url>", "RPC server URL")
311
312
  .action(async (target, options) => {
312
313
  try {
313
314
  const client = createClient(options.url);
@@ -326,7 +327,7 @@ crons
326
327
  .command("start")
327
328
  .description("Start a cron process")
328
329
  .argument("<target>", "Cron name or index")
329
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
330
+ .option("-u, --url <url>", "RPC server URL")
330
331
  .action(async (target, options) => {
331
332
  try {
332
333
  const client = createClient(options.url);
@@ -345,7 +346,7 @@ crons
345
346
  .command("stop")
346
347
  .description("Stop a cron process")
347
348
  .argument("<target>", "Cron name or index")
348
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
349
+ .option("-u, --url <url>", "RPC server URL")
349
350
  .action(async (target, options) => {
350
351
  try {
351
352
  const client = createClient(options.url);
@@ -365,7 +366,7 @@ const tasks = program.command("tasks").description("Manage tasks");
365
366
  tasks
366
367
  .command("list")
367
368
  .description("List tasks")
368
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
369
+ .option("-u, --url <url>", "RPC server URL")
369
370
  .action(async (options) => {
370
371
  try {
371
372
  const client = createClient(options.url);
@@ -386,7 +387,7 @@ tasks
386
387
  .command("get")
387
388
  .description("Get a task by id or index")
388
389
  .argument("<target>", "Task id or index")
389
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
390
+ .option("-u, --url <url>", "RPC server URL")
390
391
  .action(async (target, options) => {
391
392
  try {
392
393
  const client = createClient(options.url);
@@ -406,7 +407,7 @@ tasks
406
407
  .description("Add a task")
407
408
  .requiredOption("-n, --name <name>", "Task name")
408
409
  .requiredOption("-d, --definition <json>", "Process definition JSON")
409
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
410
+ .option("-u, --url <url>", "RPC server URL")
410
411
  .action(async (options) => {
411
412
  try {
412
413
  const client = createClient(options.url);
@@ -426,7 +427,7 @@ tasks
426
427
  .command("remove")
427
428
  .description("Remove a task by id or index")
428
429
  .argument("<target>", "Task id or index")
429
- .option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc")
430
+ .option("-u, --url <url>", "RPC server URL")
430
431
  .action(async (target, options) => {
431
432
  try {
432
433
  const client = createClient(options.url);
@@ -442,7 +443,6 @@ tasks
442
443
  });
443
444
 
444
445
  const TargetSchema = v.union([v.string(), v.number()]);
445
- type ProcessDefinition = v.InferOutput<typeof ProcessDefinitionSchema>;
446
446
 
447
447
  function parseTarget(value: string): string | number {
448
448
  const asNumber = Number(value);
@@ -452,7 +452,7 @@ function parseTarget(value: string): string | number {
452
452
  function parseDefinition(raw: string): ProcessDefinition {
453
453
  try {
454
454
  const parsed = JSON.parse(raw);
455
- return v.parse(ProcessDefinitionSchema, parsed);
455
+ return v.parse(ProcessDefinition, parsed);
456
456
  } catch (error) {
457
457
  console.error("Invalid --definition JSON. Expected a ProcessDefinition.");
458
458
  throw error;
@@ -3,25 +3,20 @@ import * as v from "valibot";
3
3
  import { LazyProcess, type ProcessDefinition } from "./lazy-process.ts";
4
4
  import type { Logger } from "./logger.ts";
5
5
 
6
- // Retry configuration schema
7
- export const RetryConfigSchema = v.object({
6
+ export const RetryConfig = v.object({
8
7
  maxRetries: v.number(),
9
8
  delayMs: v.optional(v.number()),
10
9
  });
10
+ export type RetryConfig = v.InferOutput<typeof RetryConfig>;
11
11
 
12
- export type RetryConfig = v.InferOutput<typeof RetryConfigSchema>;
13
-
14
- // Cron process options schema
15
- export const CronProcessOptionsSchema = v.object({
12
+ export const CronProcessOptions = v.object({
16
13
  schedule: v.string(),
17
- retry: v.optional(RetryConfigSchema),
14
+ retry: v.optional(RetryConfig),
18
15
  runOnStart: v.optional(v.boolean()),
19
16
  });
17
+ export type CronProcessOptions = v.InferOutput<typeof CronProcessOptions>;
20
18
 
21
- export type CronProcessOptions = v.InferOutput<typeof CronProcessOptionsSchema>;
22
-
23
- // State
24
- export const CronProcessStateSchema = v.picklist([
19
+ export const CronProcessState = v.picklist([
25
20
  "idle",
26
21
  "scheduled",
27
22
  "running",
@@ -30,19 +25,17 @@ export const CronProcessStateSchema = v.picklist([
30
25
  "stopping",
31
26
  "stopped",
32
27
  ]);
33
-
34
- export type CronProcessState = v.InferOutput<typeof CronProcessStateSchema>;
28
+ export type CronProcessState = v.InferOutput<typeof CronProcessState>;
35
29
 
36
30
  const DEFAULT_RETRY_DELAY = 1000;
37
31
 
38
32
  export class CronProcess {
39
33
  readonly name: string;
40
- private lazyProcess: LazyProcess;
34
+ readonly lazyProcess: LazyProcess;
41
35
  private options: CronProcessOptions;
42
36
  private logger: Logger;
43
37
  private cronJob: Cron | null = null;
44
38
 
45
- // State tracking
46
39
  private _state: CronProcessState = "idle";
47
40
  private _runCount: number = 0;
48
41
  private _failCount: number = 0;
@@ -93,14 +86,12 @@ export class CronProcess {
93
86
  this.stopRequested = false;
94
87
  this.logger.info(`Starting cron schedule: ${this.options.schedule}`);
95
88
 
96
- // Create cron job with UTC timezone
97
89
  this.cronJob = new Cron(this.options.schedule, { timezone: "UTC" }, () => {
98
90
  this.onCronTick();
99
91
  });
100
92
 
101
93
  this._state = "scheduled";
102
94
 
103
- // Run immediately if configured
104
95
  if (this.options.runOnStart) {
105
96
  this.executeJob();
106
97
  }
@@ -189,7 +180,7 @@ export class CronProcess {
189
180
 
190
181
  // Reset and start the process
191
182
  await this.lazyProcess.reset();
192
- this.lazyProcess.start();
183
+ await this.lazyProcess.start();
193
184
 
194
185
  const exitState = await this.lazyProcess.waitForExit();
195
186
  if (this.stopRequested && exitState === "error") {