svamp-cli 0.1.86 → 0.1.87
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/package.json +1 -1
- package/dist/agentCommands-BNeH-pql.mjs +0 -263
- package/dist/api-BRbsyqJ4.mjs +0 -147
- package/dist/cli.mjs +0 -1535
- package/dist/commands-BYbuedOK.mjs +0 -469
- package/dist/commands-BrEXFF2q.mjs +0 -593
- package/dist/commands-DJoYOM_1.mjs +0 -531
- package/dist/commands-Dsoathf7.mjs +0 -1899
- package/dist/index.mjs +0 -20
- package/dist/package-DoiRB1gD.mjs +0 -62
- package/dist/run-D7JVP8QU.mjs +0 -8260
- package/dist/run-ekaMou5p.mjs +0 -1103
- package/dist/staticServer-CWcmMF5V.mjs +0 -477
- package/dist/tunnel-BDKdemh0.mjs +0 -383
|
@@ -1,593 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, readFileSync } from 'fs';
|
|
2
|
-
import { resolve } from 'path';
|
|
3
|
-
import { connectAndGetMachine } from './commands-Dsoathf7.mjs';
|
|
4
|
-
import 'node:fs';
|
|
5
|
-
import 'node:child_process';
|
|
6
|
-
import 'node:path';
|
|
7
|
-
import 'node:os';
|
|
8
|
-
import './run-D7JVP8QU.mjs';
|
|
9
|
-
import 'os';
|
|
10
|
-
import 'fs/promises';
|
|
11
|
-
import 'url';
|
|
12
|
-
import 'child_process';
|
|
13
|
-
import 'crypto';
|
|
14
|
-
import 'node:crypto';
|
|
15
|
-
import '@agentclientprotocol/sdk';
|
|
16
|
-
import '@modelcontextprotocol/sdk/client/index.js';
|
|
17
|
-
import '@modelcontextprotocol/sdk/client/stdio.js';
|
|
18
|
-
import '@modelcontextprotocol/sdk/types.js';
|
|
19
|
-
import 'zod';
|
|
20
|
-
import 'node:fs/promises';
|
|
21
|
-
import 'node:util';
|
|
22
|
-
|
|
23
|
-
function getFlag(args, flag) {
|
|
24
|
-
const idx = args.indexOf(flag);
|
|
25
|
-
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
|
|
26
|
-
}
|
|
27
|
-
function getAllFlags(args, flag) {
|
|
28
|
-
const values = [];
|
|
29
|
-
for (let i = 0; i < args.length; i++) {
|
|
30
|
-
if (args[i] === flag && i + 1 < args.length) {
|
|
31
|
-
values.push(args[i + 1]);
|
|
32
|
-
i++;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return values;
|
|
36
|
-
}
|
|
37
|
-
function hasFlag(args, ...flags) {
|
|
38
|
-
return flags.some((f) => args.includes(f));
|
|
39
|
-
}
|
|
40
|
-
function splitOnDoubleDash(args) {
|
|
41
|
-
const sep = args.indexOf("--");
|
|
42
|
-
if (sep === -1) return { flagArgs: args, cmdArgs: [] };
|
|
43
|
-
return { flagArgs: args.slice(0, sep), cmdArgs: args.slice(sep + 1) };
|
|
44
|
-
}
|
|
45
|
-
function parseIntFlag(args, flag, defaultVal) {
|
|
46
|
-
const raw = getFlag(args, flag);
|
|
47
|
-
if (raw === void 0) return defaultVal;
|
|
48
|
-
const n = parseInt(raw, 10);
|
|
49
|
-
if (isNaN(n) || n < 0) {
|
|
50
|
-
console.error(`Error: ${flag} must be a non-negative integer`);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
return n;
|
|
54
|
-
}
|
|
55
|
-
function parseIntFlagOpt(args, flag) {
|
|
56
|
-
const raw = getFlag(args, flag);
|
|
57
|
-
if (raw === void 0) return void 0;
|
|
58
|
-
const n = parseInt(raw, 10);
|
|
59
|
-
if (isNaN(n) || n < 0) {
|
|
60
|
-
console.error(`Error: ${flag} must be a non-negative integer`);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
return n;
|
|
64
|
-
}
|
|
65
|
-
async function loadYaml() {
|
|
66
|
-
try {
|
|
67
|
-
return await import('yaml');
|
|
68
|
-
} catch {
|
|
69
|
-
console.error("Error: YAML support requires the `yaml` package. Run: yarn workspace svamp-cli add yaml");
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
async function readSpecFile(filePath) {
|
|
74
|
-
const absPath = resolve(filePath);
|
|
75
|
-
let raw;
|
|
76
|
-
try {
|
|
77
|
-
raw = readFileSync(absPath, "utf-8");
|
|
78
|
-
} catch (err) {
|
|
79
|
-
console.error(`Error reading spec file '${filePath}': ${err.message}`);
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
let parsed;
|
|
83
|
-
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
|
|
84
|
-
const yaml = await loadYaml();
|
|
85
|
-
try {
|
|
86
|
-
parsed = yaml.parse(raw);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
console.error(`Error parsing YAML: ${err.message}`);
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
try {
|
|
93
|
-
parsed = JSON.parse(raw);
|
|
94
|
-
} catch (err) {
|
|
95
|
-
console.error(`Error parsing JSON: ${err.message}`);
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return normalizeSpec(parsed);
|
|
100
|
-
}
|
|
101
|
-
async function writeSpecFile(filePath, spec) {
|
|
102
|
-
const isYaml = filePath.endsWith(".yaml") || filePath.endsWith(".yml");
|
|
103
|
-
const yaml = await loadYaml();
|
|
104
|
-
const exportSpec = {
|
|
105
|
-
name: spec.name,
|
|
106
|
-
command: spec.command,
|
|
107
|
-
args: spec.args,
|
|
108
|
-
workdir: spec.workdir,
|
|
109
|
-
keepAlive: spec.keepAlive,
|
|
110
|
-
maxRestarts: spec.maxRestarts,
|
|
111
|
-
restartDelay: spec.restartDelay,
|
|
112
|
-
...spec.env && Object.keys(spec.env).length > 0 ? { env: spec.env } : {},
|
|
113
|
-
...spec.probe ? { probe: spec.probe } : {},
|
|
114
|
-
...spec.ttl !== void 0 ? { ttl: spec.ttl } : {},
|
|
115
|
-
...spec.serviceGroup ? { serviceGroup: spec.serviceGroup } : {},
|
|
116
|
-
...spec.ports ? { ports: spec.ports } : {}
|
|
117
|
-
};
|
|
118
|
-
const content = isYaml ? `# svamp process spec \u2014 apply with: svamp process apply ${filePath}
|
|
119
|
-
${yaml.stringify(exportSpec)}` : JSON.stringify(exportSpec, null, 2);
|
|
120
|
-
writeFileSync(filePath, content, "utf-8");
|
|
121
|
-
}
|
|
122
|
-
function specToYaml(spec, yaml) {
|
|
123
|
-
const exportSpec = {
|
|
124
|
-
name: spec.name,
|
|
125
|
-
command: spec.command,
|
|
126
|
-
args: spec.args,
|
|
127
|
-
workdir: spec.workdir,
|
|
128
|
-
keepAlive: spec.keepAlive
|
|
129
|
-
};
|
|
130
|
-
if (spec.maxRestarts) exportSpec.maxRestarts = spec.maxRestarts;
|
|
131
|
-
if (spec.restartDelay !== 2) exportSpec.restartDelay = spec.restartDelay;
|
|
132
|
-
if (spec.env && Object.keys(spec.env).length > 0) exportSpec.env = spec.env;
|
|
133
|
-
if (spec.probe) exportSpec.probe = spec.probe;
|
|
134
|
-
if (spec.ttl !== void 0) exportSpec.ttl = spec.ttl;
|
|
135
|
-
if (spec.serviceGroup) exportSpec.serviceGroup = spec.serviceGroup;
|
|
136
|
-
if (spec.ports) exportSpec.ports = spec.ports;
|
|
137
|
-
return yaml.stringify(exportSpec);
|
|
138
|
-
}
|
|
139
|
-
function normalizeSpec(raw) {
|
|
140
|
-
if (!raw || typeof raw !== "object") {
|
|
141
|
-
console.error("Error: spec must be an object");
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
if (!raw.name || typeof raw.name !== "string") {
|
|
145
|
-
console.error("Error: spec.name is required");
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
148
|
-
if (!raw.command || typeof raw.command !== "string") {
|
|
149
|
-
console.error("Error: spec.command is required");
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
name: raw.name,
|
|
154
|
-
command: raw.command,
|
|
155
|
-
args: Array.isArray(raw.args) ? raw.args.map(String) : [],
|
|
156
|
-
workdir: typeof raw.workdir === "string" ? raw.workdir : process.cwd(),
|
|
157
|
-
env: raw.env && typeof raw.env === "object" ? raw.env : void 0,
|
|
158
|
-
probe: raw.probe ? {
|
|
159
|
-
type: "http",
|
|
160
|
-
port: Number(raw.probe.port),
|
|
161
|
-
path: raw.probe.path ?? "/",
|
|
162
|
-
interval: raw.probe.interval !== void 0 ? Number(raw.probe.interval) : void 0,
|
|
163
|
-
timeout: raw.probe.timeout !== void 0 ? Number(raw.probe.timeout) : void 0,
|
|
164
|
-
failureThreshold: raw.probe.failureThreshold !== void 0 ? Number(raw.probe.failureThreshold) : void 0
|
|
165
|
-
} : void 0,
|
|
166
|
-
keepAlive: Boolean(raw.keepAlive ?? false),
|
|
167
|
-
maxRestarts: Number(raw.maxRestarts ?? 0),
|
|
168
|
-
restartDelay: Number(raw.restartDelay ?? 2),
|
|
169
|
-
ttl: raw.ttl !== void 0 ? Number(raw.ttl) : void 0,
|
|
170
|
-
serviceGroup: raw.serviceGroup,
|
|
171
|
-
ports: Array.isArray(raw.ports) ? raw.ports.map(Number) : void 0
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
function statusIcon(status) {
|
|
175
|
-
switch (status) {
|
|
176
|
-
case "running":
|
|
177
|
-
return "\u25CF";
|
|
178
|
-
case "starting":
|
|
179
|
-
return "\u25CB";
|
|
180
|
-
case "stopped":
|
|
181
|
-
return "\u25A1";
|
|
182
|
-
case "failed":
|
|
183
|
-
return "\u2716";
|
|
184
|
-
case "expired":
|
|
185
|
-
return "\u231B";
|
|
186
|
-
case "stopping":
|
|
187
|
-
return "\u2193";
|
|
188
|
-
default:
|
|
189
|
-
return "?";
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
function formatUptime(startedAt) {
|
|
193
|
-
if (!startedAt) return "";
|
|
194
|
-
const s = Math.floor((Date.now() - startedAt) / 1e3);
|
|
195
|
-
if (s < 60) return `${s}s`;
|
|
196
|
-
if (s < 3600) return `${Math.floor(s / 60)}m${s % 60}s`;
|
|
197
|
-
return `${Math.floor(s / 3600)}h${Math.floor(s % 3600 / 60)}m`;
|
|
198
|
-
}
|
|
199
|
-
function formatTTL(spec) {
|
|
200
|
-
if (spec.ttl === void 0) return "";
|
|
201
|
-
const remaining = Math.floor((spec.createdAt + spec.ttl * 1e3 - Date.now()) / 1e3);
|
|
202
|
-
if (remaining <= 0) return "expired";
|
|
203
|
-
if (remaining < 60) return `ttl:${remaining}s`;
|
|
204
|
-
if (remaining < 3600) return `ttl:${Math.floor(remaining / 60)}m`;
|
|
205
|
-
return `ttl:${Math.floor(remaining / 3600)}h`;
|
|
206
|
-
}
|
|
207
|
-
function printProcessTable(processes) {
|
|
208
|
-
if (processes.length === 0) {
|
|
209
|
-
console.log("No supervised processes.");
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const cols = ["NAME", "STATUS", "PID", "RESTARTS", "UPTIME", "PROBE", "TTL"];
|
|
213
|
-
const widths = [24, 12, 8, 10, 10, 8, 8];
|
|
214
|
-
const header = cols.map((c, i) => c.padEnd(widths[i])).join(" ");
|
|
215
|
-
console.log(header);
|
|
216
|
-
console.log("\u2500".repeat(header.length));
|
|
217
|
-
for (const { spec, state } of processes) {
|
|
218
|
-
const row = [
|
|
219
|
-
spec.name.padEnd(widths[0]),
|
|
220
|
-
`${statusIcon(state.status)} ${state.status}`.padEnd(widths[1]),
|
|
221
|
-
(state.pid?.toString() ?? "\u2014").padEnd(widths[2]),
|
|
222
|
-
state.restartCount.toString().padEnd(widths[3]),
|
|
223
|
-
formatUptime(state.startedAt).padEnd(widths[4]),
|
|
224
|
-
(spec.probe ? state.lastProbe ? state.lastProbe.ok ? "ok" : "fail" : "wait" : "\u2014").padEnd(widths[5]),
|
|
225
|
-
formatTTL(spec).padEnd(widths[6])
|
|
226
|
-
];
|
|
227
|
-
console.log(row.join(" "));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
function printProcessDetail(info) {
|
|
231
|
-
const { spec, state } = info;
|
|
232
|
-
console.log(`
|
|
233
|
-
Process: ${spec.name}`);
|
|
234
|
-
console.log(` ID: ${spec.id}`);
|
|
235
|
-
console.log(` Status: ${statusIcon(state.status)} ${state.status}${state.pid ? ` (pid ${state.pid})` : ""}`);
|
|
236
|
-
console.log(` Command: ${spec.command} ${spec.args.join(" ")}`);
|
|
237
|
-
console.log(` Workdir: ${spec.workdir}`);
|
|
238
|
-
console.log(` Keep-alive: ${spec.keepAlive}${spec.maxRestarts > 0 ? ` (max ${spec.maxRestarts})` : ""}`);
|
|
239
|
-
if (state.restartCount > 0) console.log(` Restarts: ${state.restartCount}`);
|
|
240
|
-
if (state.startedAt) console.log(` Uptime: ${formatUptime(state.startedAt)}`);
|
|
241
|
-
if (spec.probe) {
|
|
242
|
-
const p = spec.probe;
|
|
243
|
-
const lp = state.lastProbe;
|
|
244
|
-
const result = lp ? lp.ok ? `OK (HTTP ${lp.statusCode})` : `FAIL (${lp.error ?? `HTTP ${lp.statusCode}`})` : "waiting...";
|
|
245
|
-
console.log(` Probe: http://localhost:${p.port}${p.path ?? "/"} [${result}]`);
|
|
246
|
-
if (state.consecutiveProbeFailures > 0) {
|
|
247
|
-
console.log(` \u26A0 ${state.consecutiveProbeFailures} consecutive failure(s)`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
if (spec.ttl !== void 0) console.log(` TTL: ${spec.ttl}s (${formatTTL(spec) || "no expiry"})`);
|
|
251
|
-
if (spec.serviceGroup) console.log(` Service: ${spec.serviceGroup} port(s): ${spec.ports?.join(", ")}`);
|
|
252
|
-
if (spec.env && Object.keys(spec.env).length > 0) {
|
|
253
|
-
console.log(` Env: ${Object.entries(spec.env).map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
254
|
-
}
|
|
255
|
-
console.log(` Created: ${new Date(spec.createdAt).toLocaleString()}`);
|
|
256
|
-
}
|
|
257
|
-
async function processCommand(args, machineId) {
|
|
258
|
-
const sub = args[0];
|
|
259
|
-
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
260
|
-
printHelp();
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const subArgs = args.slice(1);
|
|
264
|
-
switch (sub) {
|
|
265
|
-
case "apply":
|
|
266
|
-
return applyCommand(subArgs, machineId);
|
|
267
|
-
case "list":
|
|
268
|
-
case "ls":
|
|
269
|
-
return listCommand(subArgs, machineId);
|
|
270
|
-
case "get":
|
|
271
|
-
return getCommand(subArgs, machineId);
|
|
272
|
-
case "start":
|
|
273
|
-
return startCommand(subArgs, machineId);
|
|
274
|
-
case "stop":
|
|
275
|
-
return stopCommand(subArgs, machineId);
|
|
276
|
-
case "restart":
|
|
277
|
-
return restartCommand(subArgs, machineId);
|
|
278
|
-
case "delete":
|
|
279
|
-
case "remove":
|
|
280
|
-
case "rm":
|
|
281
|
-
return deleteCommand(subArgs, machineId);
|
|
282
|
-
case "logs":
|
|
283
|
-
case "log":
|
|
284
|
-
return logsCommand(subArgs, machineId);
|
|
285
|
-
default:
|
|
286
|
-
if (sub.endsWith(".yaml") || sub.endsWith(".yml") || sub.endsWith(".json")) {
|
|
287
|
-
return applyCommand(args, machineId);
|
|
288
|
-
}
|
|
289
|
-
console.error(`Unknown process subcommand: '${sub}'`);
|
|
290
|
-
printHelp();
|
|
291
|
-
process.exit(1);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
async function applyCommand(args, machineId) {
|
|
295
|
-
const filePath = args.find((a) => !a.startsWith("-"));
|
|
296
|
-
if (!filePath) {
|
|
297
|
-
console.error("Error: spec file required");
|
|
298
|
-
console.error("Usage: svamp process apply <file.yaml>");
|
|
299
|
-
process.exit(1);
|
|
300
|
-
}
|
|
301
|
-
const save = hasFlag(args, "--save");
|
|
302
|
-
const spec = await readSpecFile(filePath);
|
|
303
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
304
|
-
try {
|
|
305
|
-
const result = await machine.processApply({ spec, _rkwargs: true });
|
|
306
|
-
const { action, info } = result;
|
|
307
|
-
if (action === "no-change") {
|
|
308
|
-
console.log(`process/${info.spec.name} unchanged`);
|
|
309
|
-
} else if (action === "created") {
|
|
310
|
-
console.log(`process/${info.spec.name} created`);
|
|
311
|
-
} else {
|
|
312
|
-
console.log(`process/${info.spec.name} configured`);
|
|
313
|
-
}
|
|
314
|
-
if (info.state.pid) {
|
|
315
|
-
console.log(` pid: ${info.state.pid}, status: ${info.state.status}`);
|
|
316
|
-
}
|
|
317
|
-
if (save) {
|
|
318
|
-
await writeSpecFile(filePath, info.spec);
|
|
319
|
-
console.log(` Spec saved to ${filePath}`);
|
|
320
|
-
}
|
|
321
|
-
} finally {
|
|
322
|
-
await server.disconnect();
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
async function listCommand(args, machineId) {
|
|
326
|
-
const json = hasFlag(args, "--json");
|
|
327
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
328
|
-
try {
|
|
329
|
-
const processes = await machine.processList({ _rkwargs: true });
|
|
330
|
-
if (json) {
|
|
331
|
-
console.log(JSON.stringify(processes, null, 2));
|
|
332
|
-
} else {
|
|
333
|
-
printProcessTable(processes);
|
|
334
|
-
}
|
|
335
|
-
} finally {
|
|
336
|
-
await server.disconnect();
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
async function getCommand(args, machineId) {
|
|
340
|
-
const idOrName = args.find((a) => !a.startsWith("-"));
|
|
341
|
-
if (!idOrName) {
|
|
342
|
-
console.error("Error: process name or id required");
|
|
343
|
-
process.exit(1);
|
|
344
|
-
}
|
|
345
|
-
const outputFormat = getFlag(args, "-o") ?? getFlag(args, "--output") ?? "human";
|
|
346
|
-
const outFile = getFlag(args, "--save");
|
|
347
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
348
|
-
try {
|
|
349
|
-
const info = await machine.processGet({ idOrName, _rkwargs: true });
|
|
350
|
-
if (!info) {
|
|
351
|
-
console.error(`Error: process '${idOrName}' not found`);
|
|
352
|
-
process.exit(1);
|
|
353
|
-
}
|
|
354
|
-
if (outputFormat === "yaml" || outputFormat === "yml") {
|
|
355
|
-
const yaml = await loadYaml();
|
|
356
|
-
const content = specToYaml(info.spec, yaml);
|
|
357
|
-
if (outFile) {
|
|
358
|
-
writeFileSync(outFile, `# svamp process spec \u2014 apply with: svamp process apply ${outFile}
|
|
359
|
-
${content}`, "utf-8");
|
|
360
|
-
console.log(`Spec written to ${outFile}`);
|
|
361
|
-
} else {
|
|
362
|
-
process.stdout.write(content);
|
|
363
|
-
}
|
|
364
|
-
} else if (outputFormat === "json") {
|
|
365
|
-
const out = JSON.stringify(info, null, 2);
|
|
366
|
-
if (outFile) {
|
|
367
|
-
writeFileSync(outFile, out, "utf-8");
|
|
368
|
-
console.log(`Written to ${outFile}`);
|
|
369
|
-
} else {
|
|
370
|
-
console.log(out);
|
|
371
|
-
}
|
|
372
|
-
} else {
|
|
373
|
-
printProcessDetail(info);
|
|
374
|
-
}
|
|
375
|
-
} finally {
|
|
376
|
-
await server.disconnect();
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
async function startCommand(args, machineId) {
|
|
380
|
-
const { flagArgs, cmdArgs } = splitOnDoubleDash(args);
|
|
381
|
-
const idOrName = flagArgs.find((a) => !a.startsWith("-"));
|
|
382
|
-
if (!idOrName) {
|
|
383
|
-
console.error("Error: process name or id required");
|
|
384
|
-
process.exit(1);
|
|
385
|
-
}
|
|
386
|
-
if (cmdArgs.length === 0) {
|
|
387
|
-
const { server: server2, machine: machine2 } = await connectAndGetMachine(machineId);
|
|
388
|
-
try {
|
|
389
|
-
await machine2.processStart({ idOrName, _rkwargs: true });
|
|
390
|
-
console.log(`Started '${idOrName}'`);
|
|
391
|
-
} finally {
|
|
392
|
-
await server2.disconnect();
|
|
393
|
-
}
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
const command = cmdArgs[0];
|
|
397
|
-
const cmdArgList = cmdArgs.slice(1);
|
|
398
|
-
const workdir = getFlag(flagArgs, "--workdir") ?? process.cwd();
|
|
399
|
-
const keepAlive = hasFlag(flagArgs, "--keep-alive", "--keepalive");
|
|
400
|
-
const maxRestarts = parseIntFlag(flagArgs, "--max-restarts", 0);
|
|
401
|
-
const restartDelay = parseIntFlag(flagArgs, "--restart-delay", 2);
|
|
402
|
-
const ttlRaw = getFlag(flagArgs, "--ttl");
|
|
403
|
-
const ttl = ttlRaw !== void 0 ? parseInt(ttlRaw, 10) : void 0;
|
|
404
|
-
const probePortRaw = getFlag(flagArgs, "--probe-port");
|
|
405
|
-
let probe;
|
|
406
|
-
if (probePortRaw !== void 0) {
|
|
407
|
-
const probePort = parseInt(probePortRaw, 10);
|
|
408
|
-
if (isNaN(probePort) || probePort < 1) {
|
|
409
|
-
console.error("Error: --probe-port must be a valid port number");
|
|
410
|
-
process.exit(1);
|
|
411
|
-
}
|
|
412
|
-
probe = {
|
|
413
|
-
type: "http",
|
|
414
|
-
port: probePort,
|
|
415
|
-
...getFlag(flagArgs, "--probe-path") ? { path: getFlag(flagArgs, "--probe-path") } : {},
|
|
416
|
-
...parseIntFlagOpt(flagArgs, "--probe-interval") !== void 0 ? { interval: parseIntFlagOpt(flagArgs, "--probe-interval") } : {},
|
|
417
|
-
...parseIntFlagOpt(flagArgs, "--probe-timeout") !== void 0 ? { timeout: parseIntFlagOpt(flagArgs, "--probe-timeout") } : {},
|
|
418
|
-
...parseIntFlagOpt(flagArgs, "--probe-failures") !== void 0 ? { failureThreshold: parseIntFlagOpt(flagArgs, "--probe-failures") } : {}
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
const envPairs = getAllFlags(flagArgs, "--env");
|
|
422
|
-
const env = {};
|
|
423
|
-
for (const pair of envPairs) {
|
|
424
|
-
const eq = pair.indexOf("=");
|
|
425
|
-
if (eq === -1) {
|
|
426
|
-
console.error(`Error: --env value must be KEY=VALUE`);
|
|
427
|
-
process.exit(1);
|
|
428
|
-
}
|
|
429
|
-
env[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
430
|
-
}
|
|
431
|
-
const spec = {
|
|
432
|
-
name: idOrName,
|
|
433
|
-
command,
|
|
434
|
-
args: cmdArgList,
|
|
435
|
-
workdir,
|
|
436
|
-
...Object.keys(env).length > 0 ? { env } : {},
|
|
437
|
-
keepAlive,
|
|
438
|
-
maxRestarts,
|
|
439
|
-
restartDelay,
|
|
440
|
-
...probe ? { probe } : {},
|
|
441
|
-
...ttl !== void 0 ? { ttl } : {}
|
|
442
|
-
};
|
|
443
|
-
const saveFile = getFlag(flagArgs, "--save");
|
|
444
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
445
|
-
try {
|
|
446
|
-
const result = await machine.processApply({ spec, _rkwargs: true });
|
|
447
|
-
const { info } = result;
|
|
448
|
-
console.log(`Started '${info.spec.name}' (id: ${info.spec.id}, pid: ${info.state.pid ?? "\u2014"})`);
|
|
449
|
-
if (keepAlive) console.log(" Keep-alive: enabled");
|
|
450
|
-
if (probe) console.log(` Probe: http://localhost:${probe.port}${probe.path ?? "/"}`);
|
|
451
|
-
if (ttl !== void 0) console.log(` TTL: ${ttl}s`);
|
|
452
|
-
if (saveFile) {
|
|
453
|
-
await writeSpecFile(saveFile, info.spec);
|
|
454
|
-
console.log(` Spec saved to ${saveFile}`);
|
|
455
|
-
}
|
|
456
|
-
} finally {
|
|
457
|
-
await server.disconnect();
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
async function stopCommand(args, machineId) {
|
|
461
|
-
const idOrName = args.find((a) => !a.startsWith("-"));
|
|
462
|
-
if (!idOrName) {
|
|
463
|
-
console.error("Error: process name or id required");
|
|
464
|
-
process.exit(1);
|
|
465
|
-
}
|
|
466
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
467
|
-
try {
|
|
468
|
-
await machine.processStop({ idOrName, _rkwargs: true });
|
|
469
|
-
console.log(`Stopped '${idOrName}'`);
|
|
470
|
-
} finally {
|
|
471
|
-
await server.disconnect();
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
async function restartCommand(args, machineId) {
|
|
475
|
-
const idOrName = args.find((a) => !a.startsWith("-"));
|
|
476
|
-
if (!idOrName) {
|
|
477
|
-
console.error("Error: process name or id required");
|
|
478
|
-
process.exit(1);
|
|
479
|
-
}
|
|
480
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
481
|
-
try {
|
|
482
|
-
await machine.processRestart({ idOrName, _rkwargs: true });
|
|
483
|
-
console.log(`Restarted '${idOrName}'`);
|
|
484
|
-
} finally {
|
|
485
|
-
await server.disconnect();
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
async function deleteCommand(args, machineId) {
|
|
489
|
-
const idOrName = args.find((a) => !a.startsWith("-"));
|
|
490
|
-
if (!idOrName) {
|
|
491
|
-
console.error("Error: process name or id required");
|
|
492
|
-
process.exit(1);
|
|
493
|
-
}
|
|
494
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
495
|
-
try {
|
|
496
|
-
await machine.processRemove({ idOrName, _rkwargs: true });
|
|
497
|
-
console.log(`Deleted '${idOrName}'`);
|
|
498
|
-
} finally {
|
|
499
|
-
await server.disconnect();
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
async function logsCommand(args, machineId) {
|
|
503
|
-
const idOrName = args.find((a) => !a.startsWith("-"));
|
|
504
|
-
if (!idOrName) {
|
|
505
|
-
console.error("Error: process name or id required");
|
|
506
|
-
process.exit(1);
|
|
507
|
-
}
|
|
508
|
-
const last = parseIntFlag(args, "--last", 50);
|
|
509
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
510
|
-
try {
|
|
511
|
-
const lines = await machine.processLogs({ idOrName, last, _rkwargs: true });
|
|
512
|
-
if (lines.length === 0) console.log("(no output yet)");
|
|
513
|
-
else console.log(lines.join("\n"));
|
|
514
|
-
} finally {
|
|
515
|
-
await server.disconnect();
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
function printHelp() {
|
|
519
|
-
console.log(`
|
|
520
|
-
svamp process \u2014 Supervised process management (Kubernetes-style)
|
|
521
|
-
|
|
522
|
-
USAGE
|
|
523
|
-
svamp process <command> [options] [-m <machine>]
|
|
524
|
-
|
|
525
|
-
COMMANDS
|
|
526
|
-
apply <file.yaml> Apply a process spec (create or update)
|
|
527
|
-
list [--json] List all supervised processes
|
|
528
|
-
get <name|id> [-o yaml|json] Get process spec and status
|
|
529
|
-
start <name|id> Start a stopped process
|
|
530
|
-
start <name> [flags] -- <cmd> Create and start (inline shorthand)
|
|
531
|
-
stop <name|id> Stop (keeps config for restart)
|
|
532
|
-
restart <name|id> Restart process
|
|
533
|
-
delete <name|id> Stop and remove from supervision
|
|
534
|
-
logs <name|id> [--last N] Show recent log output
|
|
535
|
-
|
|
536
|
-
APPLY (recommended workflow)
|
|
537
|
-
# Create a spec file
|
|
538
|
-
cat > myapp.yaml << EOF
|
|
539
|
-
name: myapp
|
|
540
|
-
command: python
|
|
541
|
-
args: [app.py]
|
|
542
|
-
workdir: /data/myapp
|
|
543
|
-
keepAlive: true
|
|
544
|
-
probe:
|
|
545
|
-
type: http
|
|
546
|
-
port: 8080
|
|
547
|
-
path: /health
|
|
548
|
-
ttl: 86400
|
|
549
|
-
EOF
|
|
550
|
-
|
|
551
|
-
# Apply (idempotent \u2014 re-run to update config)
|
|
552
|
-
svamp process apply myapp.yaml
|
|
553
|
-
|
|
554
|
-
# Export current spec
|
|
555
|
-
svamp process get myapp -o yaml > myapp.yaml
|
|
556
|
-
|
|
557
|
-
START FLAGS (inline mode: svamp process start <name> [flags] -- <cmd>)
|
|
558
|
-
--workdir <dir> Working directory (default: cwd)
|
|
559
|
-
--keep-alive Restart on exit/crash
|
|
560
|
-
--max-restarts N Max restart attempts (0 = unlimited)
|
|
561
|
-
--restart-delay N Seconds between restarts (default: 2)
|
|
562
|
-
--probe-port N Enable HTTP health probe on port N
|
|
563
|
-
--probe-path <path> Health check path (default: /)
|
|
564
|
-
--probe-interval N Check interval in seconds (default: 10)
|
|
565
|
-
--probe-timeout N Check timeout in seconds (default: 5)
|
|
566
|
-
--probe-failures N Consecutive failures before restart (default: 3)
|
|
567
|
-
--ttl <seconds> Auto-kill after N seconds
|
|
568
|
-
--env KEY=VALUE Extra environment variable (repeatable)
|
|
569
|
-
--save <file.yaml> Save spec to file after creation
|
|
570
|
-
|
|
571
|
-
SPEC FILE FORMAT (YAML or JSON)
|
|
572
|
-
name: string Required. Unique process name.
|
|
573
|
-
command: string Required. Executable.
|
|
574
|
-
args: string[] Command arguments.
|
|
575
|
-
workdir: string Working directory.
|
|
576
|
-
keepAlive: boolean Restart on crash (default: false).
|
|
577
|
-
maxRestarts: number Max restarts (0 = unlimited).
|
|
578
|
-
restartDelay: number Seconds between restarts.
|
|
579
|
-
probe:
|
|
580
|
-
type: http Only 'http' supported.
|
|
581
|
-
port: number Port to probe.
|
|
582
|
-
path: string HTTP path (default: /).
|
|
583
|
-
interval: number Seconds between probes.
|
|
584
|
-
timeout: number Probe timeout in seconds.
|
|
585
|
-
failureThreshold: number Failures before restart.
|
|
586
|
-
ttl: number Seconds until auto-kill.
|
|
587
|
-
env: {KEY: value} Extra environment variables.
|
|
588
|
-
serviceGroup: string Linked service group name.
|
|
589
|
-
ports: number[] Ports exposed by this process.
|
|
590
|
-
`);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
export { processCommand };
|