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