pidnap 0.0.0-dev.5 → 0.0.0-dev.7

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/src/cli.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from "commander";
3
2
  import { createServer } from "node:http";
4
3
  import { pathToFileURL } from "node:url";
5
4
  import { resolve } from "node:path";
@@ -10,453 +9,343 @@ import { router } from "./api/server.ts";
10
9
  import { Manager, ManagerConfig } from "./manager.ts";
11
10
  import { logger } from "./logger.ts";
12
11
  import Table from "cli-table3";
13
- import { createClient } from "./api/client.ts";
12
+ import { createClient, type Client } from "./api/client.ts";
14
13
  import { ProcessDefinition } from "./lazy-process.ts";
15
14
  import { tImport } from "./utils.ts";
15
+ import { os as osBase } from "@orpc/server";
16
+ import { createCli, type TrpcCliMeta } from "trpc-cli";
17
+ import { format } from "node:util";
18
+ import pkg from "../package.json" assert { type: "json" };
16
19
 
17
- const program = new Command();
20
+ const os = osBase.$context<{ client: Client }>().$meta<TrpcCliMeta>({});
18
21
 
19
- program
20
- .name("pidnap")
21
- .description("Process manager with init system capabilities")
22
- .version("0.0.0-dev.5");
22
+ const ResourceTarget = v.union([v.string(), v.number()]);
23
23
 
24
- program
25
- .command("init")
26
- .description("Initialize and run the process manager with config file")
27
- .option("-c, --config [path]", "Path to config file", "pidnap.config.ts")
28
- .action(async (options) => {
29
- process.title = "pidnap";
30
- const initLogger = logger({ name: "pidnap" });
31
- try {
32
- // Resolve config file path
33
- const configPath = resolve(process.cwd(), options.config);
34
- const configUrl = pathToFileURL(configPath).href;
35
-
36
- // Import the config file
37
- const configModule = await tImport(configUrl);
38
- const rawConfig =
39
- configModule.default.default || configModule.default || configModule.config || {};
40
-
41
- // Parse and validate the config with Valibot
42
- const config = v.parse(ManagerConfig, rawConfig);
24
+ // Helper functions for table formatting (used multiple times)
25
+ function printProcessTable(proc: { name: string; state: string; restarts: number }) {
26
+ const table = new Table({ head: ["Name", "State", "Restarts"] });
27
+ table.push([proc.name, proc.state, proc.restarts]);
28
+ console.log(table.toString());
29
+ }
43
30
 
44
- // Extract HTTP config with defaults
45
- const host = config.http?.host ?? "127.0.0.1";
46
- const port = config.http?.port ?? 9876;
47
- const authToken = config.http?.authToken;
31
+ function printTaskTable(task: { id: string; state: string; processNames: string[] }) {
32
+ const table = new Table({ head: ["Id", "State", "Processes"] });
33
+ table.push([task.id, task.state, task.processNames.join(", ")]);
34
+ console.log(table.toString());
35
+ }
48
36
 
49
- // Create manager with config
50
- const logDir = config.logDir ?? resolve(process.cwd(), "logs");
51
- const managerLogger = logger({ name: "pidnap", logFile: resolve(logDir, "pidnap.log") });
52
- const manager = new Manager(config, managerLogger);
37
+ function printCronTable(cron: {
38
+ name: string;
39
+ state: string;
40
+ runCount: number;
41
+ failCount: number;
42
+ nextRun: string | null;
43
+ }) {
44
+ const table = new Table({ head: ["Name", "State", "Runs", "Fails", "Next Run"] });
45
+ table.push([cron.name, cron.state, cron.runCount, cron.failCount, cron.nextRun ?? "-"]);
46
+ console.log(table.toString());
47
+ }
53
48
 
54
- // Setup ORPC server with optional auth token middleware
55
- const handler = new RPCHandler(router, {
56
- interceptors: [
57
- onError((error) => {
58
- managerLogger.error(error);
59
- }),
60
- ],
61
- });
49
+ const cliRouter = os.router({
50
+ init: osBase
51
+ .meta({
52
+ description: "Initialize and run the process manager with config file",
53
+ aliases: { options: { config: "c" } },
54
+ })
55
+ .input(
56
+ v.object({
57
+ config: v.optional(
58
+ v.pipe(v.string(), v.description("Path to config file")),
59
+ "pidnap.config.ts",
60
+ ),
61
+ }),
62
+ )
63
+ .handler(async ({ input }) => {
64
+ process.title = "pidnap";
65
+ const initLogger = logger({ name: "pidnap" });
66
+ try {
67
+ // Resolve config file path
68
+ const configPath = resolve(process.cwd(), input.config);
69
+ const configUrl = pathToFileURL(configPath).href;
70
+
71
+ // Import the config file
72
+ const configModule = await tImport(configUrl);
73
+ const rawConfig =
74
+ configModule.default.default || configModule.default || configModule.config || {};
75
+
76
+ // Parse and validate the config with Valibot
77
+ const config = v.parse(ManagerConfig, rawConfig);
78
+
79
+ // Extract HTTP config with defaults
80
+ const host = config.http?.host ?? "127.0.0.1";
81
+ const port = config.http?.port ?? 9876;
82
+ const authToken = config.http?.authToken;
83
+
84
+ // Create manager with config
85
+ const logDir = config.logDir ?? resolve(process.cwd(), "logs");
86
+ const managerLogger = logger({ name: "pidnap", logFile: resolve(logDir, "pidnap.log") });
87
+ const manager = new Manager(config, managerLogger);
88
+
89
+ // Setup ORPC server with optional auth token middleware
90
+ const handler = new RPCHandler(router, {
91
+ interceptors: [
92
+ onError((error) => {
93
+ managerLogger.error(error);
94
+ }),
95
+ ],
96
+ });
62
97
 
63
- const server = createServer(async (req, res) => {
64
- // Check auth token if configured
65
- if (authToken) {
66
- const providedToken = req.headers["authorization"]?.replace("Bearer ", "");
67
- if (providedToken !== authToken) {
68
- res.statusCode = 401;
69
- res.end("Unauthorized");
70
- return;
98
+ const server = createServer(async (req, res) => {
99
+ // Check auth token if configured
100
+ if (authToken) {
101
+ const providedToken = req.headers["authorization"]?.replace("Bearer ", "");
102
+ if (providedToken !== authToken) {
103
+ res.statusCode = 401;
104
+ res.end("Unauthorized");
105
+ return;
106
+ }
71
107
  }
72
- }
73
108
 
74
- const { matched } = await handler.handle(req, res, {
75
- prefix: "/rpc",
76
- context: { manager },
109
+ const { matched } = await handler.handle(req, res, {
110
+ prefix: "/rpc",
111
+ context: { manager },
112
+ });
113
+ if (matched) return;
114
+ res.statusCode = 404;
115
+ res.end("Not found\n");
77
116
  });
78
- if (matched) return;
79
- res.statusCode = 404;
80
- res.end("Not found\n");
81
- });
82
117
 
83
- server.listen(port, host, async () => {
84
- managerLogger.info(`pidnap RPC server running on http://${host}:${port}`);
85
- if (authToken) {
86
- managerLogger.info("Auth token required for API access");
87
- }
118
+ server.listen(port, host, async () => {
119
+ managerLogger.info(`pidnap RPC server running on http://${host}:${port}`);
120
+ if (authToken) {
121
+ managerLogger.info("Auth token required for API access");
122
+ }
88
123
 
89
- try {
90
- await manager.start();
91
- } catch (err) {
92
- managerLogger.error("Failed to start manager:", err);
93
- server.close();
94
- process.exit(1);
95
- }
96
- });
124
+ try {
125
+ await manager.start();
126
+ } catch (err) {
127
+ managerLogger.error("Failed to start manager:", err);
128
+ server.close();
129
+ process.exit(1);
130
+ }
131
+ });
97
132
 
98
- // Wait for shutdown
99
- await manager.waitForShutdown();
133
+ // Wait for shutdown
134
+ await manager.waitForShutdown();
100
135
 
101
- // Close server on shutdown
102
- server.close();
103
- } catch (error) {
104
- initLogger.error("Failed to start pidnap:", error);
105
- process.exit(1);
106
- }
107
- });
136
+ // Close server on shutdown
137
+ server.close();
138
+ } catch (error) {
139
+ initLogger.error("Failed to start pidnap:", error);
140
+ process.exit(1);
141
+ }
142
+ }),
108
143
 
109
- program
110
- .command("status")
111
- .description("Show manager status")
112
- .option("-u, --url <url>", "RPC server URL")
113
- .action(async (options) => {
114
- try {
115
- const client = createClient(options.url);
144
+ status: os
145
+ .meta({ description: "Show manager status" })
146
+ .handler(async ({ context: { client } }) => {
116
147
  const status = await client.manager.status();
117
-
118
148
  const table = new Table({ head: ["State", "Processes", "Crons", "Tasks"] });
119
149
  table.push([status.state, status.processCount, status.cronCount, status.taskCount]);
120
150
  console.log(table.toString());
121
- } catch (error) {
122
- console.error("Failed to fetch status:", error);
123
- process.exit(1);
124
- }
125
- });
126
-
127
- const processGroup = program.command("process").description("Manage restarting processes");
128
-
129
- processGroup
130
- .command("list")
131
- .description("List restarting processes")
132
- .option("-u, --url <url>", "RPC server URL")
133
- .action(async (options) => {
134
- try {
135
- const client = createClient(options.url);
136
- const processes = await client.processes.list();
137
-
138
- const table = new Table({ head: ["Name", "State", "Restarts"] });
139
- for (const proc of processes) {
140
- table.push([proc.name, proc.state, proc.restarts]);
141
- }
142
- console.log(table.toString());
143
- } catch (error) {
144
- console.error("Failed to list processes:", error);
145
- process.exit(1);
146
- }
147
- });
148
-
149
- processGroup
150
- .command("get")
151
- .description("Get a restarting process by name or index")
152
- .argument("<target>", "Process name or index")
153
- .option("-u, --url <url>", "RPC server URL")
154
- .action(async (target, options) => {
155
- try {
156
- const client = createClient(options.url);
157
- const proc = await client.processes.get({ target: parseTarget(target) });
158
-
159
- const table = new Table({ head: ["Name", "State", "Restarts"] });
160
- table.push([proc.name, proc.state, proc.restarts]);
161
- console.log(table.toString());
162
- } catch (error) {
163
- console.error("Failed to get process:", error);
164
- process.exit(1);
165
- }
166
- });
167
-
168
- processGroup
169
- .command("add")
170
- .description("Add a restarting process")
171
- .requiredOption("-n, --name <name>", "Process name")
172
- .requiredOption("-d, --definition <json>", "Process definition JSON")
173
- .option("-u, --url <url>", "RPC server URL")
174
- .action(async (options) => {
175
- try {
176
- const client = createClient(options.url);
177
- const definition = parseDefinition(options.definition);
178
- const proc = await client.processes.add({ name: options.name, definition });
179
-
180
- const table = new Table({ head: ["Name", "State", "Restarts"] });
181
- table.push([proc.name, proc.state, proc.restarts]);
182
- console.log(table.toString());
183
- } catch (error) {
184
- console.error("Failed to add process:", error);
185
- process.exit(1);
186
- }
187
- });
188
-
189
- processGroup
190
- .command("start")
191
- .description("Start a restarting process")
192
- .argument("<target>", "Process name or index")
193
- .option("-u, --url <url>", "RPC server URL")
194
- .action(async (target, options) => {
195
- try {
196
- const client = createClient(options.url);
197
- const proc = await client.processes.start({ target: parseTarget(target) });
198
-
199
- const table = new Table({ head: ["Name", "State", "Restarts"] });
200
- table.push([proc.name, proc.state, proc.restarts]);
201
- console.log(table.toString());
202
- } catch (error) {
203
- console.error("Failed to start process:", error);
204
- process.exit(1);
205
- }
206
- });
207
-
208
- processGroup
209
- .command("stop")
210
- .description("Stop a restarting process")
211
- .argument("<target>", "Process name or index")
212
- .option("-u, --url <url>", "RPC server URL")
213
- .action(async (target, options) => {
214
- try {
215
- const client = createClient(options.url);
216
- const proc = await client.processes.stop({ target: parseTarget(target) });
217
-
218
- const table = new Table({ head: ["Name", "State", "Restarts"] });
219
- table.push([proc.name, proc.state, proc.restarts]);
220
- console.log(table.toString());
221
- } catch (error) {
222
- console.error("Failed to stop process:", error);
223
- process.exit(1);
224
- }
225
- });
226
-
227
- processGroup
228
- .command("restart")
229
- .description("Restart a restarting process")
230
- .argument("<target>", "Process name or index")
231
- .option("-f, --force", "Force restart")
232
- .option("-u, --url <url>", "RPC server URL")
233
- .action(async (target, options) => {
234
- try {
235
- const client = createClient(options.url);
236
- const proc = await client.processes.restart({
237
- target: parseTarget(target),
238
- force: options.force,
239
- });
240
-
241
- const table = new Table({ head: ["Name", "State", "Restarts"] });
242
- table.push([proc.name, proc.state, proc.restarts]);
243
- console.log(table.toString());
244
- } catch (error) {
245
- console.error("Failed to restart process:", error);
246
- process.exit(1);
247
- }
248
- });
249
-
250
- processGroup
251
- .command("remove")
252
- .description("Remove a restarting process")
253
- .argument("<target>", "Process name or index")
254
- .option("-u, --url <url>", "RPC server URL")
255
- .action(async (target, options) => {
256
- try {
257
- const client = createClient(options.url);
258
- await client.processes.remove({ target: parseTarget(target) });
259
- console.log("Process removed");
260
- } catch (error) {
261
- console.error("Failed to remove process:", error);
262
- process.exit(1);
263
- }
264
- });
265
-
266
- const crons = program.command("crons").description("Manage cron processes");
267
-
268
- crons
269
- .command("list")
270
- .description("List cron processes")
271
- .option("-u, --url <url>", "RPC server URL")
272
- .action(async (options) => {
273
- try {
274
- const client = createClient(options.url);
275
- const crons = await client.crons.list();
276
-
277
- const table = new Table({ head: ["Name", "State", "Runs", "Fails", "Next Run"] });
278
- for (const cron of crons) {
279
- table.push([cron.name, cron.state, cron.runCount, cron.failCount, cron.nextRun ?? "-"]);
280
- }
281
- console.log(table.toString());
282
- } catch (error) {
283
- console.error("Failed to list crons:", error);
284
- process.exit(1);
285
- }
286
- });
287
-
288
- crons
289
- .command("get")
290
- .description("Get a cron process by name or index")
291
- .argument("<target>", "Cron name or index")
292
- .option("-u, --url <url>", "RPC server URL")
293
- .action(async (target, options) => {
294
- try {
295
- const client = createClient(options.url);
296
- const cron = await client.crons.get({ target: parseTarget(target) });
297
-
298
- const table = new Table({ head: ["Name", "State", "Runs", "Fails", "Next Run"] });
299
- table.push([cron.name, cron.state, cron.runCount, cron.failCount, cron.nextRun ?? "-"]);
300
- console.log(table.toString());
301
- } catch (error) {
302
- console.error("Failed to get cron:", error);
303
- process.exit(1);
304
- }
305
- });
306
-
307
- crons
308
- .command("trigger")
309
- .description("Trigger a cron process")
310
- .argument("<target>", "Cron name or index")
311
- .option("-u, --url <url>", "RPC server URL")
312
- .action(async (target, options) => {
313
- try {
314
- const client = createClient(options.url);
315
- const cron = await client.crons.trigger({ target: parseTarget(target) });
316
-
317
- const table = new Table({ head: ["Name", "State", "Runs", "Fails", "Next Run"] });
318
- table.push([cron.name, cron.state, cron.runCount, cron.failCount, cron.nextRun ?? "-"]);
319
- console.log(table.toString());
320
- } catch (error) {
321
- console.error("Failed to trigger cron:", error);
322
- process.exit(1);
323
- }
324
- });
325
-
326
- crons
327
- .command("start")
328
- .description("Start a cron process")
329
- .argument("<target>", "Cron name or index")
330
- .option("-u, --url <url>", "RPC server URL")
331
- .action(async (target, options) => {
332
- try {
333
- const client = createClient(options.url);
334
- const cron = await client.crons.start({ target: parseTarget(target) });
335
-
336
- const table = new Table({ head: ["Name", "State", "Runs", "Fails", "Next Run"] });
337
- table.push([cron.name, cron.state, cron.runCount, cron.failCount, cron.nextRun ?? "-"]);
338
- console.log(table.toString());
339
- } catch (error) {
340
- console.error("Failed to start cron:", error);
341
- process.exit(1);
342
- }
343
- });
344
-
345
- crons
346
- .command("stop")
347
- .description("Stop a cron process")
348
- .argument("<target>", "Cron name or index")
349
- .option("-u, --url <url>", "RPC server URL")
350
- .action(async (target, options) => {
351
- try {
352
- const client = createClient(options.url);
353
- const cron = await client.crons.stop({ target: parseTarget(target) });
354
-
355
- const table = new Table({ head: ["Name", "State", "Runs", "Fails", "Next Run"] });
356
- table.push([cron.name, cron.state, cron.runCount, cron.failCount, cron.nextRun ?? "-"]);
357
- console.log(table.toString());
358
- } catch (error) {
359
- console.error("Failed to stop cron:", error);
360
- process.exit(1);
361
- }
362
- });
363
-
364
- const tasks = program.command("tasks").description("Manage tasks");
365
-
366
- tasks
367
- .command("list")
368
- .description("List tasks")
369
- .option("-u, --url <url>", "RPC server URL")
370
- .action(async (options) => {
371
- try {
372
- const client = createClient(options.url);
151
+ }),
152
+
153
+ process: os.router({
154
+ list: os
155
+ .meta({ description: "List restarting processes" })
156
+ .handler(async ({ context: { client } }) => {
157
+ const processes = await client.processes.list();
158
+ const table = new Table({ head: ["Name", "State", "Restarts"] });
159
+ for (const proc of processes) {
160
+ table.push([proc.name, proc.state, proc.restarts]);
161
+ }
162
+ console.log(table.toString());
163
+ }),
164
+ get: os
165
+ .meta({
166
+ description: "Get a restarting process by name or index",
167
+ aliases: { options: { target: "t" } },
168
+ })
169
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Process name or index")) }))
170
+ .handler(async ({ input, context: { client } }) => {
171
+ const proc = await client.processes.get({ target: input.target });
172
+ printProcessTable(proc);
173
+ }),
174
+ add: os
175
+ .meta({
176
+ description: "Add a restarting process",
177
+ aliases: { options: { name: "n", definition: "d" } },
178
+ })
179
+ .input(
180
+ v.object({
181
+ name: v.pipe(v.string(), v.description("Process name")),
182
+ definition: v.pipe(ProcessDefinition, v.description("Process definition JSON")),
183
+ }),
184
+ )
185
+ .handler(async ({ input, context: { client } }) => {
186
+ const proc = await client.processes.add({ name: input.name, definition: input.definition });
187
+ printProcessTable(proc);
188
+ }),
189
+ start: os
190
+ .meta({ description: "Start a restarting process", aliases: { options: { target: "t" } } })
191
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Process name or index")) }))
192
+ .handler(async ({ input, context: { client } }) => {
193
+ const proc = await client.processes.start({ target: input.target });
194
+ printProcessTable(proc);
195
+ }),
196
+ stop: os
197
+ .meta({ description: "Stop a restarting process", aliases: { options: { target: "t" } } })
198
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Process name or index")) }))
199
+ .handler(async ({ input, context: { client } }) => {
200
+ const proc = await client.processes.stop({ target: input.target });
201
+ printProcessTable(proc);
202
+ }),
203
+ restart: os
204
+ .meta({
205
+ description: "Restart a restarting process",
206
+ aliases: { options: { target: "t", force: "f" } },
207
+ })
208
+ .input(
209
+ v.object({
210
+ target: v.pipe(ResourceTarget, v.description("Process name or index")),
211
+ force: v.optional(v.pipe(v.boolean(), v.description("Force restart"))),
212
+ }),
213
+ )
214
+ .handler(async ({ input, context: { client } }) => {
215
+ const proc = await client.processes.restart({ target: input.target, force: input.force });
216
+ printProcessTable(proc);
217
+ }),
218
+ reload: os
219
+ .meta({
220
+ description: "Reload a restarting process definition",
221
+ aliases: { options: { target: "t", definition: "d", restartImmediately: "r" } },
222
+ })
223
+ .input(
224
+ v.object({
225
+ target: v.pipe(ResourceTarget, v.description("Process name or index")),
226
+ definition: v.pipe(ProcessDefinition, v.description("Process definition JSON")),
227
+ restartImmediately: v.optional(
228
+ v.pipe(v.boolean(), v.description("Restart immediately after reload")),
229
+ ),
230
+ }),
231
+ )
232
+ .handler(async ({ input, context: { client } }) => {
233
+ const proc = await client.processes.reload({
234
+ target: input.target,
235
+ definition: input.definition,
236
+ restartImmediately: input.restartImmediately,
237
+ });
238
+ printProcessTable(proc);
239
+ }),
240
+ remove: os
241
+ .meta({ description: "Remove a restarting process", aliases: { options: { target: "t" } } })
242
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Process name or index")) }))
243
+ .handler(async ({ input, context: { client } }) => {
244
+ await client.processes.remove({ target: input.target });
245
+ console.log("Process removed");
246
+ }),
247
+ }),
248
+
249
+ // Tasks
250
+ tasks: os.router({
251
+ list: os.meta({ description: "List tasks" }).handler(async ({ context: { client } }) => {
373
252
  const tasks = await client.tasks.list();
374
-
375
253
  const table = new Table({ head: ["Id", "State", "Processes"] });
376
254
  for (const task of tasks) {
377
255
  table.push([task.id, task.state, task.processNames.join(", ")]);
378
256
  }
379
257
  console.log(table.toString());
380
- } catch (error) {
381
- console.error("Failed to list tasks:", error);
382
- process.exit(1);
383
- }
384
- });
385
-
386
- tasks
387
- .command("get")
388
- .description("Get a task by id or index")
389
- .argument("<target>", "Task id or index")
390
- .option("-u, --url <url>", "RPC server URL")
391
- .action(async (target, options) => {
392
- try {
393
- const client = createClient(options.url);
394
- const task = await client.tasks.get({ target: parseTarget(target) });
395
-
396
- const table = new Table({ head: ["Id", "State", "Processes"] });
397
- table.push([task.id, task.state, task.processNames.join(", ")]);
398
- console.log(table.toString());
399
- } catch (error) {
400
- console.error("Failed to get task:", error);
401
- process.exit(1);
402
- }
403
- });
404
-
405
- tasks
406
- .command("add")
407
- .description("Add a task")
408
- .requiredOption("-n, --name <name>", "Task name")
409
- .requiredOption("-d, --definition <json>", "Process definition JSON")
410
- .option("-u, --url <url>", "RPC server URL")
411
- .action(async (options) => {
412
- try {
413
- const client = createClient(options.url);
414
- const definition = parseDefinition(options.definition);
415
- const task = await client.tasks.add({ name: options.name, definition });
416
-
417
- const table = new Table({ head: ["Id", "State", "Processes"] });
418
- table.push([task.id, task.state, task.processNames.join(", ")]);
419
- console.log(table.toString());
420
- } catch (error) {
421
- console.error("Failed to add task:", error);
422
- process.exit(1);
423
- }
424
- });
425
-
426
- tasks
427
- .command("remove")
428
- .description("Remove a task by id or index")
429
- .argument("<target>", "Task id or index")
430
- .option("-u, --url <url>", "RPC server URL")
431
- .action(async (target, options) => {
432
- try {
433
- const client = createClient(options.url);
434
- const task = await client.tasks.remove({ target: parseTarget(target) });
435
-
436
- const table = new Table({ head: ["Id", "State", "Processes"] });
437
- table.push([task.id, task.state, task.processNames.join(", ")]);
438
- console.log(table.toString());
439
- } catch (error) {
440
- console.error("Failed to remove task:", error);
441
- process.exit(1);
258
+ }),
259
+ get: os
260
+ .meta({ description: "Get a task by id or index", aliases: { options: { target: "t" } } })
261
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Task id or index")) }))
262
+ .handler(async ({ input, context: { client } }) => {
263
+ const task = await client.tasks.get({ target: input.target });
264
+ printTaskTable(task);
265
+ }),
266
+ add: os
267
+ .meta({ description: "Add a task", aliases: { options: { name: "n", definition: "d" } } })
268
+ .input(
269
+ v.object({
270
+ name: v.pipe(v.string(), v.description("Task name")),
271
+ definition: v.pipe(ProcessDefinition, v.description("Process definition JSON")),
272
+ }),
273
+ )
274
+ .handler(async ({ input, context: { client } }) => {
275
+ const task = await client.tasks.add({ name: input.name, definition: input.definition });
276
+ printTaskTable(task);
277
+ }),
278
+ remove: os
279
+ .meta({ description: "Remove a task by id or index", aliases: { options: { target: "t" } } })
280
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Task id or index")) }))
281
+ .handler(async ({ input, context: { client } }) => {
282
+ const task = await client.tasks.remove({ target: input.target });
283
+ printTaskTable(task);
284
+ }),
285
+ }),
286
+
287
+ // Crons
288
+ crons: os.router({
289
+ list: os
290
+ .meta({ description: "List cron processes" })
291
+ .handler(async ({ context: { client } }) => {
292
+ const crons = await client.crons.list();
293
+ const table = new Table({ head: ["Name", "State", "Runs", "Fails", "Next Run"] });
294
+ for (const cron of crons) {
295
+ table.push([cron.name, cron.state, cron.runCount, cron.failCount, cron.nextRun ?? "-"]);
296
+ }
297
+ console.log(table.toString());
298
+ }),
299
+ get: os
300
+ .meta({
301
+ description: "Get a cron process by name or index",
302
+ aliases: { options: { target: "t" } },
303
+ })
304
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Cron name or index")) }))
305
+ .handler(async ({ input, context: { client } }) => {
306
+ const cron = await client.crons.get({ target: input.target });
307
+ printCronTable(cron);
308
+ }),
309
+ trigger: os
310
+ .meta({ description: "Trigger a cron process", aliases: { options: { target: "t" } } })
311
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Cron name or index")) }))
312
+ .handler(async ({ input, context: { client } }) => {
313
+ const cron = await client.crons.trigger({ target: input.target });
314
+ printCronTable(cron);
315
+ }),
316
+ start: os
317
+ .meta({ description: "Start a cron process", aliases: { options: { target: "t" } } })
318
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Cron name or index")) }))
319
+ .handler(async ({ input, context: { client } }) => {
320
+ const cron = await client.crons.start({ target: input.target });
321
+ printCronTable(cron);
322
+ }),
323
+ stop: os
324
+ .meta({ description: "Stop a cron process", aliases: { options: { target: "t" } } })
325
+ .input(v.object({ target: v.pipe(ResourceTarget, v.description("Cron name or index")) }))
326
+ .handler(async ({ input, context: { client } }) => {
327
+ const cron = await client.crons.stop({ target: input.target });
328
+ printCronTable(cron);
329
+ }),
330
+ }),
331
+ });
332
+
333
+ const client = createClient();
334
+ const cli = createCli({
335
+ name: "pidnap",
336
+ version: pkg.version,
337
+ router: cliRouter,
338
+ context: { client },
339
+ });
340
+
341
+ cli.run({
342
+ formatError(error: any) {
343
+ if (error?.cause?.code === "ECONNREFUSED") {
344
+ return (
345
+ `Failed to connect to RPC server, are you sure the server is running? If the Server is running of different url, use PIDNAP_RPC_URL environment variable to set it.\n` +
346
+ format(error)
347
+ );
442
348
  }
443
- });
444
-
445
- const TargetSchema = v.union([v.string(), v.number()]);
446
-
447
- function parseTarget(value: string): string | number {
448
- const asNumber = Number(value);
449
- return v.parse(TargetSchema, Number.isNaN(asNumber) ? value : asNumber);
450
- }
451
-
452
- function parseDefinition(raw: string): ProcessDefinition {
453
- try {
454
- const parsed = JSON.parse(raw);
455
- return v.parse(ProcessDefinition, parsed);
456
- } catch (error) {
457
- console.error("Invalid --definition JSON. Expected a ProcessDefinition.");
458
- throw error;
459
- }
460
- }
461
-
462
- program.parse();
349
+ return format(error);
350
+ },
351
+ });