relife2 1.0.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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/cli.js +4778 -0
- package/dist/cli.js.map +7 -0
- package/package.json +39 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,4778 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/config/jsloader.ts
|
|
13
|
+
var jsloader_exports = {};
|
|
14
|
+
__export(jsloader_exports, {
|
|
15
|
+
loadJsFile: () => loadJsFile
|
|
16
|
+
});
|
|
17
|
+
import * as fs5 from "node:fs";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
import path6 from "node:path";
|
|
20
|
+
import { pathToFileURL } from "node:url";
|
|
21
|
+
async function loadJsFile(file) {
|
|
22
|
+
const ext = path6.extname(file).toLowerCase();
|
|
23
|
+
const source = fs5.readFileSync(file, "utf8");
|
|
24
|
+
if (ext === ".mjs") return importFreshEs(file);
|
|
25
|
+
if (ext === ".cjs") return loadCjs(file, source);
|
|
26
|
+
if (CJS_MARKER.test(source)) return loadCjs(file, source);
|
|
27
|
+
try {
|
|
28
|
+
return await importFreshEs(file);
|
|
29
|
+
} catch {
|
|
30
|
+
return loadCjs(file, source);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function importFreshEs(file) {
|
|
34
|
+
const url = `${pathToFileURL(file).href}?t=${Date.now()}`;
|
|
35
|
+
return import(url).then((mod) => mod.default ?? mod);
|
|
36
|
+
}
|
|
37
|
+
function loadCjs(file, source) {
|
|
38
|
+
try {
|
|
39
|
+
return compileCjs(file, source);
|
|
40
|
+
} catch {
|
|
41
|
+
const require2 = createRequire(import.meta.url);
|
|
42
|
+
try {
|
|
43
|
+
return require2(file);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throw new Error(`cannot load config '${file}': ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function compileCjs(file, source) {
|
|
50
|
+
const req = createRequire(import.meta.url);
|
|
51
|
+
const nodeModule = req("node:module");
|
|
52
|
+
const mod = new nodeModule.Module(file, null);
|
|
53
|
+
mod.filename = file;
|
|
54
|
+
mod.paths = nodeModule._nodeModulePaths(path6.dirname(file));
|
|
55
|
+
mod._compile(source, file);
|
|
56
|
+
return mod.exports;
|
|
57
|
+
}
|
|
58
|
+
var CJS_MARKER;
|
|
59
|
+
var init_jsloader = __esm({
|
|
60
|
+
"src/config/jsloader.ts"() {
|
|
61
|
+
"use strict";
|
|
62
|
+
CJS_MARKER = /^\s*(?:module\.exports|exports\.[\w$])/m;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/daemon/client.ts
|
|
67
|
+
import { spawn } from "node:child_process";
|
|
68
|
+
import net from "node:net";
|
|
69
|
+
import path3 from "node:path";
|
|
70
|
+
|
|
71
|
+
// src/ipc/protocol.ts
|
|
72
|
+
var PROTOCOL_VERSION = 1;
|
|
73
|
+
var RpcError = class extends Error {
|
|
74
|
+
constructor(code, message) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.code = code;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/util/fsx.ts
|
|
81
|
+
import fs from "node:fs";
|
|
82
|
+
import path from "node:path";
|
|
83
|
+
function ensureDirSync(dir) {
|
|
84
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
function pathExists(p) {
|
|
87
|
+
try {
|
|
88
|
+
fs.accessSync(p);
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function atomicWriteFileSync(file, data) {
|
|
95
|
+
ensureDirSync(path.dirname(file));
|
|
96
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
97
|
+
fs.writeFileSync(tmp, data, "utf8");
|
|
98
|
+
try {
|
|
99
|
+
const fd = fs.openSync(tmp, "r");
|
|
100
|
+
try {
|
|
101
|
+
fs.fsyncSync(fd);
|
|
102
|
+
} finally {
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
fs.renameSync(tmp, file);
|
|
108
|
+
}
|
|
109
|
+
function readJsonFile(file) {
|
|
110
|
+
if (!pathExists(file)) return null;
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function isAlive(pid) {
|
|
118
|
+
if (!pid || Number.isNaN(pid)) return false;
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, 0);
|
|
121
|
+
return true;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return err.code === "EPERM";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/util/time.ts
|
|
128
|
+
function parseTime(v) {
|
|
129
|
+
if (typeof v === "number") return v;
|
|
130
|
+
const s = String(v).trim();
|
|
131
|
+
const m = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i.exec(s);
|
|
132
|
+
if (!m || m[1] === void 0) {
|
|
133
|
+
throw new Error(`invalid time string '${s}' (expected e.g. '500ms', '10s', '1m')`);
|
|
134
|
+
}
|
|
135
|
+
const n = Number.parseFloat(m[1]);
|
|
136
|
+
const unit = (m[2] ?? "s").toLowerCase();
|
|
137
|
+
const factor = { ms: 1, s: 1e3, m: 6e4, h: 36e5, d: 864e5 }[unit];
|
|
138
|
+
if (factor === void 0) throw new Error(`invalid time unit in '${s}'`);
|
|
139
|
+
return Math.round(n * factor);
|
|
140
|
+
}
|
|
141
|
+
function sleep(ms) {
|
|
142
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/daemon/base.ts
|
|
146
|
+
import fs2 from "node:fs";
|
|
147
|
+
import os from "node:os";
|
|
148
|
+
import path2 from "node:path";
|
|
149
|
+
function envDir(name) {
|
|
150
|
+
const v = process.env[name]?.trim();
|
|
151
|
+
return v ? path2.resolve(v) : null;
|
|
152
|
+
}
|
|
153
|
+
function dataDir() {
|
|
154
|
+
const override = envDir("RELIFE2_DIR");
|
|
155
|
+
if (override !== null) return override;
|
|
156
|
+
const xdg = envDir("XDG_DATA_HOME");
|
|
157
|
+
if (xdg !== null) return path2.join(xdg, "relife2");
|
|
158
|
+
return path2.join(os.homedir(), ".relife2");
|
|
159
|
+
}
|
|
160
|
+
function runtimeDir() {
|
|
161
|
+
const override = envDir("RELIFE2_DIR");
|
|
162
|
+
if (override !== null) return override;
|
|
163
|
+
const xdg = envDir("XDG_RUNTIME_DIR");
|
|
164
|
+
if (xdg !== null) return path2.join(xdg, "relife2");
|
|
165
|
+
return dataDir();
|
|
166
|
+
}
|
|
167
|
+
function lockPath(base) {
|
|
168
|
+
return path2.join(base, "daemon.lock");
|
|
169
|
+
}
|
|
170
|
+
function unixSockPath(base) {
|
|
171
|
+
return path2.join(base, "relife2.sock");
|
|
172
|
+
}
|
|
173
|
+
function portFilePath(base) {
|
|
174
|
+
return path2.join(base, "daemon.port");
|
|
175
|
+
}
|
|
176
|
+
function defaultPort(base) {
|
|
177
|
+
let h = 2166136261;
|
|
178
|
+
for (let i = 0; i < base.length; i++) {
|
|
179
|
+
h ^= base.charCodeAt(i);
|
|
180
|
+
h = Math.imul(h, 16777619);
|
|
181
|
+
}
|
|
182
|
+
return 44720 + (h >>> 0) % 1e3;
|
|
183
|
+
}
|
|
184
|
+
function acquireLock(base) {
|
|
185
|
+
fs2.mkdirSync(base, { recursive: true });
|
|
186
|
+
const file = lockPath(base);
|
|
187
|
+
const payload = JSON.stringify({ app: "relife2", pid: process.pid, ts: Date.now() });
|
|
188
|
+
try {
|
|
189
|
+
fs2.writeFileSync(file, payload, { flag: "wx" });
|
|
190
|
+
return {
|
|
191
|
+
release() {
|
|
192
|
+
try {
|
|
193
|
+
fs2.unlinkSync(file);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (err.code !== "EEXIST") throw err;
|
|
200
|
+
}
|
|
201
|
+
const stale = readLockInfo(file);
|
|
202
|
+
if (stale !== null && isAlive(stale.pid)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
fs2.unlinkSync(file);
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
return acquireLock(base);
|
|
210
|
+
}
|
|
211
|
+
function readLockInfo(file) {
|
|
212
|
+
if (!pathExists(file)) return null;
|
|
213
|
+
try {
|
|
214
|
+
const data = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
215
|
+
return typeof data.pid === "number" && Number.isInteger(data.pid) ? { pid: data.pid } : null;
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/daemon/client.ts
|
|
222
|
+
var CONNECT_TIMEOUT = 1500;
|
|
223
|
+
var CALL_TIMEOUT = 3e4;
|
|
224
|
+
async function tryConnect(runtime) {
|
|
225
|
+
const opts = process.platform === "win32" ? { host: "127.0.0.1", port: currentPort(runtime) } : { path: path3.join(runtime, "relife2.sock") };
|
|
226
|
+
return new Promise((resolve) => {
|
|
227
|
+
const sock = net.connect(opts);
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
sock.destroy();
|
|
230
|
+
resolve(null);
|
|
231
|
+
}, CONNECT_TIMEOUT);
|
|
232
|
+
sock.once("connect", () => {
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
resolve(sock);
|
|
235
|
+
});
|
|
236
|
+
sock.once("error", () => {
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
resolve(null);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function currentPort(runtime) {
|
|
243
|
+
const data = readJsonFile(path3.join(runtime, "daemon.port"));
|
|
244
|
+
if (typeof data === "number" && Number.isInteger(data)) return data;
|
|
245
|
+
if (data !== null && typeof data === "object" && typeof data.port === "number") return data.port;
|
|
246
|
+
return defaultPort(runtime);
|
|
247
|
+
}
|
|
248
|
+
var RpcClient = class {
|
|
249
|
+
constructor(socket) {
|
|
250
|
+
this.socket = socket;
|
|
251
|
+
socket.setEncoding("utf8");
|
|
252
|
+
socket.on("data", (d) => this.onData(d));
|
|
253
|
+
socket.on("close", () => this.failAll(new Error("connection to daemon closed")));
|
|
254
|
+
socket.on("error", () => this.failAll(new Error("connection to daemon failed")));
|
|
255
|
+
}
|
|
256
|
+
buffer = "";
|
|
257
|
+
nextId = 1;
|
|
258
|
+
pending = /* @__PURE__ */ new Map();
|
|
259
|
+
call(method, params, timeoutMs = CALL_TIMEOUT) {
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
const id = this.nextId++;
|
|
262
|
+
const timer = setTimeout(() => {
|
|
263
|
+
this.pending.delete(id);
|
|
264
|
+
reject(new Error(`timeout waiting for daemon method '${method}'`));
|
|
265
|
+
}, timeoutMs);
|
|
266
|
+
this.pending.set(id, {
|
|
267
|
+
resolve: (v) => resolve(v),
|
|
268
|
+
reject,
|
|
269
|
+
timer
|
|
270
|
+
});
|
|
271
|
+
this.socket.write(`${JSON.stringify({ id, method, params })}
|
|
272
|
+
`);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
close() {
|
|
276
|
+
try {
|
|
277
|
+
this.socket.end();
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
onData(d) {
|
|
282
|
+
const text = typeof d === "string" ? d : d.toString("utf8");
|
|
283
|
+
this.buffer += text;
|
|
284
|
+
let idx = this.buffer.indexOf("\n");
|
|
285
|
+
while (idx >= 0) {
|
|
286
|
+
const line = this.buffer.slice(0, idx);
|
|
287
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
288
|
+
if (line.trim() !== "") this.onMessage(line);
|
|
289
|
+
idx = this.buffer.indexOf("\n");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
onMessage(line) {
|
|
293
|
+
let msg;
|
|
294
|
+
try {
|
|
295
|
+
msg = JSON.parse(line);
|
|
296
|
+
} catch {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (typeof msg.id !== "number") return;
|
|
300
|
+
const entry = this.pending.get(msg.id);
|
|
301
|
+
if (entry === void 0) return;
|
|
302
|
+
this.pending.delete(msg.id);
|
|
303
|
+
clearTimeout(entry.timer);
|
|
304
|
+
if (msg.ok === true) {
|
|
305
|
+
entry.resolve(msg.result);
|
|
306
|
+
} else {
|
|
307
|
+
entry.reject(new RpcError(msg.error?.code ?? "ERROR", msg.error?.message ?? "daemon error"));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
failAll(err) {
|
|
311
|
+
for (const entry of this.pending.values()) {
|
|
312
|
+
clearTimeout(entry.timer);
|
|
313
|
+
entry.reject(err);
|
|
314
|
+
}
|
|
315
|
+
this.pending.clear();
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
async function ensureDaemonClient() {
|
|
319
|
+
const runtime = runtimeDir();
|
|
320
|
+
const data = dataDir();
|
|
321
|
+
let sock = await tryConnect(runtime);
|
|
322
|
+
if (sock === null) {
|
|
323
|
+
spawnDaemon();
|
|
324
|
+
for (let i = 0; i < 100 && sock === null; i++) {
|
|
325
|
+
await sleep(100);
|
|
326
|
+
sock = await tryConnect(runtime);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (sock === null) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`cannot connect to relife2 daemon (runtime: ${runtime}, data: ${data}); inspect ${path3.join(data, "logs", "daemon.log")}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const client = new RpcClient(sock);
|
|
335
|
+
const ping = await client.call("ping");
|
|
336
|
+
if (ping.protocol !== PROTOCOL_VERSION) {
|
|
337
|
+
client.close();
|
|
338
|
+
throw new Error(
|
|
339
|
+
`daemon protocol mismatch (daemon: ${ping.protocol}, cli: ${PROTOCOL_VERSION}); run 'relife2 kill' to stop the stale daemon`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return client;
|
|
343
|
+
}
|
|
344
|
+
function spawnDaemon() {
|
|
345
|
+
const arg1 = process.argv[1];
|
|
346
|
+
const isSingleBinary = arg1 === void 0 || !arg1.endsWith(".js") && !arg1.endsWith(".mjs") && !arg1.endsWith(".cjs");
|
|
347
|
+
if (isSingleBinary) {
|
|
348
|
+
const child2 = spawn(process.execPath, [], {
|
|
349
|
+
env: { ...process.env, RELIFE2_DAEMON: "1" },
|
|
350
|
+
stdio: "ignore",
|
|
351
|
+
detached: true
|
|
352
|
+
});
|
|
353
|
+
child2.unref();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const entry = path3.resolve(arg1);
|
|
357
|
+
const child = spawn(process.execPath, [entry], {
|
|
358
|
+
env: { ...process.env, RELIFE2_DAEMON: "1" },
|
|
359
|
+
stdio: "ignore",
|
|
360
|
+
detached: true
|
|
361
|
+
});
|
|
362
|
+
child.unref();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/util/args.ts
|
|
366
|
+
function parseCliArgs(args, valueOpts = /* @__PURE__ */ new Set()) {
|
|
367
|
+
const positionals = [];
|
|
368
|
+
const opts = /* @__PURE__ */ new Map();
|
|
369
|
+
const passthrough = [];
|
|
370
|
+
let afterDashDash = false;
|
|
371
|
+
let i = 0;
|
|
372
|
+
while (i < args.length) {
|
|
373
|
+
const a = args[i];
|
|
374
|
+
if (a === void 0) break;
|
|
375
|
+
if (afterDashDash) {
|
|
376
|
+
passthrough.push(a);
|
|
377
|
+
i += 1;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (a === "--") {
|
|
381
|
+
afterDashDash = true;
|
|
382
|
+
i += 1;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (a.startsWith("--")) {
|
|
386
|
+
const key = a.slice(2);
|
|
387
|
+
const eq = key.indexOf("=");
|
|
388
|
+
if (eq >= 0) {
|
|
389
|
+
opts.set(key.slice(0, eq), key.slice(eq + 1));
|
|
390
|
+
i += 1;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const next = args[i + 1];
|
|
394
|
+
if (valueOpts.has(key) && next !== void 0 && !next.startsWith("-")) {
|
|
395
|
+
opts.set(key, next);
|
|
396
|
+
i += 2;
|
|
397
|
+
} else {
|
|
398
|
+
opts.set(key, true);
|
|
399
|
+
i += 1;
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (a.startsWith("-") && a.length >= 2) {
|
|
404
|
+
const key = a.slice(1);
|
|
405
|
+
const next = args[i + 1];
|
|
406
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
407
|
+
opts.set(key, next);
|
|
408
|
+
i += 2;
|
|
409
|
+
} else {
|
|
410
|
+
opts.set(key, true);
|
|
411
|
+
i += 1;
|
|
412
|
+
}
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
positionals.push(a);
|
|
416
|
+
i += 1;
|
|
417
|
+
}
|
|
418
|
+
return { positionals, opts, passthrough };
|
|
419
|
+
}
|
|
420
|
+
function splitArgs(input) {
|
|
421
|
+
const out = [];
|
|
422
|
+
let cur = "";
|
|
423
|
+
let quote = null;
|
|
424
|
+
let i = 0;
|
|
425
|
+
while (i < input.length) {
|
|
426
|
+
const c = input[i];
|
|
427
|
+
if (c === void 0) break;
|
|
428
|
+
if (quote === null) {
|
|
429
|
+
if (c === '"' || c === "'") {
|
|
430
|
+
quote = c;
|
|
431
|
+
} else if (c === "\\" && i + 1 < input.length) {
|
|
432
|
+
cur += input[i + 1] ?? "";
|
|
433
|
+
i += 1;
|
|
434
|
+
} else if (c === " " || c === " " || c === "\n") {
|
|
435
|
+
if (cur !== "") {
|
|
436
|
+
out.push(cur);
|
|
437
|
+
cur = "";
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
cur += c;
|
|
441
|
+
}
|
|
442
|
+
} else if (c === quote) {
|
|
443
|
+
quote = null;
|
|
444
|
+
} else if (c === "\\" && quote === '"' && i + 1 < input.length) {
|
|
445
|
+
cur += input[i + 1] ?? "";
|
|
446
|
+
i += 1;
|
|
447
|
+
} else {
|
|
448
|
+
cur += c;
|
|
449
|
+
}
|
|
450
|
+
i += 1;
|
|
451
|
+
}
|
|
452
|
+
if (cur !== "") out.push(cur);
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/commands/delete.ts
|
|
457
|
+
async function run(args) {
|
|
458
|
+
const { positionals } = parseCliArgs(args);
|
|
459
|
+
const name = positionals[0];
|
|
460
|
+
if (name === void 0) {
|
|
461
|
+
console.error("usage: relife2 delete <name|all>");
|
|
462
|
+
return 1;
|
|
463
|
+
}
|
|
464
|
+
const client = await ensureDaemonClient();
|
|
465
|
+
try {
|
|
466
|
+
const res = await client.call("delete", { name });
|
|
467
|
+
for (const a of res.apps) {
|
|
468
|
+
console.log(`ok deleted ${a.name}`);
|
|
469
|
+
}
|
|
470
|
+
return 0;
|
|
471
|
+
} finally {
|
|
472
|
+
client.close();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/commands/describe.ts
|
|
477
|
+
async function run2(args) {
|
|
478
|
+
const { positionals } = parseCliArgs(args);
|
|
479
|
+
const name = positionals[0];
|
|
480
|
+
if (name === void 0) {
|
|
481
|
+
console.error("usage: relife2 describe <name>");
|
|
482
|
+
return 1;
|
|
483
|
+
}
|
|
484
|
+
const client = await ensureDaemonClient();
|
|
485
|
+
try {
|
|
486
|
+
const rec = await client.call("describe", { name });
|
|
487
|
+
console.log(JSON.stringify(rec, null, 2));
|
|
488
|
+
return 0;
|
|
489
|
+
} finally {
|
|
490
|
+
client.close();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/commands/dev.ts
|
|
495
|
+
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
496
|
+
import * as fs3 from "node:fs";
|
|
497
|
+
import path4 from "node:path";
|
|
498
|
+
var DEV_VALUE_OPTS = /* @__PURE__ */ new Set(["watch", "interpreter", "env", "name", "w"]);
|
|
499
|
+
function hasBun() {
|
|
500
|
+
try {
|
|
501
|
+
const r = spawnSync("bun", ["--version"], {
|
|
502
|
+
encoding: "utf8",
|
|
503
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
504
|
+
});
|
|
505
|
+
return r.status === 0;
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function isBunProject(cwd) {
|
|
511
|
+
let cur = path4.resolve(cwd);
|
|
512
|
+
const home = path4.resolve(process.env.HOME ?? "");
|
|
513
|
+
for (let i = 0; i < 12; i++) {
|
|
514
|
+
if (fs3.existsSync(path4.join(cur, "bun.lockb")) || fs3.existsSync(path4.join(cur, "bun.lock")))
|
|
515
|
+
return true;
|
|
516
|
+
if (cur === home || cur === path4.dirname(cur)) break;
|
|
517
|
+
cur = path4.dirname(cur);
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
function collectSubDirs(root) {
|
|
522
|
+
const out = [root];
|
|
523
|
+
const stack = [root];
|
|
524
|
+
while (stack.length > 0) {
|
|
525
|
+
const dir = stack.pop();
|
|
526
|
+
let entries = [];
|
|
527
|
+
try {
|
|
528
|
+
entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
529
|
+
} catch {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
for (const e of entries) {
|
|
533
|
+
if (!e.isDirectory()) continue;
|
|
534
|
+
if (e.name === "node_modules" || e.name === ".git" || e.name.startsWith(".")) continue;
|
|
535
|
+
const p = path4.join(dir, e.name);
|
|
536
|
+
out.push(p);
|
|
537
|
+
stack.push(p);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
async function run3(args) {
|
|
543
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
544
|
+
printUsage();
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
const { positionals, opts, passthrough } = parseCliArgs(args, DEV_VALUE_OPTS);
|
|
548
|
+
const scriptArg = positionals[0];
|
|
549
|
+
if (!scriptArg) {
|
|
550
|
+
console.error(
|
|
551
|
+
"usage: relife2 dev <script> [--watch <paths>] [--hot|--no-hot] [--interpreter <bin>] [-- --args...]"
|
|
552
|
+
);
|
|
553
|
+
console.error(" e.g. relife2 dev ./server.ts --watch ./src --hot");
|
|
554
|
+
console.error(" e.g. relife2 dev ./app.js -- --port 3000");
|
|
555
|
+
return 1;
|
|
556
|
+
}
|
|
557
|
+
const script = path4.resolve(scriptArg);
|
|
558
|
+
if (!fs3.existsSync(script)) {
|
|
559
|
+
console.error(`dev: script not found: ${script}`);
|
|
560
|
+
return 1;
|
|
561
|
+
}
|
|
562
|
+
const cwd = path4.dirname(script);
|
|
563
|
+
const useHot = opts.has("no-hot") ? false : opts.has("hot") ? true : void 0;
|
|
564
|
+
const watchRaw = opts.get("watch") ?? opts.get("w");
|
|
565
|
+
const interpreterOpt = opts.get("interpreter") ?? void 0;
|
|
566
|
+
let interpreter = interpreterOpt;
|
|
567
|
+
if (!interpreter) {
|
|
568
|
+
const bunAvailable = hasBun();
|
|
569
|
+
const looksBun = script.endsWith(".ts") || script.endsWith(".tsx") || isBunProject(cwd);
|
|
570
|
+
if (bunAvailable && looksBun) interpreter = "bun";
|
|
571
|
+
else interpreter = "node";
|
|
572
|
+
}
|
|
573
|
+
const isBun = interpreter === "bun";
|
|
574
|
+
const hot = useHot ?? (isBun && (script.endsWith(".ts") || script.endsWith(".tsx")));
|
|
575
|
+
if (hot && !isBun) {
|
|
576
|
+
console.error(
|
|
577
|
+
"dev: --hot requires bun interpreter (use --interpreter bun or run a .ts file with bun available)"
|
|
578
|
+
);
|
|
579
|
+
return 1;
|
|
580
|
+
}
|
|
581
|
+
if (hot && isBun) {
|
|
582
|
+
return runBunHot(script, cwd, passthrough, watchRaw);
|
|
583
|
+
}
|
|
584
|
+
return runManualWatch(script, cwd, interpreter, passthrough, watchRaw);
|
|
585
|
+
}
|
|
586
|
+
async function runBunHot(script, cwd, appArgs, watchRaw) {
|
|
587
|
+
const extraWatch = watchRaw ? ["--watch"] : [];
|
|
588
|
+
const bunArgs = ["--hot", script, ...appArgs];
|
|
589
|
+
console.log(`dev (bun --hot): bun ${bunArgs.join(" ")} [cwd ${cwd}]`);
|
|
590
|
+
if (watchRaw)
|
|
591
|
+
console.log(` note: --watch ${watchRaw} is ignored in --hot mode (bun watches imports)`);
|
|
592
|
+
console.log(` press Ctrl+C to stop`);
|
|
593
|
+
const child = spawn2("bun", bunArgs, { cwd, stdio: "inherit", env: process.env });
|
|
594
|
+
return waitForExit(child);
|
|
595
|
+
}
|
|
596
|
+
async function runManualWatch(script, cwd, interpreter, appArgs, watchRaw) {
|
|
597
|
+
const targets = [];
|
|
598
|
+
if (watchRaw) {
|
|
599
|
+
for (const p of watchRaw.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
600
|
+
targets.push(path4.resolve(cwd, p));
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
targets.push(cwd);
|
|
604
|
+
}
|
|
605
|
+
console.log(
|
|
606
|
+
`dev (watch): ${interpreter} ${path4.basename(script)} ${appArgs.join(" ")} [cwd ${cwd}]`
|
|
607
|
+
);
|
|
608
|
+
console.log(` watching: ${targets.join(", ")}`);
|
|
609
|
+
console.log(` press Ctrl+C to stop`);
|
|
610
|
+
let child = null;
|
|
611
|
+
let stopped = false;
|
|
612
|
+
let restarting = false;
|
|
613
|
+
let debounce = null;
|
|
614
|
+
const watchers = [];
|
|
615
|
+
const startChild = () => {
|
|
616
|
+
if (stopped) return;
|
|
617
|
+
const cmd = interpreter === "none" ? script : interpreter;
|
|
618
|
+
const cmdArgs = interpreter === "none" ? [...appArgs] : [script, ...appArgs];
|
|
619
|
+
child = spawn2(cmd, cmdArgs, { cwd, stdio: "inherit", env: process.env, detached: false });
|
|
620
|
+
child.on("exit", (code, signal) => {
|
|
621
|
+
if (stopped) return;
|
|
622
|
+
if (restarting) {
|
|
623
|
+
restarting = false;
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (code !== null && code !== 0) {
|
|
627
|
+
console.log(`dev: process exited with code ${code} \u2014 waiting for file change to restart`);
|
|
628
|
+
} else if (signal) {
|
|
629
|
+
console.log(`dev: process killed by ${signal} \u2014 waiting for file change`);
|
|
630
|
+
} else {
|
|
631
|
+
console.log(`dev: process exited \u2014 waiting for file change`);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
};
|
|
635
|
+
const restart = () => {
|
|
636
|
+
if (child?.pid && !child.killed) {
|
|
637
|
+
restarting = true;
|
|
638
|
+
try {
|
|
639
|
+
child.kill("SIGTERM");
|
|
640
|
+
setTimeout(() => {
|
|
641
|
+
try {
|
|
642
|
+
if (child?.pid && !child.killed) child.kill("SIGKILL");
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
}, 1200);
|
|
646
|
+
} catch {
|
|
647
|
+
}
|
|
648
|
+
child.once("exit", () => {
|
|
649
|
+
if (stopped) return;
|
|
650
|
+
console.log(`dev: restarting\u2026`);
|
|
651
|
+
startChild();
|
|
652
|
+
});
|
|
653
|
+
} else {
|
|
654
|
+
console.log(`dev: restarting\u2026`);
|
|
655
|
+
startChild();
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
const scheduleRestart = (changedPath) => {
|
|
659
|
+
if (debounce) clearTimeout(debounce);
|
|
660
|
+
debounce = setTimeout(() => {
|
|
661
|
+
console.log(`dev: change detected (${changedPath})`);
|
|
662
|
+
restart();
|
|
663
|
+
}, 200);
|
|
664
|
+
};
|
|
665
|
+
for (const target of targets) {
|
|
666
|
+
if (!fs3.existsSync(target)) {
|
|
667
|
+
console.error(`dev: watch path does not exist: ${target} \u2014 skipping`);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const stat = fs3.statSync(target);
|
|
671
|
+
if (!stat.isDirectory()) {
|
|
672
|
+
const dir = path4.dirname(target);
|
|
673
|
+
const base = path4.basename(target);
|
|
674
|
+
try {
|
|
675
|
+
const w = fs3.watch(dir, (_ev, filename) => {
|
|
676
|
+
if (filename && filename !== base) return;
|
|
677
|
+
scheduleRestart(target);
|
|
678
|
+
});
|
|
679
|
+
watchers.push(w);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
console.error(`dev: cannot watch ${dir}: ${err.message}`);
|
|
682
|
+
}
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (process.platform === "win32" || process.platform === "darwin") {
|
|
686
|
+
try {
|
|
687
|
+
const w = fs3.watch(target, { recursive: true }, () => scheduleRestart(target));
|
|
688
|
+
watchers.push(w);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.error(`dev: cannot watch ${target}: ${err.message}`);
|
|
691
|
+
}
|
|
692
|
+
} else {
|
|
693
|
+
const dirs = collectSubDirs(target);
|
|
694
|
+
for (const dir of dirs) {
|
|
695
|
+
try {
|
|
696
|
+
const w = fs3.watch(dir, () => scheduleRestart(dir));
|
|
697
|
+
watchers.push(w);
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const cleanup = () => {
|
|
704
|
+
stopped = true;
|
|
705
|
+
if (debounce) clearTimeout(debounce);
|
|
706
|
+
for (const w of watchers)
|
|
707
|
+
try {
|
|
708
|
+
w.close();
|
|
709
|
+
} catch {
|
|
710
|
+
}
|
|
711
|
+
if (child?.pid && !child.killed) {
|
|
712
|
+
try {
|
|
713
|
+
child.kill("SIGTERM");
|
|
714
|
+
} catch {
|
|
715
|
+
}
|
|
716
|
+
setTimeout(() => {
|
|
717
|
+
try {
|
|
718
|
+
if (child?.pid && !child.killed) child.kill("SIGKILL");
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
}, 800);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
process.on("SIGINT", () => {
|
|
725
|
+
cleanup();
|
|
726
|
+
setTimeout(() => process.exit(0), 400);
|
|
727
|
+
});
|
|
728
|
+
process.on("SIGTERM", () => {
|
|
729
|
+
cleanup();
|
|
730
|
+
setTimeout(() => process.exit(0), 400);
|
|
731
|
+
});
|
|
732
|
+
startChild();
|
|
733
|
+
return new Promise((resolve) => {
|
|
734
|
+
const onSig = () => resolve(0);
|
|
735
|
+
process.once("SIGINT", onSig);
|
|
736
|
+
process.once("SIGTERM", onSig);
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function waitForExit(child) {
|
|
740
|
+
return new Promise((resolve) => {
|
|
741
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
742
|
+
child.on("error", (err) => {
|
|
743
|
+
console.error(`dev: failed to spawn: ${err.message}`);
|
|
744
|
+
resolve(1);
|
|
745
|
+
});
|
|
746
|
+
process.on("SIGINT", () => {
|
|
747
|
+
try {
|
|
748
|
+
child.kill("SIGTERM");
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
setTimeout(() => {
|
|
752
|
+
try {
|
|
753
|
+
child.kill("SIGKILL");
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
756
|
+
}, 800);
|
|
757
|
+
});
|
|
758
|
+
process.on("SIGTERM", () => {
|
|
759
|
+
try {
|
|
760
|
+
child.kill("SIGTERM");
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
function printUsage() {
|
|
767
|
+
console.log(`relife2 dev \u2014 run a script with watch+restart (backlog, now ready)
|
|
768
|
+
|
|
769
|
+
Usage: relife2 dev <script> [--watch <paths>] [--hot|--no-hot] [--interpreter <bin>] [-- --args...]
|
|
770
|
+
|
|
771
|
+
<script> path to .js/.mjs/.ts file (resolved from cwd)
|
|
772
|
+
--watch <paths> comma-separated paths to watch (default: script's directory)
|
|
773
|
+
--hot force bun --hot (HMR, bun only)
|
|
774
|
+
--no-hot force manual watch+restart even for bun/ts
|
|
775
|
+
--interpreter <bin> node|bun|path (default: auto \u2014 bun if .ts or bun.lockb nearby)
|
|
776
|
+
-- app args after -- are passed to the script
|
|
777
|
+
|
|
778
|
+
Mode:
|
|
779
|
+
bun + --hot (default for .ts under bun) \u2192 bun --hot <script> (bun watches imports, instant HMR)
|
|
780
|
+
otherwise \u2192 manual fs.watch + SIGTERM\u2192SIGKILL restart (works with node/bun/java\u2026)
|
|
781
|
+
|
|
782
|
+
Examples:
|
|
783
|
+
relife2 dev ./server.ts
|
|
784
|
+
relife2 dev ./app.js --watch ./src,./config
|
|
785
|
+
relife2 dev ./bot.ts --no-hot --watch ./src
|
|
786
|
+
relife2 dev ./main.js -- --port 3000
|
|
787
|
+
`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/commands/doctor.ts
|
|
791
|
+
async function run4(_args) {
|
|
792
|
+
const client = await ensureDaemonClient();
|
|
793
|
+
try {
|
|
794
|
+
const report = await client.call("doctor", {});
|
|
795
|
+
let hadErrors = false;
|
|
796
|
+
for (const check of report.checks) {
|
|
797
|
+
const icon = check.ok ? "\u2713" : "\u2717";
|
|
798
|
+
console.log(`${icon} ${check.name}`);
|
|
799
|
+
for (const issue of check.issues) {
|
|
800
|
+
const prefix = issue.severity === "error" ? " \u26A0 ERROR" : issue.severity === "warn" ? " \u26A0 WARN " : " \u2713 OK ";
|
|
801
|
+
console.log(`${prefix} [${issue.code}] ${issue.message}`);
|
|
802
|
+
if (issue.severity === "error") hadErrors = true;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
console.log("---");
|
|
806
|
+
console.log(
|
|
807
|
+
`checks: ${report.summary.total} total, ${report.summary.ok} ok, ${report.summary.warnings} warnings, ${report.summary.errors} errors`
|
|
808
|
+
);
|
|
809
|
+
return hadErrors ? 1 : 0;
|
|
810
|
+
} finally {
|
|
811
|
+
client.close();
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/commands/find.ts
|
|
816
|
+
import * as fs7 from "node:fs";
|
|
817
|
+
import os4 from "node:os";
|
|
818
|
+
import path8 from "node:path";
|
|
819
|
+
|
|
820
|
+
// src/config/loader.ts
|
|
821
|
+
import * as fs6 from "node:fs";
|
|
822
|
+
import os3 from "node:os";
|
|
823
|
+
import path7 from "node:path";
|
|
824
|
+
|
|
825
|
+
// src/util/cron.ts
|
|
826
|
+
var FIELD_RANGES = [
|
|
827
|
+
[0, 59],
|
|
828
|
+
// minute
|
|
829
|
+
[0, 23],
|
|
830
|
+
// hour
|
|
831
|
+
[1, 31],
|
|
832
|
+
// day of month
|
|
833
|
+
[1, 12],
|
|
834
|
+
// month
|
|
835
|
+
[0, 6]
|
|
836
|
+
// day of week (0 = Sunday)
|
|
837
|
+
];
|
|
838
|
+
function assertInRange(v, min, max, field) {
|
|
839
|
+
if (v < min || v > max) {
|
|
840
|
+
throw new Error(`cron value ${v} out of range [${min}, ${max}] in field '${field}'`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function parseField(field, min, max) {
|
|
844
|
+
if (field === "") throw new Error("empty cron field");
|
|
845
|
+
const values = /* @__PURE__ */ new Set();
|
|
846
|
+
for (const part of field.split(",")) {
|
|
847
|
+
if (part === "") throw new Error("empty cron list element");
|
|
848
|
+
const stepMatch = /^(.+)\/(\d+)$/.exec(part);
|
|
849
|
+
const step = stepMatch?.[2] !== void 0 ? Number(stepMatch[2]) : 1;
|
|
850
|
+
if (step <= 0 || !Number.isInteger(step)) throw new Error(`invalid cron step '${part}'`);
|
|
851
|
+
const rangePart = stepMatch?.[1] !== void 0 ? stepMatch[1] : part;
|
|
852
|
+
if (rangePart === "*") {
|
|
853
|
+
for (let v = min; v <= max; v += step) values.add(v);
|
|
854
|
+
} else {
|
|
855
|
+
const rangeMatch = /^(\d+)-(\d+)$/.exec(rangePart);
|
|
856
|
+
if (rangeMatch !== null) {
|
|
857
|
+
const start = Number(rangeMatch[1]);
|
|
858
|
+
const end = Number(rangeMatch[2]);
|
|
859
|
+
assertInRange(start, min, max, field);
|
|
860
|
+
assertInRange(end, min, max, field);
|
|
861
|
+
if (start > end) throw new Error(`invalid cron range '${rangePart}': start > end`);
|
|
862
|
+
for (let v = start; v <= end; v += step) values.add(v);
|
|
863
|
+
} else {
|
|
864
|
+
const v = Number(rangePart);
|
|
865
|
+
if (!Number.isInteger(v)) {
|
|
866
|
+
throw new Error(`invalid cron field value '${rangePart}'`);
|
|
867
|
+
}
|
|
868
|
+
assertInRange(v, min, max, field);
|
|
869
|
+
values.add(v);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (values.size === 0) throw new Error(`cron field '${field}' produced no values`);
|
|
874
|
+
return values;
|
|
875
|
+
}
|
|
876
|
+
function parseCron(expr) {
|
|
877
|
+
const fields = expr.trim().split(/\s+/);
|
|
878
|
+
if (fields.length !== 5 || fields[0] === void 0 || fields[1] === void 0 || fields[2] === void 0 || fields[3] === void 0 || fields[4] === void 0) {
|
|
879
|
+
throw new Error(`cron expression must have 5 fields, got ${fields.length}`);
|
|
880
|
+
}
|
|
881
|
+
return [
|
|
882
|
+
parseField(fields[0], FIELD_RANGES[0][0], FIELD_RANGES[0][1]),
|
|
883
|
+
parseField(fields[1], FIELD_RANGES[1][0], FIELD_RANGES[1][1]),
|
|
884
|
+
parseField(fields[2], FIELD_RANGES[2][0], FIELD_RANGES[2][1]),
|
|
885
|
+
parseField(fields[3], FIELD_RANGES[3][0], FIELD_RANGES[3][1]),
|
|
886
|
+
parseField(fields[4], FIELD_RANGES[4][0], FIELD_RANGES[4][1])
|
|
887
|
+
];
|
|
888
|
+
}
|
|
889
|
+
function cronMatches(expr, date) {
|
|
890
|
+
const [minutes, hours, dom, months, dow] = parseCron(expr);
|
|
891
|
+
return minutes.has(date.getMinutes()) && hours.has(date.getHours()) && dom.has(date.getDate()) && months.has(date.getMonth() + 1) && dow.has(date.getDay());
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/util/dotenv.ts
|
|
895
|
+
function parseDotenv(text) {
|
|
896
|
+
const out = {};
|
|
897
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
898
|
+
let line = rawLine.trim();
|
|
899
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
900
|
+
if (line.startsWith("export ")) line = line.slice("export ".length).trim();
|
|
901
|
+
const eq = line.indexOf("=");
|
|
902
|
+
if (eq < 0) continue;
|
|
903
|
+
const key = line.slice(0, eq).trim();
|
|
904
|
+
if (key === "") continue;
|
|
905
|
+
let value = line.slice(eq + 1).trim();
|
|
906
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
907
|
+
const quote = value.charAt(0);
|
|
908
|
+
const inner = value.slice(1, -1);
|
|
909
|
+
value = quote === '"' ? inner.replaceAll("\\n", "\n").replaceAll("\\r", "\r").replaceAll("\\t", " ").replaceAll('\\"', '"').replaceAll("\\\\", "\\") : inner;
|
|
910
|
+
} else {
|
|
911
|
+
const hash = value.indexOf(" #");
|
|
912
|
+
if (hash >= 0) value = value.slice(0, hash).trim();
|
|
913
|
+
}
|
|
914
|
+
out[key] = value;
|
|
915
|
+
}
|
|
916
|
+
return out;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/util/memory.ts
|
|
920
|
+
function parseMemory(v) {
|
|
921
|
+
if (typeof v === "number") return Math.max(0, Math.round(v));
|
|
922
|
+
const s = String(v).trim();
|
|
923
|
+
const m = /^(\d+(?:\.\d+)?)\s*(k|m|g|t)?b?$/i.exec(s);
|
|
924
|
+
if (!m || m[1] === void 0) {
|
|
925
|
+
throw new Error(`invalid memory string '${s}' (expected e.g. '256M', '1G', '512K')`);
|
|
926
|
+
}
|
|
927
|
+
const n = Number.parseFloat(m[1]);
|
|
928
|
+
const unit = (m[2] ?? "").toLowerCase();
|
|
929
|
+
const factor = {
|
|
930
|
+
"": 1,
|
|
931
|
+
k: 1024,
|
|
932
|
+
m: 1048576,
|
|
933
|
+
g: 1073741824,
|
|
934
|
+
t: 1099511627776
|
|
935
|
+
};
|
|
936
|
+
const f = factor[unit];
|
|
937
|
+
if (f === void 0) throw new Error(`invalid memory unit in '${s}'`);
|
|
938
|
+
return Math.round(n * f);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/util/portable.ts
|
|
942
|
+
import * as fs4 from "node:fs";
|
|
943
|
+
import os2 from "node:os";
|
|
944
|
+
import path5 from "node:path";
|
|
945
|
+
function isDir(p) {
|
|
946
|
+
try {
|
|
947
|
+
return fs4.statSync(p).isDirectory();
|
|
948
|
+
} catch {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function isFile(p) {
|
|
953
|
+
try {
|
|
954
|
+
return fs4.statSync(p).isFile();
|
|
955
|
+
} catch {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function stripHomePrefix(p) {
|
|
960
|
+
const m = p.match(/^\/(?:home|Users|opt)\/[^/\\]+[/\\](.*)$/);
|
|
961
|
+
if (m?.[1] !== void 0) return m[1];
|
|
962
|
+
const dm = p.match(/^[A-Za-z]:[/\\]+(?:home|Users|opt)[/\\]+[^/\\]+[/\\]+(.*)$/i);
|
|
963
|
+
if (dm?.[1] !== void 0) return dm[1].replaceAll("\\", "/");
|
|
964
|
+
const wm = p.match(/^[A-Za-z]:[\\/]+Users[\\/]+[^\\/]+[\\/]+(.*)$/i);
|
|
965
|
+
if (wm?.[1] !== void 0) return wm[1].replaceAll("\\", "/");
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
function searchByBasename(root, basename, maxDepth = 4) {
|
|
969
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
970
|
+
const seen = /* @__PURE__ */ new Set();
|
|
971
|
+
while (queue.length > 0) {
|
|
972
|
+
const cur = queue.shift();
|
|
973
|
+
if (cur.depth > maxDepth) continue;
|
|
974
|
+
let entries;
|
|
975
|
+
try {
|
|
976
|
+
entries = fs4.readdirSync(cur.dir, { withFileTypes: true });
|
|
977
|
+
} catch {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
for (const e of entries) {
|
|
981
|
+
const full = path5.join(cur.dir, e.name);
|
|
982
|
+
if (e.isFile() && e.name === basename) return full;
|
|
983
|
+
if (e.isDirectory() && !e.isSymbolicLink()) {
|
|
984
|
+
if (e.name === "node_modules" || e.name === ".git") continue;
|
|
985
|
+
const real = (() => {
|
|
986
|
+
try {
|
|
987
|
+
return fs4.realpathSync(full);
|
|
988
|
+
} catch {
|
|
989
|
+
return full;
|
|
990
|
+
}
|
|
991
|
+
})();
|
|
992
|
+
if (seen.has(real)) continue;
|
|
993
|
+
seen.add(real);
|
|
994
|
+
queue.push({ dir: full, depth: cur.depth + 1 });
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
function fixCwd(staleCwd, sourceDir) {
|
|
1001
|
+
if (isDir(staleCwd)) return { path: staleCwd, fixed: false };
|
|
1002
|
+
const candidates = [];
|
|
1003
|
+
const tail = stripHomePrefix(staleCwd);
|
|
1004
|
+
if (tail !== null) {
|
|
1005
|
+
candidates.push(path5.join(os2.homedir(), tail));
|
|
1006
|
+
candidates.push(path5.join(path5.dirname(os2.homedir()), tail));
|
|
1007
|
+
candidates.push(path5.join(path5.dirname(sourceDir), tail));
|
|
1008
|
+
if (isDir(sourceDir) && !staleCwd.startsWith(os2.homedir())) candidates.push(sourceDir);
|
|
1009
|
+
}
|
|
1010
|
+
const proj = path5.basename(sourceDir);
|
|
1011
|
+
if (proj) {
|
|
1012
|
+
const marker = `${path5.sep}${proj}${path5.sep}`;
|
|
1013
|
+
const altMarker = `/${proj}/`;
|
|
1014
|
+
const idx = staleCwd.includes(marker) ? staleCwd.lastIndexOf(marker) : staleCwd.lastIndexOf(altMarker);
|
|
1015
|
+
if (idx >= 0) {
|
|
1016
|
+
const suffix = staleCwd.slice(idx + proj.length + 2);
|
|
1017
|
+
candidates.push(path5.join(sourceDir, suffix));
|
|
1018
|
+
candidates.push(path5.join(path5.dirname(sourceDir), proj, suffix));
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
for (const c of candidates) {
|
|
1022
|
+
if (c && isDir(c))
|
|
1023
|
+
return { path: c, fixed: true, reason: `cwd '${staleCwd}' not found \u2192 auto-fixed to '${c}'` };
|
|
1024
|
+
}
|
|
1025
|
+
return {
|
|
1026
|
+
path: staleCwd,
|
|
1027
|
+
fixed: false,
|
|
1028
|
+
reason: `cwd '${staleCwd}' not found and no auto-fix candidate exists`
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function fixScript(staleScript, cwd, sourceDir) {
|
|
1032
|
+
if (isFile(staleScript)) return { path: staleScript, fixed: false };
|
|
1033
|
+
const base = path5.basename(staleScript);
|
|
1034
|
+
const candidates = [];
|
|
1035
|
+
candidates.push(path5.join(cwd, base));
|
|
1036
|
+
candidates.push(path5.join(sourceDir, base));
|
|
1037
|
+
const tail = stripHomePrefix(staleScript);
|
|
1038
|
+
if (tail !== null) {
|
|
1039
|
+
candidates.push(path5.join(os2.homedir(), tail));
|
|
1040
|
+
candidates.push(path5.join(path5.dirname(os2.homedir()), tail));
|
|
1041
|
+
candidates.push(path5.join(sourceDir, tail));
|
|
1042
|
+
const tailParts = tail.split("/");
|
|
1043
|
+
if (tailParts.length > 1) candidates.push(path5.join(sourceDir, tailParts.slice(1).join("/")));
|
|
1044
|
+
}
|
|
1045
|
+
const proj = path5.basename(sourceDir);
|
|
1046
|
+
if (proj) {
|
|
1047
|
+
const marker = `/${proj}/`;
|
|
1048
|
+
const idx = staleScript.lastIndexOf(marker);
|
|
1049
|
+
if (idx >= 0) {
|
|
1050
|
+
const suffix = staleScript.slice(idx + proj.length + 2);
|
|
1051
|
+
candidates.push(path5.join(sourceDir, suffix));
|
|
1052
|
+
candidates.push(path5.join(cwd, suffix));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const parts = staleScript.split(/[/\\]/).filter(Boolean);
|
|
1056
|
+
if (parts.length >= 2) {
|
|
1057
|
+
const lastTwo = parts.slice(-2).join(path5.sep);
|
|
1058
|
+
candidates.push(path5.join(sourceDir, lastTwo));
|
|
1059
|
+
candidates.push(path5.join(cwd, lastTwo));
|
|
1060
|
+
}
|
|
1061
|
+
for (const c of candidates) {
|
|
1062
|
+
if (c && isFile(c))
|
|
1063
|
+
return {
|
|
1064
|
+
path: c,
|
|
1065
|
+
fixed: true,
|
|
1066
|
+
reason: `script '${staleScript}' not found \u2192 auto-fixed to '${c}'`
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
const found = searchByBasename(sourceDir, base, 4);
|
|
1070
|
+
if (found !== null)
|
|
1071
|
+
return {
|
|
1072
|
+
path: found,
|
|
1073
|
+
fixed: true,
|
|
1074
|
+
reason: `script '${staleScript}' not found \u2192 found '${found}' under '${sourceDir}'`
|
|
1075
|
+
};
|
|
1076
|
+
if (cwd !== sourceDir) {
|
|
1077
|
+
const found2 = searchByBasename(cwd, base, 4);
|
|
1078
|
+
if (found2 !== null)
|
|
1079
|
+
return {
|
|
1080
|
+
path: found2,
|
|
1081
|
+
fixed: true,
|
|
1082
|
+
reason: `script '${staleScript}' not found \u2192 found '${found2}' under cwd`
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
path: staleScript,
|
|
1087
|
+
fixed: false,
|
|
1088
|
+
reason: `script '${staleScript}' not found and no auto-fix candidate exists`
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/config/loader.ts
|
|
1093
|
+
init_jsloader();
|
|
1094
|
+
var ConfigError = class extends Error {
|
|
1095
|
+
constructor(message) {
|
|
1096
|
+
super(message);
|
|
1097
|
+
this.name = "ConfigError";
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
1101
|
+
"name",
|
|
1102
|
+
"script",
|
|
1103
|
+
"args",
|
|
1104
|
+
"interpreter",
|
|
1105
|
+
"cwd",
|
|
1106
|
+
"env",
|
|
1107
|
+
"autorestart",
|
|
1108
|
+
"max_restarts",
|
|
1109
|
+
"restart_delay",
|
|
1110
|
+
"min_uptime",
|
|
1111
|
+
"kill_timeout",
|
|
1112
|
+
"instances",
|
|
1113
|
+
"exec_mode",
|
|
1114
|
+
// M2: logging + env_file + wait_ready (+ pm2 aliases)
|
|
1115
|
+
"out_file",
|
|
1116
|
+
"output",
|
|
1117
|
+
"error_file",
|
|
1118
|
+
"err_file",
|
|
1119
|
+
"merge_logs",
|
|
1120
|
+
"merge",
|
|
1121
|
+
"time",
|
|
1122
|
+
"timestamp",
|
|
1123
|
+
"log_date_format",
|
|
1124
|
+
"wait_ready",
|
|
1125
|
+
"env_file",
|
|
1126
|
+
// M4: memory
|
|
1127
|
+
"max_memory_restart",
|
|
1128
|
+
// M5: scheduling + watch
|
|
1129
|
+
"cron_restart",
|
|
1130
|
+
"watch",
|
|
1131
|
+
"watch_delay"
|
|
1132
|
+
]);
|
|
1133
|
+
var LATER_KEYS = {
|
|
1134
|
+
user: "M6",
|
|
1135
|
+
group: "M6",
|
|
1136
|
+
uid: "M6",
|
|
1137
|
+
gid: "M6"
|
|
1138
|
+
};
|
|
1139
|
+
var CONFIG_FILE_RE = /^(?:(?:ecosystem|pm2|relife2)[\w.-]*|[\w.-]*?)\.config\.(?:c?js|mjs|json|ts)$/i;
|
|
1140
|
+
function looksLikeConfigFile(file) {
|
|
1141
|
+
return CONFIG_FILE_RE.test(path7.basename(file));
|
|
1142
|
+
}
|
|
1143
|
+
function findDefaultConfig(cwd) {
|
|
1144
|
+
const candidates = [
|
|
1145
|
+
"relife2.config.ts",
|
|
1146
|
+
"relife2.config.js",
|
|
1147
|
+
"relife2.config.cjs",
|
|
1148
|
+
"relife2.config.mjs",
|
|
1149
|
+
"relife2.config.json",
|
|
1150
|
+
"ecosystem.config.js",
|
|
1151
|
+
"ecosystem.config.cjs",
|
|
1152
|
+
"ecosystem.config.mjs",
|
|
1153
|
+
"ecosystem.config.json",
|
|
1154
|
+
"pm2.config.js",
|
|
1155
|
+
"pm2.config.cjs"
|
|
1156
|
+
];
|
|
1157
|
+
for (const name of candidates) {
|
|
1158
|
+
const full = path7.join(cwd, name);
|
|
1159
|
+
if (fs6.existsSync(full)) return full;
|
|
1160
|
+
}
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
async function loadConfigFile(filePath, opts) {
|
|
1164
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
1165
|
+
let exported;
|
|
1166
|
+
if (ext === ".json") {
|
|
1167
|
+
exported = JSON.parse(fs6.readFileSync(filePath, "utf8"));
|
|
1168
|
+
} else if (ext === ".ts") {
|
|
1169
|
+
exported = await loadTsFile(filePath);
|
|
1170
|
+
} else {
|
|
1171
|
+
exported = await loadJsFile(filePath);
|
|
1172
|
+
if (typeof exported === "function") {
|
|
1173
|
+
exported = await exported();
|
|
1174
|
+
}
|
|
1175
|
+
if (exported instanceof Promise) exported = await exported;
|
|
1176
|
+
}
|
|
1177
|
+
return normalizeExported(exported, path7.dirname(filePath), opts);
|
|
1178
|
+
}
|
|
1179
|
+
async function loadTsFile(file) {
|
|
1180
|
+
try {
|
|
1181
|
+
const { pathToFileURL: pathToFileURL2 } = await import("node:url");
|
|
1182
|
+
const url = `${pathToFileURL2(file).href}?t=${Date.now()}`;
|
|
1183
|
+
const mod = await import(url);
|
|
1184
|
+
return mod.default ?? mod;
|
|
1185
|
+
} catch {
|
|
1186
|
+
const raw = fs6.readFileSync(file, "utf8");
|
|
1187
|
+
const stripped = raw.replace(/^\s*import\s+type\s+.*$/gm, "").replace(/:\s*[\w<>[\]|&\s,?]+(?=[=;,\n)}])/g, "").replace(/\s+as\s+const/g, "");
|
|
1188
|
+
const tmp = `${file}.tmp.cjs`;
|
|
1189
|
+
try {
|
|
1190
|
+
fs6.writeFileSync(tmp, stripped, "utf8");
|
|
1191
|
+
const { loadJsFile: lf } = await Promise.resolve().then(() => (init_jsloader(), jsloader_exports));
|
|
1192
|
+
return await lf(tmp);
|
|
1193
|
+
} finally {
|
|
1194
|
+
try {
|
|
1195
|
+
fs6.unlinkSync(tmp);
|
|
1196
|
+
} catch {
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
function normalizeExported(exported, sourceDir, opts) {
|
|
1202
|
+
let rawApps;
|
|
1203
|
+
if (Array.isArray(exported)) {
|
|
1204
|
+
rawApps = exported;
|
|
1205
|
+
} else if (exported !== null && typeof exported === "object") {
|
|
1206
|
+
rawApps = exported.apps;
|
|
1207
|
+
}
|
|
1208
|
+
if (!Array.isArray(rawApps) || rawApps.length === 0) {
|
|
1209
|
+
throw new ConfigError("config must export { apps: [...] } with at least one app");
|
|
1210
|
+
}
|
|
1211
|
+
const warnings = [];
|
|
1212
|
+
const apps = rawApps.map((raw, i) => normalizeApp(raw, sourceDir, warnings, i, opts?.envName));
|
|
1213
|
+
return { apps, warnings };
|
|
1214
|
+
}
|
|
1215
|
+
function normalizeApp(raw, sourceDir, warnings, index, envName) {
|
|
1216
|
+
if (raw === null || typeof raw !== "object") {
|
|
1217
|
+
throw new ConfigError(`apps[${index}] must be an object`);
|
|
1218
|
+
}
|
|
1219
|
+
const input = raw;
|
|
1220
|
+
const envProfiles = {};
|
|
1221
|
+
for (const key of Object.keys(input)) {
|
|
1222
|
+
if (key.startsWith("env_")) {
|
|
1223
|
+
const profile = key.slice(4);
|
|
1224
|
+
const val = input[key];
|
|
1225
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
1226
|
+
const out = {};
|
|
1227
|
+
for (const [k, v] of Object.entries(val)) {
|
|
1228
|
+
out[k] = v === void 0 || v === null ? "" : String(v);
|
|
1229
|
+
}
|
|
1230
|
+
envProfiles[profile] = out;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
for (const key of Object.keys(input)) {
|
|
1235
|
+
if (KNOWN_KEYS.has(key)) continue;
|
|
1236
|
+
if (key.startsWith("env_")) continue;
|
|
1237
|
+
const later = LATER_KEYS[key];
|
|
1238
|
+
if (later !== void 0) {
|
|
1239
|
+
warnings.push(`apps[${index}] '${key}' is ignored until milestone ${later} (TODO.md \xA75)`);
|
|
1240
|
+
} else {
|
|
1241
|
+
warnings.push(`apps[${index}] unknown field '${key}' is ignored`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const scriptRaw = input.script;
|
|
1245
|
+
if (typeof scriptRaw !== "string" || scriptRaw === "") {
|
|
1246
|
+
throw new ConfigError(`apps[${index}] requires a string 'script'`);
|
|
1247
|
+
}
|
|
1248
|
+
const instances = parseInstances(input.instances, index);
|
|
1249
|
+
const execMode = parseExecMode(input.exec_mode, index);
|
|
1250
|
+
const rawCwd = typeof input.cwd === "string" ? input.cwd : void 0;
|
|
1251
|
+
const cwdRaw = path7.resolve(sourceDir, rawCwd ?? ".");
|
|
1252
|
+
const cwdFix = fixCwd(cwdRaw, sourceDir);
|
|
1253
|
+
const cwd = cwdFix.path;
|
|
1254
|
+
if (cwdFix.fixed && cwdFix.reason) warnings.push(cwdFix.reason);
|
|
1255
|
+
const hasSep = scriptRaw.includes("/") || scriptRaw.includes("\\");
|
|
1256
|
+
const isJsExt = /\.(?:c?js|mjs|ts|mts|cts)$/i.test(scriptRaw);
|
|
1257
|
+
const scriptRawResolved = hasSep || isJsExt ? path7.resolve(cwd, scriptRaw) : scriptRaw;
|
|
1258
|
+
let script = scriptRawResolved;
|
|
1259
|
+
if (path7.isAbsolute(scriptRawResolved)) {
|
|
1260
|
+
const f = fixScript(scriptRawResolved, cwd, sourceDir);
|
|
1261
|
+
if (f.fixed) {
|
|
1262
|
+
warnings.push(f.reason ?? `script '${scriptRawResolved}' \u2192 '${f.path}'`);
|
|
1263
|
+
script = f.path;
|
|
1264
|
+
} else if (!fs6.existsSync(scriptRawResolved) && f.reason) {
|
|
1265
|
+
warnings.push(f.reason);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const name = typeof input.name === "string" && input.name !== "" ? input.name : defaultName(script);
|
|
1269
|
+
const env = normalizeEnv(input.env, warnings, index);
|
|
1270
|
+
const selectedEnv = envName ?? process.env.NODE_ENV;
|
|
1271
|
+
if (selectedEnv !== void 0 && selectedEnv !== "") {
|
|
1272
|
+
const profile = envProfiles[selectedEnv];
|
|
1273
|
+
if (profile !== void 0) {
|
|
1274
|
+
Object.assign(env, profile);
|
|
1275
|
+
} else if (envName !== void 0) {
|
|
1276
|
+
warnings.push(`apps[${index}] --env '${envName}' has no matching env_${envName} profile`);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const envFile = typeof input.env_file === "string" && input.env_file !== "" ? input.env_file : void 0;
|
|
1280
|
+
if (envFile !== void 0) {
|
|
1281
|
+
const envPath = path7.resolve(cwd, envFile);
|
|
1282
|
+
let parsed = null;
|
|
1283
|
+
if (fs6.existsSync(envPath) && fs6.statSync(envPath).isFile()) {
|
|
1284
|
+
try {
|
|
1285
|
+
parsed = parseDotenv(fs6.readFileSync(envPath, "utf8"));
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
warnings.push(
|
|
1288
|
+
`apps[${index}] env_file '${envFile}' could not be read: ${err.message}`
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (parsed === null) {
|
|
1293
|
+
warnings.push(`apps[${index}] env_file '${envFile}': ${envPath} not found; ignored`);
|
|
1294
|
+
} else {
|
|
1295
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1296
|
+
if (!(k in env) && !(k in process.env)) env[k] = v;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
name,
|
|
1302
|
+
script,
|
|
1303
|
+
args: normalizeArgs(input.args, index),
|
|
1304
|
+
interpreter: typeof input.interpreter === "string" ? input.interpreter : void 0,
|
|
1305
|
+
cwd,
|
|
1306
|
+
env,
|
|
1307
|
+
autorestart: input.autorestart !== false,
|
|
1308
|
+
maxRestarts: toInt(input.max_restarts, 15, "max_restarts", index),
|
|
1309
|
+
restartDelay: toInt(input.restart_delay, 0, "restart_delay", index),
|
|
1310
|
+
minUptime: parseTimeField(input.min_uptime, "1s", "min_uptime", index),
|
|
1311
|
+
killTimeout: toInt(input.kill_timeout, 1600, "kill_timeout", index),
|
|
1312
|
+
outFile: strOrUndef(input.out_file ?? input.output),
|
|
1313
|
+
errFile: strOrUndef(input.error_file ?? input.err_file),
|
|
1314
|
+
mergeLogs: input.merge_logs === true || input.merge === true,
|
|
1315
|
+
time: input.time === true || input.timestamp === true,
|
|
1316
|
+
logDateFormat: strOrUndef(input.log_date_format),
|
|
1317
|
+
waitReady: input.wait_ready === true,
|
|
1318
|
+
envFile,
|
|
1319
|
+
maxMemoryRestart: parseMemoryField(input.max_memory_restart, "max_memory_restart", index),
|
|
1320
|
+
cronRestart: parseCronField(input.cron_restart, "cron_restart", index),
|
|
1321
|
+
watch: parseWatchField(input.watch, "watch", index),
|
|
1322
|
+
watchDelay: toInt(input.watch_delay, 1e3, "watch_delay", index),
|
|
1323
|
+
instances,
|
|
1324
|
+
execMode,
|
|
1325
|
+
envName: selectedEnv,
|
|
1326
|
+
sourceDir
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function strOrUndef(v) {
|
|
1330
|
+
return typeof v === "string" && v !== "" ? v : void 0;
|
|
1331
|
+
}
|
|
1332
|
+
function normalizeEnv(env, warnings, index) {
|
|
1333
|
+
if (env === void 0 || env === null) return {};
|
|
1334
|
+
if (typeof env !== "object" || Array.isArray(env)) {
|
|
1335
|
+
warnings.push(`apps[${index}] 'env' must be an object; ignoring`);
|
|
1336
|
+
return {};
|
|
1337
|
+
}
|
|
1338
|
+
const out = {};
|
|
1339
|
+
for (const [k, v] of Object.entries(env)) {
|
|
1340
|
+
out[k] = v === void 0 || v === null ? "" : String(v);
|
|
1341
|
+
}
|
|
1342
|
+
return out;
|
|
1343
|
+
}
|
|
1344
|
+
function normalizeArgs(args, index) {
|
|
1345
|
+
if (args === void 0 || args === null) return [];
|
|
1346
|
+
if (Array.isArray(args)) return args.map((a) => String(a));
|
|
1347
|
+
if (typeof args === "string") return splitArgs(args);
|
|
1348
|
+
throw new ConfigError(`apps[${index}] 'args' must be a string or string[]`);
|
|
1349
|
+
}
|
|
1350
|
+
function toInt(v, fallback, field, index) {
|
|
1351
|
+
if (v === void 0 || v === null) return fallback;
|
|
1352
|
+
const n = Number(v);
|
|
1353
|
+
if (!Number.isFinite(n)) throw new ConfigError(`apps[${index}] '${field}' must be a number`);
|
|
1354
|
+
return Math.max(0, Math.round(n));
|
|
1355
|
+
}
|
|
1356
|
+
function parseTimeField(v, fallback, field, index) {
|
|
1357
|
+
if (v === void 0 || v === null) return parseTime(fallback);
|
|
1358
|
+
try {
|
|
1359
|
+
return parseTime(v);
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
throw new ConfigError(`apps[${index}] '${field}': ${err.message}`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
function parseMemoryField(v, field, index) {
|
|
1365
|
+
if (v === void 0 || v === null) return void 0;
|
|
1366
|
+
try {
|
|
1367
|
+
return parseMemory(v);
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
throw new ConfigError(`apps[${index}] '${field}': ${err.message}`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function parseCronField(v, field, index) {
|
|
1373
|
+
if (v === void 0 || v === null) return void 0;
|
|
1374
|
+
if (typeof v !== "string" || v.trim() === "") {
|
|
1375
|
+
throw new ConfigError(`apps[${index}] '${field}' must be a non-empty cron expression string`);
|
|
1376
|
+
}
|
|
1377
|
+
const trimmed = v.trim();
|
|
1378
|
+
const parts = trimmed.split(/\s+/);
|
|
1379
|
+
if (parts.length !== 5) {
|
|
1380
|
+
throw new ConfigError(
|
|
1381
|
+
`apps[${index}] '${field}': invalid cron expression '${v}' (expected 5 fields: 'min hour dom month dow')`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
try {
|
|
1385
|
+
parseCron(trimmed);
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
throw new ConfigError(
|
|
1388
|
+
`apps[${index}] '${field}': invalid cron expression '${v}': ${err.message}`
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
return trimmed;
|
|
1392
|
+
}
|
|
1393
|
+
function parseWatchField(v, field, index) {
|
|
1394
|
+
if (v === void 0 || v === null) return void 0;
|
|
1395
|
+
if (v === false) return false;
|
|
1396
|
+
if (v === true) return true;
|
|
1397
|
+
if (Array.isArray(v)) {
|
|
1398
|
+
const paths = v.filter((p) => typeof p === "string" && p !== "");
|
|
1399
|
+
if (paths.length === 0) {
|
|
1400
|
+
throw new ConfigError(`apps[${index}] '${field}': array must contain at least one path`);
|
|
1401
|
+
}
|
|
1402
|
+
return paths;
|
|
1403
|
+
}
|
|
1404
|
+
throw new ConfigError(`apps[${index}] '${field}' must be false, true, or an array of paths`);
|
|
1405
|
+
}
|
|
1406
|
+
function parseInstances(v, index) {
|
|
1407
|
+
if (v === void 0 || v === null) return 1;
|
|
1408
|
+
if (v === "max" || v === "MAX") return os3.cpus().length || 1;
|
|
1409
|
+
const n = Number(v);
|
|
1410
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
1411
|
+
throw new ConfigError(`apps[${index}] 'instances' must be a positive number or 'max'`);
|
|
1412
|
+
}
|
|
1413
|
+
return Math.max(1, Math.round(n));
|
|
1414
|
+
}
|
|
1415
|
+
function parseExecMode(v, index) {
|
|
1416
|
+
if (v === void 0 || v === null) return "fork";
|
|
1417
|
+
const s = String(v).toLowerCase();
|
|
1418
|
+
if (s === "fork" || s === "fork_mode") return "fork";
|
|
1419
|
+
if (s === "cluster" || s === "cluster_mode") return "cluster";
|
|
1420
|
+
throw new ConfigError(`apps[${index}] 'exec_mode' must be 'fork' or 'cluster'`);
|
|
1421
|
+
}
|
|
1422
|
+
function defaultName(script) {
|
|
1423
|
+
const base = path7.basename(script);
|
|
1424
|
+
return base.replace(/\.(?:c?js|mjs|ts|mts|cts)$/i, "") || base;
|
|
1425
|
+
}
|
|
1426
|
+
function normalizeSingleApp(input, sourceDir, envName) {
|
|
1427
|
+
const warnings = [];
|
|
1428
|
+
const effectiveEnv = input._envName ?? envName;
|
|
1429
|
+
const app = normalizeApp(input, sourceDir, warnings, 0, effectiveEnv);
|
|
1430
|
+
return { app, warnings };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/commands/find.ts
|
|
1434
|
+
var VALUE_OPTS = /* @__PURE__ */ new Set(["root", "depth"]);
|
|
1435
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
1436
|
+
"node_modules",
|
|
1437
|
+
".git",
|
|
1438
|
+
".hg",
|
|
1439
|
+
".svn",
|
|
1440
|
+
"vendor",
|
|
1441
|
+
".next",
|
|
1442
|
+
".output",
|
|
1443
|
+
"dist",
|
|
1444
|
+
"build",
|
|
1445
|
+
"target",
|
|
1446
|
+
"__pycache__",
|
|
1447
|
+
".cache",
|
|
1448
|
+
".pnpm",
|
|
1449
|
+
".yarn",
|
|
1450
|
+
".turbo",
|
|
1451
|
+
".parcel-cache",
|
|
1452
|
+
"Library",
|
|
1453
|
+
"AppData",
|
|
1454
|
+
".local",
|
|
1455
|
+
// broad but contains share/nvm etc — scanning it is expensive
|
|
1456
|
+
"proc",
|
|
1457
|
+
"sys",
|
|
1458
|
+
"dev"
|
|
1459
|
+
]);
|
|
1460
|
+
function findConfigs(root, maxDepth) {
|
|
1461
|
+
const out = [];
|
|
1462
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
1463
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1464
|
+
while (stack.length > 0) {
|
|
1465
|
+
const cur = stack.pop();
|
|
1466
|
+
if (cur === void 0) break;
|
|
1467
|
+
const { dir, depth } = cur;
|
|
1468
|
+
if (depth > maxDepth) continue;
|
|
1469
|
+
let real;
|
|
1470
|
+
try {
|
|
1471
|
+
real = fs7.realpathSync(dir);
|
|
1472
|
+
} catch {
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (visited.has(real)) continue;
|
|
1476
|
+
visited.add(real);
|
|
1477
|
+
let entries;
|
|
1478
|
+
try {
|
|
1479
|
+
entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
1480
|
+
} catch {
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
for (const ent of entries) {
|
|
1484
|
+
const full = path8.join(dir, ent.name);
|
|
1485
|
+
if (ent.isFile() && looksLikeConfigFile(ent.name)) {
|
|
1486
|
+
out.push(full);
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
if (!ent.isDirectory()) continue;
|
|
1490
|
+
if (IGNORED_DIRS.has(ent.name)) continue;
|
|
1491
|
+
if (ent.name.startsWith(".") && ![".config"].includes(ent.name)) {
|
|
1492
|
+
if ([".vscode", ".idea", ".github", ".husky"].includes(ent.name)) continue;
|
|
1493
|
+
if (depth < 2) continue;
|
|
1494
|
+
}
|
|
1495
|
+
if (ent.isSymbolicLink()) continue;
|
|
1496
|
+
if (depth + 1 <= maxDepth) stack.push({ dir: full, depth: depth + 1 });
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
return out;
|
|
1500
|
+
}
|
|
1501
|
+
async function enrich(found) {
|
|
1502
|
+
const entries = [];
|
|
1503
|
+
for (const p of found) {
|
|
1504
|
+
try {
|
|
1505
|
+
const loaded = await loadConfigFile(p);
|
|
1506
|
+
entries.push({ path: p, apps: loaded.apps.map((a) => a.name), warnings: loaded.warnings });
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
entries.push({ path: p, apps: [], error: err.message, warnings: [] });
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return entries;
|
|
1512
|
+
}
|
|
1513
|
+
function relToRoot(p, root) {
|
|
1514
|
+
const rel = path8.relative(root, p);
|
|
1515
|
+
return rel === "" ? path8.basename(p) : rel;
|
|
1516
|
+
}
|
|
1517
|
+
async function run5(args) {
|
|
1518
|
+
const { positionals, opts } = parseCliArgs(args, VALUE_OPTS);
|
|
1519
|
+
const wantsStart = positionals[0] === "start" || opts.has("start");
|
|
1520
|
+
const jsonMode = opts.has("json");
|
|
1521
|
+
const allFlag = opts.has("all");
|
|
1522
|
+
let root = opts.get("root");
|
|
1523
|
+
if (root === void 0) {
|
|
1524
|
+
if (allFlag) {
|
|
1525
|
+
root = process.platform === "win32" ? path8.parse(process.cwd()).root : "/";
|
|
1526
|
+
} else {
|
|
1527
|
+
root = os4.homedir() || process.cwd();
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
root = path8.resolve(root);
|
|
1531
|
+
let depth = 6;
|
|
1532
|
+
const depthRaw = opts.get("depth");
|
|
1533
|
+
if (depthRaw !== void 0) {
|
|
1534
|
+
const n = Number(depthRaw);
|
|
1535
|
+
if (!Number.isFinite(n) || n < 1 || n > 20) {
|
|
1536
|
+
console.error(`find: --depth must be 1..20, got '${depthRaw}'`);
|
|
1537
|
+
return 1;
|
|
1538
|
+
}
|
|
1539
|
+
depth = Math.round(n);
|
|
1540
|
+
}
|
|
1541
|
+
if (allFlag) depth = Math.max(depth, 8);
|
|
1542
|
+
if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) {
|
|
1543
|
+
console.error(`find: root '${root}' does not exist or is not a directory`);
|
|
1544
|
+
return 1;
|
|
1545
|
+
}
|
|
1546
|
+
if (!jsonMode) console.log(`relife2 find \u2014 scanning ${root} (depth ${depth}) \u2026`);
|
|
1547
|
+
const t0 = Date.now();
|
|
1548
|
+
const foundPaths = findConfigs(root, depth);
|
|
1549
|
+
foundPaths.sort();
|
|
1550
|
+
const enriched = await enrich(foundPaths);
|
|
1551
|
+
const dt = Date.now() - t0;
|
|
1552
|
+
let running = /* @__PURE__ */ new Set();
|
|
1553
|
+
try {
|
|
1554
|
+
const client2 = await ensureDaemonClient();
|
|
1555
|
+
try {
|
|
1556
|
+
const res = await client2.call("list");
|
|
1557
|
+
running = new Set(res.apps.map((a) => a.name));
|
|
1558
|
+
for (const n of [...running]) {
|
|
1559
|
+
const base = n.split(":")[0];
|
|
1560
|
+
if (base !== void 0) running.add(base);
|
|
1561
|
+
}
|
|
1562
|
+
} finally {
|
|
1563
|
+
client2.close();
|
|
1564
|
+
}
|
|
1565
|
+
} catch {
|
|
1566
|
+
}
|
|
1567
|
+
const rows = enriched.map((e) => {
|
|
1568
|
+
const isError = e.error !== void 0;
|
|
1569
|
+
const statuses = e.apps.length === 0 ? isError ? "error" : "empty" : e.apps.every((n) => running.has(n) || running.has(n.split(":")[0])) ? "running" : e.apps.some((n) => running.has(n)) ? "partial" : "stopped";
|
|
1570
|
+
return { ...e, statuses };
|
|
1571
|
+
});
|
|
1572
|
+
if (jsonMode) {
|
|
1573
|
+
const jsonOut = rows.map((r) => ({
|
|
1574
|
+
path: r.path,
|
|
1575
|
+
rel: relToRoot(r.path, root),
|
|
1576
|
+
apps: r.apps,
|
|
1577
|
+
status: r.statuses,
|
|
1578
|
+
error: r.error,
|
|
1579
|
+
warnings: r.warnings
|
|
1580
|
+
}));
|
|
1581
|
+
console.log(
|
|
1582
|
+
JSON.stringify({ root, depth, count: rows.length, tookMs: dt, configs: jsonOut }, null, 2)
|
|
1583
|
+
);
|
|
1584
|
+
} else {
|
|
1585
|
+
if (rows.length === 0) {
|
|
1586
|
+
console.log(`No configs found under ${root} (pattern *.config.{js,cjs,mjs,json,ts})`);
|
|
1587
|
+
console.log(`Hints: try --root <dir> --depth 8, or --all for full scan`);
|
|
1588
|
+
return 0;
|
|
1589
|
+
}
|
|
1590
|
+
console.log(`Found ${rows.length} config(s) in ${dt}ms:
|
|
1591
|
+
`);
|
|
1592
|
+
const rels = rows.map((r) => relToRoot(r.path, root));
|
|
1593
|
+
const maxRel = Math.max(12, ...rels.map((s) => s.length), 20);
|
|
1594
|
+
const maxApps = Math.max(4, ...rows.map((r) => r.apps.join(",").length), 12);
|
|
1595
|
+
console.log(`${"path".padEnd(maxRel)} ${"apps".padEnd(maxApps)} status notes`);
|
|
1596
|
+
console.log(`${"-".repeat(maxRel)} ${"-".repeat(maxApps)} ------ -----`);
|
|
1597
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1598
|
+
const r = rows[i];
|
|
1599
|
+
const rel = rels[i];
|
|
1600
|
+
const appsStr = r.apps.join(",") || (r.error ? "!" : "-");
|
|
1601
|
+
const note = r.error ? r.error.slice(0, 80) : r.warnings.length > 0 ? r.warnings[0]?.slice(0, 60) ?? "" : "";
|
|
1602
|
+
let line = `${rel.padEnd(maxRel)} ${appsStr.padEnd(maxApps)} ${r.statuses.padEnd(6)} ${note}`;
|
|
1603
|
+
if (r.statuses === "running") line = `\x1B[32m${line}\x1B[0m`;
|
|
1604
|
+
else if (r.statuses === "error") line = `\x1B[31m${line}\x1B[0m`;
|
|
1605
|
+
else if (r.statuses === "stopped") line = `\x1B[90m${line}\x1B[0m`;
|
|
1606
|
+
console.log(line);
|
|
1607
|
+
}
|
|
1608
|
+
console.log(`
|
|
1609
|
+
Tip: relife2 find start --root ${root} \u2192 start all stopped configs`);
|
|
1610
|
+
console.log(` relife2 find --json | jq . \u2192 scripting`);
|
|
1611
|
+
}
|
|
1612
|
+
if (!wantsStart) return 0;
|
|
1613
|
+
const toStart = rows.filter(
|
|
1614
|
+
(r) => r.error === void 0 && r.apps.length > 0 && r.statuses !== "running" && r.statuses !== "partial"
|
|
1615
|
+
);
|
|
1616
|
+
if (toStart.length === 0) {
|
|
1617
|
+
if (!jsonMode)
|
|
1618
|
+
console.log("\nNothing to start \u2014 all found configs are already running or broken.");
|
|
1619
|
+
return 0;
|
|
1620
|
+
}
|
|
1621
|
+
if (!jsonMode) console.log(`
|
|
1622
|
+
Starting ${toStart.length} config(s) not yet running\u2026
|
|
1623
|
+
`);
|
|
1624
|
+
const client = await ensureDaemonClient();
|
|
1625
|
+
try {
|
|
1626
|
+
let started = 0;
|
|
1627
|
+
let failed = 0;
|
|
1628
|
+
for (const r of toStart) {
|
|
1629
|
+
try {
|
|
1630
|
+
const res = await client.call("start", {
|
|
1631
|
+
target: { type: "config", path: r.path }
|
|
1632
|
+
});
|
|
1633
|
+
for (const w of res.warnings ?? []) console.error(`warning ${path8.basename(r.path)}: ${w}`);
|
|
1634
|
+
for (const a of res.apps) {
|
|
1635
|
+
if (a.status === "errored") {
|
|
1636
|
+
console.error(`x ${a.name} (${path8.basename(r.path)}): ${a.message ?? "failed"}`);
|
|
1637
|
+
failed++;
|
|
1638
|
+
} else {
|
|
1639
|
+
console.log(
|
|
1640
|
+
`ok ${a.name}: ${a.status}${a.pid !== void 0 ? ` (pid ${a.pid})` : ""} \u2190 ${relToRoot(r.path, root)}`
|
|
1641
|
+
);
|
|
1642
|
+
if (!a.message?.includes("already")) started++;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
console.error(`x ${path8.basename(r.path)}: ${err.message}`);
|
|
1647
|
+
failed++;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
if (!jsonMode)
|
|
1651
|
+
console.log(
|
|
1652
|
+
`
|
|
1653
|
+
Done: started ${started}, failed ${failed}, skipped ${rows.length - toStart.length} already running`
|
|
1654
|
+
);
|
|
1655
|
+
return failed > 0 ? 1 : 0;
|
|
1656
|
+
} finally {
|
|
1657
|
+
client.close();
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/commands/flush.ts
|
|
1662
|
+
async function run6(args) {
|
|
1663
|
+
const { positionals } = parseCliArgs(args);
|
|
1664
|
+
const name = positionals[0] ?? "all";
|
|
1665
|
+
const client = await ensureDaemonClient();
|
|
1666
|
+
try {
|
|
1667
|
+
const res = await client.call("flush", {
|
|
1668
|
+
name
|
|
1669
|
+
});
|
|
1670
|
+
for (const a of res.apps) {
|
|
1671
|
+
console.log(`ok flushed ${a.name}${a.files.length > 0 ? "" : " (no log files)"}`);
|
|
1672
|
+
}
|
|
1673
|
+
return 0;
|
|
1674
|
+
} finally {
|
|
1675
|
+
client.close();
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/commands/fromCaddy.ts
|
|
1680
|
+
import * as fs8 from "node:fs";
|
|
1681
|
+
import os5 from "node:os";
|
|
1682
|
+
import path9 from "node:path";
|
|
1683
|
+
var LOOPBACK_RE = /(?:127\.0\.0\.1|localhost|::1):(\d{2,5})/g;
|
|
1684
|
+
var ENTRY_PATTERNS = [
|
|
1685
|
+
".output/server/index.mjs",
|
|
1686
|
+
"dist/server/entry.mjs",
|
|
1687
|
+
"dist/index.mjs",
|
|
1688
|
+
".output/server/index.js",
|
|
1689
|
+
"dist/server/entry.js",
|
|
1690
|
+
"index.mjs",
|
|
1691
|
+
"index.js",
|
|
1692
|
+
"server.mjs",
|
|
1693
|
+
"server.js",
|
|
1694
|
+
"bot.js",
|
|
1695
|
+
"app.js",
|
|
1696
|
+
"main.js"
|
|
1697
|
+
];
|
|
1698
|
+
function parseHint(s) {
|
|
1699
|
+
const out = {};
|
|
1700
|
+
for (const part of s.trim().split(/\s+/)) {
|
|
1701
|
+
const eq = part.indexOf("=");
|
|
1702
|
+
if (eq > 0) {
|
|
1703
|
+
const k = part.slice(0, eq).trim();
|
|
1704
|
+
const v = part.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
1705
|
+
if (k) out[k] = v;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return out;
|
|
1709
|
+
}
|
|
1710
|
+
function sanitizeName(site, port) {
|
|
1711
|
+
let s = site.trim().split(",")[0].trim();
|
|
1712
|
+
s = s.replace(/^https?:\/\//, "");
|
|
1713
|
+
s = s.split(":")[0];
|
|
1714
|
+
s = s.split("/")[0];
|
|
1715
|
+
if (s.startsWith("*.")) s = s.slice(2);
|
|
1716
|
+
if (!s || s === "*" || s.startsWith(":")) return port ? `app-${port}` : "app";
|
|
1717
|
+
const label = s.split(".")[0];
|
|
1718
|
+
let name = label.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
|
|
1719
|
+
if (!name) name = "app";
|
|
1720
|
+
return name.toLowerCase();
|
|
1721
|
+
}
|
|
1722
|
+
function extractUpstreams(block) {
|
|
1723
|
+
const out = [];
|
|
1724
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1725
|
+
const lines = block.split("\n");
|
|
1726
|
+
for (const line of lines) {
|
|
1727
|
+
const t = line.trim();
|
|
1728
|
+
if (!t || t.startsWith("#")) continue;
|
|
1729
|
+
if (!/reverse_proxy|php_fastcgi/.test(t)) continue;
|
|
1730
|
+
let m;
|
|
1731
|
+
LOOPBACK_RE.lastIndex = 0;
|
|
1732
|
+
while ((m = LOOPBACK_RE.exec(t)) !== null) {
|
|
1733
|
+
const port = Number(m[1]);
|
|
1734
|
+
if (port < 1 || port > 65535) continue;
|
|
1735
|
+
const raw = m[0];
|
|
1736
|
+
if (seen.has(raw)) continue;
|
|
1737
|
+
seen.add(raw);
|
|
1738
|
+
const host = raw.split(":")[0];
|
|
1739
|
+
out.push({ host, port, raw });
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (out.length === 0) {
|
|
1743
|
+
const blockLoopbacks = [...block.matchAll(LOOPBACK_RE)];
|
|
1744
|
+
for (const b of blockLoopbacks) {
|
|
1745
|
+
const raw = b[0];
|
|
1746
|
+
if (seen.has(raw)) continue;
|
|
1747
|
+
const idx = b.index ?? 0;
|
|
1748
|
+
const before = block.slice(Math.max(0, idx - 200), idx);
|
|
1749
|
+
if (!/reverse_proxy|php_fastcgi/.test(before)) continue;
|
|
1750
|
+
const port = Number(b[1]);
|
|
1751
|
+
if (port < 1 || port > 65535) continue;
|
|
1752
|
+
seen.add(raw);
|
|
1753
|
+
out.push({ host: raw.split(":")[0], port, raw });
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return out;
|
|
1757
|
+
}
|
|
1758
|
+
function parseCaddyfile(content) {
|
|
1759
|
+
const warnings = [];
|
|
1760
|
+
const sites = [];
|
|
1761
|
+
const lines = content.split("\n");
|
|
1762
|
+
let depth = 0;
|
|
1763
|
+
let pendingHint = null;
|
|
1764
|
+
let currentSite = null;
|
|
1765
|
+
let siteDepth = -1;
|
|
1766
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1767
|
+
const rawLine = lines[i];
|
|
1768
|
+
const trimmed = rawLine.trim();
|
|
1769
|
+
if (trimmed.startsWith("#")) {
|
|
1770
|
+
const hm = trimmed.match(/#\s*relife2:\s*(.*)/i);
|
|
1771
|
+
if (hm) pendingHint = parseHint(hm[1]);
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if (trimmed === "") continue;
|
|
1775
|
+
if (currentSite === null && depth === 0) {
|
|
1776
|
+
const braceIdx = rawLine.indexOf("{");
|
|
1777
|
+
if (braceIdx !== -1) {
|
|
1778
|
+
const before = rawLine.slice(0, braceIdx).trim();
|
|
1779
|
+
const firstWord = before.split(/\s+/)[0].toLowerCase();
|
|
1780
|
+
const isDirective = [
|
|
1781
|
+
"handle",
|
|
1782
|
+
"handle_path",
|
|
1783
|
+
"route",
|
|
1784
|
+
"reverse_proxy",
|
|
1785
|
+
"php_fastcgi",
|
|
1786
|
+
"file_server",
|
|
1787
|
+
"encode",
|
|
1788
|
+
"header",
|
|
1789
|
+
"redir",
|
|
1790
|
+
"respond",
|
|
1791
|
+
"tls",
|
|
1792
|
+
"log",
|
|
1793
|
+
"import"
|
|
1794
|
+
].includes(firstWord);
|
|
1795
|
+
if (!isDirective && before !== "" && before !== "*") {
|
|
1796
|
+
const site = {
|
|
1797
|
+
label: before.split(",")[0].trim(),
|
|
1798
|
+
rawLabel: before,
|
|
1799
|
+
hint: pendingHint,
|
|
1800
|
+
block: "",
|
|
1801
|
+
upstreams: []
|
|
1802
|
+
};
|
|
1803
|
+
pendingHint = null;
|
|
1804
|
+
currentSite = site;
|
|
1805
|
+
siteDepth = depth;
|
|
1806
|
+
sites.push(site);
|
|
1807
|
+
const open2 = (rawLine.match(/\{/g) || []).length;
|
|
1808
|
+
const close2 = (rawLine.match(/\}/g) || []).length;
|
|
1809
|
+
depth += open2 - close2;
|
|
1810
|
+
const after = rawLine.slice(braceIdx + 1);
|
|
1811
|
+
if (after.trim() && !after.trim().startsWith("}")) site.block += `${after}
|
|
1812
|
+
`;
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
} else {
|
|
1816
|
+
let j = i + 1;
|
|
1817
|
+
while (j < lines.length && lines[j].trim() === "") j++;
|
|
1818
|
+
if (j < lines.length && lines[j].trim().startsWith("{")) {
|
|
1819
|
+
const before = trimmed;
|
|
1820
|
+
const firstWord = before.split(/\s+/)[0].toLowerCase();
|
|
1821
|
+
const isDirective = ["handle", "route"].includes(firstWord);
|
|
1822
|
+
if (!isDirective && before !== "") {
|
|
1823
|
+
const site = {
|
|
1824
|
+
label: before.split(",")[0].trim(),
|
|
1825
|
+
rawLabel: before,
|
|
1826
|
+
hint: pendingHint,
|
|
1827
|
+
block: "",
|
|
1828
|
+
upstreams: []
|
|
1829
|
+
};
|
|
1830
|
+
pendingHint = null;
|
|
1831
|
+
currentSite = site;
|
|
1832
|
+
siteDepth = depth;
|
|
1833
|
+
sites.push(site);
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (currentSite !== null) currentSite.block += `${rawLine}
|
|
1840
|
+
`;
|
|
1841
|
+
const open = (rawLine.match(/\{/g) || []).length;
|
|
1842
|
+
const close = (rawLine.match(/\}/g) || []).length;
|
|
1843
|
+
depth += open - close;
|
|
1844
|
+
if (depth < 0) depth = 0;
|
|
1845
|
+
if (currentSite !== null && depth <= siteDepth) {
|
|
1846
|
+
currentSite = null;
|
|
1847
|
+
siteDepth = -1;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
for (const s of sites) s.upstreams = extractUpstreams(s.block);
|
|
1851
|
+
return { sites, warnings };
|
|
1852
|
+
}
|
|
1853
|
+
function findEntry(dir) {
|
|
1854
|
+
for (const pat of ENTRY_PATTERNS) {
|
|
1855
|
+
const p = path9.join(dir, pat);
|
|
1856
|
+
if (fs8.existsSync(p)) return { script: pat, exists: true };
|
|
1857
|
+
}
|
|
1858
|
+
return null;
|
|
1859
|
+
}
|
|
1860
|
+
function guessDir(site, _port, hint, caddyfileDir) {
|
|
1861
|
+
const warnings = [];
|
|
1862
|
+
if (hint?.dir) {
|
|
1863
|
+
const d = path9.isAbsolute(hint.dir) ? hint.dir : path9.resolve(caddyfileDir, hint.dir);
|
|
1864
|
+
return { dir: d, warnings };
|
|
1865
|
+
}
|
|
1866
|
+
const searchRoots = [];
|
|
1867
|
+
const homedir = os5.homedir();
|
|
1868
|
+
if (homedir) searchRoots.push(homedir);
|
|
1869
|
+
if (caddyfileDir) searchRoots.push(caddyfileDir);
|
|
1870
|
+
if (caddyfileDir) searchRoots.push(path9.resolve(caddyfileDir, ".."));
|
|
1871
|
+
const uniqRoots = [...new Set(searchRoots)].filter(Boolean);
|
|
1872
|
+
const siteName = sanitizeName(site);
|
|
1873
|
+
const candidates = [];
|
|
1874
|
+
if (homedir) candidates.push(path9.join(homedir, siteName));
|
|
1875
|
+
if (homedir) candidates.push(path9.join(homedir, siteName.split("-")[0]));
|
|
1876
|
+
for (const r of uniqRoots) candidates.push(path9.join(r, siteName));
|
|
1877
|
+
const rawDir = site.split(".")[0].replace(/[^a-zA-Z0-9_-]/g, "");
|
|
1878
|
+
if (rawDir && homedir) candidates.push(path9.join(homedir, rawDir));
|
|
1879
|
+
for (const cand of candidates) {
|
|
1880
|
+
if (fs8.existsSync(cand) && fs8.statSync(cand).isDirectory()) {
|
|
1881
|
+
const e = findEntry(cand);
|
|
1882
|
+
if (e) return { dir: cand, warnings };
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const fallback = homedir ? path9.join(homedir, siteName) : path9.join(caddyfileDir || process.cwd(), siteName);
|
|
1886
|
+
warnings.push(
|
|
1887
|
+
`no existing dir found for ${site} \u2192 using fallback ${fallback} (create or set via '# relife2: dir=...')`
|
|
1888
|
+
);
|
|
1889
|
+
return { dir: fallback, warnings };
|
|
1890
|
+
}
|
|
1891
|
+
async function run7(args) {
|
|
1892
|
+
let caddyfilePath = null;
|
|
1893
|
+
let outPath = null;
|
|
1894
|
+
let dryRun = false;
|
|
1895
|
+
let withCaddy = false;
|
|
1896
|
+
let jsonOut = false;
|
|
1897
|
+
for (let i = 0; i < args.length; i++) {
|
|
1898
|
+
const a = args[i];
|
|
1899
|
+
if (a === "--dry-run" || a === "-n") dryRun = true;
|
|
1900
|
+
else if (a === "--with-caddy") withCaddy = true;
|
|
1901
|
+
else if (a === "--json") jsonOut = true;
|
|
1902
|
+
else if (a === "-o" || a === "--output") {
|
|
1903
|
+
outPath = args[i + 1] ?? null;
|
|
1904
|
+
i++;
|
|
1905
|
+
} else if (a.startsWith("-o=")) outPath = a.slice(3);
|
|
1906
|
+
else if (!a.startsWith("-") && caddyfilePath === null) caddyfilePath = a;
|
|
1907
|
+
else if (a === "--help" || a === "-h") {
|
|
1908
|
+
printUsage2();
|
|
1909
|
+
return 0;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
const candidates = [
|
|
1913
|
+
caddyfilePath,
|
|
1914
|
+
caddyfilePath ? null : "./Caddyfile",
|
|
1915
|
+
caddyfilePath ? null : "Caddyfile",
|
|
1916
|
+
caddyfilePath ? null : "/etc/caddy/Caddyfile",
|
|
1917
|
+
caddyfilePath ? null : path9.join(os5.homedir(), "Caddyfile")
|
|
1918
|
+
].filter(Boolean);
|
|
1919
|
+
let resolvedCaddy = null;
|
|
1920
|
+
let content = null;
|
|
1921
|
+
for (const cand of candidates) {
|
|
1922
|
+
const p = path9.resolve(cand);
|
|
1923
|
+
if (fs8.existsSync(p)) {
|
|
1924
|
+
resolvedCaddy = p;
|
|
1925
|
+
content = fs8.readFileSync(p, "utf8");
|
|
1926
|
+
break;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
if (!resolvedCaddy || content === null) {
|
|
1930
|
+
console.error(`from-caddy: Caddyfile not found. Tried: ${candidates.join(", ")}`);
|
|
1931
|
+
console.error(
|
|
1932
|
+
` Usage: relife2 from-caddy [/path/to/Caddyfile] [-o relife2.config.cjs] [--dry-run]`
|
|
1933
|
+
);
|
|
1934
|
+
return 1;
|
|
1935
|
+
}
|
|
1936
|
+
const caddyfileDir = path9.dirname(resolvedCaddy);
|
|
1937
|
+
const { sites } = parseCaddyfile(content);
|
|
1938
|
+
if (sites.length === 0) {
|
|
1939
|
+
console.error(`from-caddy: no sites found in ${resolvedCaddy}`);
|
|
1940
|
+
console.error(` Is it a valid Caddyfile? Sites should look like 'example.com { ... }'`);
|
|
1941
|
+
return 1;
|
|
1942
|
+
}
|
|
1943
|
+
const planned = [];
|
|
1944
|
+
const nameCounts = /* @__PURE__ */ new Map();
|
|
1945
|
+
const portCounts = /* @__PURE__ */ new Map();
|
|
1946
|
+
const warnings = [];
|
|
1947
|
+
for (const site of sites) {
|
|
1948
|
+
if (site.upstreams.length === 0) {
|
|
1949
|
+
warnings.push(
|
|
1950
|
+
`site '${site.rawLabel}' has no reverse_proxy/php_fastcgi to loopback \u2014 static only, skipping`
|
|
1951
|
+
);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
for (const up of site.upstreams) {
|
|
1955
|
+
const isLoopback = up.host === "127.0.0.1" || up.host === "localhost" || up.host === "::1";
|
|
1956
|
+
if (!isLoopback) {
|
|
1957
|
+
warnings.push(`upstream ${up.raw} for site '${site.rawLabel}' is not loopback \u2014 skipping`);
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
const baseName = site.hint?.name ?? sanitizeName(site.label, up.port);
|
|
1961
|
+
const count = nameCounts.get(baseName) ?? 0;
|
|
1962
|
+
nameCounts.set(baseName, count + 1);
|
|
1963
|
+
let name = baseName;
|
|
1964
|
+
if (count > 0) {
|
|
1965
|
+
const suffix = String(up.port);
|
|
1966
|
+
name = `${baseName}-${suffix}`;
|
|
1967
|
+
const c2 = nameCounts.get(name) ?? 0;
|
|
1968
|
+
if (c2 > 0) name = `${baseName}-${suffix}-${c2}`;
|
|
1969
|
+
nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
|
|
1970
|
+
}
|
|
1971
|
+
const pc = portCounts.get(up.port) ?? 0;
|
|
1972
|
+
portCounts.set(up.port, pc + 1);
|
|
1973
|
+
if (pc > 0)
|
|
1974
|
+
warnings.push(
|
|
1975
|
+
`duplicate upstream port ${up.port} for site '${site.rawLabel}' (also used elsewhere) \u2014 check for collision`
|
|
1976
|
+
);
|
|
1977
|
+
const { dir, warnings: dirWarns } = guessDir(site.label, up.port, site.hint, caddyfileDir);
|
|
1978
|
+
warnings.push(...dirWarns);
|
|
1979
|
+
const entry = site.hint?.script ?? site.hint?.entry ?? null;
|
|
1980
|
+
let exists = false;
|
|
1981
|
+
let relScript = "";
|
|
1982
|
+
if (entry) {
|
|
1983
|
+
relScript = entry;
|
|
1984
|
+
exists = fs8.existsSync(path9.isAbsolute(entry) ? entry : path9.join(dir, entry));
|
|
1985
|
+
if (!exists) warnings.push(`hint script '${entry}' for ${name} not found under ${dir}`);
|
|
1986
|
+
} else {
|
|
1987
|
+
const found = findEntry(dir);
|
|
1988
|
+
if (found) {
|
|
1989
|
+
relScript = found.script;
|
|
1990
|
+
exists = true;
|
|
1991
|
+
} else {
|
|
1992
|
+
relScript = ".output/server/index.mjs";
|
|
1993
|
+
exists = false;
|
|
1994
|
+
warnings.push(
|
|
1995
|
+
`no entry found under ${dir} for ${name} \u2014 using placeholder '${relScript}'`
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
let interpreter = site.hint?.interpreter;
|
|
2000
|
+
if (!interpreter) {
|
|
2001
|
+
if (relScript.endsWith(".mjs") || relScript.endsWith(".ts")) {
|
|
2002
|
+
const hasBun2 = fs8.existsSync(path9.join(dir, "bun.lockb")) || fs8.existsSync(path9.join(dir, "bun.lock"));
|
|
2003
|
+
if (hasBun2) interpreter = "bun";
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
const item = {
|
|
2007
|
+
name,
|
|
2008
|
+
site: site.rawLabel,
|
|
2009
|
+
upstream: up,
|
|
2010
|
+
dir,
|
|
2011
|
+
script: relScript,
|
|
2012
|
+
exists,
|
|
2013
|
+
hint: site.hint,
|
|
2014
|
+
warnings: []
|
|
2015
|
+
};
|
|
2016
|
+
item._interpreter = interpreter;
|
|
2017
|
+
planned.push(item);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
if (withCaddy) {
|
|
2021
|
+
const caddyName = "caddy";
|
|
2022
|
+
if (!nameCounts.has(caddyName)) {
|
|
2023
|
+
const item = {
|
|
2024
|
+
name: caddyName,
|
|
2025
|
+
site: "system",
|
|
2026
|
+
upstream: { host: "127.0.0.1", port: 0, raw: "caddy" },
|
|
2027
|
+
dir: caddyfileDir,
|
|
2028
|
+
script: "caddy",
|
|
2029
|
+
exists: true,
|
|
2030
|
+
hint: null,
|
|
2031
|
+
warnings: []
|
|
2032
|
+
};
|
|
2033
|
+
item._interpreter = "none";
|
|
2034
|
+
planned.push(item);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (planned.length === 0) {
|
|
2038
|
+
console.error(`from-caddy: no loopback upstreams found in ${resolvedCaddy}`);
|
|
2039
|
+
if (warnings.length) {
|
|
2040
|
+
console.error(" warnings:");
|
|
2041
|
+
for (const w of warnings) console.error(` - ${w}`);
|
|
2042
|
+
}
|
|
2043
|
+
console.error(
|
|
2044
|
+
" Tip: add '# relife2: dir=/path/to/app name=myapp' above a site block to hint."
|
|
2045
|
+
);
|
|
2046
|
+
return 1;
|
|
2047
|
+
}
|
|
2048
|
+
if (jsonOut) {
|
|
2049
|
+
console.log(JSON.stringify({ caddyfile: resolvedCaddy, sites, planned, warnings }, null, 2));
|
|
2050
|
+
return 0;
|
|
2051
|
+
}
|
|
2052
|
+
if (dryRun) {
|
|
2053
|
+
console.log(`from-caddy \u2014 dry-run for ${resolvedCaddy}
|
|
2054
|
+
`);
|
|
2055
|
+
console.log(`Sites: ${sites.length}, upstreams: ${planned.length}
|
|
2056
|
+
`);
|
|
2057
|
+
console.log(
|
|
2058
|
+
`site upstream \u2192 app dir entry`
|
|
2059
|
+
);
|
|
2060
|
+
console.log(
|
|
2061
|
+
`------------------------------------------------------------------------------------------------`
|
|
2062
|
+
);
|
|
2063
|
+
for (const p of planned) {
|
|
2064
|
+
const siteCol = p.site.padEnd(30).slice(0, 30);
|
|
2065
|
+
const upCol = p.upstream.raw.padEnd(18).slice(0, 18);
|
|
2066
|
+
const appCol = p.name.padEnd(16).slice(0, 16);
|
|
2067
|
+
const dirCol = p.dir.padEnd(28).slice(0, 28);
|
|
2068
|
+
const entryCol = p.script + (p.exists ? "" : " (missing)");
|
|
2069
|
+
console.log(`${siteCol} ${upCol} \u2192 ${appCol} ${dirCol} ${entryCol}`);
|
|
2070
|
+
}
|
|
2071
|
+
if (warnings.length) {
|
|
2072
|
+
console.log(`
|
|
2073
|
+
Warnings:`);
|
|
2074
|
+
for (const w of warnings) console.log(` - ${w}`);
|
|
2075
|
+
}
|
|
2076
|
+
console.log(
|
|
2077
|
+
`
|
|
2078
|
+
Would write ${planned.length} app(s) to ${outPath ?? "relife2.config.cjs (dry-run, no file written)"}`
|
|
2079
|
+
);
|
|
2080
|
+
console.log(`Run without --dry-run to generate.`);
|
|
2081
|
+
return 0;
|
|
2082
|
+
}
|
|
2083
|
+
const resolvedOut = path9.resolve(outPath ?? "relife2.config.cjs");
|
|
2084
|
+
const appsCode = planned.map((p) => {
|
|
2085
|
+
const interp = p._interpreter;
|
|
2086
|
+
const isCaddy = p.name === "caddy";
|
|
2087
|
+
const env = isCaddy ? {} : { NODE_ENV: "production", HOST: "127.0.0.1", PORT: String(p.upstream.port) };
|
|
2088
|
+
const siteClean = (p.site.split(",")[0] ?? "").trim().replace(/^https?:\/\//, "").split(" ")[0] ?? "";
|
|
2089
|
+
if (!isCaddy && siteClean && siteClean !== "*") {
|
|
2090
|
+
if (siteClean.includes(".")) env.SITE_URL = `https://${siteClean}`;
|
|
2091
|
+
}
|
|
2092
|
+
const lines = [];
|
|
2093
|
+
lines.push(` {`);
|
|
2094
|
+
lines.push(` name: '${p.name.replace(/'/g, "\\'")}',`);
|
|
2095
|
+
if (isCaddy) {
|
|
2096
|
+
lines.push(` script: 'caddy',`);
|
|
2097
|
+
lines.push(
|
|
2098
|
+
` args: ['run', '--config', '${resolvedCaddy.replace(/'/g, "\\'")}', '--adapter', 'caddyfile'],`
|
|
2099
|
+
);
|
|
2100
|
+
lines.push(` cwd: '${p.dir.replace(/'/g, "\\'")}',`);
|
|
2101
|
+
lines.push(` interpreter: 'none',`);
|
|
2102
|
+
} else {
|
|
2103
|
+
const scriptEsc = p.script.replace(/'/g, "\\'");
|
|
2104
|
+
const dirEsc = p.dir.replace(/'/g, "\\'");
|
|
2105
|
+
lines.push(` script: ${JSON.stringify(p.script)},`);
|
|
2106
|
+
lines.push(` cwd: ${JSON.stringify(p.dir)},`);
|
|
2107
|
+
if (interp) lines.push(` interpreter: ${JSON.stringify(interp)},`);
|
|
2108
|
+
lines.push(` instances: 1,`);
|
|
2109
|
+
lines.push(` exec_mode: 'fork',`);
|
|
2110
|
+
lines.push(` autorestart: true,`);
|
|
2111
|
+
lines.push(` max_memory_restart: '512M',`);
|
|
2112
|
+
lines.push(` env: {`);
|
|
2113
|
+
for (const [k, v] of Object.entries(env))
|
|
2114
|
+
lines.push(` ${k}: '${String(v).replace(/'/g, "\\'")}',`);
|
|
2115
|
+
lines.push(` },`);
|
|
2116
|
+
lines.push(` out_file: ${JSON.stringify(`./logs/${p.name}-out.log`)},`);
|
|
2117
|
+
lines.push(` error_file: ${JSON.stringify(`./logs/${p.name}-err.log`)},`);
|
|
2118
|
+
lines.push(` merge_logs: true,`);
|
|
2119
|
+
lines.push(` time: true,`);
|
|
2120
|
+
}
|
|
2121
|
+
if (!p.exists && !isCaddy) {
|
|
2122
|
+
lines.push(` // TODO: entry '${p.script}' not found under ${p.dir}`);
|
|
2123
|
+
lines.push(` // checked: ${ENTRY_PATTERNS.join(", ")}`);
|
|
2124
|
+
lines.push(` // fix: set correct 'script' or create placeholder, or add hint:`);
|
|
2125
|
+
lines.push(` // # relife2: dir=${p.dir} script=./your/entry.js`);
|
|
2126
|
+
}
|
|
2127
|
+
lines.push(` },`);
|
|
2128
|
+
return lines.join("\n");
|
|
2129
|
+
}).join("\n");
|
|
2130
|
+
const fileContent = `// Generated by relife2 from-caddy \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2131
|
+
// Source: ${resolvedCaddy}
|
|
2132
|
+
// Caddy handles TLS/static/proxy, relife2 supervises apps.
|
|
2133
|
+
// Review and adjust cwd/script/PORT before relife2 start.
|
|
2134
|
+
module.exports = {
|
|
2135
|
+
apps: [
|
|
2136
|
+
${appsCode}
|
|
2137
|
+
]
|
|
2138
|
+
};
|
|
2139
|
+
`;
|
|
2140
|
+
fs8.writeFileSync(resolvedOut, fileContent, "utf8");
|
|
2141
|
+
console.log(
|
|
2142
|
+
`from-caddy: parsed ${sites.length} site(s), ${planned.length} upstream(s) from ${resolvedCaddy}`
|
|
2143
|
+
);
|
|
2144
|
+
for (const p of planned) {
|
|
2145
|
+
const status = p.exists ? "found" : "missing";
|
|
2146
|
+
console.log(
|
|
2147
|
+
` - ${p.site} \u2192 ${p.upstream.raw} \u2192 app '${p.name}' @ ${p.dir} / ${p.script} [${status}]`
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
if (warnings.length) {
|
|
2151
|
+
console.log(`
|
|
2152
|
+
Warnings:`);
|
|
2153
|
+
for (const w of warnings) console.log(` - ${w}`);
|
|
2154
|
+
}
|
|
2155
|
+
console.log(`
|
|
2156
|
+
Wrote ${resolvedOut} (${planned.length} app(s))`);
|
|
2157
|
+
console.log(
|
|
2158
|
+
`Next: relife2 inspect-config ${path9.basename(resolvedOut)} \u2192 relife2 start ${path9.basename(resolvedOut)}`
|
|
2159
|
+
);
|
|
2160
|
+
return 0;
|
|
2161
|
+
}
|
|
2162
|
+
function printUsage2() {
|
|
2163
|
+
console.log(`Usage: relife2 from-caddy [Caddyfile] [-o relife2.config.cjs] [--dry-run] [--json] [--with-caddy]
|
|
2164
|
+
|
|
2165
|
+
Parses a Caddyfile, finds reverse_proxy upstreams to 127.0.0.1/localhost,
|
|
2166
|
+
and generates a relife2 config with one app per upstream.
|
|
2167
|
+
|
|
2168
|
+
Options:
|
|
2169
|
+
Caddyfile Path to Caddyfile (default: ./Caddyfile, /etc/caddy/Caddyfile, ~/Caddyfile)
|
|
2170
|
+
-o, --output <file> Output config path (default: ./relife2.config.cjs)
|
|
2171
|
+
-n, --dry-run Show plan without writing file
|
|
2172
|
+
--json Output parsed sites/upstreams as JSON (for scripting)
|
|
2173
|
+
--with-caddy Also add Caddy itself as a managed app (interpreter: none)
|
|
2174
|
+
|
|
2175
|
+
Hints:
|
|
2176
|
+
Add a comment above a site block to override detection:
|
|
2177
|
+
# relife2: dir=/home/debian/uwc name=uwc script=.output/server/index.mjs interpreter=bun
|
|
2178
|
+
|
|
2179
|
+
Example:
|
|
2180
|
+
relife2 from-caddy /etc/caddy/Caddyfile --dry-run
|
|
2181
|
+
relife2 from-caddy -o relife2.config.cjs
|
|
2182
|
+
relife2 from-caddy --with-caddy -o relife2.config.cjs && relife2 start relife2.config.cjs
|
|
2183
|
+
`);
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// src/version.ts
|
|
2187
|
+
var VERSION = "1.0.0";
|
|
2188
|
+
|
|
2189
|
+
// src/commands/help.ts
|
|
2190
|
+
var HELP_TEXT = `relife2 ${VERSION} \u2014 process manager for Node.js & Bun apps (PM2 alternative, done right)
|
|
2191
|
+
|
|
2192
|
+
Usage: relife2 <command> [options]
|
|
2193
|
+
|
|
2194
|
+
Commands:
|
|
2195
|
+
start <config|script> [--name <n>] [--env <env>] [--instances <n|max>] [--exec-mode <fork|cluster>] [-- --app-args...]
|
|
2196
|
+
Start all apps from a config, or one script (M1/M6).
|
|
2197
|
+
No args \u2192 auto-discovers relife2.config.{ts,js,cjs,mjs,json} or ecosystem.config.* in cwd.
|
|
2198
|
+
--env <name> Select env profile (merges env_<name> over env; e.g. env_production)
|
|
2199
|
+
--instances <n|max> Override instances count per app (max = cpu count)
|
|
2200
|
+
--exec-mode <mode> Override exec_mode (fork|cluster)
|
|
2201
|
+
stop <name|all> Gracefully stop app(s) (SIGTERM \u2192 kill_timeout \u2192 SIGKILL)
|
|
2202
|
+
restart <name|all> Stop and respawn app(s)
|
|
2203
|
+
reload <name|all> Zero-downtime reload (M6): graceful restart, sequentially per instance
|
|
2204
|
+
delete <name|all> Stop and forget app(s)
|
|
2205
|
+
list | ls | status Table of managed apps
|
|
2206
|
+
describe <name> Full state + resolved config of one app
|
|
2207
|
+
ping Show daemon status and protocol version
|
|
2208
|
+
logs <name> [--lines N] [-f]
|
|
2209
|
+
Print app log lines (tail; -f follows)
|
|
2210
|
+
flush [name|all] Empty the app log file(s)
|
|
2211
|
+
rotate [--max-size <bytes>] [--retain <n>] [name|all]
|
|
2212
|
+
Rotate log files exceeding the size threshold (M3)
|
|
2213
|
+
doctor Run integrity checks on the running daemon (M3)
|
|
2214
|
+
metrics Show daemon self-metrics + per-app summary (M3)
|
|
2215
|
+
monit [--once] Live dashboard (M6): cpu/mem/uptime per app, q to quit
|
|
2216
|
+
find [start] [--root <dir>] [--depth <n>] [--json] [--all]
|
|
2217
|
+
Find all configs under a root (default $HOME, depth 6) \u2014 solves PM2 pain: no need to remember dirs
|
|
2218
|
+
find \u2192 list with status (running/stopped/error)
|
|
2219
|
+
find start \u2192 start every stopped config (or --start flag)
|
|
2220
|
+
save Save current state to disk (snapshot)
|
|
2221
|
+
resurrect Start all apps from the saved state (M5)
|
|
2222
|
+
import-pm2 [dump.pm2] Import PM2 dump file (default ~/.pm2/dump.pm2) (M6)
|
|
2223
|
+
from-caddy [Caddyfile] [-o relife2.config.cjs] [--dry-run]
|
|
2224
|
+
Generate relife2 config from Caddyfile reverse_proxy upstreams (M6)
|
|
2225
|
+
dev <script> [--watch <paths>] [--hot|--no-hot] [--interpreter <bin>] [-- --args...]
|
|
2226
|
+
Watch+restart for local dev \u2014 bun --hot for .ts, manual fs.watch otherwise
|
|
2227
|
+
daemon-upgrade Upgrade daemon in place: save \u2192 shutdown old \u2192 spawn new \u2192 resurrect (no second daemon, ADR-0005)
|
|
2228
|
+
startup Install systemd user unit for autostart on boot (M5, Linux)
|
|
2229
|
+
kill Stop the daemon (apps are stopped gracefully)
|
|
2230
|
+
version Print version and runtime info
|
|
2231
|
+
help Show this help
|
|
2232
|
+
|
|
2233
|
+
Global flags:
|
|
2234
|
+
-h, --help Show help
|
|
2235
|
+
-v, --version Print version
|
|
2236
|
+
|
|
2237
|
+
Design: exactly one locked daemon; commands that silently respawn the daemon
|
|
2238
|
+
(pm2 update style) do not exist by design. See TODO.md \xA70 (anti-pitfalls).
|
|
2239
|
+
`;
|
|
2240
|
+
function printHelp() {
|
|
2241
|
+
console.log(HELP_TEXT);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// src/commands/importpm2.ts
|
|
2245
|
+
import * as fs9 from "node:fs";
|
|
2246
|
+
import os6 from "node:os";
|
|
2247
|
+
import path10 from "node:path";
|
|
2248
|
+
async function run8(args) {
|
|
2249
|
+
const target = args[0];
|
|
2250
|
+
const dumpPath = target !== void 0 ? path10.resolve(target) : path10.join(os6.homedir(), ".pm2", "dump.pm2");
|
|
2251
|
+
if (!fs9.existsSync(dumpPath)) {
|
|
2252
|
+
console.error(`import-pm2: dump file not found: ${dumpPath}`);
|
|
2253
|
+
console.error(
|
|
2254
|
+
" pm2 saves to ~/.pm2/dump.pm2 via `pm2 save` \u2014 run that first, or pass a path:"
|
|
2255
|
+
);
|
|
2256
|
+
console.error(" relife2 import-pm2 /path/to/dump.pm2");
|
|
2257
|
+
return 1;
|
|
2258
|
+
}
|
|
2259
|
+
let raw;
|
|
2260
|
+
try {
|
|
2261
|
+
raw = JSON.parse(fs9.readFileSync(dumpPath, "utf8"));
|
|
2262
|
+
} catch (err) {
|
|
2263
|
+
console.error(`import-pm2: cannot parse ${dumpPath}: ${err.message}`);
|
|
2264
|
+
return 1;
|
|
2265
|
+
}
|
|
2266
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
2267
|
+
if (list.length === 0) {
|
|
2268
|
+
console.error(`import-pm2: dump file is empty: ${dumpPath}`);
|
|
2269
|
+
return 1;
|
|
2270
|
+
}
|
|
2271
|
+
const client = await ensureDaemonClient();
|
|
2272
|
+
try {
|
|
2273
|
+
let imported = 0;
|
|
2274
|
+
for (const entry of list) {
|
|
2275
|
+
const pm2env = entry.pm2_env ?? {};
|
|
2276
|
+
const name = pm2env.name ?? entry.name ?? "app";
|
|
2277
|
+
const script = pm2env.pm_exec_path ?? entry.pm_exec_path;
|
|
2278
|
+
if (typeof script !== "string" || script === "") continue;
|
|
2279
|
+
const cwd = pm2env.pm_cwd ?? entry.pm_cwd ?? process.cwd();
|
|
2280
|
+
const argsRaw = pm2env.args ?? entry.pm_args;
|
|
2281
|
+
const args2 = Array.isArray(argsRaw) ? argsRaw : typeof argsRaw === "string" && argsRaw !== "" ? argsRaw.split(" ") : [];
|
|
2282
|
+
const env = pm2env.env ?? entry.pm_env ?? {};
|
|
2283
|
+
const outFile = pm2env.pm_out_log_path ?? entry.pm_out_log_path;
|
|
2284
|
+
const errFile = pm2env.pm_err_log_path ?? entry.pm_err_log_path;
|
|
2285
|
+
const interpreterRaw = pm2env.exec_interpreter ?? pm2env.interpreter;
|
|
2286
|
+
const interpreter = typeof interpreterRaw === "string" && interpreterRaw !== "" && interpreterRaw !== "none" ? interpreterRaw : void 0;
|
|
2287
|
+
const tmpConfig = {
|
|
2288
|
+
apps: [
|
|
2289
|
+
{
|
|
2290
|
+
name,
|
|
2291
|
+
script,
|
|
2292
|
+
args: args2,
|
|
2293
|
+
cwd,
|
|
2294
|
+
env,
|
|
2295
|
+
out_file: outFile,
|
|
2296
|
+
error_file: errFile,
|
|
2297
|
+
...interpreter !== void 0 ? { interpreter } : {},
|
|
2298
|
+
autorestart: pm2env.autorestart !== false,
|
|
2299
|
+
instances: pm2env.instances ?? 1,
|
|
2300
|
+
exec_mode: pm2env.exec_mode ?? "fork"
|
|
2301
|
+
}
|
|
2302
|
+
]
|
|
2303
|
+
};
|
|
2304
|
+
const tmpPath = path10.join(
|
|
2305
|
+
os6.tmpdir(),
|
|
2306
|
+
`relife2-import-${Date.now()}-${Math.random().toString(36).slice(2)}.cjs`
|
|
2307
|
+
);
|
|
2308
|
+
fs9.writeFileSync(tmpPath, `module.exports = ${JSON.stringify(tmpConfig)}`, "utf8");
|
|
2309
|
+
try {
|
|
2310
|
+
const res = await client.call("start", {
|
|
2311
|
+
target: { type: "config", path: tmpPath }
|
|
2312
|
+
});
|
|
2313
|
+
for (const a of res.apps) {
|
|
2314
|
+
if (a.status === "errored") console.error(`x ${a.name}: ${a.message ?? "failed"}`);
|
|
2315
|
+
else {
|
|
2316
|
+
console.log(`ok import ${a.name}: ${a.status}`);
|
|
2317
|
+
imported++;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
} catch (err) {
|
|
2321
|
+
console.error(`import ${name}: ${err.message}`);
|
|
2322
|
+
} finally {
|
|
2323
|
+
try {
|
|
2324
|
+
fs9.unlinkSync(tmpPath);
|
|
2325
|
+
} catch {
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
console.log(`
|
|
2330
|
+
imported ${imported} app(s) from ${dumpPath}`);
|
|
2331
|
+
console.log(
|
|
2332
|
+
"Note: review `relife2 list` and `relife2 describe <name>` \u2014 PM2 fields like cron/watch are imported if present."
|
|
2333
|
+
);
|
|
2334
|
+
return 0;
|
|
2335
|
+
} finally {
|
|
2336
|
+
client.close();
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// src/commands/inspectconfig.ts
|
|
2341
|
+
import path11 from "node:path";
|
|
2342
|
+
async function run9(args) {
|
|
2343
|
+
const { positionals } = parseCliArgs(args);
|
|
2344
|
+
const file = positionals[0];
|
|
2345
|
+
if (file === void 0) {
|
|
2346
|
+
console.error("usage: relife2 inspect-config <config-file>");
|
|
2347
|
+
return 1;
|
|
2348
|
+
}
|
|
2349
|
+
let loaded;
|
|
2350
|
+
try {
|
|
2351
|
+
loaded = await loadConfigFile(path11.resolve(file));
|
|
2352
|
+
} catch (err) {
|
|
2353
|
+
console.error(`relife2: ${err.message}`);
|
|
2354
|
+
return 1;
|
|
2355
|
+
}
|
|
2356
|
+
console.log(JSON.stringify({ apps: loaded.apps, warnings: loaded.warnings }, null, 2));
|
|
2357
|
+
return 0;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// src/commands/kill.ts
|
|
2361
|
+
async function run10(_args) {
|
|
2362
|
+
const sock = await tryConnect(runtimeDir());
|
|
2363
|
+
if (sock === null) {
|
|
2364
|
+
console.log("daemon is not running");
|
|
2365
|
+
return 0;
|
|
2366
|
+
}
|
|
2367
|
+
const client = new RpcClient(sock);
|
|
2368
|
+
try {
|
|
2369
|
+
await client.call("shutdown");
|
|
2370
|
+
console.log("daemon stopped");
|
|
2371
|
+
return 0;
|
|
2372
|
+
} finally {
|
|
2373
|
+
client.close();
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// src/commands/list.ts
|
|
2378
|
+
async function run11(_args) {
|
|
2379
|
+
const client = await ensureDaemonClient();
|
|
2380
|
+
try {
|
|
2381
|
+
const res = await client.call("list");
|
|
2382
|
+
if (res.apps.length === 0) {
|
|
2383
|
+
console.log("no apps (use `relife2 start <config|script>`)");
|
|
2384
|
+
return 0;
|
|
2385
|
+
}
|
|
2386
|
+
const rows = res.apps.map((a) => ({
|
|
2387
|
+
name: a.name,
|
|
2388
|
+
status: a.status,
|
|
2389
|
+
pid: a.pid !== void 0 ? String(a.pid) : "-",
|
|
2390
|
+
uptime: a.startTime !== void 0 ? formatUptime(Date.now() - a.startTime) : "-",
|
|
2391
|
+
restarts: String(a.restarts),
|
|
2392
|
+
detail: a.error ?? ""
|
|
2393
|
+
}));
|
|
2394
|
+
printTable(rows);
|
|
2395
|
+
return 0;
|
|
2396
|
+
} finally {
|
|
2397
|
+
client.close();
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
function formatUptime(ms) {
|
|
2401
|
+
const s = Math.floor(ms / 1e3);
|
|
2402
|
+
if (s < 60) return `${s}s`;
|
|
2403
|
+
const m = Math.floor(s / 60);
|
|
2404
|
+
if (m < 60) return `${m}m${s % 60}s`;
|
|
2405
|
+
const h = Math.floor(m / 60);
|
|
2406
|
+
return `${h}h${m % 60}m`;
|
|
2407
|
+
}
|
|
2408
|
+
function printTable(rows) {
|
|
2409
|
+
const first = rows[0];
|
|
2410
|
+
if (first === void 0) return;
|
|
2411
|
+
const cols = Object.keys(first);
|
|
2412
|
+
const widths = cols.map((c) => Math.max(c.length, ...rows.map((r) => (r[c] ?? "").length)));
|
|
2413
|
+
console.log(cols.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" "));
|
|
2414
|
+
for (const row of rows) {
|
|
2415
|
+
console.log(cols.map((c, i) => (row[c] ?? "").padEnd(widths[i] ?? 0)).join(" "));
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// src/commands/logs.ts
|
|
2420
|
+
import * as fs10 from "node:fs";
|
|
2421
|
+
var VALUE_OPTS2 = /* @__PURE__ */ new Set(["lines"]);
|
|
2422
|
+
async function run12(args) {
|
|
2423
|
+
const { positionals, opts } = parseCliArgs(args, VALUE_OPTS2);
|
|
2424
|
+
const name = positionals[0];
|
|
2425
|
+
if (name === void 0) {
|
|
2426
|
+
console.error("usage: relife2 logs <name> [--lines N] [-f]");
|
|
2427
|
+
return 1;
|
|
2428
|
+
}
|
|
2429
|
+
const lines = parseLines(opts.get("lines") ?? opts.get("n"));
|
|
2430
|
+
const follow = opts.has("f") || opts.has("follow");
|
|
2431
|
+
const client = await ensureDaemonClient();
|
|
2432
|
+
try {
|
|
2433
|
+
const info = await client.call("logs", { name });
|
|
2434
|
+
if (info.files.length === 0) {
|
|
2435
|
+
console.log(`no log files configured for ${name}`);
|
|
2436
|
+
return 0;
|
|
2437
|
+
}
|
|
2438
|
+
let printed = 0;
|
|
2439
|
+
for (const file of info.files) {
|
|
2440
|
+
printed += printTail(file, lines);
|
|
2441
|
+
}
|
|
2442
|
+
if (printed === 0) {
|
|
2443
|
+
console.log(`(no log output yet for ${name})`);
|
|
2444
|
+
}
|
|
2445
|
+
if (follow) {
|
|
2446
|
+
console.log(`(following ${name}; press Ctrl+C to stop)`);
|
|
2447
|
+
await followFiles(info.files);
|
|
2448
|
+
}
|
|
2449
|
+
return 0;
|
|
2450
|
+
} finally {
|
|
2451
|
+
client.close();
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
function parseLines(v) {
|
|
2455
|
+
const n = typeof v === "string" ? Number.parseInt(v, 10) : Number.NaN;
|
|
2456
|
+
return Number.isFinite(n) && n > 0 ? n : 50;
|
|
2457
|
+
}
|
|
2458
|
+
function printTail(file, lines) {
|
|
2459
|
+
try {
|
|
2460
|
+
const text = fs10.readFileSync(file, "utf8");
|
|
2461
|
+
const all = text.split(/\r?\n/);
|
|
2462
|
+
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
2463
|
+
const slice = all.slice(Math.max(0, all.length - lines));
|
|
2464
|
+
for (const line of slice) {
|
|
2465
|
+
if (line !== "") console.log(line);
|
|
2466
|
+
}
|
|
2467
|
+
return slice.length;
|
|
2468
|
+
} catch {
|
|
2469
|
+
return 0;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
async function followFiles(files) {
|
|
2473
|
+
const positions = /* @__PURE__ */ new Map();
|
|
2474
|
+
for (const f of files) {
|
|
2475
|
+
positions.set(f, 0);
|
|
2476
|
+
}
|
|
2477
|
+
process.once("SIGINT", () => process.exit(0));
|
|
2478
|
+
process.once("SIGTERM", () => process.exit(0));
|
|
2479
|
+
for (; ; ) {
|
|
2480
|
+
await sleep(400);
|
|
2481
|
+
for (const f of files) {
|
|
2482
|
+
const size = fileSize(f);
|
|
2483
|
+
const from = positions.get(f) ?? 0;
|
|
2484
|
+
if (size > from) {
|
|
2485
|
+
try {
|
|
2486
|
+
const fd = fs10.openSync(f, "r");
|
|
2487
|
+
try {
|
|
2488
|
+
const buf = Buffer.alloc(size - from);
|
|
2489
|
+
fs10.readSync(fd, buf, 0, buf.length, from);
|
|
2490
|
+
process.stdout.write(buf);
|
|
2491
|
+
} finally {
|
|
2492
|
+
fs10.closeSync(fd);
|
|
2493
|
+
}
|
|
2494
|
+
} catch {
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
positions.set(f, size);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
function fileSize(file) {
|
|
2502
|
+
try {
|
|
2503
|
+
return fs10.statSync(file).size;
|
|
2504
|
+
} catch {
|
|
2505
|
+
return 0;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// src/commands/metrics.ts
|
|
2510
|
+
function fmtBytes(n) {
|
|
2511
|
+
if (n < 1024) return `${n} B`;
|
|
2512
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
2513
|
+
let v = n / 1024;
|
|
2514
|
+
let i = 0;
|
|
2515
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
2516
|
+
v /= 1024;
|
|
2517
|
+
i++;
|
|
2518
|
+
}
|
|
2519
|
+
return `${v.toFixed(1)} ${units[i] ?? "KB"}`;
|
|
2520
|
+
}
|
|
2521
|
+
function fmtDuration(ms) {
|
|
2522
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
2523
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
2524
|
+
if (ms < 36e5) return `${Math.floor(ms / 6e4)}m ${Math.floor(ms % 6e4 / 1e3)}s`;
|
|
2525
|
+
return `${Math.floor(ms / 36e5)}h ${Math.floor(ms % 36e5 / 6e4)}m`;
|
|
2526
|
+
}
|
|
2527
|
+
async function run13(_args) {
|
|
2528
|
+
const client = await ensureDaemonClient();
|
|
2529
|
+
try {
|
|
2530
|
+
const report = await client.call("metrics", {});
|
|
2531
|
+
console.log(
|
|
2532
|
+
`daemon: pid=${report.daemon.pid} uptime=${fmtDuration(report.daemon.uptimeMs)} protocol=${report.daemon.protocolVersion} apps=${report.daemon.appCount}`
|
|
2533
|
+
);
|
|
2534
|
+
for (const a of report.apps) {
|
|
2535
|
+
const memStr = a.memory !== void 0 ? ` mem=${fmtBytes(a.memory)}` : "";
|
|
2536
|
+
const uptimeStr = a.uptimeMs !== void 0 ? ` uptime=${fmtDuration(a.uptimeMs)}` : "";
|
|
2537
|
+
console.log(
|
|
2538
|
+
` ${a.name}: pid=${a.pid ?? "?"} status=${a.status} restarts=${a.restarts}${uptimeStr}${memStr}`
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
return 0;
|
|
2542
|
+
} finally {
|
|
2543
|
+
client.close();
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// src/commands/monit.ts
|
|
2548
|
+
function formatUptime2(ms) {
|
|
2549
|
+
const s = Math.floor(ms / 1e3);
|
|
2550
|
+
if (s < 60) return `${s}s`;
|
|
2551
|
+
const m = Math.floor(s / 60);
|
|
2552
|
+
if (m < 60) return `${m}m${s % 60}s`;
|
|
2553
|
+
const h = Math.floor(m / 60);
|
|
2554
|
+
return `${h}h${m % 60}m`;
|
|
2555
|
+
}
|
|
2556
|
+
function formatMem(bytes) {
|
|
2557
|
+
if (bytes === void 0) return "-";
|
|
2558
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
2559
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
2560
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
2561
|
+
}
|
|
2562
|
+
function clearScreen() {
|
|
2563
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2564
|
+
}
|
|
2565
|
+
function pad(s, n) {
|
|
2566
|
+
return s.length >= n ? s.slice(0, n) : s + " ".repeat(n - s.length);
|
|
2567
|
+
}
|
|
2568
|
+
async function run14(args) {
|
|
2569
|
+
const once = args.includes("--once");
|
|
2570
|
+
const client = await ensureDaemonClient();
|
|
2571
|
+
let running = true;
|
|
2572
|
+
function onExit() {
|
|
2573
|
+
running = false;
|
|
2574
|
+
try {
|
|
2575
|
+
client.close();
|
|
2576
|
+
} catch {
|
|
2577
|
+
}
|
|
2578
|
+
process.stdout.write("\x1B[?25h");
|
|
2579
|
+
process.exit(0);
|
|
2580
|
+
}
|
|
2581
|
+
process.on("SIGINT", onExit);
|
|
2582
|
+
process.on("SIGTERM", onExit);
|
|
2583
|
+
if (!once) process.stdout.write("\x1B[?25l");
|
|
2584
|
+
async function render() {
|
|
2585
|
+
try {
|
|
2586
|
+
const [listRes, metricsRes] = await Promise.all([
|
|
2587
|
+
client.call("list"),
|
|
2588
|
+
client.call("metrics")
|
|
2589
|
+
]);
|
|
2590
|
+
const memMap = new Map(metricsRes.apps.map((a) => [a.name, a.memory]));
|
|
2591
|
+
const uptimeMap = new Map(metricsRes.apps.map((a) => [a.name, a.uptimeMs]));
|
|
2592
|
+
if (!once) clearScreen();
|
|
2593
|
+
console.log(
|
|
2594
|
+
`relife2 monit \u2014 ${(/* @__PURE__ */ new Date()).toLocaleString()} (q to quit${once ? ", --once" : ""})`
|
|
2595
|
+
);
|
|
2596
|
+
console.log(
|
|
2597
|
+
`daemon pid=${metricsRes.daemon.pid} uptime=${formatUptime2(metricsRes.daemon.uptimeMs)} apps=${metricsRes.daemon.appCount}`
|
|
2598
|
+
);
|
|
2599
|
+
console.log("");
|
|
2600
|
+
const header = `${pad("name", 20)} ${pad("status", 10)} ${pad("pid", 8)} ${pad("uptime", 10)} ${pad("restarts", 9)} ${pad("memory", 10)} ${"error"}`;
|
|
2601
|
+
console.log(header);
|
|
2602
|
+
console.log("-".repeat(header.length));
|
|
2603
|
+
for (const app of listRes.apps) {
|
|
2604
|
+
const mem = formatMem(memMap.get(app.name));
|
|
2605
|
+
const up = app.startTime !== void 0 ? formatUptime2(Date.now() - app.startTime) : uptimeMap.get(app.name) !== void 0 ? formatUptime2(uptimeMap.get(app.name)) : "-";
|
|
2606
|
+
const line = `${pad(app.name, 20)} ${pad(app.status, 10)} ${pad(app.pid !== void 0 ? String(app.pid) : "-", 8)} ${pad(up, 10)} ${pad(String(app.restarts), 9)} ${pad(mem, 10)} ${app.error ?? ""}`;
|
|
2607
|
+
let colored = line;
|
|
2608
|
+
if (app.status === "online") colored = `\x1B[32m${line}\x1B[0m`;
|
|
2609
|
+
else if (app.status === "errored") colored = `\x1B[31m${line}\x1B[0m`;
|
|
2610
|
+
else if (app.status === "stopped") colored = `\x1B[90m${line}\x1B[0m`;
|
|
2611
|
+
console.log(colored);
|
|
2612
|
+
}
|
|
2613
|
+
console.log("");
|
|
2614
|
+
if (!once) console.log("Press q or Ctrl+C to exit.");
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
if (!once) clearScreen();
|
|
2617
|
+
console.error(`monit error: ${err.message}`);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (once) {
|
|
2621
|
+
await render();
|
|
2622
|
+
client.close();
|
|
2623
|
+
return 0;
|
|
2624
|
+
}
|
|
2625
|
+
await render();
|
|
2626
|
+
if (process.stdin.isTTY) {
|
|
2627
|
+
try {
|
|
2628
|
+
process.stdin.setRawMode(true);
|
|
2629
|
+
process.stdin.resume();
|
|
2630
|
+
process.stdin.on("data", (d) => {
|
|
2631
|
+
const s = d.toString("utf8");
|
|
2632
|
+
if (s === "q" || s === "Q" || s === "") onExit();
|
|
2633
|
+
});
|
|
2634
|
+
} catch {
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
while (running) {
|
|
2638
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
2639
|
+
if (running) await render();
|
|
2640
|
+
}
|
|
2641
|
+
return 0;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// src/commands/ping.ts
|
|
2645
|
+
async function run15(_args) {
|
|
2646
|
+
const client = await ensureDaemonClient();
|
|
2647
|
+
try {
|
|
2648
|
+
const info = await client.call("ping");
|
|
2649
|
+
console.log(
|
|
2650
|
+
`relife2 daemon: online (pid ${info.pid}, v${info.version}, protocol ${info.protocol}, uptime ${info.uptime}s)`
|
|
2651
|
+
);
|
|
2652
|
+
return 0;
|
|
2653
|
+
} finally {
|
|
2654
|
+
client.close();
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
// src/commands/reload.ts
|
|
2659
|
+
async function run16(args) {
|
|
2660
|
+
const { positionals } = parseCliArgs(args);
|
|
2661
|
+
const name = positionals[0];
|
|
2662
|
+
if (name === void 0) {
|
|
2663
|
+
console.error("usage: relife2 reload <name|all>");
|
|
2664
|
+
return 1;
|
|
2665
|
+
}
|
|
2666
|
+
const client = await ensureDaemonClient();
|
|
2667
|
+
try {
|
|
2668
|
+
const res = await client.call(
|
|
2669
|
+
"reload",
|
|
2670
|
+
{ name }
|
|
2671
|
+
);
|
|
2672
|
+
for (const a of res.apps) {
|
|
2673
|
+
console.log(`ok ${a.name}: ${a.status}${a.pid !== void 0 ? ` (pid ${a.pid})` : ""}`);
|
|
2674
|
+
}
|
|
2675
|
+
return 0;
|
|
2676
|
+
} finally {
|
|
2677
|
+
client.close();
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// src/commands/restart.ts
|
|
2682
|
+
async function run17(args) {
|
|
2683
|
+
const { positionals } = parseCliArgs(args);
|
|
2684
|
+
const name = positionals[0];
|
|
2685
|
+
if (name === void 0) {
|
|
2686
|
+
console.error("usage: relife2 restart <name|all>");
|
|
2687
|
+
return 1;
|
|
2688
|
+
}
|
|
2689
|
+
const client = await ensureDaemonClient();
|
|
2690
|
+
try {
|
|
2691
|
+
const res = await client.call(
|
|
2692
|
+
"restart",
|
|
2693
|
+
{ name }
|
|
2694
|
+
);
|
|
2695
|
+
for (const a of res.apps) {
|
|
2696
|
+
console.log(`ok ${a.name}: ${a.status}${a.pid !== void 0 ? ` (pid ${a.pid})` : ""}`);
|
|
2697
|
+
}
|
|
2698
|
+
return 0;
|
|
2699
|
+
} finally {
|
|
2700
|
+
client.close();
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// src/commands/resurrect.ts
|
|
2705
|
+
async function run18(_args) {
|
|
2706
|
+
const client = await ensureDaemonClient();
|
|
2707
|
+
try {
|
|
2708
|
+
const res = await client.call("resurrect");
|
|
2709
|
+
let failed = false;
|
|
2710
|
+
for (const app of res.apps) {
|
|
2711
|
+
if (app.status === "errored") {
|
|
2712
|
+
console.error(`x ${app.name}: ${app.message ?? "failed to start"}`);
|
|
2713
|
+
failed = true;
|
|
2714
|
+
} else if (app.message === "already running") {
|
|
2715
|
+
console.log(` ${app.name}: already running`);
|
|
2716
|
+
} else {
|
|
2717
|
+
console.log(
|
|
2718
|
+
`ok ${app.name}: ${app.status}${app.pid !== void 0 ? ` (pid ${app.pid})` : ""}`
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return failed ? 1 : 0;
|
|
2723
|
+
} finally {
|
|
2724
|
+
client.close();
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
// src/commands/rotate.ts
|
|
2729
|
+
var VALUE_OPTS3 = /* @__PURE__ */ new Set(["max-size", "retain", "app"]);
|
|
2730
|
+
async function run19(args) {
|
|
2731
|
+
const { positionals, opts } = parseCliArgs(args, VALUE_OPTS3);
|
|
2732
|
+
const maxSize = parseSize(opts.get("max-size") ?? "10485760");
|
|
2733
|
+
const retain = parseCount(opts.get("retain") ?? "7");
|
|
2734
|
+
const appName = opts.get("app");
|
|
2735
|
+
const client = await ensureDaemonClient();
|
|
2736
|
+
try {
|
|
2737
|
+
const res = await client.call("rotate", {
|
|
2738
|
+
app: appName ?? positionals[0],
|
|
2739
|
+
maxSizeBytes: maxSize,
|
|
2740
|
+
retain
|
|
2741
|
+
});
|
|
2742
|
+
if (res.length === 0) {
|
|
2743
|
+
console.log("no log files to rotate");
|
|
2744
|
+
return 0;
|
|
2745
|
+
}
|
|
2746
|
+
let totalRotated = 0;
|
|
2747
|
+
for (const entry of res) {
|
|
2748
|
+
if (entry.rotated.length > 0) {
|
|
2749
|
+
console.log(`ok ${entry.name}: rotated ${entry.rotated.length} file(s)`);
|
|
2750
|
+
for (const f of entry.rotated) console.log(` ${f}`);
|
|
2751
|
+
totalRotated += entry.rotated.length;
|
|
2752
|
+
} else if (entry.skipped.length > 0) {
|
|
2753
|
+
console.log(
|
|
2754
|
+
`ok ${entry.name}: no files exceed the size threshold (${entry.skipped.length} file(s) skipped)`
|
|
2755
|
+
);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
console.log(`---
|
|
2759
|
+
${totalRotated} file(s) rotated, retain=${retain}`);
|
|
2760
|
+
return 0;
|
|
2761
|
+
} finally {
|
|
2762
|
+
client.close();
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
function parseSize(v) {
|
|
2766
|
+
if (typeof v !== "string") return 10485760;
|
|
2767
|
+
const m = /^(\d+(?:\.\d+)?)\s*([kmg]?)$/i.exec(v.trim());
|
|
2768
|
+
if (!m) return 10485760;
|
|
2769
|
+
const num = Number.parseFloat(m[1] ?? "");
|
|
2770
|
+
if (!Number.isFinite(num)) return 10485760;
|
|
2771
|
+
const mult = { k: 1024, m: 1024 ** 2, g: 1024 ** 3, K: 1024, M: 1024 ** 2, G: 1024 ** 3 }[m[2] ?? ""] ?? 1;
|
|
2772
|
+
return Math.round(num * mult);
|
|
2773
|
+
}
|
|
2774
|
+
function parseCount(v) {
|
|
2775
|
+
if (typeof v !== "string") return 7;
|
|
2776
|
+
const n = Number.parseInt(v, 10);
|
|
2777
|
+
return Number.isFinite(n) && n > 0 ? n : 7;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// src/commands/save.ts
|
|
2781
|
+
async function run20(_args) {
|
|
2782
|
+
const client = await ensureDaemonClient();
|
|
2783
|
+
try {
|
|
2784
|
+
const res = await client.call("save");
|
|
2785
|
+
console.log(`state saved (${res.savedAt})`);
|
|
2786
|
+
return 0;
|
|
2787
|
+
} finally {
|
|
2788
|
+
client.close();
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// src/commands/start.ts
|
|
2793
|
+
import path12 from "node:path";
|
|
2794
|
+
var VALUE_OPTS4 = /* @__PURE__ */ new Set([
|
|
2795
|
+
"name",
|
|
2796
|
+
"env",
|
|
2797
|
+
"instances",
|
|
2798
|
+
"exec-mode",
|
|
2799
|
+
"exec_mode",
|
|
2800
|
+
"interpreter",
|
|
2801
|
+
"cwd",
|
|
2802
|
+
"max-memory-restart",
|
|
2803
|
+
"max_memory_restart",
|
|
2804
|
+
"kill-timeout",
|
|
2805
|
+
"restart-delay",
|
|
2806
|
+
"watch-delay"
|
|
2807
|
+
]);
|
|
2808
|
+
async function run21(args) {
|
|
2809
|
+
const { positionals, opts, passthrough } = parseCliArgs(args, VALUE_OPTS4);
|
|
2810
|
+
let target = positionals[0];
|
|
2811
|
+
if (target === void 0) {
|
|
2812
|
+
const found = findDefaultConfig(process.cwd());
|
|
2813
|
+
if (found === null) {
|
|
2814
|
+
console.error(
|
|
2815
|
+
"usage: relife2 start <config|script> [--name <n>] [--env <env>] [--instances <n>] [-- --app-args...]\n also: relife2 start (no args) will look for relife2.config.{ts,js,cjs,mjs,json} or ecosystem.config.* in cwd"
|
|
2816
|
+
);
|
|
2817
|
+
return 1;
|
|
2818
|
+
}
|
|
2819
|
+
target = found;
|
|
2820
|
+
}
|
|
2821
|
+
const nameVal = opts.get("name");
|
|
2822
|
+
const name = typeof nameVal === "string" && nameVal !== "" ? nameVal : void 0;
|
|
2823
|
+
const envName = opts.get("env") ?? void 0;
|
|
2824
|
+
const cliOverrides = {};
|
|
2825
|
+
if (envName !== void 0 && envName !== "") cliOverrides._envName = envName;
|
|
2826
|
+
if (opts.has("instances")) cliOverrides.instances = opts.get("instances");
|
|
2827
|
+
if (opts.has("exec-mode") || opts.has("exec_mode"))
|
|
2828
|
+
cliOverrides.exec_mode = opts.get("exec-mode") ?? opts.get("exec_mode");
|
|
2829
|
+
if (opts.has("interpreter")) cliOverrides.interpreter = opts.get("interpreter");
|
|
2830
|
+
if (opts.has("cwd")) cliOverrides.cwd = opts.get("cwd");
|
|
2831
|
+
if (opts.has("max-memory-restart") || opts.has("max_memory_restart"))
|
|
2832
|
+
cliOverrides.max_memory_restart = opts.get("max-memory-restart") ?? opts.get("max_memory_restart");
|
|
2833
|
+
if (opts.has("kill-timeout")) cliOverrides.kill_timeout = opts.get("kill-timeout");
|
|
2834
|
+
if (opts.has("restart-delay")) cliOverrides.restart_delay = opts.get("restart-delay");
|
|
2835
|
+
if (opts.has("watch-delay")) cliOverrides.watch_delay = opts.get("watch-delay");
|
|
2836
|
+
const isConfig = looksLikeConfigFile(target);
|
|
2837
|
+
const client = await ensureDaemonClient();
|
|
2838
|
+
try {
|
|
2839
|
+
const res = await client.call("start", {
|
|
2840
|
+
target: {
|
|
2841
|
+
type: isConfig ? "config" : "script",
|
|
2842
|
+
path: path12.resolve(target),
|
|
2843
|
+
name,
|
|
2844
|
+
args: passthrough.length > 0 ? passthrough : void 0,
|
|
2845
|
+
envName: envName ?? void 0,
|
|
2846
|
+
cliOverrides: Object.keys(cliOverrides).length > 0 ? cliOverrides : void 0
|
|
2847
|
+
}
|
|
2848
|
+
});
|
|
2849
|
+
for (const w of res.warnings ?? []) console.error(`warning: ${w}`);
|
|
2850
|
+
let failed = false;
|
|
2851
|
+
for (const app of res.apps) {
|
|
2852
|
+
if (app.status === "errored") {
|
|
2853
|
+
console.error(`x ${app.name}: ${app.message ?? "failed to start"}`);
|
|
2854
|
+
failed = true;
|
|
2855
|
+
} else {
|
|
2856
|
+
console.log(
|
|
2857
|
+
`ok ${app.name}: ${app.status}${app.pid !== void 0 ? ` (pid ${app.pid})` : ""}${app.message ? ` \u2014 ${app.message}` : ""}`
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
return failed ? 1 : 0;
|
|
2862
|
+
} finally {
|
|
2863
|
+
client.close();
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// src/commands/startup.ts
|
|
2868
|
+
import { execSync, spawnSync as spawnSync2 } from "node:child_process";
|
|
2869
|
+
import * as fs11 from "node:fs";
|
|
2870
|
+
import os7 from "node:os";
|
|
2871
|
+
import path13 from "node:path";
|
|
2872
|
+
import { fileURLToPath } from "node:url";
|
|
2873
|
+
var SYSTEMD_USER_DIR = path13.join(os7.homedir(), ".config", "systemd", "user");
|
|
2874
|
+
var UNIT_NAME = "relife2.service";
|
|
2875
|
+
function getRelife2Bin() {
|
|
2876
|
+
const which = spawnSync2(process.platform === "win32" ? "where" : "which", ["relife2"], {
|
|
2877
|
+
encoding: "utf8",
|
|
2878
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2879
|
+
});
|
|
2880
|
+
const firstLine = which.stdout.trim().split("\n")[0]?.trim();
|
|
2881
|
+
if (which.status === 0 && firstLine !== void 0 && firstLine !== "") {
|
|
2882
|
+
return firstLine;
|
|
2883
|
+
}
|
|
2884
|
+
const arg1 = process.argv[1];
|
|
2885
|
+
const isSingleBinary = arg1 === void 0 || !arg1.endsWith(".js") && !arg1.endsWith(".mjs") && !arg1.endsWith(".cjs");
|
|
2886
|
+
if (isSingleBinary) {
|
|
2887
|
+
return process.execPath;
|
|
2888
|
+
}
|
|
2889
|
+
try {
|
|
2890
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
2891
|
+
const cliPath = path13.resolve(path13.dirname(thisFile), "..", "cli.js");
|
|
2892
|
+
if (fs11.existsSync(cliPath)) {
|
|
2893
|
+
return `node ${cliPath}`;
|
|
2894
|
+
}
|
|
2895
|
+
} catch {
|
|
2896
|
+
}
|
|
2897
|
+
return `node ${path13.resolve(arg1 ?? process.cwd())}`;
|
|
2898
|
+
}
|
|
2899
|
+
function generateUnit(bin) {
|
|
2900
|
+
const envHint = process.env.RELIFE2_DIR !== void 0 ? `
|
|
2901
|
+
Environment=RELIFE2_DIR=${process.env.RELIFE2_DIR}` : "";
|
|
2902
|
+
return `[Unit]
|
|
2903
|
+
Description=relife2 process manager daemon
|
|
2904
|
+
After=network.target
|
|
2905
|
+
|
|
2906
|
+
[Service]
|
|
2907
|
+
Type=simple
|
|
2908
|
+
ExecStart=${bin}
|
|
2909
|
+
Restart=always
|
|
2910
|
+
RestartSec=2
|
|
2911
|
+
KillMode=process
|
|
2912
|
+
Environment=RELIFE2_DAEMON=1${envHint}
|
|
2913
|
+
|
|
2914
|
+
[Install]
|
|
2915
|
+
WantedBy=default.target
|
|
2916
|
+
`;
|
|
2917
|
+
}
|
|
2918
|
+
async function run22(_args) {
|
|
2919
|
+
if (process.platform === "win32") {
|
|
2920
|
+
console.error(
|
|
2921
|
+
"relife2 startup: systemd is not available on Windows (use Task Scheduler instead)"
|
|
2922
|
+
);
|
|
2923
|
+
return 1;
|
|
2924
|
+
}
|
|
2925
|
+
if (process.platform === "darwin") {
|
|
2926
|
+
console.error("relife2 startup: systemd is not available on macOS (use launchd instead)");
|
|
2927
|
+
return 1;
|
|
2928
|
+
}
|
|
2929
|
+
const bin = getRelife2Bin();
|
|
2930
|
+
const unitContent = generateUnit(bin);
|
|
2931
|
+
const unitPath = path13.join(SYSTEMD_USER_DIR, UNIT_NAME);
|
|
2932
|
+
try {
|
|
2933
|
+
fs11.mkdirSync(SYSTEMD_USER_DIR, { recursive: true });
|
|
2934
|
+
} catch (err) {
|
|
2935
|
+
console.error(`cannot create ${SYSTEMD_USER_DIR}: ${err.message}`);
|
|
2936
|
+
return 1;
|
|
2937
|
+
}
|
|
2938
|
+
try {
|
|
2939
|
+
fs11.writeFileSync(unitPath, unitContent);
|
|
2940
|
+
console.log(`written systemd user unit: ${unitPath}`);
|
|
2941
|
+
} catch (err) {
|
|
2942
|
+
console.error(`cannot write ${unitPath}: ${err.message}`);
|
|
2943
|
+
return 1;
|
|
2944
|
+
}
|
|
2945
|
+
try {
|
|
2946
|
+
const user = process.env.USER ?? os7.userInfo().username ?? "root";
|
|
2947
|
+
execSync(`loginctl enable-linger ${user}`, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2948
|
+
console.log(`enabled linger for user ${user}`);
|
|
2949
|
+
} catch (err) {
|
|
2950
|
+
console.error(
|
|
2951
|
+
`warning: could not run 'loginctl enable-linger' (is systemd-logind running?): ${err instanceof Error ? err.message : String(err)}`
|
|
2952
|
+
);
|
|
2953
|
+
console.error("your apps will only start after you log in. Run manually:");
|
|
2954
|
+
console.error(` loginctl enable-linger ${process.env.USER ?? os7.userInfo().username}`);
|
|
2955
|
+
}
|
|
2956
|
+
try {
|
|
2957
|
+
execSync("systemctl --user daemon-reload", { stdio: ["ignore", "pipe", "pipe"] });
|
|
2958
|
+
execSync(`systemctl --user enable ${UNIT_NAME}`, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2959
|
+
console.log(`enabled and reloaded: systemctl --user enable ${UNIT_NAME}`);
|
|
2960
|
+
} catch (err) {
|
|
2961
|
+
console.error(
|
|
2962
|
+
`warning: could not enable unit (is systemd available?): ${err instanceof Error ? err.message : String(err)}`
|
|
2963
|
+
);
|
|
2964
|
+
console.error("run manually:");
|
|
2965
|
+
console.error(" systemctl --user daemon-reload");
|
|
2966
|
+
console.error(` systemctl --user enable ${UNIT_NAME}`);
|
|
2967
|
+
}
|
|
2968
|
+
console.log("");
|
|
2969
|
+
console.log("relife2 will now start automatically on boot.");
|
|
2970
|
+
console.log(`ExecStart is: ${bin}`);
|
|
2971
|
+
console.log("To start now, run: systemctl --user start relife2.service");
|
|
2972
|
+
console.log("To check status: systemctl --user status relife2.service");
|
|
2973
|
+
return 0;
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// src/commands/stop.ts
|
|
2977
|
+
async function run23(args) {
|
|
2978
|
+
const { positionals } = parseCliArgs(args);
|
|
2979
|
+
const name = positionals[0];
|
|
2980
|
+
if (name === void 0) {
|
|
2981
|
+
console.error("usage: relife2 stop <name|all>");
|
|
2982
|
+
return 1;
|
|
2983
|
+
}
|
|
2984
|
+
const client = await ensureDaemonClient();
|
|
2985
|
+
try {
|
|
2986
|
+
const res = await client.call(
|
|
2987
|
+
"stop",
|
|
2988
|
+
{ name }
|
|
2989
|
+
);
|
|
2990
|
+
for (const a of res.apps) {
|
|
2991
|
+
console.log(`ok ${a.name}: ${a.status}${a.pid !== void 0 ? ` (pid ${a.pid})` : ""}`);
|
|
2992
|
+
}
|
|
2993
|
+
return 0;
|
|
2994
|
+
} finally {
|
|
2995
|
+
client.close();
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// src/commands/upgrade.ts
|
|
3000
|
+
async function run24(args) {
|
|
3001
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3002
|
+
printUsage3();
|
|
3003
|
+
return 0;
|
|
3004
|
+
}
|
|
3005
|
+
const runtime = runtimeDir();
|
|
3006
|
+
const data = dataDir();
|
|
3007
|
+
let sock = await tryConnect(runtime);
|
|
3008
|
+
if (sock === null) {
|
|
3009
|
+
console.log("daemon is not running \u2014 starting fresh");
|
|
3010
|
+
spawnDaemon();
|
|
3011
|
+
for (let i = 0; i < 100; i++) {
|
|
3012
|
+
await sleep(100);
|
|
3013
|
+
sock = await tryConnect(runtime);
|
|
3014
|
+
if (sock !== null) break;
|
|
3015
|
+
}
|
|
3016
|
+
if (sock === null) {
|
|
3017
|
+
console.error(`cannot start daemon (runtime: ${runtime}); inspect logs/daemon.log`);
|
|
3018
|
+
return 1;
|
|
3019
|
+
}
|
|
3020
|
+
const client2 = new RpcClient(sock);
|
|
3021
|
+
try {
|
|
3022
|
+
const ping = await client2.call("ping");
|
|
3023
|
+
if (ping.protocol !== PROTOCOL_VERSION) {
|
|
3024
|
+
console.error(
|
|
3025
|
+
`daemon protocol mismatch after start (daemon: ${ping.protocol}, cli: ${PROTOCOL_VERSION})`
|
|
3026
|
+
);
|
|
3027
|
+
return 1;
|
|
3028
|
+
}
|
|
3029
|
+
console.log(`daemon started (pid ${ping.pid}, v${ping.version}, protocol ${ping.protocol})`);
|
|
3030
|
+
return 0;
|
|
3031
|
+
} finally {
|
|
3032
|
+
client2.close();
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
const client = new RpcClient(sock);
|
|
3036
|
+
let oldPid;
|
|
3037
|
+
let oldVersion;
|
|
3038
|
+
let appsCount = 0;
|
|
3039
|
+
try {
|
|
3040
|
+
try {
|
|
3041
|
+
const ping = await client.call("ping");
|
|
3042
|
+
oldPid = ping.pid;
|
|
3043
|
+
oldVersion = ping.version;
|
|
3044
|
+
if (ping.protocol !== PROTOCOL_VERSION) {
|
|
3045
|
+
console.error(
|
|
3046
|
+
`warning: daemon protocol ${ping.protocol} != cli ${PROTOCOL_VERSION} \u2014 forcing upgrade`
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
3049
|
+
} catch {
|
|
3050
|
+
}
|
|
3051
|
+
try {
|
|
3052
|
+
const list = await client.call("list");
|
|
3053
|
+
appsCount = list.apps.length;
|
|
3054
|
+
} catch {
|
|
3055
|
+
}
|
|
3056
|
+
try {
|
|
3057
|
+
await client.call("save");
|
|
3058
|
+
} catch {
|
|
3059
|
+
}
|
|
3060
|
+
console.log(
|
|
3061
|
+
`upgrading daemon${oldPid ? ` pid ${oldPid}` : ""}${oldVersion ? ` v${oldVersion}` : ""} (${appsCount} app(s)) \u2026`
|
|
3062
|
+
);
|
|
3063
|
+
try {
|
|
3064
|
+
await client.call("shutdown");
|
|
3065
|
+
} catch (err) {
|
|
3066
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3067
|
+
if (!msg.includes("connection to daemon closed") && !msg.includes("timeout")) {
|
|
3068
|
+
console.error(`warning: shutdown call failed: ${msg}`);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
} finally {
|
|
3072
|
+
client.close();
|
|
3073
|
+
}
|
|
3074
|
+
for (let i = 0; i < 50; i++) {
|
|
3075
|
+
await sleep(100);
|
|
3076
|
+
const probe = await tryConnect(runtime);
|
|
3077
|
+
if (probe === null) break;
|
|
3078
|
+
probe.destroy();
|
|
3079
|
+
if (i === 49) {
|
|
3080
|
+
console.error("old daemon did not exit within 5s \u2014 try `relife2 kill` and retry");
|
|
3081
|
+
return 1;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
await sleep(200);
|
|
3085
|
+
console.log(`starting new daemon \u2026`);
|
|
3086
|
+
spawnDaemon();
|
|
3087
|
+
let newSock = null;
|
|
3088
|
+
for (let i = 0; i < 100; i++) {
|
|
3089
|
+
await sleep(100);
|
|
3090
|
+
newSock = await tryConnect(runtime);
|
|
3091
|
+
if (newSock !== null) break;
|
|
3092
|
+
}
|
|
3093
|
+
if (newSock === null) {
|
|
3094
|
+
console.error(`new daemon did not appear within 10s (runtime: ${runtime}, data: ${data})`);
|
|
3095
|
+
return 1;
|
|
3096
|
+
}
|
|
3097
|
+
const newClient = new RpcClient(newSock);
|
|
3098
|
+
try {
|
|
3099
|
+
const ping = await newClient.call("ping");
|
|
3100
|
+
if (ping.protocol !== PROTOCOL_VERSION) {
|
|
3101
|
+
console.error(
|
|
3102
|
+
`new daemon protocol mismatch (daemon: ${ping.protocol}, cli: ${PROTOCOL_VERSION})`
|
|
3103
|
+
);
|
|
3104
|
+
return 1;
|
|
3105
|
+
}
|
|
3106
|
+
console.log(`new daemon ready (pid ${ping.pid}, v${ping.version}, protocol ${ping.protocol})`);
|
|
3107
|
+
if (appsCount > 0) {
|
|
3108
|
+
try {
|
|
3109
|
+
const res = await newClient.call(
|
|
3110
|
+
"resurrect"
|
|
3111
|
+
);
|
|
3112
|
+
const started = res.apps.filter(
|
|
3113
|
+
(a) => a.status === "online" || a.status === "starting"
|
|
3114
|
+
).length;
|
|
3115
|
+
console.log(`resurrected ${started}/${res.apps.length} app(s)`);
|
|
3116
|
+
} catch (err) {
|
|
3117
|
+
console.error(
|
|
3118
|
+
`warning: resurrect failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
try {
|
|
3123
|
+
const list = await newClient.call("list");
|
|
3124
|
+
const online = list.apps.filter(
|
|
3125
|
+
(a) => a.status === "online" || a.status === "starting"
|
|
3126
|
+
).length;
|
|
3127
|
+
console.log(
|
|
3128
|
+
`daemon-upgrade: ${oldPid ?? "?"} \u2192 ${ping.pid} (${online}/${list.apps.length} online)`
|
|
3129
|
+
);
|
|
3130
|
+
} catch {
|
|
3131
|
+
console.log(`daemon-upgrade: ${oldPid ?? "?"} \u2192 ${ping.pid}`);
|
|
3132
|
+
}
|
|
3133
|
+
return 0;
|
|
3134
|
+
} finally {
|
|
3135
|
+
newClient.close();
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
function printUsage3() {
|
|
3139
|
+
console.log(`relife2 daemon-upgrade \u2014 upgrade daemon without losing app list
|
|
3140
|
+
|
|
3141
|
+
Usage: relife2 daemon-upgrade
|
|
3142
|
+
|
|
3143
|
+
Flow: save snapshot \u2192 graceful shutdown of old daemon (under lock) \u2192 spawn new
|
|
3144
|
+
daemon \u2192 wait for ping (protocol check) \u2192 resurrect apps from snapshot.
|
|
3145
|
+
|
|
3146
|
+
If no daemon is running, just starts one. Never spawns a second daemon while
|
|
3147
|
+
the old one is alive \u2014 relies on socket bind + lock file for singleton (ADR-0005).
|
|
3148
|
+
Use when you updated the relife2 binary or need to cycle the daemon cleanly.
|
|
3149
|
+
`);
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
// src/commands/version.ts
|
|
3153
|
+
function printVersion() {
|
|
3154
|
+
console.log(`relife2 ${VERSION}`);
|
|
3155
|
+
const [runtime, runtimeVersion] = detectRuntime();
|
|
3156
|
+
console.log(`runtime: ${runtime} ${runtimeVersion} (${process.platform} ${process.arch})`);
|
|
3157
|
+
}
|
|
3158
|
+
function detectRuntime() {
|
|
3159
|
+
const g = globalThis;
|
|
3160
|
+
if (g.Bun !== void 0) {
|
|
3161
|
+
return ["bun", g.Bun.version];
|
|
3162
|
+
}
|
|
3163
|
+
return ["node", process.version];
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// src/daemon/daemon.ts
|
|
3167
|
+
import * as fs18 from "node:fs";
|
|
3168
|
+
import os8 from "node:os";
|
|
3169
|
+
import path19 from "node:path";
|
|
3170
|
+
import * as zlib from "node:zlib";
|
|
3171
|
+
|
|
3172
|
+
// src/daemon/applog.ts
|
|
3173
|
+
import * as fs12 from "node:fs";
|
|
3174
|
+
import path14 from "node:path";
|
|
3175
|
+
|
|
3176
|
+
// src/util/datefmt.ts
|
|
3177
|
+
function formatDate(pattern, date = /* @__PURE__ */ new Date()) {
|
|
3178
|
+
const y = date.getFullYear();
|
|
3179
|
+
const mo = date.getMonth() + 1;
|
|
3180
|
+
const d = date.getDate();
|
|
3181
|
+
const h = date.getHours();
|
|
3182
|
+
const mi = date.getMinutes();
|
|
3183
|
+
const s = date.getSeconds();
|
|
3184
|
+
const ms = date.getMilliseconds();
|
|
3185
|
+
const offset = date.getTimezoneOffset();
|
|
3186
|
+
const sign = offset <= 0 ? "+" : "-";
|
|
3187
|
+
const abs = Math.abs(offset);
|
|
3188
|
+
const oh = String(Math.floor(abs / 60)).padStart(2, "0");
|
|
3189
|
+
const om = String(abs % 60).padStart(2, "0");
|
|
3190
|
+
const z = `${sign}${oh}${om}`;
|
|
3191
|
+
const zz = `${sign}${oh}:${om}`;
|
|
3192
|
+
const ampm = h < 12 ? "AM" : "PM";
|
|
3193
|
+
const pad2 = (n, len = 2) => String(n).padStart(len, "0");
|
|
3194
|
+
return pattern.replaceAll("YYYY", String(y)).replaceAll("YY", String(y).slice(-2)).replaceAll("MM", pad2(mo)).replaceAll("DD", pad2(d)).replaceAll("HH", pad2(h)).replaceAll("mm", pad2(mi)).replaceAll("ss", pad2(s)).replaceAll("SSS", pad2(ms, 3)).replaceAll("ZZ", zz).replaceAll("Z", z).replaceAll("A", ampm).replaceAll("a", ampm.toLowerCase()).replaceAll("M", String(mo)).replaceAll("D", String(d)).replaceAll("H", String(h));
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
// src/daemon/applog.ts
|
|
3198
|
+
var DEFAULT_LOG_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS Z";
|
|
3199
|
+
var DEFAULT_LOGS_DIR = (base) => path14.join(base, "logs");
|
|
3200
|
+
function resolveLogPaths(app, base) {
|
|
3201
|
+
const safe = app.name.replace(/[^\w.-]/g, "_");
|
|
3202
|
+
const out = app.outFile ? path14.resolve(app.cwd, app.outFile) : path14.join(DEFAULT_LOGS_DIR(base), `${safe}.out.log`);
|
|
3203
|
+
if (app.mergeLogs) return { out, err: null };
|
|
3204
|
+
const err = app.errFile ? path14.resolve(app.cwd, app.errFile) : path14.join(DEFAULT_LOGS_DIR(base), `${safe}.err.log`);
|
|
3205
|
+
return { out, err };
|
|
3206
|
+
}
|
|
3207
|
+
var AppLogSink = class {
|
|
3208
|
+
constructor(app, paths) {
|
|
3209
|
+
this.paths = paths;
|
|
3210
|
+
const timestamped = app.time || app.logDateFormat !== void 0;
|
|
3211
|
+
this.prefix = timestamped ? `${formatDate(app.logDateFormat ?? DEFAULT_LOG_FORMAT)} ` : "";
|
|
3212
|
+
}
|
|
3213
|
+
prefix;
|
|
3214
|
+
outSink = "";
|
|
3215
|
+
errSink = "";
|
|
3216
|
+
out(chunk) {
|
|
3217
|
+
this.writeLeftover(
|
|
3218
|
+
this.paths.out,
|
|
3219
|
+
chunk,
|
|
3220
|
+
() => this.outSink,
|
|
3221
|
+
(v) => this.outSink = v
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
err(chunk) {
|
|
3225
|
+
if (this.paths.err === null) {
|
|
3226
|
+
this.writeLeftover(
|
|
3227
|
+
this.paths.out,
|
|
3228
|
+
chunk,
|
|
3229
|
+
() => this.outSink,
|
|
3230
|
+
(v) => this.outSink = v
|
|
3231
|
+
);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
this.writeLeftover(
|
|
3235
|
+
this.paths.err,
|
|
3236
|
+
chunk,
|
|
3237
|
+
() => this.errSink,
|
|
3238
|
+
(v) => this.errSink = v
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
writeLeftover(target, text, get, set) {
|
|
3242
|
+
const joined = get() + text;
|
|
3243
|
+
const lines = joined.split("\n");
|
|
3244
|
+
set(lines.pop() ?? "");
|
|
3245
|
+
for (const line of lines) {
|
|
3246
|
+
const out = `${this.prefix}${line}
|
|
3247
|
+
`;
|
|
3248
|
+
try {
|
|
3249
|
+
ensureDirSync(path14.dirname(target));
|
|
3250
|
+
fs12.appendFileSync(target, out);
|
|
3251
|
+
} catch {
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
};
|
|
3256
|
+
|
|
3257
|
+
// src/daemon/logger.ts
|
|
3258
|
+
import fs13 from "node:fs";
|
|
3259
|
+
import path15 from "node:path";
|
|
3260
|
+
function createDaemonLogger(base) {
|
|
3261
|
+
const logsDir = path15.join(base, "logs");
|
|
3262
|
+
ensureDirSync(logsDir);
|
|
3263
|
+
const mainFile = path15.join(logsDir, "daemon.log");
|
|
3264
|
+
const outRoot = logsDir;
|
|
3265
|
+
function log(msg, extra) {
|
|
3266
|
+
const line = `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), msg, ...extra ?? {} })}
|
|
3267
|
+
`;
|
|
3268
|
+
try {
|
|
3269
|
+
fs13.appendFileSync(mainFile, line);
|
|
3270
|
+
} catch {
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
function safeName(name) {
|
|
3274
|
+
return name.replace(/[^\w.-]/g, "_");
|
|
3275
|
+
}
|
|
3276
|
+
function appOut(name, chunk) {
|
|
3277
|
+
try {
|
|
3278
|
+
fs13.appendFileSync(path15.join(outRoot, `${safeName(name)}.out.log`), chunk);
|
|
3279
|
+
} catch {
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
function appErr(name, chunk) {
|
|
3283
|
+
try {
|
|
3284
|
+
fs13.appendFileSync(path15.join(outRoot, `${safeName(name)}.err.log`), chunk);
|
|
3285
|
+
} catch {
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
return { log, appOut, appErr };
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
// src/daemon/pm.ts
|
|
3292
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
3293
|
+
import * as fs15 from "node:fs";
|
|
3294
|
+
import path17 from "node:path";
|
|
3295
|
+
|
|
3296
|
+
// src/daemon/command.ts
|
|
3297
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
3298
|
+
import path16 from "node:path";
|
|
3299
|
+
import process2 from "node:process";
|
|
3300
|
+
var JS_RE = /\.(?:c?js|mjs|ts|mts|cts)$/i;
|
|
3301
|
+
function isJsScript(p) {
|
|
3302
|
+
return JS_RE.test(p);
|
|
3303
|
+
}
|
|
3304
|
+
function isBunProject2(scriptDir) {
|
|
3305
|
+
let dir = scriptDir;
|
|
3306
|
+
const home = process2.env.HOME ?? "";
|
|
3307
|
+
while (true) {
|
|
3308
|
+
if (existsSync8(path16.join(dir, "bun.lockb")) || existsSync8(path16.join(dir, "bun.lock"))) {
|
|
3309
|
+
return true;
|
|
3310
|
+
}
|
|
3311
|
+
const parent = path16.dirname(dir);
|
|
3312
|
+
if (parent === dir) break;
|
|
3313
|
+
if (home !== "" && dir === home) break;
|
|
3314
|
+
dir = parent;
|
|
3315
|
+
}
|
|
3316
|
+
return false;
|
|
3317
|
+
}
|
|
3318
|
+
function buildSpawnSpec(app) {
|
|
3319
|
+
const script = app.script;
|
|
3320
|
+
let cmd;
|
|
3321
|
+
let note;
|
|
3322
|
+
if (app.interpreter === "none") {
|
|
3323
|
+
cmd = [script, ...app.args];
|
|
3324
|
+
} else if (app.interpreter !== void 0) {
|
|
3325
|
+
cmd = [app.interpreter, script, ...app.args];
|
|
3326
|
+
} else if (isJsScript(script)) {
|
|
3327
|
+
if (isBunProject2(app.cwd)) {
|
|
3328
|
+
cmd = ["bun", script, ...app.args];
|
|
3329
|
+
note = "auto-detected bun project (bun.lockb/bun.lock); using bun";
|
|
3330
|
+
} else {
|
|
3331
|
+
cmd = [process2.execPath, script, ...app.args];
|
|
3332
|
+
}
|
|
3333
|
+
} else {
|
|
3334
|
+
cmd = [script, ...app.args];
|
|
3335
|
+
}
|
|
3336
|
+
if (!cmd[0]) throw new Error("empty command");
|
|
3337
|
+
const env = {};
|
|
3338
|
+
for (const [k, v] of Object.entries(process2.env)) {
|
|
3339
|
+
if (v !== void 0) env[k] = v;
|
|
3340
|
+
}
|
|
3341
|
+
Object.assign(env, app.env);
|
|
3342
|
+
return { cmd, cwd: app.cwd, env, note };
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// src/daemon/memproc.ts
|
|
3346
|
+
import { execFileSync } from "node:child_process";
|
|
3347
|
+
import * as fs14 from "node:fs";
|
|
3348
|
+
function processRss(pid) {
|
|
3349
|
+
if (pid <= 0) return void 0;
|
|
3350
|
+
try {
|
|
3351
|
+
if (process.platform === "linux") {
|
|
3352
|
+
return rssFromProc(pid);
|
|
3353
|
+
}
|
|
3354
|
+
if (process.platform === "darwin") {
|
|
3355
|
+
return rssFromPs(pid);
|
|
3356
|
+
}
|
|
3357
|
+
return rssFromWmic(pid);
|
|
3358
|
+
} catch {
|
|
3359
|
+
return void 0;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
function rssFromProc(pid) {
|
|
3363
|
+
const status = fs14.readFileSync(`/proc/${pid}/status`, "utf8");
|
|
3364
|
+
const match = /^VmRSS:\s+(\d+)\s+kB/m.exec(status);
|
|
3365
|
+
if (match?.[1] === void 0) return void 0;
|
|
3366
|
+
return Number(match[1]) * 1024;
|
|
3367
|
+
}
|
|
3368
|
+
function rssFromPs(pid) {
|
|
3369
|
+
const out = execFileSync("ps", ["-o", "rss=", "-p", String(pid)], {
|
|
3370
|
+
encoding: "utf8",
|
|
3371
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3372
|
+
}).trim();
|
|
3373
|
+
if (out === "") return void 0;
|
|
3374
|
+
const kb = Number(out);
|
|
3375
|
+
if (!Number.isFinite(kb) || kb < 0) return void 0;
|
|
3376
|
+
return Math.round(kb * 1024);
|
|
3377
|
+
}
|
|
3378
|
+
function rssFromWmic(pid) {
|
|
3379
|
+
const out = execFileSync(
|
|
3380
|
+
"wmic",
|
|
3381
|
+
["process", "where", `ProcessId=${pid}`, "get", "WorkingSetSize"],
|
|
3382
|
+
{
|
|
3383
|
+
encoding: "utf8",
|
|
3384
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3385
|
+
}
|
|
3386
|
+
);
|
|
3387
|
+
const lines = out.split("\n").map((l) => l.trim()).filter((l) => l !== "" && l !== "WorkingSetSize");
|
|
3388
|
+
if (lines.length === 0) return void 0;
|
|
3389
|
+
const bytes = Number(lines[0]);
|
|
3390
|
+
if (!Number.isFinite(bytes) || bytes < 0) return void 0;
|
|
3391
|
+
return Math.round(bytes);
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
// src/daemon/pm.ts
|
|
3395
|
+
function errMsg(err) {
|
|
3396
|
+
return err instanceof Error ? err.message : String(err);
|
|
3397
|
+
}
|
|
3398
|
+
var READY_TIMEOUT_MS = 3e4;
|
|
3399
|
+
var MEMORY_CHECK_INTERVAL_MS = 5e3;
|
|
3400
|
+
var CRON_CHECK_INTERVAL_MS = 6e4;
|
|
3401
|
+
function canUseIpc(app) {
|
|
3402
|
+
const interp = app.interpreter;
|
|
3403
|
+
if (interp === "none") return false;
|
|
3404
|
+
if (interp === "bun" || interp === "node") return true;
|
|
3405
|
+
if (interp !== void 0) return false;
|
|
3406
|
+
return process.execPath.toLowerCase().includes("node");
|
|
3407
|
+
}
|
|
3408
|
+
var ProcessManager = class {
|
|
3409
|
+
constructor(store, logger, base) {
|
|
3410
|
+
this.store = store;
|
|
3411
|
+
this.logger = logger;
|
|
3412
|
+
this.base = base;
|
|
3413
|
+
}
|
|
3414
|
+
timers = /* @__PURE__ */ new Map();
|
|
3415
|
+
killTimers = /* @__PURE__ */ new Map();
|
|
3416
|
+
readyTimers = /* @__PURE__ */ new Map();
|
|
3417
|
+
watchTimers = /* @__PURE__ */ new Map();
|
|
3418
|
+
watchers = /* @__PURE__ */ new Map();
|
|
3419
|
+
memMonitor;
|
|
3420
|
+
cronMonitor;
|
|
3421
|
+
cronLastCheck = 0;
|
|
3422
|
+
spawnApp(record, opts) {
|
|
3423
|
+
this.clearTimer(record.name);
|
|
3424
|
+
this.clearKillTimer(record.name);
|
|
3425
|
+
this.clearReadyTimer(record.name);
|
|
3426
|
+
this.stopWatch(record.name);
|
|
3427
|
+
const cwdFix = fixCwd(record.config.cwd, record.config.sourceDir);
|
|
3428
|
+
if (cwdFix.fixed && cwdFix.reason) {
|
|
3429
|
+
this.logger.log(cwdFix.reason);
|
|
3430
|
+
this.store.journal("app-portable-fix", {
|
|
3431
|
+
name: record.name,
|
|
3432
|
+
field: "cwd",
|
|
3433
|
+
from: record.config.cwd,
|
|
3434
|
+
to: cwdFix.path
|
|
3435
|
+
});
|
|
3436
|
+
record.config.cwd = cwdFix.path;
|
|
3437
|
+
}
|
|
3438
|
+
if (path17.isAbsolute(record.config.script) && !fs15.existsSync(record.config.script)) {
|
|
3439
|
+
const sFix = fixScript(record.config.script, record.config.cwd, record.config.sourceDir);
|
|
3440
|
+
if (sFix.fixed && sFix.reason) {
|
|
3441
|
+
this.logger.log(sFix.reason);
|
|
3442
|
+
this.store.journal("app-portable-fix", {
|
|
3443
|
+
name: record.name,
|
|
3444
|
+
field: "script",
|
|
3445
|
+
from: record.config.script,
|
|
3446
|
+
to: sFix.path
|
|
3447
|
+
});
|
|
3448
|
+
record.config.script = sFix.path;
|
|
3449
|
+
} else if (!sFix.fixed) {
|
|
3450
|
+
this.fail(
|
|
3451
|
+
record,
|
|
3452
|
+
`script not found: '${record.config.script}'. ${sFix.reason ?? ""} Run 'relife2 find --root ${path17.dirname(record.config.sourceDir)}' to locate configs or fix the config.`
|
|
3453
|
+
);
|
|
3454
|
+
this.store.saveSync();
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
let spec;
|
|
3459
|
+
try {
|
|
3460
|
+
spec = buildSpawnSpec(record.config);
|
|
3461
|
+
} catch (err) {
|
|
3462
|
+
this.fail(record, `spawn config: ${errMsg(err)}`);
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
record.status = "starting";
|
|
3466
|
+
if (opts?.resetCrashStreak !== false) {
|
|
3467
|
+
record.crashStreak = 0;
|
|
3468
|
+
}
|
|
3469
|
+
this.touch(record);
|
|
3470
|
+
record.sink = new AppLogSink(record.config, resolveLogPaths(record.config, this.base));
|
|
3471
|
+
const sink = record.sink;
|
|
3472
|
+
let child;
|
|
3473
|
+
const bin = spec.cmd[0];
|
|
3474
|
+
if (bin === void 0) {
|
|
3475
|
+
this.fail(record, "spawn config: empty command");
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
3478
|
+
const useIpc = canUseIpc(record.config);
|
|
3479
|
+
try {
|
|
3480
|
+
child = spawn3(bin, spec.cmd.slice(1), {
|
|
3481
|
+
cwd: spec.cwd,
|
|
3482
|
+
env: spec.env,
|
|
3483
|
+
stdio: useIpc ? ["ignore", "pipe", "pipe", "ipc"] : ["ignore", "pipe", "pipe"],
|
|
3484
|
+
detached: process.platform !== "win32"
|
|
3485
|
+
});
|
|
3486
|
+
} catch (err) {
|
|
3487
|
+
this.fail(record, `spawn failed: ${errMsg(err)}`);
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
record.child = child;
|
|
3491
|
+
record.pid = child.pid;
|
|
3492
|
+
record.startTime = Date.now();
|
|
3493
|
+
record.exitCode = void 0;
|
|
3494
|
+
record.exitSignal = void 0;
|
|
3495
|
+
record.error = void 0;
|
|
3496
|
+
record.stopRequested = void 0;
|
|
3497
|
+
record.exitAt = void 0;
|
|
3498
|
+
record.status = record.config.waitReady ? "starting" : "online";
|
|
3499
|
+
this.touch(record);
|
|
3500
|
+
this.store.journal("app-start", { name: record.name, pid: child.pid, cmd: spec.cmd.join(" ") });
|
|
3501
|
+
this.logger.log(
|
|
3502
|
+
`started ${record.name} (pid ${child.pid}) ${spec.cmd.join(" ")}${spec.note ? ` [${spec.note}]` : ""}`
|
|
3503
|
+
);
|
|
3504
|
+
child.stdout?.on("data", (d) => sink.out(d.toString("utf8")));
|
|
3505
|
+
child.stderr?.on("data", (d) => sink.err(d.toString("utf8")));
|
|
3506
|
+
if (useIpc && record.config.waitReady) {
|
|
3507
|
+
child.on("message", (msg) => {
|
|
3508
|
+
if (record.child !== child) return;
|
|
3509
|
+
if (msg === "ready" || msg !== null && typeof msg === "object" && msg.type === "ready") {
|
|
3510
|
+
this.markReady(record);
|
|
3511
|
+
}
|
|
3512
|
+
});
|
|
3513
|
+
const timer = setTimeout(() => {
|
|
3514
|
+
this.readyTimers.delete(record.name);
|
|
3515
|
+
if (record.child === child && record.status === "starting") {
|
|
3516
|
+
this.logger.log(
|
|
3517
|
+
`app ${record.name} did not send 'ready' within ${READY_TIMEOUT_MS}ms; marking online`
|
|
3518
|
+
);
|
|
3519
|
+
record.status = "online";
|
|
3520
|
+
this.touch(record);
|
|
3521
|
+
}
|
|
3522
|
+
}, READY_TIMEOUT_MS);
|
|
3523
|
+
this.readyTimers.set(record.name, timer);
|
|
3524
|
+
}
|
|
3525
|
+
child.on("error", (err) => this.fail(record, `process error: ${errMsg(err)}`));
|
|
3526
|
+
child.on("exit", (code, signal) => this.onExit(record, code, signal));
|
|
3527
|
+
this.maybeStartMemMonitor();
|
|
3528
|
+
this.maybeStartWatch(record);
|
|
3529
|
+
this.maybeStartCronMonitor();
|
|
3530
|
+
}
|
|
3531
|
+
maybeStartMemMonitor() {
|
|
3532
|
+
if (this.memMonitor !== void 0) return;
|
|
3533
|
+
const anyLimited = [...this.store.apps.values()].some(
|
|
3534
|
+
(rec) => rec.config.maxMemoryRestart !== void 0 && rec.config.maxMemoryRestart > 0
|
|
3535
|
+
);
|
|
3536
|
+
if (!anyLimited) return;
|
|
3537
|
+
this.memMonitor = setInterval(() => this.checkMemory(), MEMORY_CHECK_INTERVAL_MS);
|
|
3538
|
+
if (typeof this.memMonitor.unref === "function") this.memMonitor.unref();
|
|
3539
|
+
}
|
|
3540
|
+
checkMemory() {
|
|
3541
|
+
for (const rec of this.store.apps.values()) {
|
|
3542
|
+
const limit = rec.config.maxMemoryRestart;
|
|
3543
|
+
if (limit === void 0 || limit <= 0) continue;
|
|
3544
|
+
const pid = rec.pid;
|
|
3545
|
+
if (pid === void 0) continue;
|
|
3546
|
+
const rss = processRss(pid);
|
|
3547
|
+
if (rss === void 0) continue;
|
|
3548
|
+
if (rss > limit) {
|
|
3549
|
+
this.logger.log(
|
|
3550
|
+
`app ${rec.name} exceeded memory limit (${rss} > ${limit} bytes); restarting`
|
|
3551
|
+
);
|
|
3552
|
+
this.store.journal("app-restart-memory", {
|
|
3553
|
+
name: rec.name,
|
|
3554
|
+
pid,
|
|
3555
|
+
rss,
|
|
3556
|
+
limit
|
|
3557
|
+
});
|
|
3558
|
+
this.stop(rec, "restart");
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
/** Marks a wait_ready app as online once it signals readiness over IPC. */
|
|
3563
|
+
markReady(record) {
|
|
3564
|
+
const t = this.readyTimers.get(record.name);
|
|
3565
|
+
if (t !== void 0) {
|
|
3566
|
+
clearTimeout(t);
|
|
3567
|
+
this.readyTimers.delete(record.name);
|
|
3568
|
+
}
|
|
3569
|
+
if (record.status === "starting") {
|
|
3570
|
+
record.status = "online";
|
|
3571
|
+
this.store.journal("app-ready", { name: record.name });
|
|
3572
|
+
this.logger.log(`app ${record.name} ready`);
|
|
3573
|
+
this.touch(record);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
/** Graceful stop. `reason === 'restart'` respawns after the process actually exits. */
|
|
3577
|
+
stop(record, reason) {
|
|
3578
|
+
this.clearTimer(record.name);
|
|
3579
|
+
this.clearKillTimer(record.name);
|
|
3580
|
+
this.clearReadyTimer(record.name);
|
|
3581
|
+
this.stopWatch(record.name);
|
|
3582
|
+
record.stopRequested = reason;
|
|
3583
|
+
if (record.pid === void 0) {
|
|
3584
|
+
if (reason === "restart") {
|
|
3585
|
+
if (record.status === "stopped" || record.status === "errored") {
|
|
3586
|
+
record.stopRequested = void 0;
|
|
3587
|
+
this.spawnApp(record);
|
|
3588
|
+
}
|
|
3589
|
+
return;
|
|
3590
|
+
}
|
|
3591
|
+
this.setStateStopped(record);
|
|
3592
|
+
return;
|
|
3593
|
+
}
|
|
3594
|
+
record.status = "stopping";
|
|
3595
|
+
this.touch(record);
|
|
3596
|
+
this.store.journal("app-stop", { name: record.name, reason });
|
|
3597
|
+
this.signalTree(record, "SIGTERM");
|
|
3598
|
+
const timeout = record.config.killTimeout;
|
|
3599
|
+
this.killTimers.set(
|
|
3600
|
+
record.name,
|
|
3601
|
+
setTimeout(() => {
|
|
3602
|
+
this.killTimers.delete(record.name);
|
|
3603
|
+
if (record.pid !== void 0) {
|
|
3604
|
+
this.logger.log(`kill_timeout reached for ${record.name}; sending SIGKILL`);
|
|
3605
|
+
this.signalTree(record, "SIGKILL");
|
|
3606
|
+
}
|
|
3607
|
+
}, timeout)
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
stopAll(reason) {
|
|
3611
|
+
for (const record of this.store.apps.values()) {
|
|
3612
|
+
this.stop(record, reason);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
waitForAllStopped(timeoutMs) {
|
|
3616
|
+
const deadline = Date.now() + timeoutMs;
|
|
3617
|
+
return new Promise((resolve) => {
|
|
3618
|
+
const tick = () => {
|
|
3619
|
+
const running = [...this.store.apps.values()].some((r) => r.pid !== void 0);
|
|
3620
|
+
if (!running || Date.now() >= deadline) {
|
|
3621
|
+
resolve();
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
setTimeout(tick, 100);
|
|
3625
|
+
};
|
|
3626
|
+
tick();
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
onExit(record, code, signal) {
|
|
3630
|
+
if (!record.child) return;
|
|
3631
|
+
record.child = void 0;
|
|
3632
|
+
record.pid = void 0;
|
|
3633
|
+
record.exitCode = code;
|
|
3634
|
+
record.exitSignal = signal;
|
|
3635
|
+
record.exitAt = Date.now();
|
|
3636
|
+
this.clearKillTimer(record.name);
|
|
3637
|
+
const uptime = record.startTime !== void 0 ? Math.max(0, Date.now() - record.startTime) : 0;
|
|
3638
|
+
this.store.journal("app-exit", { name: record.name, code, signal, uptime });
|
|
3639
|
+
this.logger.log(`app ${record.name} exited (code=${code} signal=${signal} uptime=${uptime}ms)`);
|
|
3640
|
+
const reason = record.stopRequested;
|
|
3641
|
+
if (reason !== void 0) {
|
|
3642
|
+
record.stopRequested = void 0;
|
|
3643
|
+
if (reason === "restart") {
|
|
3644
|
+
this.spawnApp(record);
|
|
3645
|
+
} else {
|
|
3646
|
+
this.setStateStopped(record);
|
|
3647
|
+
}
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
if (uptime >= record.config.minUptime) {
|
|
3651
|
+
record.crashStreak = 0;
|
|
3652
|
+
} else {
|
|
3653
|
+
record.crashStreak += 1;
|
|
3654
|
+
}
|
|
3655
|
+
this.touch(record);
|
|
3656
|
+
if (!record.config.autorestart) {
|
|
3657
|
+
this.setStateStopped(record);
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
if (record.crashStreak > record.config.maxRestarts) {
|
|
3661
|
+
record.status = "errored";
|
|
3662
|
+
record.error = `exited ${String(code)} after ${record.crashStreak} crashes inside min_uptime (${record.config.minUptime}ms); max_restarts=${record.config.maxRestarts}`;
|
|
3663
|
+
this.store.journal("app-errored", { name: record.name, error: record.error });
|
|
3664
|
+
this.touch(record);
|
|
3665
|
+
this.logger.log(`app ${record.name} entered errored state: ${record.error}`);
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
record.restarts += 1;
|
|
3669
|
+
record.status = "restarting";
|
|
3670
|
+
this.store.journal("app-restart-scheduled", { name: record.name, restarts: record.restarts });
|
|
3671
|
+
this.touch(record);
|
|
3672
|
+
this.timers.set(
|
|
3673
|
+
record.name,
|
|
3674
|
+
setTimeout(() => {
|
|
3675
|
+
this.timers.delete(record.name);
|
|
3676
|
+
if (record.status === "restarting") {
|
|
3677
|
+
this.spawnApp(record, { resetCrashStreak: false });
|
|
3678
|
+
}
|
|
3679
|
+
}, record.config.restartDelay)
|
|
3680
|
+
);
|
|
3681
|
+
}
|
|
3682
|
+
fail(record, message) {
|
|
3683
|
+
record.child = void 0;
|
|
3684
|
+
record.pid = void 0;
|
|
3685
|
+
record.status = "errored";
|
|
3686
|
+
record.error = message;
|
|
3687
|
+
this.store.journal("app-errored", { name: record.name, error: message });
|
|
3688
|
+
this.touch(record);
|
|
3689
|
+
this.logger.log(`app ${record.name} errored: ${message}`);
|
|
3690
|
+
}
|
|
3691
|
+
setStateStopped(record) {
|
|
3692
|
+
record.status = "stopped";
|
|
3693
|
+
this.store.journal("app-stopped", { name: record.name });
|
|
3694
|
+
this.touch(record);
|
|
3695
|
+
this.logger.log(`app ${record.name} stopped`);
|
|
3696
|
+
}
|
|
3697
|
+
signalTree(record, signal) {
|
|
3698
|
+
const pid = record.pid;
|
|
3699
|
+
if (pid === void 0) return;
|
|
3700
|
+
if (process.platform === "win32") {
|
|
3701
|
+
try {
|
|
3702
|
+
spawn3("taskkill", ["/pid", String(pid), "/T", "/F"]);
|
|
3703
|
+
} catch {
|
|
3704
|
+
}
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
try {
|
|
3708
|
+
process.kill(-pid, signal);
|
|
3709
|
+
} catch {
|
|
3710
|
+
try {
|
|
3711
|
+
process.kill(pid, signal);
|
|
3712
|
+
} catch {
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
clearTimer(name) {
|
|
3717
|
+
const t = this.timers.get(name);
|
|
3718
|
+
if (t !== void 0) {
|
|
3719
|
+
clearTimeout(t);
|
|
3720
|
+
this.timers.delete(name);
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
clearKillTimer(name) {
|
|
3724
|
+
const t = this.killTimers.get(name);
|
|
3725
|
+
if (t !== void 0) {
|
|
3726
|
+
clearTimeout(t);
|
|
3727
|
+
this.killTimers.delete(name);
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
clearReadyTimer(name) {
|
|
3731
|
+
const t = this.readyTimers.get(name);
|
|
3732
|
+
if (t !== void 0) {
|
|
3733
|
+
clearTimeout(t);
|
|
3734
|
+
this.readyTimers.delete(name);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
/** Stops the background memory monitor (called during shutdown). */
|
|
3738
|
+
stopMemMonitor() {
|
|
3739
|
+
if (this.memMonitor !== void 0) {
|
|
3740
|
+
clearInterval(this.memMonitor);
|
|
3741
|
+
this.memMonitor = void 0;
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
/** Stops all watch + cron monitors (called during shutdown). */
|
|
3745
|
+
stopAllMonitors() {
|
|
3746
|
+
this.stopMemMonitor();
|
|
3747
|
+
if (this.cronMonitor !== void 0) {
|
|
3748
|
+
clearInterval(this.cronMonitor);
|
|
3749
|
+
this.cronMonitor = void 0;
|
|
3750
|
+
}
|
|
3751
|
+
for (const watchers of this.watchers.values()) {
|
|
3752
|
+
for (const w of watchers) {
|
|
3753
|
+
try {
|
|
3754
|
+
w.close();
|
|
3755
|
+
} catch {
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
this.watchers.clear();
|
|
3760
|
+
for (const t of this.watchTimers.values()) clearTimeout(t);
|
|
3761
|
+
this.watchTimers.clear();
|
|
3762
|
+
}
|
|
3763
|
+
// ─── Watch ──────────────────────────────────────────────────────────────
|
|
3764
|
+
maybeStartWatch(record) {
|
|
3765
|
+
const watch3 = record.config.watch;
|
|
3766
|
+
if (watch3 === void 0 || watch3 === false) return;
|
|
3767
|
+
if (this.watchers.has(record.name)) return;
|
|
3768
|
+
const rawPaths = Array.isArray(watch3) ? watch3.map((p) => path17.resolve(record.config.cwd, p)) : [record.config.cwd];
|
|
3769
|
+
const watchers = [];
|
|
3770
|
+
for (const target of rawPaths) {
|
|
3771
|
+
try {
|
|
3772
|
+
if (!fs15.existsSync(target)) {
|
|
3773
|
+
this.logger.log(`watch: path '${target}' does not exist yet; skipping`);
|
|
3774
|
+
continue;
|
|
3775
|
+
}
|
|
3776
|
+
const stat = fs15.statSync(target);
|
|
3777
|
+
if (!stat.isDirectory()) {
|
|
3778
|
+
const dir = path17.dirname(target);
|
|
3779
|
+
const base = path17.basename(target);
|
|
3780
|
+
const watcher = fs15.watch(dir, (_eventType, filename) => {
|
|
3781
|
+
if (filename !== null && filename !== base) return;
|
|
3782
|
+
this.onWatchChange(record.name, target);
|
|
3783
|
+
});
|
|
3784
|
+
watchers.push(watcher);
|
|
3785
|
+
continue;
|
|
3786
|
+
}
|
|
3787
|
+
if (process.platform === "win32" || process.platform === "darwin") {
|
|
3788
|
+
const watcher = fs15.watch(target, { recursive: true }, () => {
|
|
3789
|
+
this.onWatchChange(record.name, target);
|
|
3790
|
+
});
|
|
3791
|
+
watchers.push(watcher);
|
|
3792
|
+
} else {
|
|
3793
|
+
const dirs = this.collectSubDirs(target);
|
|
3794
|
+
for (const dir of dirs) {
|
|
3795
|
+
try {
|
|
3796
|
+
const watcher = fs15.watch(dir, () => {
|
|
3797
|
+
this.onWatchChange(record.name, target);
|
|
3798
|
+
});
|
|
3799
|
+
watchers.push(watcher);
|
|
3800
|
+
} catch (err) {
|
|
3801
|
+
this.logger.log(`watch: cannot watch '${dir}': ${errMsg(err)}`);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
} catch (err) {
|
|
3806
|
+
this.logger.log(`watch: cannot watch '${target}': ${errMsg(err)}`);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
if (watchers.length > 0) {
|
|
3810
|
+
this.watchers.set(record.name, watchers);
|
|
3811
|
+
this.logger.log(`watch: started for ${record.name} (${watchers.length} path(s))`);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
/** Collects `root` and all its nested sub-directories (BFS, ignores errors/symlink loops). */
|
|
3815
|
+
collectSubDirs(root) {
|
|
3816
|
+
const out = [root];
|
|
3817
|
+
const seen = /* @__PURE__ */ new Set([root]);
|
|
3818
|
+
const queue = [root];
|
|
3819
|
+
while (queue.length > 0) {
|
|
3820
|
+
const dir = queue.shift();
|
|
3821
|
+
let entries;
|
|
3822
|
+
try {
|
|
3823
|
+
entries = fs15.readdirSync(dir, { withFileTypes: true });
|
|
3824
|
+
} catch {
|
|
3825
|
+
continue;
|
|
3826
|
+
}
|
|
3827
|
+
for (const entry of entries) {
|
|
3828
|
+
if (!entry.isDirectory()) continue;
|
|
3829
|
+
if (entry.name.startsWith(".")) continue;
|
|
3830
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
3831
|
+
const full = path17.join(dir, entry.name);
|
|
3832
|
+
if (seen.has(full)) continue;
|
|
3833
|
+
seen.add(full);
|
|
3834
|
+
out.push(full);
|
|
3835
|
+
queue.push(full);
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
return out;
|
|
3839
|
+
}
|
|
3840
|
+
onWatchChange(name, dir) {
|
|
3841
|
+
const existing = this.watchTimers.get(name);
|
|
3842
|
+
if (existing !== void 0) clearTimeout(existing);
|
|
3843
|
+
const rec = this.store.apps.get(name);
|
|
3844
|
+
if (rec === void 0) return;
|
|
3845
|
+
const delay = rec.config.watchDelay ?? 1e3;
|
|
3846
|
+
this.watchTimers.set(
|
|
3847
|
+
name,
|
|
3848
|
+
setTimeout(() => {
|
|
3849
|
+
this.watchTimers.delete(name);
|
|
3850
|
+
if (rec.pid !== void 0 && (rec.status === "online" || rec.status === "starting")) {
|
|
3851
|
+
this.logger.log(`watch: change detected in ${dir}; restarting ${name}`);
|
|
3852
|
+
this.store.journal("app-restart-watch", { name, dir });
|
|
3853
|
+
this.stop(rec, "restart");
|
|
3854
|
+
}
|
|
3855
|
+
}, delay)
|
|
3856
|
+
);
|
|
3857
|
+
}
|
|
3858
|
+
/** Stops and removes all watchers for a single app (called on stop/spawn). */
|
|
3859
|
+
stopWatch(name) {
|
|
3860
|
+
const watchers = this.watchers.get(name);
|
|
3861
|
+
if (watchers !== void 0) {
|
|
3862
|
+
for (const w of watchers) {
|
|
3863
|
+
try {
|
|
3864
|
+
w.close();
|
|
3865
|
+
} catch {
|
|
3866
|
+
}
|
|
3867
|
+
}
|
|
3868
|
+
this.watchers.delete(name);
|
|
3869
|
+
}
|
|
3870
|
+
const t = this.watchTimers.get(name);
|
|
3871
|
+
if (t !== void 0) {
|
|
3872
|
+
clearTimeout(t);
|
|
3873
|
+
this.watchTimers.delete(name);
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
// ─── Cron ───────────────────────────────────────────────────────────────
|
|
3877
|
+
maybeStartCronMonitor() {
|
|
3878
|
+
if (this.cronMonitor !== void 0) return;
|
|
3879
|
+
const anyCron = [...this.store.apps.values()].some(
|
|
3880
|
+
(rec) => rec.config.cronRestart !== void 0
|
|
3881
|
+
);
|
|
3882
|
+
if (!anyCron) return;
|
|
3883
|
+
this.cronMonitor = setInterval(() => this.checkCron(), CRON_CHECK_INTERVAL_MS);
|
|
3884
|
+
if (typeof this.cronMonitor.unref === "function") this.cronMonitor.unref();
|
|
3885
|
+
this.checkCron();
|
|
3886
|
+
}
|
|
3887
|
+
checkCron() {
|
|
3888
|
+
const now = Date.now();
|
|
3889
|
+
const currentMinute = Math.floor(now / 6e4);
|
|
3890
|
+
if (currentMinute === this.cronLastCheck) return;
|
|
3891
|
+
this.cronLastCheck = currentMinute;
|
|
3892
|
+
for (const rec of this.store.apps.values()) {
|
|
3893
|
+
const expr = rec.config.cronRestart;
|
|
3894
|
+
if (expr === void 0) continue;
|
|
3895
|
+
try {
|
|
3896
|
+
if (cronMatches(expr, new Date(now))) {
|
|
3897
|
+
this.logger.log(`cron: '${expr}' matched for ${rec.name}; restarting`);
|
|
3898
|
+
this.store.journal("app-restart-cron", { name: rec.name, expr });
|
|
3899
|
+
this.stop(rec, "restart");
|
|
3900
|
+
}
|
|
3901
|
+
} catch (err) {
|
|
3902
|
+
this.logger.log(`cron: error checking '${expr}' for ${rec.name}: ${errMsg(err)}`);
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
touch(_record) {
|
|
3907
|
+
this.store.saveSync();
|
|
3908
|
+
}
|
|
3909
|
+
/** Returns the daemon's own RSS in bytes (for self-metrics). */
|
|
3910
|
+
daemonMemory() {
|
|
3911
|
+
try {
|
|
3912
|
+
return process.memoryUsage().rss;
|
|
3913
|
+
} catch {
|
|
3914
|
+
return 0;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
/** Returns a sanitized snapshot of the managed apps for the metrics command. */
|
|
3918
|
+
appMetrics() {
|
|
3919
|
+
return [...this.store.apps.values()].map((rec) => ({
|
|
3920
|
+
name: rec.name,
|
|
3921
|
+
pid: rec.pid,
|
|
3922
|
+
status: rec.status,
|
|
3923
|
+
uptimeMs: rec.startTime !== void 0 ? Date.now() - rec.startTime : void 0,
|
|
3924
|
+
restarts: rec.restarts,
|
|
3925
|
+
memory: rec.pid !== void 0 ? processRss(rec.pid) : void 0
|
|
3926
|
+
}));
|
|
3927
|
+
}
|
|
3928
|
+
};
|
|
3929
|
+
|
|
3930
|
+
// src/daemon/state.ts
|
|
3931
|
+
import fs16 from "node:fs";
|
|
3932
|
+
import path18 from "node:path";
|
|
3933
|
+
var StateStore = class {
|
|
3934
|
+
apps = /* @__PURE__ */ new Map();
|
|
3935
|
+
snapshotPath;
|
|
3936
|
+
journalPath;
|
|
3937
|
+
constructor(base) {
|
|
3938
|
+
const stateDir = path18.join(base, "state");
|
|
3939
|
+
ensureDirSync(stateDir);
|
|
3940
|
+
this.snapshotPath = path18.join(stateDir, "snapshot.json");
|
|
3941
|
+
this.journalPath = path18.join(stateDir, "journal.jsonl");
|
|
3942
|
+
}
|
|
3943
|
+
load() {
|
|
3944
|
+
const data = readJsonFile(this.snapshotPath);
|
|
3945
|
+
if (!data?.apps) return;
|
|
3946
|
+
for (const [name, rec] of Object.entries(data.apps)) {
|
|
3947
|
+
if (rec === null || typeof rec !== "object" || rec.config === void 0) continue;
|
|
3948
|
+
this.apps.set(name, {
|
|
3949
|
+
name: rec.name ?? name,
|
|
3950
|
+
config: rec.config,
|
|
3951
|
+
status: rec.status ?? "stopped",
|
|
3952
|
+
pid: typeof rec.pid === "number" ? rec.pid : void 0,
|
|
3953
|
+
startTime: void 0,
|
|
3954
|
+
exitAt: rec.exitAt,
|
|
3955
|
+
exitCode: rec.exitCode,
|
|
3956
|
+
exitSignal: rec.exitSignal,
|
|
3957
|
+
restarts: rec.restarts ?? 0,
|
|
3958
|
+
crashStreak: rec.crashStreak ?? 0,
|
|
3959
|
+
error: rec.error
|
|
3960
|
+
});
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
saveSync() {
|
|
3964
|
+
const appsObj = {};
|
|
3965
|
+
for (const [name, rec] of this.apps) {
|
|
3966
|
+
const { child: _child, sink: _sink, ...rest } = rec;
|
|
3967
|
+
appsObj[name] = rest;
|
|
3968
|
+
}
|
|
3969
|
+
atomicWriteFileSync(
|
|
3970
|
+
this.snapshotPath,
|
|
3971
|
+
JSON.stringify({ schema: 1, savedAt: (/* @__PURE__ */ new Date()).toISOString(), apps: appsObj }, null, 2)
|
|
3972
|
+
);
|
|
3973
|
+
}
|
|
3974
|
+
journal(type, data) {
|
|
3975
|
+
const line = `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), type, ...data ?? {} })}
|
|
3976
|
+
`;
|
|
3977
|
+
try {
|
|
3978
|
+
fs16.appendFileSync(this.journalPath, line);
|
|
3979
|
+
} catch {
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
requireApp(name) {
|
|
3983
|
+
const rec = this.apps.get(name);
|
|
3984
|
+
if (!rec) throw new RpcError("NOT_FOUND", `app '${name}' is not in this daemon's list`);
|
|
3985
|
+
return rec;
|
|
3986
|
+
}
|
|
3987
|
+
};
|
|
3988
|
+
|
|
3989
|
+
// src/daemon/transport.ts
|
|
3990
|
+
import fs17 from "node:fs";
|
|
3991
|
+
import net2 from "node:net";
|
|
3992
|
+
async function createDaemonTransport(base) {
|
|
3993
|
+
return process.platform === "win32" ? createWinTransport(base) : createPosixTransport(base);
|
|
3994
|
+
}
|
|
3995
|
+
async function createPosixTransport(base) {
|
|
3996
|
+
const sockPath = unixSockPath(base);
|
|
3997
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3998
|
+
const server = net2.createServer();
|
|
3999
|
+
const err = await listenOnce(server, sockPath);
|
|
4000
|
+
if (err === null) return serverTransport(server, sockPath);
|
|
4001
|
+
if (err.code !== "EADDRINUSE") return null;
|
|
4002
|
+
if (await probeActive(sockPath)) return null;
|
|
4003
|
+
try {
|
|
4004
|
+
fs17.unlinkSync(sockPath);
|
|
4005
|
+
} catch {
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
return null;
|
|
4009
|
+
}
|
|
4010
|
+
async function createWinTransport(base) {
|
|
4011
|
+
const preferred = readPortFile(base) ?? defaultPort(base);
|
|
4012
|
+
for (let port = preferred; port < preferred + 20; port++) {
|
|
4013
|
+
const server = net2.createServer();
|
|
4014
|
+
const err = await listenOnce(server, { host: "127.0.0.1", port });
|
|
4015
|
+
if (err === null) {
|
|
4016
|
+
writePortFile(base, port);
|
|
4017
|
+
return serverTransport(server, `127.0.0.1:${port}`);
|
|
4018
|
+
}
|
|
4019
|
+
if (err.code !== "EADDRINUSE") return null;
|
|
4020
|
+
}
|
|
4021
|
+
return null;
|
|
4022
|
+
}
|
|
4023
|
+
function serverTransport(server, address) {
|
|
4024
|
+
return {
|
|
4025
|
+
address: () => address,
|
|
4026
|
+
close: () => server.close(),
|
|
4027
|
+
onConnection: (cb) => server.on("connection", cb)
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
function listenOnce(server, addr) {
|
|
4031
|
+
return new Promise((resolve) => {
|
|
4032
|
+
server.once("error", (err) => resolve(err));
|
|
4033
|
+
server.once("listening", () => resolve(null));
|
|
4034
|
+
server.listen(addr);
|
|
4035
|
+
});
|
|
4036
|
+
}
|
|
4037
|
+
function probeActive(sockPath) {
|
|
4038
|
+
return new Promise((resolve) => {
|
|
4039
|
+
const sock = net2.connect(sockPath);
|
|
4040
|
+
const timer = setTimeout(() => {
|
|
4041
|
+
sock.destroy();
|
|
4042
|
+
resolve(false);
|
|
4043
|
+
}, 400);
|
|
4044
|
+
sock.once("connect", () => {
|
|
4045
|
+
clearTimeout(timer);
|
|
4046
|
+
sock.destroy();
|
|
4047
|
+
resolve(true);
|
|
4048
|
+
});
|
|
4049
|
+
sock.once("error", () => {
|
|
4050
|
+
clearTimeout(timer);
|
|
4051
|
+
resolve(false);
|
|
4052
|
+
});
|
|
4053
|
+
});
|
|
4054
|
+
}
|
|
4055
|
+
function readPortFile(base) {
|
|
4056
|
+
try {
|
|
4057
|
+
const raw = fs17.readFileSync(portFilePath(base), "utf8");
|
|
4058
|
+
return Number.parseInt(raw.trim(), 10) || null;
|
|
4059
|
+
} catch {
|
|
4060
|
+
return null;
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
function writePortFile(base, port) {
|
|
4064
|
+
try {
|
|
4065
|
+
fs17.writeFileSync(portFilePath(base), String(port));
|
|
4066
|
+
} catch {
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
// src/daemon/daemon.ts
|
|
4071
|
+
async function runDaemon() {
|
|
4072
|
+
const data = dataDir();
|
|
4073
|
+
const runtime = runtimeDir();
|
|
4074
|
+
ensureDirSync(path19.join(data, "logs"));
|
|
4075
|
+
const logger = createDaemonLogger(data);
|
|
4076
|
+
logger.log("daemon starting", { pid: process.pid, version: VERSION, platform: process.platform });
|
|
4077
|
+
const lock = acquireLock(runtime);
|
|
4078
|
+
if (lock === null) {
|
|
4079
|
+
logger.log("another daemon is already running; exiting");
|
|
4080
|
+
return;
|
|
4081
|
+
}
|
|
4082
|
+
const transport = await createDaemonTransport(runtime);
|
|
4083
|
+
if (transport === null) {
|
|
4084
|
+
logger.log("transport is taken by another daemon; exiting");
|
|
4085
|
+
lock.release();
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
const store = new StateStore(data);
|
|
4089
|
+
store.load();
|
|
4090
|
+
for (const rec of store.apps.values()) {
|
|
4091
|
+
if (rec.pid !== void 0 && isAlive(rec.pid)) {
|
|
4092
|
+
try {
|
|
4093
|
+
process.kill(-rec.pid, "SIGKILL");
|
|
4094
|
+
} catch {
|
|
4095
|
+
try {
|
|
4096
|
+
process.kill(rec.pid, "SIGKILL");
|
|
4097
|
+
} catch {
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
logger.log(`reaped orphan ${rec.name} pid=${rec.pid}`);
|
|
4101
|
+
}
|
|
4102
|
+
rec.pid = void 0;
|
|
4103
|
+
rec.child = void 0;
|
|
4104
|
+
rec.startTime = void 0;
|
|
4105
|
+
}
|
|
4106
|
+
const autoResurrect = [...store.apps.values()].filter(
|
|
4107
|
+
(rec) => rec.status === "online" || rec.status === "starting" || rec.status === "restarting"
|
|
4108
|
+
);
|
|
4109
|
+
for (const rec of autoResurrect) {
|
|
4110
|
+
rec.status = "stopped";
|
|
4111
|
+
}
|
|
4112
|
+
store.saveSync();
|
|
4113
|
+
store.journal("daemon-start", { pid: process.pid, version: VERSION });
|
|
4114
|
+
const pm = new ProcessManager(store, logger, data);
|
|
4115
|
+
const ctx = { store, pm, logger, transport, lock, base: data };
|
|
4116
|
+
transport.onConnection((socket) => handleConnection(socket, ctx));
|
|
4117
|
+
logger.log("daemon ready", { address: transport.address(), pid: process.pid });
|
|
4118
|
+
for (const rec of autoResurrect) {
|
|
4119
|
+
const cwdFix = fixCwd(rec.config.cwd, rec.config.sourceDir);
|
|
4120
|
+
if (cwdFix.fixed && cwdFix.reason) {
|
|
4121
|
+
logger.log(cwdFix.reason);
|
|
4122
|
+
rec.config.cwd = cwdFix.path;
|
|
4123
|
+
}
|
|
4124
|
+
if (!pathExists(rec.config.cwd)) {
|
|
4125
|
+
logger.log(`autosave: skipping ${rec.name} \u2014 cwd ${rec.config.cwd} does not exist`);
|
|
4126
|
+
rec.status = "errored";
|
|
4127
|
+
rec.error = `cwd does not exist: ${rec.config.cwd}`;
|
|
4128
|
+
continue;
|
|
4129
|
+
}
|
|
4130
|
+
logger.log(`autosave: resurrecting ${rec.name}`);
|
|
4131
|
+
pm.spawnApp(rec);
|
|
4132
|
+
}
|
|
4133
|
+
if (autoResurrect.length > 0) store.saveSync();
|
|
4134
|
+
process.on("SIGTERM", onSignal);
|
|
4135
|
+
process.on("SIGINT", onSignal);
|
|
4136
|
+
process.on(
|
|
4137
|
+
"uncaughtException",
|
|
4138
|
+
(e) => logger.log("uncaughtException", { error: String(e?.stack ?? e) })
|
|
4139
|
+
);
|
|
4140
|
+
process.on("unhandledRejection", (r) => logger.log("unhandledRejection", { error: String(r) }));
|
|
4141
|
+
function onSignal() {
|
|
4142
|
+
logger.log("daemon received stop signal; shutting down");
|
|
4143
|
+
void shutdownDaemon(ctx);
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
async function shutdownDaemon(ctx) {
|
|
4147
|
+
const { store, pm, logger, transport, lock } = ctx;
|
|
4148
|
+
pm.stopAllMonitors();
|
|
4149
|
+
pm.stopAll("stop");
|
|
4150
|
+
await pm.waitForAllStopped(3e3);
|
|
4151
|
+
store.saveSync();
|
|
4152
|
+
store.journal("daemon-stop", {});
|
|
4153
|
+
transport.close();
|
|
4154
|
+
lock.release();
|
|
4155
|
+
logger.log("daemon exited");
|
|
4156
|
+
setTimeout(() => process.exit(0), 50);
|
|
4157
|
+
}
|
|
4158
|
+
function handleConnection(socket, ctx) {
|
|
4159
|
+
socket.setEncoding("utf8");
|
|
4160
|
+
let buffer = "";
|
|
4161
|
+
socket.on("data", (d) => {
|
|
4162
|
+
buffer += typeof d === "string" ? d : d.toString("utf8");
|
|
4163
|
+
let idx = buffer.indexOf("\n");
|
|
4164
|
+
while (idx >= 0) {
|
|
4165
|
+
const line = buffer.slice(0, idx);
|
|
4166
|
+
buffer = buffer.slice(idx + 1);
|
|
4167
|
+
if (line.trim() !== "") void onLine(socket, ctx, line);
|
|
4168
|
+
idx = buffer.indexOf("\n");
|
|
4169
|
+
}
|
|
4170
|
+
});
|
|
4171
|
+
socket.on("error", () => {
|
|
4172
|
+
try {
|
|
4173
|
+
socket.destroy();
|
|
4174
|
+
} catch {
|
|
4175
|
+
}
|
|
4176
|
+
});
|
|
4177
|
+
}
|
|
4178
|
+
async function onLine(socket, ctx, line) {
|
|
4179
|
+
let req;
|
|
4180
|
+
try {
|
|
4181
|
+
req = JSON.parse(line);
|
|
4182
|
+
} catch {
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
if (typeof req.id !== "number" || typeof req.method !== "string") return;
|
|
4186
|
+
const handler = METHODS[req.method];
|
|
4187
|
+
if (handler === void 0) {
|
|
4188
|
+
send(socket, {
|
|
4189
|
+
id: req.id,
|
|
4190
|
+
ok: false,
|
|
4191
|
+
error: { code: "NO_METHOD", message: `unknown method '${req.method}'` }
|
|
4192
|
+
});
|
|
4193
|
+
return;
|
|
4194
|
+
}
|
|
4195
|
+
try {
|
|
4196
|
+
const result = await handler(req.params, ctx);
|
|
4197
|
+
send(socket, { id: req.id, ok: true, result });
|
|
4198
|
+
} catch (err) {
|
|
4199
|
+
const code = err instanceof Error && "code" in err ? String(err.code) : "ERROR";
|
|
4200
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4201
|
+
send(socket, { id: req.id, ok: false, error: { code, message } });
|
|
4202
|
+
}
|
|
4203
|
+
if (req.method === "shutdown") {
|
|
4204
|
+
setImmediate(() => void shutdownDaemon(ctx));
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
function send(socket, obj) {
|
|
4208
|
+
try {
|
|
4209
|
+
socket.write(`${JSON.stringify(obj)}
|
|
4210
|
+
`);
|
|
4211
|
+
} catch {
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
var METHODS = {
|
|
4215
|
+
ping: () => ({
|
|
4216
|
+
version: VERSION,
|
|
4217
|
+
protocol: 1,
|
|
4218
|
+
pid: process.pid,
|
|
4219
|
+
uptime: Math.round(process.uptime())
|
|
4220
|
+
}),
|
|
4221
|
+
list: (_p, { store }) => ({
|
|
4222
|
+
apps: [...store.apps.values()].map(summarize)
|
|
4223
|
+
}),
|
|
4224
|
+
describe: (params, { store }) => {
|
|
4225
|
+
const { name } = params;
|
|
4226
|
+
if (typeof name !== "string" || name === "") {
|
|
4227
|
+
throw new Error("describe: missing app name");
|
|
4228
|
+
}
|
|
4229
|
+
const rec = store.requireApp(name);
|
|
4230
|
+
const { child: _child, sink: _sink, ...rest } = rec;
|
|
4231
|
+
return rest;
|
|
4232
|
+
},
|
|
4233
|
+
start: async (params, ctx) => {
|
|
4234
|
+
const target = params.target;
|
|
4235
|
+
if (target === void 0 || typeof target.path !== "string") {
|
|
4236
|
+
throw new Error("start: missing target");
|
|
4237
|
+
}
|
|
4238
|
+
return startApps(target, ctx);
|
|
4239
|
+
},
|
|
4240
|
+
stop: (params, { store, pm }) => operate("stop", params, store, pm),
|
|
4241
|
+
restart: (params, { store, pm }) => operate("restart", params, store, pm),
|
|
4242
|
+
reload: async (params, ctx) => {
|
|
4243
|
+
const { store, pm, logger } = ctx;
|
|
4244
|
+
const name = params.name;
|
|
4245
|
+
const expanded = name === void 0 || name === "all" ? [...store.apps.values()] : expandTargets(store, name);
|
|
4246
|
+
const results = [];
|
|
4247
|
+
for (const rec of expanded) {
|
|
4248
|
+
if (rec.status !== "online" && rec.status !== "starting") {
|
|
4249
|
+
pm.spawnApp(rec);
|
|
4250
|
+
results.push({ name: rec.name, status: rec.status, pid: rec.pid });
|
|
4251
|
+
continue;
|
|
4252
|
+
}
|
|
4253
|
+
logger.log(`reload: graceful restart of ${rec.name}`);
|
|
4254
|
+
const prevPid = rec.pid;
|
|
4255
|
+
pm.stop(rec, "restart");
|
|
4256
|
+
const start = Date.now();
|
|
4257
|
+
while (Date.now() - start < 5e3) {
|
|
4258
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
4259
|
+
const st = rec.status;
|
|
4260
|
+
if (rec.pid !== void 0 && rec.pid !== prevPid && (st === "online" || st === "starting"))
|
|
4261
|
+
break;
|
|
4262
|
+
if (st === "errored" || st === "stopped") break;
|
|
4263
|
+
}
|
|
4264
|
+
store.saveSync();
|
|
4265
|
+
results.push({ name: rec.name, status: rec.status, pid: rec.pid });
|
|
4266
|
+
}
|
|
4267
|
+
store.saveSync();
|
|
4268
|
+
store.journal("app-reload", { names: results.map((r) => r.name) });
|
|
4269
|
+
return { apps: results };
|
|
4270
|
+
},
|
|
4271
|
+
logs: (params, { store, base }) => {
|
|
4272
|
+
const name = params.name;
|
|
4273
|
+
if (typeof name !== "string" || name === "") {
|
|
4274
|
+
throw new Error("logs: missing app name");
|
|
4275
|
+
}
|
|
4276
|
+
const rec = store.requireApp(name);
|
|
4277
|
+
const paths = resolveLogPaths(rec.config, base);
|
|
4278
|
+
const files = paths.err === null ? [paths.out] : [paths.out, paths.err];
|
|
4279
|
+
return { name, files };
|
|
4280
|
+
},
|
|
4281
|
+
flush: (params, { store, base, logger }) => {
|
|
4282
|
+
const name = params.name;
|
|
4283
|
+
const targets = name === void 0 || name === "all" ? [...store.apps.values()] : [store.requireApp(name)];
|
|
4284
|
+
const apps = targets.map((rec) => {
|
|
4285
|
+
const paths = resolveLogPaths(rec.config, base);
|
|
4286
|
+
const files = paths.err === null ? [paths.out] : [paths.out, paths.err];
|
|
4287
|
+
for (const f of files) {
|
|
4288
|
+
try {
|
|
4289
|
+
ensureDirSync(path19.dirname(f));
|
|
4290
|
+
fs18.writeFileSync(f, "");
|
|
4291
|
+
} catch (err) {
|
|
4292
|
+
logger.log(`flush ${rec.name}: cannot truncate ${f}: ${err.message}`);
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
store.journal("app-flush", { name: rec.name, files });
|
|
4296
|
+
return { name: rec.name, files };
|
|
4297
|
+
});
|
|
4298
|
+
return { apps };
|
|
4299
|
+
},
|
|
4300
|
+
delete: (params, { store, pm }) => {
|
|
4301
|
+
const res = operate("stop", params, store, pm);
|
|
4302
|
+
for (const a of res.apps) {
|
|
4303
|
+
store.apps.delete(a.name);
|
|
4304
|
+
store.journal("app-delete", { name: a.name });
|
|
4305
|
+
}
|
|
4306
|
+
store.saveSync();
|
|
4307
|
+
return res;
|
|
4308
|
+
},
|
|
4309
|
+
rotate: (params, { store, base, logger }) => {
|
|
4310
|
+
const { app, maxSizeBytes, retain } = params;
|
|
4311
|
+
const max = maxSizeBytes ?? 10485760;
|
|
4312
|
+
const n = retain ?? 7;
|
|
4313
|
+
const targets = app === void 0 || app === "all" ? [...store.apps.values()] : [store.requireApp(app)];
|
|
4314
|
+
const results = [];
|
|
4315
|
+
for (const rec of targets) {
|
|
4316
|
+
const paths = resolveLogPaths(rec.config, base);
|
|
4317
|
+
const files = paths.err === null ? [paths.out] : [paths.out, paths.err];
|
|
4318
|
+
const rotated = [];
|
|
4319
|
+
const skipped = [];
|
|
4320
|
+
for (const f of files) {
|
|
4321
|
+
if (!pathExists(f)) continue;
|
|
4322
|
+
try {
|
|
4323
|
+
const stat = fs18.statSync(f);
|
|
4324
|
+
if (stat.size >= max) {
|
|
4325
|
+
rotateFile(f, n);
|
|
4326
|
+
rotated.push(f);
|
|
4327
|
+
} else {
|
|
4328
|
+
skipped.push(f);
|
|
4329
|
+
}
|
|
4330
|
+
} catch (err) {
|
|
4331
|
+
logger.log(`rotate ${rec.name}: cannot rotate ${f}: ${err.message}`);
|
|
4332
|
+
skipped.push(f);
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
if (rotated.length > 0 || skipped.length > 0) {
|
|
4336
|
+
store.journal("app-rotate", { name: rec.name, files: rotated, skipped });
|
|
4337
|
+
results.push({ name: rec.name, rotated, skipped });
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
return results;
|
|
4341
|
+
},
|
|
4342
|
+
metrics: (_params, { store, pm }) => {
|
|
4343
|
+
return {
|
|
4344
|
+
daemon: {
|
|
4345
|
+
pid: process.pid,
|
|
4346
|
+
uptimeMs: Math.round(process.uptime() * 1e3),
|
|
4347
|
+
protocolVersion: 1,
|
|
4348
|
+
appCount: store.apps.size
|
|
4349
|
+
},
|
|
4350
|
+
apps: pm.appMetrics()
|
|
4351
|
+
};
|
|
4352
|
+
},
|
|
4353
|
+
save: (_params, { store, logger }) => {
|
|
4354
|
+
store.saveSync();
|
|
4355
|
+
store.journal("daemon-save", {});
|
|
4356
|
+
logger.log("daemon state saved");
|
|
4357
|
+
return { ok: true, savedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4358
|
+
},
|
|
4359
|
+
resurrect: async (_params, ctx) => {
|
|
4360
|
+
const { store, pm, logger } = ctx;
|
|
4361
|
+
const results = [];
|
|
4362
|
+
for (const rec of store.apps.values()) {
|
|
4363
|
+
if (rec.status === "online" || rec.status === "starting" || rec.status === "restarting") {
|
|
4364
|
+
results.push({ name: rec.name, status: rec.status, message: "already running" });
|
|
4365
|
+
continue;
|
|
4366
|
+
}
|
|
4367
|
+
logger.log(`resurrect: starting ${rec.name}`);
|
|
4368
|
+
pm.spawnApp(rec);
|
|
4369
|
+
results.push({ name: rec.name, status: rec.status, pid: rec.pid, message: rec.error });
|
|
4370
|
+
}
|
|
4371
|
+
store.saveSync();
|
|
4372
|
+
store.journal("daemon-resurrect", { apps: results.length });
|
|
4373
|
+
return { apps: results };
|
|
4374
|
+
},
|
|
4375
|
+
doctor: (_params, { store, base, transport }) => {
|
|
4376
|
+
const checks = [];
|
|
4377
|
+
const snapshotPath = path19.join(base, "state", "snapshot.json");
|
|
4378
|
+
const stateIssues = [];
|
|
4379
|
+
if (pathExists(snapshotPath)) {
|
|
4380
|
+
try {
|
|
4381
|
+
const data = JSON.parse(fs18.readFileSync(snapshotPath, "utf8"));
|
|
4382
|
+
if (data.schema === 1 && typeof data.apps === "object") {
|
|
4383
|
+
stateIssues.push({
|
|
4384
|
+
severity: "ok",
|
|
4385
|
+
code: "SNAPSHOT_OK",
|
|
4386
|
+
message: `snapshot.json valid (schema 1, ${Object.keys(data.apps ?? {}).length} apps)`
|
|
4387
|
+
});
|
|
4388
|
+
} else {
|
|
4389
|
+
stateIssues.push({
|
|
4390
|
+
severity: "error",
|
|
4391
|
+
code: "SNAPSHOT_SCHEMA",
|
|
4392
|
+
message: `unexpected snapshot schema: ${data.schema}`
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
} catch (err) {
|
|
4396
|
+
stateIssues.push({
|
|
4397
|
+
severity: "error",
|
|
4398
|
+
code: "SNAPSHOT_PARSE",
|
|
4399
|
+
message: `cannot parse snapshot.json: ${err.message}`
|
|
4400
|
+
});
|
|
4401
|
+
}
|
|
4402
|
+
} else {
|
|
4403
|
+
stateIssues.push({
|
|
4404
|
+
severity: "warn",
|
|
4405
|
+
code: "SNAPSHOT_MISSING",
|
|
4406
|
+
message: "snapshot.json does not exist yet \u2014 no apps loaded"
|
|
4407
|
+
});
|
|
4408
|
+
}
|
|
4409
|
+
checks.push({
|
|
4410
|
+
name: "State integrity",
|
|
4411
|
+
ok: !stateIssues.some((i) => i.severity === "error"),
|
|
4412
|
+
issues: stateIssues
|
|
4413
|
+
});
|
|
4414
|
+
const lockFile = path19.join(runtimeDir(), "daemon.lock");
|
|
4415
|
+
const lockIssues = [];
|
|
4416
|
+
if (pathExists(lockFile)) {
|
|
4417
|
+
try {
|
|
4418
|
+
const lock = JSON.parse(fs18.readFileSync(lockFile, "utf8"));
|
|
4419
|
+
if (typeof lock.pid === "number" && isAlive(lock.pid)) {
|
|
4420
|
+
lockIssues.push({
|
|
4421
|
+
severity: "ok",
|
|
4422
|
+
code: "LOCK_HELD",
|
|
4423
|
+
message: `lock held by alive daemon pid=${lock.pid}`
|
|
4424
|
+
});
|
|
4425
|
+
} else {
|
|
4426
|
+
lockIssues.push({
|
|
4427
|
+
severity: "error",
|
|
4428
|
+
code: "LOCK_STALE",
|
|
4429
|
+
message: `stale lock file (pid=${lock.pid} not alive)`
|
|
4430
|
+
});
|
|
4431
|
+
}
|
|
4432
|
+
} catch (err) {
|
|
4433
|
+
lockIssues.push({
|
|
4434
|
+
severity: "error",
|
|
4435
|
+
code: "LOCK_PARSE",
|
|
4436
|
+
message: `cannot parse daemon.lock: ${err.message}`
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
} else {
|
|
4440
|
+
lockIssues.push({
|
|
4441
|
+
severity: "error",
|
|
4442
|
+
code: "LOCK_MISSING",
|
|
4443
|
+
message: "daemon.lock missing \u2014 daemon may be unhealthy"
|
|
4444
|
+
});
|
|
4445
|
+
}
|
|
4446
|
+
checks.push({
|
|
4447
|
+
name: "Lock consistency",
|
|
4448
|
+
ok: !lockIssues.some((i) => i.severity === "error"),
|
|
4449
|
+
issues: lockIssues
|
|
4450
|
+
});
|
|
4451
|
+
const orphanIssues = [];
|
|
4452
|
+
const orphans = [];
|
|
4453
|
+
for (const rec of store.apps.values()) {
|
|
4454
|
+
if (rec.pid !== void 0 && rec.status === "online" && !isAlive(rec.pid)) {
|
|
4455
|
+
orphans.push(`${rec.name}(pid=${rec.pid})`);
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
if (orphans.length === 0) {
|
|
4459
|
+
orphanIssues.push({
|
|
4460
|
+
severity: "ok",
|
|
4461
|
+
code: "NO_ORPHANS",
|
|
4462
|
+
message: "all online apps have live pids"
|
|
4463
|
+
});
|
|
4464
|
+
} else {
|
|
4465
|
+
orphanIssues.push({
|
|
4466
|
+
severity: "error",
|
|
4467
|
+
code: "ORPHAN_APPS",
|
|
4468
|
+
message: `online apps with dead pids: ${orphans.join(", ")}`
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
4471
|
+
checks.push({
|
|
4472
|
+
name: "Orphan processes",
|
|
4473
|
+
ok: !orphanIssues.some((i) => i.severity === "error"),
|
|
4474
|
+
issues: orphanIssues
|
|
4475
|
+
});
|
|
4476
|
+
const transportIssues = [];
|
|
4477
|
+
const addr = transport.address();
|
|
4478
|
+
if (addr) {
|
|
4479
|
+
transportIssues.push({
|
|
4480
|
+
severity: "ok",
|
|
4481
|
+
code: "TRANSPORT_OK",
|
|
4482
|
+
message: `listening on ${addr}`
|
|
4483
|
+
});
|
|
4484
|
+
} else {
|
|
4485
|
+
transportIssues.push({
|
|
4486
|
+
severity: "error",
|
|
4487
|
+
code: "TRANSPORT_DOWN",
|
|
4488
|
+
message: "transport is not accepting connections"
|
|
4489
|
+
});
|
|
4490
|
+
}
|
|
4491
|
+
checks.push({
|
|
4492
|
+
name: "Transport health",
|
|
4493
|
+
ok: !transportIssues.some((i) => i.severity === "error"),
|
|
4494
|
+
issues: transportIssues
|
|
4495
|
+
});
|
|
4496
|
+
const uptimeIssues = [
|
|
4497
|
+
{
|
|
4498
|
+
severity: "ok",
|
|
4499
|
+
code: "UPTIME_OK",
|
|
4500
|
+
message: `daemon running uptime=${Math.round(process.uptime())}s pid=${process.pid}`
|
|
4501
|
+
}
|
|
4502
|
+
];
|
|
4503
|
+
checks.push({ name: "Daemon uptime", ok: true, issues: uptimeIssues });
|
|
4504
|
+
return { checks, summary: summarizeChecks(checks) };
|
|
4505
|
+
},
|
|
4506
|
+
shutdown: () => ({ ok: true, message: "shutting down" })
|
|
4507
|
+
};
|
|
4508
|
+
async function startApps(target, ctx) {
|
|
4509
|
+
const { store, pm } = ctx;
|
|
4510
|
+
let apps;
|
|
4511
|
+
let warnings;
|
|
4512
|
+
if (target.type === "config") {
|
|
4513
|
+
const loaded = await loadConfigFile(target.path, { envName: target.envName });
|
|
4514
|
+
apps = loaded.apps;
|
|
4515
|
+
warnings = loaded.warnings;
|
|
4516
|
+
if (target.cliOverrides) {
|
|
4517
|
+
for (const app of apps) applyCliOverrides(app, target.cliOverrides);
|
|
4518
|
+
}
|
|
4519
|
+
} else {
|
|
4520
|
+
const input = {
|
|
4521
|
+
script: target.path,
|
|
4522
|
+
args: target.args,
|
|
4523
|
+
env: target.env
|
|
4524
|
+
};
|
|
4525
|
+
if (target.name !== void 0) input.name = target.name;
|
|
4526
|
+
if (target.interpreter !== void 0) input.interpreter = target.interpreter;
|
|
4527
|
+
if (target.cwd !== void 0) input.cwd = target.cwd;
|
|
4528
|
+
if (target.cliOverrides) Object.assign(input, target.cliOverrides);
|
|
4529
|
+
const single = normalizeSingleApp(input, path19.dirname(target.path), target.envName);
|
|
4530
|
+
apps = [single.app];
|
|
4531
|
+
warnings = single.warnings;
|
|
4532
|
+
}
|
|
4533
|
+
const expanded = [];
|
|
4534
|
+
for (const app of apps) {
|
|
4535
|
+
if (app.instances <= 1) expanded.push(app);
|
|
4536
|
+
else {
|
|
4537
|
+
for (let i = 0; i < app.instances; i++) {
|
|
4538
|
+
const clone = {
|
|
4539
|
+
...app,
|
|
4540
|
+
name: `${app.name}:${i}`,
|
|
4541
|
+
env: { ...app.env, NODE_APP_INSTANCE: String(i) }
|
|
4542
|
+
};
|
|
4543
|
+
expanded.push(clone);
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
const results = [];
|
|
4548
|
+
for (const app of expanded) {
|
|
4549
|
+
const existing = store.apps.get(app.name);
|
|
4550
|
+
if (existing !== void 0 && (existing.status === "online" || existing.status === "restarting" || existing.status === "starting")) {
|
|
4551
|
+
results.push({
|
|
4552
|
+
name: app.name,
|
|
4553
|
+
status: existing.status,
|
|
4554
|
+
pid: existing.pid,
|
|
4555
|
+
message: "already running"
|
|
4556
|
+
});
|
|
4557
|
+
continue;
|
|
4558
|
+
}
|
|
4559
|
+
let record;
|
|
4560
|
+
if (existing === void 0) {
|
|
4561
|
+
record = { name: app.name, config: app, status: "stopped", restarts: 0, crashStreak: 0 };
|
|
4562
|
+
store.apps.set(app.name, record);
|
|
4563
|
+
} else {
|
|
4564
|
+
record = existing;
|
|
4565
|
+
record.config = app;
|
|
4566
|
+
}
|
|
4567
|
+
pm.spawnApp(record);
|
|
4568
|
+
store.saveSync();
|
|
4569
|
+
results.push({ name: app.name, status: record.status, pid: record.pid, message: record.error });
|
|
4570
|
+
}
|
|
4571
|
+
return { apps: results, warnings };
|
|
4572
|
+
}
|
|
4573
|
+
function operate(kind, params, store, pm) {
|
|
4574
|
+
const name = params.name;
|
|
4575
|
+
const targets = name === void 0 || name === "all" ? [...store.apps.values()] : expandTargets(store, name);
|
|
4576
|
+
const result = targets.map((rec) => {
|
|
4577
|
+
pm.stop(rec, kind);
|
|
4578
|
+
store.saveSync();
|
|
4579
|
+
return { name: rec.name, status: rec.status, pid: rec.pid };
|
|
4580
|
+
});
|
|
4581
|
+
return { apps: result };
|
|
4582
|
+
}
|
|
4583
|
+
function expandTargets(store, name) {
|
|
4584
|
+
if (store.apps.has(name)) return [store.requireApp(name)];
|
|
4585
|
+
const matched = [...store.apps.values()].filter(
|
|
4586
|
+
(r) => r.name === name || r.name.startsWith(`${name}:`)
|
|
4587
|
+
);
|
|
4588
|
+
if (matched.length > 0) return matched;
|
|
4589
|
+
return [store.requireApp(name)];
|
|
4590
|
+
}
|
|
4591
|
+
function applyCliOverrides(app, overrides) {
|
|
4592
|
+
if (overrides.instances !== void 0) {
|
|
4593
|
+
const v = overrides.instances;
|
|
4594
|
+
if (v === "max" || v === "MAX") app.instances = os8.cpus().length || 1;
|
|
4595
|
+
else {
|
|
4596
|
+
const n = Number(v);
|
|
4597
|
+
if (Number.isFinite(n) && n >= 1) app.instances = Math.round(n);
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
if (overrides.exec_mode !== void 0) {
|
|
4601
|
+
const m = String(overrides.exec_mode).toLowerCase();
|
|
4602
|
+
if (m === "cluster" || m === "cluster_mode") app.execMode = "cluster";
|
|
4603
|
+
else if (m === "fork" || m === "fork_mode") app.execMode = "fork";
|
|
4604
|
+
}
|
|
4605
|
+
if (overrides.interpreter !== void 0) app.interpreter = String(overrides.interpreter);
|
|
4606
|
+
if (overrides.cwd !== void 0) app.cwd = String(overrides.cwd);
|
|
4607
|
+
if (overrides.max_memory_restart !== void 0) {
|
|
4608
|
+
try {
|
|
4609
|
+
const mm = String(overrides.max_memory_restart);
|
|
4610
|
+
const num = Number(mm);
|
|
4611
|
+
if (Number.isFinite(num)) app.maxMemoryRestart = num;
|
|
4612
|
+
else {
|
|
4613
|
+
const units = {
|
|
4614
|
+
b: 1,
|
|
4615
|
+
kb: 1024,
|
|
4616
|
+
k: 1024,
|
|
4617
|
+
mb: 1024 * 1024,
|
|
4618
|
+
m: 1024 * 1024,
|
|
4619
|
+
gb: 1024 * 1024 * 1024,
|
|
4620
|
+
g: 1024 * 1024 * 1024
|
|
4621
|
+
};
|
|
4622
|
+
const m = mm.toLowerCase().match(/^([0-9.]+)s*([a-z]*)$/);
|
|
4623
|
+
if (m) app.maxMemoryRestart = Math.round(Number(m[1]) * (units[m[2]] ?? 1));
|
|
4624
|
+
}
|
|
4625
|
+
} catch {
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
if (overrides.kill_timeout !== void 0)
|
|
4629
|
+
app.killTimeout = Math.max(0, Number(overrides.kill_timeout) || 1600);
|
|
4630
|
+
if (overrides.restart_delay !== void 0)
|
|
4631
|
+
app.restartDelay = Math.max(0, Number(overrides.restart_delay) || 0);
|
|
4632
|
+
if (overrides.watch_delay !== void 0)
|
|
4633
|
+
app.watchDelay = Math.max(0, Number(overrides.watch_delay) || 1e3);
|
|
4634
|
+
}
|
|
4635
|
+
function summarize(rec) {
|
|
4636
|
+
return {
|
|
4637
|
+
name: rec.name,
|
|
4638
|
+
status: rec.status,
|
|
4639
|
+
pid: rec.pid,
|
|
4640
|
+
startTime: rec.startTime,
|
|
4641
|
+
restarts: rec.restarts,
|
|
4642
|
+
crashStreak: rec.crashStreak,
|
|
4643
|
+
exitCode: rec.exitCode,
|
|
4644
|
+
exitSignal: rec.exitSignal,
|
|
4645
|
+
error: rec.error
|
|
4646
|
+
};
|
|
4647
|
+
}
|
|
4648
|
+
function rotateFile(file, retain) {
|
|
4649
|
+
for (let i = retain - 1; i >= 0; i--) {
|
|
4650
|
+
const oldName = i === 0 ? file : `${file}.${i}.gz`;
|
|
4651
|
+
const newName = `${file}.${i + 1}.gz`;
|
|
4652
|
+
if (pathExists(oldName)) {
|
|
4653
|
+
if (i + 1 >= retain) {
|
|
4654
|
+
fs18.unlinkSync(oldName);
|
|
4655
|
+
} else {
|
|
4656
|
+
if (i === 0) {
|
|
4657
|
+
const content = fs18.readFileSync(file);
|
|
4658
|
+
fs18.unlinkSync(file);
|
|
4659
|
+
const gz = zlib.gzipSync(content);
|
|
4660
|
+
fs18.writeFileSync(newName, gz);
|
|
4661
|
+
} else {
|
|
4662
|
+
fs18.renameSync(oldName, newName);
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
function summarizeChecks(checks) {
|
|
4669
|
+
let ok = 0;
|
|
4670
|
+
let warnings = 0;
|
|
4671
|
+
for (const c of checks) {
|
|
4672
|
+
const hasError = c.issues.some((i) => i.severity === "error");
|
|
4673
|
+
const hasWarn = c.issues.some((i) => i.severity === "warn");
|
|
4674
|
+
if (hasError) {
|
|
4675
|
+
} else if (hasWarn) {
|
|
4676
|
+
warnings++;
|
|
4677
|
+
} else {
|
|
4678
|
+
ok++;
|
|
4679
|
+
}
|
|
4680
|
+
}
|
|
4681
|
+
const errors = checks.filter((c) => !c.ok).length;
|
|
4682
|
+
return { total: checks.length, ok, warnings, errors };
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
// src/cli.ts
|
|
4686
|
+
var COMMANDS = {
|
|
4687
|
+
start: run21,
|
|
4688
|
+
stop: run23,
|
|
4689
|
+
restart: run17,
|
|
4690
|
+
reload: run16,
|
|
4691
|
+
delete: run,
|
|
4692
|
+
list: run11,
|
|
4693
|
+
ls: run11,
|
|
4694
|
+
status: run11,
|
|
4695
|
+
describe: run2,
|
|
4696
|
+
ping: run15,
|
|
4697
|
+
kill: run10,
|
|
4698
|
+
logs: run12,
|
|
4699
|
+
flush: run6,
|
|
4700
|
+
rotate: run19,
|
|
4701
|
+
doctor: run4,
|
|
4702
|
+
metrics: run13,
|
|
4703
|
+
monit: run14,
|
|
4704
|
+
find: run5,
|
|
4705
|
+
"from-caddy": run7,
|
|
4706
|
+
save: run20,
|
|
4707
|
+
resurrect: run18,
|
|
4708
|
+
startup: run22,
|
|
4709
|
+
"import-pm2": run8,
|
|
4710
|
+
"inspect-config": run9,
|
|
4711
|
+
dev: run3,
|
|
4712
|
+
"daemon-upgrade": run24,
|
|
4713
|
+
upgrade: run24,
|
|
4714
|
+
version: async () => {
|
|
4715
|
+
printVersion();
|
|
4716
|
+
return 0;
|
|
4717
|
+
},
|
|
4718
|
+
help: async () => {
|
|
4719
|
+
printHelp();
|
|
4720
|
+
return 0;
|
|
4721
|
+
}
|
|
4722
|
+
};
|
|
4723
|
+
var PLANNED = {};
|
|
4724
|
+
async function run25(argv) {
|
|
4725
|
+
if (process.env.RELIFE2_DAEMON === "1") {
|
|
4726
|
+
await runDaemon();
|
|
4727
|
+
return 0;
|
|
4728
|
+
}
|
|
4729
|
+
const args = argv.slice(2);
|
|
4730
|
+
const command = args[0];
|
|
4731
|
+
if (command === void 0) {
|
|
4732
|
+
if (hasFlag(args, "v", "version") || hasFlag(args, "h", "help")) {
|
|
4733
|
+
if (hasFlag(args, "v", "version")) printVersion();
|
|
4734
|
+
else printHelp();
|
|
4735
|
+
return 0;
|
|
4736
|
+
}
|
|
4737
|
+
printHelp();
|
|
4738
|
+
return 0;
|
|
4739
|
+
}
|
|
4740
|
+
if (COMMANDS[command] !== void 0) {
|
|
4741
|
+
if ((command === "--help" || command === "-h") && args.length === 1) {
|
|
4742
|
+
printHelp();
|
|
4743
|
+
return 0;
|
|
4744
|
+
}
|
|
4745
|
+
if ((command === "--version" || command === "-v") && args.length === 1) {
|
|
4746
|
+
printVersion();
|
|
4747
|
+
return 0;
|
|
4748
|
+
}
|
|
4749
|
+
const impl = COMMANDS[command];
|
|
4750
|
+
return impl(args.slice(1));
|
|
4751
|
+
}
|
|
4752
|
+
if (hasFlag(args, "v", "version")) {
|
|
4753
|
+
printVersion();
|
|
4754
|
+
return 0;
|
|
4755
|
+
}
|
|
4756
|
+
if (hasFlag(args, "h", "help")) {
|
|
4757
|
+
printHelp();
|
|
4758
|
+
return 0;
|
|
4759
|
+
}
|
|
4760
|
+
const milestone = PLANNED[command];
|
|
4761
|
+
if (milestone !== void 0) {
|
|
4762
|
+
console.error(
|
|
4763
|
+
`relife2: command '${command}' is not implemented yet (planned in ${milestone}, see TODO.md)`
|
|
4764
|
+
);
|
|
4765
|
+
return 1;
|
|
4766
|
+
}
|
|
4767
|
+
console.error(`relife2: unknown command '${command}'`);
|
|
4768
|
+
console.error("run 'relife2 --help' to see available commands");
|
|
4769
|
+
return 1;
|
|
4770
|
+
}
|
|
4771
|
+
function hasFlag(args, short, long) {
|
|
4772
|
+
return args.includes(`-${short}`) || args.includes(`--${long}`);
|
|
4773
|
+
}
|
|
4774
|
+
process.exitCode = await run25(process.argv);
|
|
4775
|
+
export {
|
|
4776
|
+
run25 as run
|
|
4777
|
+
};
|
|
4778
|
+
//# sourceMappingURL=cli.js.map
|