pidnap 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.mjs ADDED
@@ -0,0 +1,1166 @@
1
+ #!/usr/bin/env node
2
+ import { _ as ProcessDefinitionSchema, a as TaskStateSchema, c as CronProcessStateSchema, h as RestartingProcessStateSchema, i as TaskList, m as RestartingProcessOptionsSchema, n as EnvManager, o as CronProcess, p as RestartingProcess, s as CronProcessOptionsSchema, t as logger } from "./logger-crc5neL8.mjs";
3
+ import { createClient } from "./client.mjs";
4
+ import { Command } from "commander";
5
+ import { createServer } from "node:http";
6
+ import { pathToFileURL } from "node:url";
7
+ import { join, resolve } from "node:path";
8
+ import { RPCHandler } from "@orpc/server/node";
9
+ import { implement, onError } from "@orpc/server";
10
+ import * as v from "valibot";
11
+ import { oc } from "@orpc/contract";
12
+ import { cwd } from "node:process";
13
+ import { mkdirSync } from "node:fs";
14
+ import { tsImport } from "tsx/esm/api";
15
+ import Table from "cli-table3";
16
+
17
+ //#region src/api/contract.ts
18
+ const oc$1 = oc.$input(v.void());
19
+ const ResourceTarget = v.union([v.string(), v.number()]);
20
+ const ManagerStateSchema = v.picklist([
21
+ "idle",
22
+ "initializing",
23
+ "running",
24
+ "stopping",
25
+ "stopped"
26
+ ]);
27
+ const ManagerStatusSchema = v.object({
28
+ state: ManagerStateSchema,
29
+ processCount: v.number(),
30
+ cronCount: v.number(),
31
+ taskCount: v.number()
32
+ });
33
+ const RestartingProcessInfoSchema = v.object({
34
+ name: v.string(),
35
+ state: RestartingProcessStateSchema,
36
+ restarts: v.number()
37
+ });
38
+ const CronProcessInfoSchema = v.object({
39
+ name: v.string(),
40
+ state: CronProcessStateSchema,
41
+ runCount: v.number(),
42
+ failCount: v.number(),
43
+ nextRun: v.nullable(v.string())
44
+ });
45
+ const TaskEntryInfoSchema = v.object({
46
+ id: v.string(),
47
+ state: TaskStateSchema,
48
+ processNames: v.array(v.string())
49
+ });
50
+ const manager = { status: oc$1.output(ManagerStatusSchema) };
51
+ const processes$1 = {
52
+ get: oc$1.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
53
+ list: oc$1.output(v.array(RestartingProcessInfoSchema)),
54
+ add: oc$1.input(v.object({
55
+ name: v.string(),
56
+ definition: ProcessDefinitionSchema
57
+ })).output(RestartingProcessInfoSchema),
58
+ start: oc$1.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
59
+ stop: oc$1.input(v.object({ target: ResourceTarget })).output(RestartingProcessInfoSchema),
60
+ restart: oc$1.input(v.object({
61
+ target: ResourceTarget,
62
+ force: v.optional(v.boolean())
63
+ })).output(RestartingProcessInfoSchema),
64
+ reload: oc$1.input(v.object({
65
+ target: ResourceTarget,
66
+ definition: ProcessDefinitionSchema,
67
+ restartImmediately: v.optional(v.boolean())
68
+ })).output(RestartingProcessInfoSchema),
69
+ remove: oc$1.input(v.object({ target: ResourceTarget })).output(v.object({ success: v.boolean() }))
70
+ };
71
+ const tasks$1 = {
72
+ get: oc$1.input(v.object({ target: ResourceTarget })).output(TaskEntryInfoSchema),
73
+ list: oc$1.output(v.array(TaskEntryInfoSchema)),
74
+ add: oc$1.input(v.object({
75
+ name: v.string(),
76
+ definition: ProcessDefinitionSchema
77
+ })).output(TaskEntryInfoSchema),
78
+ remove: oc$1.input(v.object({ target: ResourceTarget })).output(TaskEntryInfoSchema)
79
+ };
80
+ const crons$1 = {
81
+ get: oc$1.input(v.object({ target: ResourceTarget })).output(CronProcessInfoSchema),
82
+ list: oc$1.output(v.array(CronProcessInfoSchema)),
83
+ trigger: oc$1.input(v.object({ target: ResourceTarget })).output(CronProcessInfoSchema),
84
+ start: oc$1.input(v.object({ target: ResourceTarget })).output(CronProcessInfoSchema),
85
+ stop: oc$1.input(v.object({ target: ResourceTarget })).output(CronProcessInfoSchema)
86
+ };
87
+ const api = {
88
+ manager,
89
+ processes: processes$1,
90
+ tasks: tasks$1,
91
+ crons: crons$1
92
+ };
93
+
94
+ //#endregion
95
+ //#region src/api/server.ts
96
+ const os = implement(api).$context();
97
+ function serializeProcess(proc) {
98
+ return {
99
+ name: proc.name,
100
+ state: proc.state,
101
+ restarts: proc.restarts
102
+ };
103
+ }
104
+ function serializeCron(cron) {
105
+ return {
106
+ name: cron.name,
107
+ state: cron.state,
108
+ runCount: cron.runCount,
109
+ failCount: cron.failCount,
110
+ nextRun: cron.nextRun?.toISOString() ?? null
111
+ };
112
+ }
113
+ const managerStatus = os.manager.status.handler(async ({ context }) => {
114
+ const manager = context.manager;
115
+ const taskList = manager.getTaskList();
116
+ return {
117
+ state: manager.state,
118
+ processCount: manager.getRestartingProcesses().size,
119
+ cronCount: manager.getCronProcesses().size,
120
+ taskCount: taskList?.tasks.length ?? 0
121
+ };
122
+ });
123
+ const getProcess = os.processes.get.handler(async ({ input, context }) => {
124
+ const proc = context.manager.getProcessByTarget(input.target);
125
+ if (!proc) throw new Error(`Process not found: ${input.target}`);
126
+ return serializeProcess(proc);
127
+ });
128
+ const listProcesses = os.processes.list.handler(async ({ context }) => {
129
+ return Array.from(context.manager.getRestartingProcesses().values()).map(serializeProcess);
130
+ });
131
+ const addProcess = os.processes.add.handler(async ({ input, context }) => {
132
+ return serializeProcess(context.manager.addProcess(input.name, input.definition));
133
+ });
134
+ const startProcess = os.processes.start.handler(async ({ input, context }) => {
135
+ return serializeProcess(context.manager.startProcessByTarget(input.target));
136
+ });
137
+ const stopProcess = os.processes.stop.handler(async ({ input, context }) => {
138
+ return serializeProcess(await context.manager.stopProcessByTarget(input.target));
139
+ });
140
+ const restartProcess = os.processes.restart.handler(async ({ input, context }) => {
141
+ return serializeProcess(await context.manager.restartProcessByTarget(input.target, input.force));
142
+ });
143
+ const reloadProcess = os.processes.reload.handler(async ({ input, context }) => {
144
+ return serializeProcess(await context.manager.reloadProcessByTarget(input.target, input.definition, { restartImmediately: input.restartImmediately }));
145
+ });
146
+ const removeProcess = os.processes.remove.handler(async ({ input, context }) => {
147
+ await context.manager.removeProcessByTarget(input.target);
148
+ return { success: true };
149
+ });
150
+ const getCron = os.crons.get.handler(async ({ input, context }) => {
151
+ const cron = context.manager.getCronByTarget(input.target);
152
+ if (!cron) throw new Error(`Cron not found: ${input.target}`);
153
+ return serializeCron(cron);
154
+ });
155
+ const listCrons = os.crons.list.handler(async ({ context }) => {
156
+ return Array.from(context.manager.getCronProcesses().values()).map(serializeCron);
157
+ });
158
+ const triggerCron = os.crons.trigger.handler(async ({ input, context }) => {
159
+ return serializeCron(await context.manager.triggerCronByTarget(input.target));
160
+ });
161
+ const startCron = os.crons.start.handler(async ({ input, context }) => {
162
+ return serializeCron(context.manager.startCronByTarget(input.target));
163
+ });
164
+ const stopCron = os.crons.stop.handler(async ({ input, context }) => {
165
+ return serializeCron(await context.manager.stopCronByTarget(input.target));
166
+ });
167
+ const getTask = os.tasks.get.handler(async ({ input, context }) => {
168
+ const task = context.manager.getTaskByTarget(input.target);
169
+ if (!task) throw new Error(`Task not found: ${input.target}`);
170
+ return task;
171
+ });
172
+ const listTasks = os.tasks.list.handler(async ({ context }) => {
173
+ const taskList = context.manager.getTaskList();
174
+ if (!taskList) return [];
175
+ return taskList.tasks.map((t) => ({
176
+ id: t.id,
177
+ state: t.state,
178
+ processNames: t.processes.map((p) => p.name)
179
+ }));
180
+ });
181
+ const addTask = os.tasks.add.handler(async ({ input, context }) => {
182
+ return context.manager.addTask(input.name, input.definition);
183
+ });
184
+ const removeTask = os.tasks.remove.handler(async ({ input, context }) => {
185
+ return context.manager.removeTaskByTarget(input.target);
186
+ });
187
+ const router = os.router({
188
+ manager: { status: managerStatus },
189
+ processes: {
190
+ add: addProcess,
191
+ get: getProcess,
192
+ list: listProcesses,
193
+ start: startProcess,
194
+ stop: stopProcess,
195
+ restart: restartProcess,
196
+ reload: reloadProcess,
197
+ remove: removeProcess
198
+ },
199
+ crons: {
200
+ get: getCron,
201
+ list: listCrons,
202
+ trigger: triggerCron,
203
+ start: startCron,
204
+ stop: stopCron
205
+ },
206
+ tasks: {
207
+ get: getTask,
208
+ list: listTasks,
209
+ add: addTask,
210
+ remove: removeTask
211
+ }
212
+ });
213
+
214
+ //#endregion
215
+ //#region src/manager.ts
216
+ const HttpServerConfigSchema = v.object({
217
+ host: v.optional(v.string()),
218
+ port: v.optional(v.number()),
219
+ authToken: v.optional(v.string())
220
+ });
221
+ const CronProcessEntrySchema = v.object({
222
+ name: v.string(),
223
+ definition: ProcessDefinitionSchema,
224
+ options: CronProcessOptionsSchema,
225
+ envFile: v.optional(v.string())
226
+ });
227
+ const EnvReloadDelaySchema = v.union([
228
+ v.number(),
229
+ v.boolean(),
230
+ v.literal("immediately")
231
+ ]);
232
+ const RestartingProcessEntrySchema = v.object({
233
+ name: v.string(),
234
+ definition: ProcessDefinitionSchema,
235
+ options: v.optional(RestartingProcessOptionsSchema),
236
+ envFile: v.optional(v.string()),
237
+ envReloadDelay: v.optional(EnvReloadDelaySchema)
238
+ });
239
+ const TaskEntrySchema = v.object({
240
+ name: v.string(),
241
+ definition: ProcessDefinitionSchema,
242
+ envFile: v.optional(v.string())
243
+ });
244
+ const ManagerConfigSchema = v.object({
245
+ http: v.optional(HttpServerConfigSchema),
246
+ cwd: v.optional(v.string()),
247
+ logDir: v.optional(v.string()),
248
+ env: v.optional(v.record(v.string(), v.string())),
249
+ envFiles: v.optional(v.record(v.string(), v.string())),
250
+ tasks: v.optional(v.array(TaskEntrySchema)),
251
+ crons: v.optional(v.array(CronProcessEntrySchema)),
252
+ processes: v.optional(v.array(RestartingProcessEntrySchema))
253
+ });
254
+ const DEFAULT_RESTART_OPTIONS = { restartPolicy: "always" };
255
+ const SHUTDOWN_TIMEOUT_MS = 15e3;
256
+ const SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
257
+ const SHUTDOWN_EXIT_CODE = 0;
258
+ var Manager = class {
259
+ config;
260
+ logger;
261
+ envManager;
262
+ envFileKeyByProcess = /* @__PURE__ */ new Map();
263
+ _state = "idle";
264
+ taskList = null;
265
+ cronProcesses = /* @__PURE__ */ new Map();
266
+ restartingProcesses = /* @__PURE__ */ new Map();
267
+ logDir;
268
+ processEnvReloadConfig = /* @__PURE__ */ new Map();
269
+ envReloadTimers = /* @__PURE__ */ new Map();
270
+ envChangeUnsubscribe = null;
271
+ signalHandlers = /* @__PURE__ */ new Map();
272
+ shutdownPromise = null;
273
+ isShuttingDown = false;
274
+ constructor(config, logger) {
275
+ this.config = config;
276
+ this.logger = logger;
277
+ this.logDir = config.logDir ?? join(cwd(), "logs");
278
+ this.ensureLogDirs();
279
+ this.envManager = new EnvManager({
280
+ cwd: config.cwd,
281
+ files: config.envFiles,
282
+ watch: true
283
+ });
284
+ this.envChangeUnsubscribe = this.envManager.onChange((changedKeys) => {
285
+ this.handleEnvChange(changedKeys);
286
+ });
287
+ this.registerShutdownHandlers();
288
+ }
289
+ /**
290
+ * Merge global env with process-specific env and apply cwd inheritance
291
+ * Merge order: .env (global), config.env (global), .env.<processName>, processEnvFile, definition.env
292
+ */
293
+ applyDefaults(processName, definition, envFile) {
294
+ const envFromFiles = this.envManager.getEnvVars(processName);
295
+ let envFromCustomFile = {};
296
+ if (envFile) {
297
+ let key = this.envFileKeyByProcess.get(processName);
298
+ if (!key) {
299
+ key = `custom:${processName}`;
300
+ this.envFileKeyByProcess.set(processName, key);
301
+ this.envManager.registerFile(key, envFile);
302
+ }
303
+ envFromCustomFile = this.envManager.getEnvForKey(key);
304
+ }
305
+ return {
306
+ ...definition,
307
+ cwd: definition.cwd ?? this.config.cwd,
308
+ env: {
309
+ ...envFromFiles,
310
+ ...this.config.env,
311
+ ...envFromCustomFile,
312
+ ...definition.env
313
+ }
314
+ };
315
+ }
316
+ processLogFile(name) {
317
+ return join(this.logDir, "process", `${name}.log`);
318
+ }
319
+ taskLogFile(name) {
320
+ return join(this.logDir, "tasks", `${name}.log`);
321
+ }
322
+ cronLogFile(name) {
323
+ return join(this.logDir, "cron", `${name}.log`);
324
+ }
325
+ ensureLogDirs() {
326
+ mkdirSync(this.logDir, { recursive: true });
327
+ mkdirSync(join(this.logDir, "process"), { recursive: true });
328
+ mkdirSync(join(this.logDir, "tasks"), { recursive: true });
329
+ mkdirSync(join(this.logDir, "cron"), { recursive: true });
330
+ }
331
+ /**
332
+ * Handle env file changes
333
+ */
334
+ handleEnvChange(changedKeys) {
335
+ if (this._state !== "running") return;
336
+ this.logger.info(`Env files changed for keys: ${changedKeys.join(", ")}`);
337
+ const affectedProcesses = /* @__PURE__ */ new Set();
338
+ for (const key of changedKeys) if (key === "global") for (const processName of this.restartingProcesses.keys()) affectedProcesses.add(processName);
339
+ else {
340
+ if (this.restartingProcesses.has(key)) affectedProcesses.add(key);
341
+ for (const [processName, customKey] of this.envFileKeyByProcess.entries()) if (customKey === key && this.restartingProcesses.has(processName)) affectedProcesses.add(processName);
342
+ }
343
+ for (const processName of affectedProcesses) {
344
+ const reloadDelay = this.processEnvReloadConfig.get(processName);
345
+ if (reloadDelay === false) continue;
346
+ this.scheduleProcessReload(processName, reloadDelay);
347
+ }
348
+ }
349
+ /**
350
+ * Schedule a process reload with debouncing
351
+ */
352
+ scheduleProcessReload(processName, reloadDelay) {
353
+ const existingTimer = this.envReloadTimers.get(processName);
354
+ if (existingTimer) clearTimeout(existingTimer);
355
+ let delayMs;
356
+ if (reloadDelay === false) return;
357
+ else if (reloadDelay === true || reloadDelay === "immediately") delayMs = 0;
358
+ else if (typeof reloadDelay === "number") delayMs = reloadDelay;
359
+ else delayMs = 5e3;
360
+ this.logger.info(`Scheduling reload for process "${processName}" in ${delayMs}ms`);
361
+ const timer = setTimeout(async () => {
362
+ await this.reloadProcessEnv(processName);
363
+ this.envReloadTimers.delete(processName);
364
+ }, delayMs);
365
+ this.envReloadTimers.set(processName, timer);
366
+ }
367
+ /**
368
+ * Reload a process with updated env vars
369
+ */
370
+ async reloadProcessEnv(processName) {
371
+ const proc = this.restartingProcesses.get(processName);
372
+ if (!proc) {
373
+ this.logger.warn(`Process "${processName}" not found for env reload`);
374
+ return;
375
+ }
376
+ this.logger.info(`Reloading process "${processName}" due to env change`);
377
+ const processConfig = this.config.processes?.find((p) => p.name === processName);
378
+ if (!processConfig) {
379
+ this.logger.warn(`Process config for "${processName}" not found`);
380
+ return;
381
+ }
382
+ const updatedDefinition = this.applyDefaults(processName, processConfig.definition, processConfig.envFile);
383
+ await proc.reload(updatedDefinition, true);
384
+ }
385
+ get state() {
386
+ return this._state;
387
+ }
388
+ /**
389
+ * Get all cron processes (read-only access)
390
+ */
391
+ getCronProcesses() {
392
+ return this.cronProcesses;
393
+ }
394
+ /**
395
+ * Get a specific cron process by name
396
+ */
397
+ getCronProcess(name) {
398
+ return this.cronProcesses.get(name);
399
+ }
400
+ /**
401
+ * Get all restarting processes (read-only access)
402
+ */
403
+ getRestartingProcesses() {
404
+ return this.restartingProcesses;
405
+ }
406
+ /**
407
+ * Get a specific restarting process by name
408
+ */
409
+ getRestartingProcess(name) {
410
+ return this.restartingProcesses.get(name);
411
+ }
412
+ /**
413
+ * Get the task list (read-only access)
414
+ */
415
+ getTaskList() {
416
+ return this.taskList;
417
+ }
418
+ /**
419
+ * Get a restarting process by name or index
420
+ */
421
+ getProcessByTarget(target) {
422
+ if (typeof target === "string") return this.restartingProcesses.get(target);
423
+ return Array.from(this.restartingProcesses.values())[target];
424
+ }
425
+ /**
426
+ * Get a cron process by name or index
427
+ */
428
+ getCronByTarget(target) {
429
+ if (typeof target === "string") return this.cronProcesses.get(target);
430
+ return Array.from(this.cronProcesses.values())[target];
431
+ }
432
+ /**
433
+ * Get a task by id or index
434
+ */
435
+ getTaskByTarget(target) {
436
+ if (!this.taskList) return void 0;
437
+ const tasks = this.taskList.tasks;
438
+ if (typeof target === "string") {
439
+ const task = tasks.find((t) => t.id === target);
440
+ if (!task) return void 0;
441
+ return {
442
+ id: task.id,
443
+ state: task.state,
444
+ processNames: task.processes.map((p) => p.name)
445
+ };
446
+ }
447
+ const task = tasks[target];
448
+ if (!task) return void 0;
449
+ return {
450
+ id: task.id,
451
+ state: task.state,
452
+ processNames: task.processes.map((p) => p.name)
453
+ };
454
+ }
455
+ /**
456
+ * Start a restarting process by target
457
+ */
458
+ startProcessByTarget(target) {
459
+ const proc = this.getProcessByTarget(target);
460
+ if (!proc) throw new Error(`Process not found: ${target}`);
461
+ proc.start();
462
+ return proc;
463
+ }
464
+ /**
465
+ * Stop a restarting process by target
466
+ */
467
+ async stopProcessByTarget(target, timeout) {
468
+ const proc = this.getProcessByTarget(target);
469
+ if (!proc) throw new Error(`Process not found: ${target}`);
470
+ await proc.stop(timeout);
471
+ return proc;
472
+ }
473
+ /**
474
+ * Restart a restarting process by target
475
+ */
476
+ async restartProcessByTarget(target, force = false) {
477
+ const proc = this.getProcessByTarget(target);
478
+ if (!proc) throw new Error(`Process not found: ${target}`);
479
+ await proc.restart(force);
480
+ return proc;
481
+ }
482
+ /**
483
+ * Reload a restarting process with new definition
484
+ */
485
+ async reloadProcessByTarget(target, newDefinition, options) {
486
+ const proc = this.getProcessByTarget(target);
487
+ if (!proc) throw new Error(`Process not found: ${target}`);
488
+ const definitionWithDefaults = this.applyDefaults(proc.name, newDefinition);
489
+ if (options?.updateOptions) proc.updateOptions(options.updateOptions);
490
+ await proc.reload(definitionWithDefaults, options?.restartImmediately ?? true);
491
+ this.logger.info(`Reloaded process: ${proc.name}`);
492
+ return proc;
493
+ }
494
+ /**
495
+ * Remove a restarting process by target
496
+ */
497
+ async removeProcessByTarget(target, timeout) {
498
+ const proc = this.getProcessByTarget(target);
499
+ if (!proc) throw new Error(`Process not found: ${target}`);
500
+ await proc.stop(timeout);
501
+ this.restartingProcesses.delete(proc.name);
502
+ this.logger.info(`Removed process: ${proc.name}`);
503
+ }
504
+ /**
505
+ * Add a task to the task list
506
+ * Creates the task list if it doesn't exist and starts it
507
+ */
508
+ addTask(name, definition) {
509
+ if (!this.taskList) this.taskList = new TaskList("runtime", this.logger.child("tasks", { logFile: this.taskLogFile("tasks") }), void 0, (processName) => {
510
+ return this.taskLogFile(processName);
511
+ });
512
+ const namedProcess = {
513
+ name,
514
+ process: this.applyDefaults(name, definition)
515
+ };
516
+ const id = this.taskList.addTask(namedProcess);
517
+ if (this.taskList.state === "idle") this.taskList.start();
518
+ return {
519
+ id,
520
+ state: "pending",
521
+ processNames: [name]
522
+ };
523
+ }
524
+ removeTaskByTarget(target) {
525
+ if (!this.taskList) throw new Error(`Task list not initialized`);
526
+ const removed = this.taskList.removeTaskByTarget(target);
527
+ return {
528
+ id: removed.id,
529
+ state: removed.state,
530
+ processNames: removed.processes.map((p) => p.name)
531
+ };
532
+ }
533
+ /**
534
+ * Add a restarting process at runtime
535
+ */
536
+ addProcess(name, definition, options, envReloadDelay) {
537
+ if (this.restartingProcesses.has(name)) throw new Error(`Process "${name}" already exists`);
538
+ const processLogger = this.logger.child(name, { logFile: this.processLogFile(name) });
539
+ const restartingProcess = new RestartingProcess(name, this.applyDefaults(name, definition), options ?? DEFAULT_RESTART_OPTIONS, processLogger);
540
+ this.restartingProcesses.set(name, restartingProcess);
541
+ restartingProcess.start();
542
+ this.processEnvReloadConfig.set(name, envReloadDelay ?? 5e3);
543
+ this.logger.info(`Added and started restarting process: ${name}`);
544
+ return restartingProcess;
545
+ }
546
+ /**
547
+ * Trigger a cron process by target
548
+ */
549
+ async triggerCronByTarget(target) {
550
+ const cron = this.getCronByTarget(target);
551
+ if (!cron) throw new Error(`Cron not found: ${target}`);
552
+ await cron.trigger();
553
+ return cron;
554
+ }
555
+ /**
556
+ * Start a cron process by target
557
+ */
558
+ startCronByTarget(target) {
559
+ const cron = this.getCronByTarget(target);
560
+ if (!cron) throw new Error(`Cron not found: ${target}`);
561
+ cron.start();
562
+ return cron;
563
+ }
564
+ /**
565
+ * Stop a cron process by target
566
+ */
567
+ async stopCronByTarget(target, timeout) {
568
+ const cron = this.getCronByTarget(target);
569
+ if (!cron) throw new Error(`Cron not found: ${target}`);
570
+ await cron.stop(timeout);
571
+ return cron;
572
+ }
573
+ /**
574
+ * Start the manager:
575
+ * 1. Run task list (if configured) and wait for completion
576
+ * 2. Create and start all cron/restarting processes
577
+ */
578
+ async start() {
579
+ if (this._state !== "idle" && this._state !== "stopped") throw new Error(`Manager is already ${this._state}`);
580
+ this.logger.info(`Starting manager`);
581
+ if (this.config.tasks && this.config.tasks.length > 0) {
582
+ this._state = "initializing";
583
+ this.logger.info(`Running initialization tasks`);
584
+ this.taskList = new TaskList("init", this.logger.child("tasks"), this.config.tasks.map((task) => ({
585
+ name: task.name,
586
+ process: this.applyDefaults(task.name, task.definition, task.envFile)
587
+ })), (processName) => {
588
+ return this.taskLogFile(processName);
589
+ });
590
+ this.taskList.start();
591
+ await this.taskList.waitUntilIdle();
592
+ const failedTasks = this.taskList.tasks.filter((t) => t.state === "failed");
593
+ if (failedTasks.length > 0) {
594
+ this._state = "stopped";
595
+ const failedNames = failedTasks.map((t) => t.id).join(", ");
596
+ throw new Error(`Initialization failed: tasks [${failedNames}] failed`);
597
+ }
598
+ this.logger.info(`Initialization tasks completed`);
599
+ }
600
+ if (this.config.crons) for (const entry of this.config.crons) {
601
+ const processLogger = this.logger.child(entry.name, { logFile: this.cronLogFile(entry.name) });
602
+ const cronProcess = new CronProcess(entry.name, this.applyDefaults(entry.name, entry.definition, entry.envFile), entry.options, processLogger);
603
+ this.cronProcesses.set(entry.name, cronProcess);
604
+ cronProcess.start();
605
+ this.logger.info(`Started cron process: ${entry.name}`);
606
+ }
607
+ if (this.config.processes) for (const entry of this.config.processes) {
608
+ const processLogger = this.logger.child(entry.name, { logFile: this.processLogFile(entry.name) });
609
+ const restartingProcess = new RestartingProcess(entry.name, this.applyDefaults(entry.name, entry.definition, entry.envFile), entry.options ?? DEFAULT_RESTART_OPTIONS, processLogger);
610
+ this.restartingProcesses.set(entry.name, restartingProcess);
611
+ restartingProcess.start();
612
+ this.processEnvReloadConfig.set(entry.name, entry.envReloadDelay ?? 5e3);
613
+ this.logger.info(`Started restarting process: ${entry.name}`);
614
+ }
615
+ this._state = "running";
616
+ this.logger.info(`Manager started with ${this.cronProcesses.size} cron process(es) and ${this.restartingProcesses.size} restarting process(es)`);
617
+ }
618
+ /**
619
+ * Stop all managed processes
620
+ */
621
+ async stop(timeout) {
622
+ if (this._state === "idle" || this._state === "stopped") {
623
+ this._state = "stopped";
624
+ this.unregisterShutdownHandlers();
625
+ return;
626
+ }
627
+ this._state = "stopping";
628
+ this.logger.info(`Stopping manager`);
629
+ for (const timer of this.envReloadTimers.values()) clearTimeout(timer);
630
+ this.envReloadTimers.clear();
631
+ if (this.envChangeUnsubscribe) {
632
+ this.envChangeUnsubscribe();
633
+ this.envChangeUnsubscribe = null;
634
+ }
635
+ this.envManager.dispose();
636
+ if (this.taskList) await this.taskList.stop(timeout);
637
+ const cronStopPromises = Array.from(this.cronProcesses.values()).map((p) => p.stop(timeout));
638
+ const restartingStopPromises = Array.from(this.restartingProcesses.values()).map((p) => p.stop(timeout));
639
+ await Promise.all([...cronStopPromises, ...restartingStopPromises]);
640
+ this._state = "stopped";
641
+ this.logger.info(`Manager stopped`);
642
+ this.unregisterShutdownHandlers();
643
+ }
644
+ /**
645
+ * Register signal handlers for graceful shutdown
646
+ * Called automatically by constructor
647
+ */
648
+ registerShutdownHandlers() {
649
+ if (this.signalHandlers.size > 0) {
650
+ this.logger.warn(`Shutdown handlers already registered`);
651
+ return;
652
+ }
653
+ for (const signal of SHUTDOWN_SIGNALS) {
654
+ const handler = () => this.handleSignal(signal);
655
+ this.signalHandlers.set(signal, handler);
656
+ process.on(signal, handler);
657
+ this.logger.debug(`Registered handler for ${signal}`);
658
+ }
659
+ this.logger.info(`Shutdown handlers registered for signals: ${SHUTDOWN_SIGNALS.join(", ")}`);
660
+ }
661
+ /**
662
+ * Unregister signal handlers for graceful shutdown
663
+ */
664
+ unregisterShutdownHandlers() {
665
+ if (this.signalHandlers.size === 0) return;
666
+ for (const [signal, handler] of this.signalHandlers.entries()) {
667
+ process.off(signal, handler);
668
+ this.logger.debug(`Unregistered handler for ${signal}`);
669
+ }
670
+ this.signalHandlers.clear();
671
+ }
672
+ /**
673
+ * Wait for shutdown to complete (useful for keeping process alive)
674
+ * Resolves when the manager has fully stopped
675
+ */
676
+ async waitForShutdown() {
677
+ if (this._state === "stopped") return;
678
+ if (this.shutdownPromise) {
679
+ await this.shutdownPromise;
680
+ return;
681
+ }
682
+ return new Promise((resolve) => {
683
+ const checkInterval = setInterval(() => {
684
+ if (this._state === "stopped") {
685
+ clearInterval(checkInterval);
686
+ resolve();
687
+ }
688
+ }, 100);
689
+ });
690
+ }
691
+ /**
692
+ * Trigger graceful shutdown programmatically
693
+ */
694
+ async shutdown() {
695
+ if (this.isShuttingDown) {
696
+ this.logger.warn(`Shutdown already in progress`);
697
+ if (this.shutdownPromise) await this.shutdownPromise;
698
+ return;
699
+ }
700
+ this.isShuttingDown = true;
701
+ this.logger.info(`Initiating graceful shutdown (timeout: ${SHUTDOWN_TIMEOUT_MS}ms)`);
702
+ this.shutdownPromise = this.performShutdown();
703
+ await this.shutdownPromise;
704
+ }
705
+ handleSignal(signal) {
706
+ this.logger.info(`Received ${signal}, initiating graceful shutdown...`);
707
+ if (this.isShuttingDown) {
708
+ this.logger.warn(`Shutdown already in progress, ignoring ${signal}`);
709
+ return;
710
+ }
711
+ this.isShuttingDown = true;
712
+ this.shutdownPromise = this.performShutdown();
713
+ this.shutdownPromise.then(() => {
714
+ this.logger.info(`Exiting with code ${SHUTDOWN_EXIT_CODE}`);
715
+ process.exit(SHUTDOWN_EXIT_CODE);
716
+ }).catch((err) => {
717
+ this.logger.error(`Shutdown error:`, err);
718
+ process.exit(1);
719
+ });
720
+ }
721
+ async performShutdown() {
722
+ const timeoutPromise = new Promise((_, reject) => {
723
+ setTimeout(() => {
724
+ reject(/* @__PURE__ */ new Error(`Shutdown timed out after ${SHUTDOWN_TIMEOUT_MS}ms`));
725
+ }, SHUTDOWN_TIMEOUT_MS);
726
+ });
727
+ try {
728
+ await Promise.race([this.stop(SHUTDOWN_TIMEOUT_MS), timeoutPromise]);
729
+ this.logger.info(`Graceful shutdown completed`);
730
+ } catch (err) {
731
+ this.logger.error(`Shutdown error:`, err);
732
+ this._state = "stopped";
733
+ throw err;
734
+ } finally {
735
+ this.isShuttingDown = false;
736
+ this.shutdownPromise = null;
737
+ }
738
+ }
739
+ };
740
+
741
+ //#endregion
742
+ //#region src/cli.ts
743
+ const program = new Command();
744
+ program.name("pidnap").description("Process manager with init system capabilities").version("0.0.0-dev.1");
745
+ program.command("init").description("Initialize and run the process manager with config file").option("-c, --config <path>", "Path to config file", "pidnap.config.ts").action(async (options) => {
746
+ const initLogger = logger({ name: "pidnap" });
747
+ try {
748
+ const configUrl = pathToFileURL(resolve(process.cwd(), options.config)).href;
749
+ const configModule = await tsImport(configUrl, { parentURL: import.meta.url });
750
+ const rawConfig = configModule.default.default || configModule.default || configModule.config || {};
751
+ const config = v.parse(ManagerConfigSchema, rawConfig);
752
+ const host = config.http?.host ?? "localhost";
753
+ const port = config.http?.port ?? 3e3;
754
+ const authToken = config.http?.authToken;
755
+ const managerLogger = logger({
756
+ name: "pidnap",
757
+ logFile: resolve(config.logDir ?? resolve(process.cwd(), "logs"), "pidnap.log")
758
+ });
759
+ const manager = new Manager(config, managerLogger);
760
+ const handler = new RPCHandler(router, { interceptors: [onError((error) => {
761
+ managerLogger.error(error);
762
+ })] });
763
+ const server = createServer(async (req, res) => {
764
+ if (authToken) {
765
+ if (req.headers["authorization"]?.replace("Bearer ", "") !== authToken) {
766
+ res.statusCode = 401;
767
+ res.end("Unauthorized");
768
+ return;
769
+ }
770
+ }
771
+ const { matched } = await handler.handle(req, res, {
772
+ prefix: "/rpc",
773
+ context: { manager }
774
+ });
775
+ if (matched) return;
776
+ res.statusCode = 404;
777
+ res.end("Not found");
778
+ });
779
+ server.listen(port, host, async () => {
780
+ managerLogger.info(`pidnap RPC server running on http://${host}:${port}`);
781
+ if (authToken) managerLogger.info("Auth token required for API access");
782
+ try {
783
+ await manager.start();
784
+ } catch (err) {
785
+ managerLogger.error("Failed to start manager:", err);
786
+ server.close();
787
+ process.exit(1);
788
+ }
789
+ });
790
+ await manager.waitForShutdown();
791
+ server.close();
792
+ } catch (error) {
793
+ initLogger.error("Failed to start pidnap:", error);
794
+ process.exit(1);
795
+ }
796
+ });
797
+ program.command("status").description("Show manager status").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
798
+ try {
799
+ const status = await createClient(options.url).manager.status();
800
+ const table = new Table({ head: [
801
+ "State",
802
+ "Processes",
803
+ "Crons",
804
+ "Tasks"
805
+ ] });
806
+ table.push([
807
+ status.state,
808
+ status.processCount,
809
+ status.cronCount,
810
+ status.taskCount
811
+ ]);
812
+ console.log(table.toString());
813
+ } catch (error) {
814
+ console.error("Failed to fetch status:", error);
815
+ process.exit(1);
816
+ }
817
+ });
818
+ const processes = program.command("processes").description("Manage restarting processes");
819
+ processes.command("list").description("List restarting processes").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
820
+ try {
821
+ const processes = await createClient(options.url).processes.list();
822
+ const table = new Table({ head: [
823
+ "Name",
824
+ "State",
825
+ "Restarts"
826
+ ] });
827
+ for (const proc of processes) table.push([
828
+ proc.name,
829
+ proc.state,
830
+ proc.restarts
831
+ ]);
832
+ console.log(table.toString());
833
+ } catch (error) {
834
+ console.error("Failed to list processes:", error);
835
+ process.exit(1);
836
+ }
837
+ });
838
+ processes.command("get").description("Get a restarting process by name or index").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
839
+ try {
840
+ const proc = await createClient(options.url).processes.get({ target: parseTarget(target) });
841
+ const table = new Table({ head: [
842
+ "Name",
843
+ "State",
844
+ "Restarts"
845
+ ] });
846
+ table.push([
847
+ proc.name,
848
+ proc.state,
849
+ proc.restarts
850
+ ]);
851
+ console.log(table.toString());
852
+ } catch (error) {
853
+ console.error("Failed to get process:", error);
854
+ process.exit(1);
855
+ }
856
+ });
857
+ processes.command("add").description("Add a restarting process").requiredOption("-n, --name <name>", "Process name").requiredOption("-d, --definition <json>", "Process definition JSON").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
858
+ try {
859
+ const client = createClient(options.url);
860
+ const definition = parseDefinition(options.definition);
861
+ const proc = await client.processes.add({
862
+ name: options.name,
863
+ definition
864
+ });
865
+ const table = new Table({ head: [
866
+ "Name",
867
+ "State",
868
+ "Restarts"
869
+ ] });
870
+ table.push([
871
+ proc.name,
872
+ proc.state,
873
+ proc.restarts
874
+ ]);
875
+ console.log(table.toString());
876
+ } catch (error) {
877
+ console.error("Failed to add process:", error);
878
+ process.exit(1);
879
+ }
880
+ });
881
+ processes.command("start").description("Start a restarting process").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
882
+ try {
883
+ const proc = await createClient(options.url).processes.start({ target: parseTarget(target) });
884
+ const table = new Table({ head: [
885
+ "Name",
886
+ "State",
887
+ "Restarts"
888
+ ] });
889
+ table.push([
890
+ proc.name,
891
+ proc.state,
892
+ proc.restarts
893
+ ]);
894
+ console.log(table.toString());
895
+ } catch (error) {
896
+ console.error("Failed to start process:", error);
897
+ process.exit(1);
898
+ }
899
+ });
900
+ processes.command("stop").description("Stop a restarting process").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
901
+ try {
902
+ const proc = await createClient(options.url).processes.stop({ target: parseTarget(target) });
903
+ const table = new Table({ head: [
904
+ "Name",
905
+ "State",
906
+ "Restarts"
907
+ ] });
908
+ table.push([
909
+ proc.name,
910
+ proc.state,
911
+ proc.restarts
912
+ ]);
913
+ console.log(table.toString());
914
+ } catch (error) {
915
+ console.error("Failed to stop process:", error);
916
+ process.exit(1);
917
+ }
918
+ });
919
+ processes.command("restart").description("Restart a restarting process").argument("<target>", "Process name or index").option("-f, --force", "Force restart").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
920
+ try {
921
+ const proc = await createClient(options.url).processes.restart({
922
+ target: parseTarget(target),
923
+ force: options.force
924
+ });
925
+ const table = new Table({ head: [
926
+ "Name",
927
+ "State",
928
+ "Restarts"
929
+ ] });
930
+ table.push([
931
+ proc.name,
932
+ proc.state,
933
+ proc.restarts
934
+ ]);
935
+ console.log(table.toString());
936
+ } catch (error) {
937
+ console.error("Failed to restart process:", error);
938
+ process.exit(1);
939
+ }
940
+ });
941
+ processes.command("remove").description("Remove a restarting process").argument("<target>", "Process name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
942
+ try {
943
+ await createClient(options.url).processes.remove({ target: parseTarget(target) });
944
+ console.log("Process removed");
945
+ } catch (error) {
946
+ console.error("Failed to remove process:", error);
947
+ process.exit(1);
948
+ }
949
+ });
950
+ const crons = program.command("crons").description("Manage cron processes");
951
+ crons.command("list").description("List cron processes").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
952
+ try {
953
+ const crons = await createClient(options.url).crons.list();
954
+ const table = new Table({ head: [
955
+ "Name",
956
+ "State",
957
+ "Runs",
958
+ "Fails",
959
+ "Next Run"
960
+ ] });
961
+ for (const cron of crons) table.push([
962
+ cron.name,
963
+ cron.state,
964
+ cron.runCount,
965
+ cron.failCount,
966
+ cron.nextRun ?? "-"
967
+ ]);
968
+ console.log(table.toString());
969
+ } catch (error) {
970
+ console.error("Failed to list crons:", error);
971
+ process.exit(1);
972
+ }
973
+ });
974
+ crons.command("get").description("Get a cron process by name or index").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
975
+ try {
976
+ const cron = await createClient(options.url).crons.get({ target: parseTarget(target) });
977
+ const table = new Table({ head: [
978
+ "Name",
979
+ "State",
980
+ "Runs",
981
+ "Fails",
982
+ "Next Run"
983
+ ] });
984
+ table.push([
985
+ cron.name,
986
+ cron.state,
987
+ cron.runCount,
988
+ cron.failCount,
989
+ cron.nextRun ?? "-"
990
+ ]);
991
+ console.log(table.toString());
992
+ } catch (error) {
993
+ console.error("Failed to get cron:", error);
994
+ process.exit(1);
995
+ }
996
+ });
997
+ crons.command("trigger").description("Trigger a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
998
+ try {
999
+ const cron = await createClient(options.url).crons.trigger({ target: parseTarget(target) });
1000
+ const table = new Table({ head: [
1001
+ "Name",
1002
+ "State",
1003
+ "Runs",
1004
+ "Fails",
1005
+ "Next Run"
1006
+ ] });
1007
+ table.push([
1008
+ cron.name,
1009
+ cron.state,
1010
+ cron.runCount,
1011
+ cron.failCount,
1012
+ cron.nextRun ?? "-"
1013
+ ]);
1014
+ console.log(table.toString());
1015
+ } catch (error) {
1016
+ console.error("Failed to trigger cron:", error);
1017
+ process.exit(1);
1018
+ }
1019
+ });
1020
+ crons.command("start").description("Start a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
1021
+ try {
1022
+ const cron = await createClient(options.url).crons.start({ target: parseTarget(target) });
1023
+ const table = new Table({ head: [
1024
+ "Name",
1025
+ "State",
1026
+ "Runs",
1027
+ "Fails",
1028
+ "Next Run"
1029
+ ] });
1030
+ table.push([
1031
+ cron.name,
1032
+ cron.state,
1033
+ cron.runCount,
1034
+ cron.failCount,
1035
+ cron.nextRun ?? "-"
1036
+ ]);
1037
+ console.log(table.toString());
1038
+ } catch (error) {
1039
+ console.error("Failed to start cron:", error);
1040
+ process.exit(1);
1041
+ }
1042
+ });
1043
+ crons.command("stop").description("Stop a cron process").argument("<target>", "Cron name or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
1044
+ try {
1045
+ const cron = await createClient(options.url).crons.stop({ target: parseTarget(target) });
1046
+ const table = new Table({ head: [
1047
+ "Name",
1048
+ "State",
1049
+ "Runs",
1050
+ "Fails",
1051
+ "Next Run"
1052
+ ] });
1053
+ table.push([
1054
+ cron.name,
1055
+ cron.state,
1056
+ cron.runCount,
1057
+ cron.failCount,
1058
+ cron.nextRun ?? "-"
1059
+ ]);
1060
+ console.log(table.toString());
1061
+ } catch (error) {
1062
+ console.error("Failed to stop cron:", error);
1063
+ process.exit(1);
1064
+ }
1065
+ });
1066
+ const tasks = program.command("tasks").description("Manage tasks");
1067
+ tasks.command("list").description("List tasks").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
1068
+ try {
1069
+ const tasks = await createClient(options.url).tasks.list();
1070
+ const table = new Table({ head: [
1071
+ "Id",
1072
+ "State",
1073
+ "Processes"
1074
+ ] });
1075
+ for (const task of tasks) table.push([
1076
+ task.id,
1077
+ task.state,
1078
+ task.processNames.join(", ")
1079
+ ]);
1080
+ console.log(table.toString());
1081
+ } catch (error) {
1082
+ console.error("Failed to list tasks:", error);
1083
+ process.exit(1);
1084
+ }
1085
+ });
1086
+ tasks.command("get").description("Get a task by id or index").argument("<target>", "Task id or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
1087
+ try {
1088
+ const task = await createClient(options.url).tasks.get({ target: parseTarget(target) });
1089
+ const table = new Table({ head: [
1090
+ "Id",
1091
+ "State",
1092
+ "Processes"
1093
+ ] });
1094
+ table.push([
1095
+ task.id,
1096
+ task.state,
1097
+ task.processNames.join(", ")
1098
+ ]);
1099
+ console.log(table.toString());
1100
+ } catch (error) {
1101
+ console.error("Failed to get task:", error);
1102
+ process.exit(1);
1103
+ }
1104
+ });
1105
+ tasks.command("add").description("Add a task").requiredOption("-n, --name <name>", "Task name").requiredOption("-d, --definition <json>", "Process definition JSON").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (options) => {
1106
+ try {
1107
+ const client = createClient(options.url);
1108
+ const definition = parseDefinition(options.definition);
1109
+ const task = await client.tasks.add({
1110
+ name: options.name,
1111
+ definition
1112
+ });
1113
+ const table = new Table({ head: [
1114
+ "Id",
1115
+ "State",
1116
+ "Processes"
1117
+ ] });
1118
+ table.push([
1119
+ task.id,
1120
+ task.state,
1121
+ task.processNames.join(", ")
1122
+ ]);
1123
+ console.log(table.toString());
1124
+ } catch (error) {
1125
+ console.error("Failed to add task:", error);
1126
+ process.exit(1);
1127
+ }
1128
+ });
1129
+ tasks.command("remove").description("Remove a task by id or index").argument("<target>", "Task id or index").option("-u, --url <url>", "RPC server URL", "http://localhost:3000/rpc").action(async (target, options) => {
1130
+ try {
1131
+ const task = await createClient(options.url).tasks.remove({ target: parseTarget(target) });
1132
+ const table = new Table({ head: [
1133
+ "Id",
1134
+ "State",
1135
+ "Processes"
1136
+ ] });
1137
+ table.push([
1138
+ task.id,
1139
+ task.state,
1140
+ task.processNames.join(", ")
1141
+ ]);
1142
+ console.log(table.toString());
1143
+ } catch (error) {
1144
+ console.error("Failed to remove task:", error);
1145
+ process.exit(1);
1146
+ }
1147
+ });
1148
+ const TargetSchema = v.union([v.string(), v.number()]);
1149
+ function parseTarget(value) {
1150
+ const asNumber = Number(value);
1151
+ return v.parse(TargetSchema, Number.isNaN(asNumber) ? value : asNumber);
1152
+ }
1153
+ function parseDefinition(raw) {
1154
+ try {
1155
+ const parsed = JSON.parse(raw);
1156
+ return v.parse(ProcessDefinitionSchema, parsed);
1157
+ } catch (error) {
1158
+ console.error("Invalid --definition JSON. Expected a ProcessDefinition.");
1159
+ throw error;
1160
+ }
1161
+ }
1162
+ program.parse();
1163
+
1164
+ //#endregion
1165
+ export { };
1166
+ //# sourceMappingURL=cli.mjs.map