@lifeaitools/clauth 1.31.1 → 2.0.1
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/.clauth-skill/SKILL.md +31 -0
- package/.clauth-skill/references/keys-guide.md +270 -270
- package/.clauth-skill/references/operator-guide.md +27 -0
- package/README.md +48 -0
- package/cli/api.js +238 -238
- package/cli/commands/install.js +396 -396
- package/cli/commands/ops-install.js +211 -0
- package/cli/commands/ops.js +69 -0
- package/cli/commands/serve.js +1381 -1644
- package/cli/commands/uninstall.js +164 -164
- package/cli/commands/watchdog.js +1 -1
- package/cli/index.js +165 -1
- package/cli/ops/coolify-adapter.js +80 -0
- package/cli/ops/deployment-adapter.js +63 -0
- package/cli/ops/job-store.js +116 -0
- package/cli/ops/operation-policy.js +51 -0
- package/cli/ops/pm2-adapter.js +128 -0
- package/cli/ops/serialized-executor.js +9 -0
- package/cli/supervisor-registry.js +403 -6
- package/cli/supervisor-registry.test.js +496 -4
- package/cli/supervisor-ui.test.js +436 -0
- package/cli/watchdog-registry.js +30 -2
- package/cli/watchdog-registry.test.js +28 -5
- package/install.ps1 +102 -102
- package/install.sh +49 -49
- package/package.json +4 -3
- package/scripts/bin/bootstrap-linux +0 -0
- package/scripts/bin/bootstrap-macos +0 -0
- package/scripts/bin/bootstrap-win.exe +0 -0
- package/scripts/bootstrap.cjs +121 -121
- package/supabase/functions/auth-vault/index.ts +350 -350
- package/supabase/migrations/001_clauth_schema.sql +94 -94
- package/supabase/migrations/002_vault_helpers.sql +90 -90
- package/supabase/migrations/20260317_lockout.sql +26 -26
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function execFile(command, args, options) {
|
|
4
|
+
return new Promise((resolve, reject) => execFileCallback(command, args, { ...options, windowsHide: true }, (error, stdout, stderr) => {
|
|
5
|
+
if (error) return reject(new Error(`${command} ${args.join(" ")}: ${String(stderr || error.message).trim()}`));
|
|
6
|
+
resolve({ stdout: String(stdout || "").trim(), stderr: String(stderr || "").trim() });
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function requireString(value, name) {
|
|
11
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${name} is required`);
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validateApplication(config) {
|
|
16
|
+
if (!config || typeof config !== "object") throw new Error("deployment_not_registered");
|
|
17
|
+
requireString(config.repo_path, "deployment.repo_path");
|
|
18
|
+
requireString(config.pm2_name, "deployment.pm2_name");
|
|
19
|
+
if (!Array.isArray(config.build) || !config.build.every((value) => typeof value === "string" && value)) throw new Error("deployment.build must be a non-empty argv array");
|
|
20
|
+
if (!Array.isArray(config.allowed_refs) || !config.allowed_refs.every((value) => typeof value === "string" && value)) throw new Error("deployment.allowed_refs must be a string array");
|
|
21
|
+
if (!requireString(config.health_url, "deployment.health_url").startsWith("http")) throw new Error("deployment.health_url must be HTTP(S)");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseDeploymentRegistry(raw = "{}") {
|
|
25
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
26
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("CLAUTH_OPS_DEPLOYMENTS must be a JSON object");
|
|
27
|
+
for (const config of Object.values(parsed)) validateApplication(config);
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createDeploymentAdapter({ deployments, run = execFile, fetch = globalThis.fetch, delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), reload }) {
|
|
32
|
+
if (typeof reload !== "function") throw new Error("reload callback is required");
|
|
33
|
+
return {
|
|
34
|
+
async deploy({ application, ref }) {
|
|
35
|
+
const name = requireString(application, "application");
|
|
36
|
+
const config = deployments[name];
|
|
37
|
+
validateApplication(config);
|
|
38
|
+
const requestedRef = requireString(ref || config.default_ref, "ref");
|
|
39
|
+
if (!config.allowed_refs.includes(requestedRef)) throw new Error("deployment_ref_not_allowed");
|
|
40
|
+
const cwd = config.repo_path;
|
|
41
|
+
const steps = [];
|
|
42
|
+
await run("git", ["fetch", "origin", requestedRef], { cwd });
|
|
43
|
+
steps.push("fetched");
|
|
44
|
+
const sha = (await run("git", ["rev-parse", "FETCH_HEAD"], { cwd })).stdout;
|
|
45
|
+
if (!/^[0-9a-f]{7,64}$/i.test(sha)) throw new Error("deployment_invalid_git_sha");
|
|
46
|
+
await run("git", ["checkout", "--detach", sha], { cwd });
|
|
47
|
+
steps.push("checked_out");
|
|
48
|
+
await run(config.build[0], config.build.slice(1), { cwd });
|
|
49
|
+
steps.push("built");
|
|
50
|
+
await reload(config.pm2_name);
|
|
51
|
+
steps.push("reloaded");
|
|
52
|
+
const attempts = Number(config.health_attempts || 30);
|
|
53
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(config.health_url, { signal: AbortSignal.timeout(Number(config.health_timeout_ms || 5000)) });
|
|
56
|
+
if (response.ok) return { application: name, ref: requestedRef, sha, health_url: config.health_url, attempts: attempt, steps };
|
|
57
|
+
} catch {}
|
|
58
|
+
await delay(Number(config.health_delay_ms || 1000));
|
|
59
|
+
}
|
|
60
|
+
throw new Error("deployment_health_check_timed_out");
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const TERMINAL = new Set(["succeeded", "failed", "timed_out", "rejected"]);
|
|
6
|
+
|
|
7
|
+
export function createJobStore({ now = () => new Date().toISOString(), maxJobs = 500, filePath = null } = {}) {
|
|
8
|
+
const { jobs, recovered } = load(filePath, maxJobs, now);
|
|
9
|
+
const subscribers = new Map();
|
|
10
|
+
const persist = () => {
|
|
11
|
+
if (!filePath) return;
|
|
12
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
13
|
+
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
14
|
+
fs.writeFileSync(tmp, JSON.stringify({ schema: "clauth.ops.jobs.v1", jobs: [...jobs.values()].map(snapshot) }), { mode: 0o600 });
|
|
15
|
+
fs.renameSync(tmp, filePath);
|
|
16
|
+
};
|
|
17
|
+
if (recovered) persist();
|
|
18
|
+
const publish = (id) => {
|
|
19
|
+
const value = get(id);
|
|
20
|
+
for (const listener of subscribers.get(id) || []) listener(value);
|
|
21
|
+
};
|
|
22
|
+
const create = ({ kind, operation, target = null }) => {
|
|
23
|
+
const id = crypto.randomUUID();
|
|
24
|
+
const job = { id, kind, operation, target, phase: "queued", created_at: now(), updated_at: now(), events: [] };
|
|
25
|
+
jobs.set(id, job);
|
|
26
|
+
event(id, "queued");
|
|
27
|
+
while (jobs.size > maxJobs) jobs.delete(jobs.keys().next().value);
|
|
28
|
+
persist();
|
|
29
|
+
return snapshot(job);
|
|
30
|
+
};
|
|
31
|
+
const event = (id, phase, detail = null) => {
|
|
32
|
+
const job = jobs.get(id);
|
|
33
|
+
if (!job) return null;
|
|
34
|
+
if (TERMINAL.has(job.phase)) throw new Error(`job already terminal: ${job.phase}`);
|
|
35
|
+
job.phase = phase;
|
|
36
|
+
job.updated_at = now();
|
|
37
|
+
job.events.push({ phase, at: job.updated_at, detail: detail == null ? null : sanitize(detail) });
|
|
38
|
+
persist();
|
|
39
|
+
publish(id);
|
|
40
|
+
return snapshot(job);
|
|
41
|
+
};
|
|
42
|
+
const get = (id) => jobs.has(id) ? snapshot(jobs.get(id)) : null;
|
|
43
|
+
const subscribe = (id, listener) => {
|
|
44
|
+
if (!jobs.has(id)) return null;
|
|
45
|
+
const listeners = subscribers.get(id) || new Set();
|
|
46
|
+
subscribers.set(id, listeners);
|
|
47
|
+
listeners.add(listener);
|
|
48
|
+
listener(get(id));
|
|
49
|
+
return () => {
|
|
50
|
+
listeners.delete(listener);
|
|
51
|
+
if (!listeners.size) subscribers.delete(id);
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
return { create, event, get, subscribe };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function load(filePath, maxJobs, now) {
|
|
58
|
+
if (!filePath || !fs.existsSync(filePath)) return { jobs: new Map(), recovered: false };
|
|
59
|
+
try {
|
|
60
|
+
const stored = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
61
|
+
const values = Array.isArray(stored?.jobs) ? stored.jobs.slice(-maxJobs) : [];
|
|
62
|
+
let recovered = false;
|
|
63
|
+
const restored = values.filter((job) => job && typeof job.id === "string").map((job) => {
|
|
64
|
+
const safe = snapshot(job);
|
|
65
|
+
if (!TERMINAL.has(safe.phase)) {
|
|
66
|
+
recovered = true;
|
|
67
|
+
safe.phase = "failed";
|
|
68
|
+
safe.updated_at = now();
|
|
69
|
+
safe.events.push({ phase: "failed", at: safe.updated_at, detail: { code: "daemon_restarted" } });
|
|
70
|
+
}
|
|
71
|
+
return [safe.id, safe];
|
|
72
|
+
});
|
|
73
|
+
return { jobs: new Map(restored), recovered };
|
|
74
|
+
} catch {
|
|
75
|
+
return { jobs: new Map(), recovered: false };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sanitize(value) {
|
|
80
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
81
|
+
// `error` is deliberately NOT allowlisted. Free-form error strings are the
|
|
82
|
+
// classic credential-leak path -- an upstream message like
|
|
83
|
+
// `API_KEY=... rejected` would be persisted verbatim to disk and served over
|
|
84
|
+
// the API. ops-job-store.test.mjs pins this behaviour with an explicit
|
|
85
|
+
// API_KEY=hidden payload; do not "fix" a missing failure reason by adding
|
|
86
|
+
// `error` here.
|
|
87
|
+
//
|
|
88
|
+
// Producers that need a machine-readable failure reason must emit `code` with
|
|
89
|
+
// an enumerated value (see submitPromotionJob's coolify_* codes). That keeps
|
|
90
|
+
// diagnosis available without letting arbitrary upstream text through.
|
|
91
|
+
const allowed = new Set(["code", "checkout_sha", "ref", "pm2_name", "health_url", "health_status", "deployment_uuid", "status", "processes"]);
|
|
92
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => {
|
|
93
|
+
if (!allowed.has(key)) return [];
|
|
94
|
+
if (key === "processes" && Array.isArray(entry)) {
|
|
95
|
+
return [[key, entry.filter((process) => process && typeof process === "object").map((process) => ({
|
|
96
|
+
name: typeof process.name === "string" ? process.name : null,
|
|
97
|
+
pm_id: typeof process.pm_id === "number" ? process.pm_id : null,
|
|
98
|
+
pid: typeof process.pid === "number" ? process.pid : null,
|
|
99
|
+
status: typeof process.status === "string" ? process.status : null,
|
|
100
|
+
namespace: typeof process.namespace === "string" ? process.namespace : null,
|
|
101
|
+
}))]];
|
|
102
|
+
}
|
|
103
|
+
return (typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean" || entry === null) ? [[key, entry]] : [];
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function snapshot(job) {
|
|
108
|
+
return JSON.parse(JSON.stringify({
|
|
109
|
+
...job,
|
|
110
|
+
events: job.events.map((event) => ({
|
|
111
|
+
phase: event.phase,
|
|
112
|
+
at: event.at,
|
|
113
|
+
detail: sanitize(event.detail),
|
|
114
|
+
})),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PM2_OPERATION_CATALOG, isReadOperation } from "./pm2-adapter.js";
|
|
2
|
+
|
|
3
|
+
function configuredProfile(config, role) {
|
|
4
|
+
if (config?.[role] && typeof config[role] === "object" && !Array.isArray(config[role])) return config[role];
|
|
5
|
+
return config && typeof config === "object" && !Array.isArray(config) ? config : {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function targetFrom(input = {}) {
|
|
9
|
+
const value = input.target || input.name || input.options?.name || (Array.isArray(input.args) ? input.args[0] : null);
|
|
10
|
+
return value == null ? null : String(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function enabled(enabled, operation) {
|
|
14
|
+
return enabled.has(operation) || enabled.has("*");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createOperationPolicy({ enabled: agentEnabled = [], applications = {}, adminEnabled = [], adminApplications = {}, allowHostWide = false } = {}) {
|
|
18
|
+
const agentEnabledSet = new Set(agentEnabled);
|
|
19
|
+
const adminEnabledSet = new Set(adminEnabled);
|
|
20
|
+
return {
|
|
21
|
+
authorize(operation, input = {}, role = "agent") {
|
|
22
|
+
if (!PM2_OPERATION_CATALOG[operation]) return { ok: false, code: "service_not_available" };
|
|
23
|
+
const isAdmin = role === "admin";
|
|
24
|
+
const profile = configuredProfile(isAdmin ? adminApplications : applications, role);
|
|
25
|
+
const allowed = Array.isArray(profile[operation])
|
|
26
|
+
? profile[operation].filter((target) => typeof target === "string" && target.length > 0)
|
|
27
|
+
: [];
|
|
28
|
+
const target = targetFrom(input);
|
|
29
|
+
const hostWide = operation === "kill_daemon" || operation === "pm2_killDaemon" || target === "all";
|
|
30
|
+
|
|
31
|
+
// The public catalog remains complete. Only the remote admin capability
|
|
32
|
+
// may execute raw PM2 calls or host-level lifecycle actions, and every
|
|
33
|
+
// one still has to be enabled and allowlisted in Vultr configuration.
|
|
34
|
+
if (!isAdmin && (operation.startsWith("pm2_") || ["start", "dump", "resurrect", "kill_daemon", "startup", "unstartup", "update", "deep_update"].includes(operation))) {
|
|
35
|
+
return { ok: false, code: "service_not_available" };
|
|
36
|
+
}
|
|
37
|
+
if (hostWide && (!isAdmin || !allowHostWide)) return { ok: false, code: "service_not_available" };
|
|
38
|
+
if (!isReadOperation(operation) && !enabled(isAdmin ? adminEnabledSet : agentEnabledSet, operation)) {
|
|
39
|
+
return { ok: false, code: "service_not_available" };
|
|
40
|
+
}
|
|
41
|
+
if (!allowed.length) return { ok: false, code: "service_not_available" };
|
|
42
|
+
if (hostWide) {
|
|
43
|
+
if (!allowed.includes("*") && !allowed.includes("all")) return { ok: false, code: "service_not_available" };
|
|
44
|
+
return { ok: true, mode: isReadOperation(operation) ? "read" : "mutate", allowed_targets: allowed };
|
|
45
|
+
}
|
|
46
|
+
if (operation === "list" || operation === "ping") return { ok: true, mode: isReadOperation(operation) ? "read" : "mutate", allowed_targets: allowed };
|
|
47
|
+
if (!target || (!allowed.includes("*") && !allowed.includes(target))) return { ok: false, code: "service_not_available" };
|
|
48
|
+
return { ok: true, mode: isReadOperation(operation) ? "read" : "mutate", allowed_targets: allowed };
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const READ_OPERATIONS = new Set(["list", "describe", "ping", "logs", "bus", "pm2_list", "pm2_describe", "pm2_ping", "pm2_get", "pm2_getPID", "pm2_getVersion", "pm2_inspect", "pm2_jlist", "pm2_slist", "pm2_monitorState"]);
|
|
2
|
+
|
|
3
|
+
// Derived from PM2 v7's public API prototype. We intentionally expose every
|
|
4
|
+
// method through a stable prefixed operation name; potentially destructive or
|
|
5
|
+
// interactive methods stay policy-disabled unless an operator explicitly opts in.
|
|
6
|
+
export const PM2_PUBLIC_METHODS = Object.freeze([
|
|
7
|
+
"agentInfos", "attach", "autodump", "autoinstall", "backward", "boilerplate", "clearDump", "clearSetup", "close", "conf", "connect", "dashboard", "deepUpdate", "delete", "deleteModule", "deploy", "describe", "destroy", "disconnect", "dockerMode", "dump", "env", "exitCli", "flush", "forward", "generateDockerfile", "generateModuleSample", "generateSample", "get", "getPID", "getProcessIdByName", "getVersion", "inspect", "install", "jlist", "kill", "killAgent", "killDaemon", "launchAll", "launchBus", "launchModules", "launchSysMonitoring", "link", "linkManagement", "list", "logrotate", "minimumSetup", "monit", "monitorState", "msgProcess", "multiset", "openDashboard", "package", "ping", "printLogs", "profile", "publish", "pullAndReload", "pullAndRestart", "pullCommitId", "reload", "reloadLogs", "remote", "remoteV2", "report", "reset", "restart", "resurrect", "scale", "sendDataToProcessId", "sendLineToStdin", "sendSignalToProcessId", "sendSignalToProcessName", "serve", "set", "slist", "speedList", "start", "startup", "stop", "streamLogs", "trigger", "uninstall", "uninstallStartup", "unlink", "unset", "update",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const CORE_OPERATIONS = {
|
|
11
|
+
list: { mode: "read", args: [] },
|
|
12
|
+
describe: { mode: "read", args: ["target"] },
|
|
13
|
+
ping: { mode: "read", args: [] },
|
|
14
|
+
logs: { mode: "read", args: ["target"] },
|
|
15
|
+
bus: { mode: "read", args: [] },
|
|
16
|
+
start: { mode: "mutate", args: ["script", "options"] },
|
|
17
|
+
stop: { mode: "mutate", args: ["target"] },
|
|
18
|
+
restart: { mode: "mutate", args: ["target", "options"] },
|
|
19
|
+
reload: { mode: "mutate", args: ["target", "options"] },
|
|
20
|
+
delete: { mode: "mutate", args: ["target"] },
|
|
21
|
+
scale: { mode: "mutate", args: ["target", "instances"] },
|
|
22
|
+
reset: { mode: "mutate", args: ["target"] },
|
|
23
|
+
dump: { mode: "mutate", args: [] },
|
|
24
|
+
resurrect: { mode: "mutate", args: [] },
|
|
25
|
+
kill_daemon: { mode: "mutate", args: [] },
|
|
26
|
+
startup: { mode: "mutate", args: ["platform", "options"] },
|
|
27
|
+
unstartup: { mode: "mutate", args: ["platform"] },
|
|
28
|
+
update: { mode: "mutate", args: [] },
|
|
29
|
+
deep_update: { mode: "mutate", args: [] },
|
|
30
|
+
send_data: { mode: "mutate", args: ["target", "packet"] },
|
|
31
|
+
send_signal: { mode: "mutate", args: ["target", "signal"] },
|
|
32
|
+
trigger: { mode: "mutate", args: ["target", "action", "params"] },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const PM2_OPERATION_CATALOG = Object.freeze({
|
|
36
|
+
...CORE_OPERATIONS,
|
|
37
|
+
...Object.fromEntries(PM2_PUBLIC_METHODS.map((method) => [`pm2_${method}`, {
|
|
38
|
+
mode: READ_OPERATIONS.has(`pm2_${method}`) ? "read" : "mutate",
|
|
39
|
+
args: ["args"],
|
|
40
|
+
pm2_method: method,
|
|
41
|
+
}])),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function missing(value, name) {
|
|
45
|
+
if (value === undefined || value === null || value === "") throw new Error(`${name} is required`);
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function callbackCall(pm2, method, args = []) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
if (typeof pm2?.[method] !== "function") return reject(new Error(`PM2 method unavailable: ${method}`));
|
|
52
|
+
pm2[method](...args, (error, result) => error ? reject(error) : resolve(result));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeProcess(process) {
|
|
57
|
+
if (!process || typeof process !== "object") return process;
|
|
58
|
+
return {
|
|
59
|
+
name: process.name || process.pm2_env?.name || null,
|
|
60
|
+
pm_id: process.pm_id ?? process.pm2_env?.pm_id ?? null,
|
|
61
|
+
pid: process.pid ?? null,
|
|
62
|
+
status: process.pm2_env?.status ?? process.status ?? null,
|
|
63
|
+
restart_time: process.pm2_env?.restart_time ?? null,
|
|
64
|
+
created_at: process.pm2_env?.created_at ?? null,
|
|
65
|
+
cwd: process.pm2_env?.pm_cwd ?? null,
|
|
66
|
+
script: process.pm2_env?.pm_exec_path ?? null,
|
|
67
|
+
namespace: process.pm2_env?.namespace ?? null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeProcessResult(result) {
|
|
72
|
+
// PM2's lower-level aliases (jlist, inspect, get) can return the same rich
|
|
73
|
+
// process object as list/describe. Normalize it at the adapter boundary so a
|
|
74
|
+
// future caller cannot accidentally serialize pm2_env or other credentials.
|
|
75
|
+
if (Array.isArray(result)) {
|
|
76
|
+
return result.map((item) => item && typeof item === "object" && (item.pm2_env || "pm_id" in item || "pid" in item)
|
|
77
|
+
? normalizeProcess(item)
|
|
78
|
+
: item);
|
|
79
|
+
}
|
|
80
|
+
if (result && typeof result === "object" && (result.pm2_env || "pm_id" in result || "pid" in result)) return normalizeProcess(result);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createPm2Adapter(pm2) {
|
|
85
|
+
if (!pm2) throw new Error("pm2 facade is required");
|
|
86
|
+
return {
|
|
87
|
+
catalog() { return PM2_OPERATION_CATALOG; },
|
|
88
|
+
async connect() { await callbackCall(pm2, "connect", [false]); return { connected: true }; },
|
|
89
|
+
async disconnect() { await callbackCall(pm2, "disconnect"); return { connected: false }; },
|
|
90
|
+
async execute(operation, input = {}) {
|
|
91
|
+
if (!PM2_OPERATION_CATALOG[operation]) throw new Error(`unknown PM2 operation: ${operation}`);
|
|
92
|
+
if (operation.startsWith("pm2_")) {
|
|
93
|
+
const method = PM2_OPERATION_CATALOG[operation].pm2_method;
|
|
94
|
+
const args = Array.isArray(input.args) ? input.args : [];
|
|
95
|
+
return normalizeProcessResult(await callbackCall(pm2, method, args));
|
|
96
|
+
}
|
|
97
|
+
switch (operation) {
|
|
98
|
+
case "list": return (await callbackCall(pm2, "list")).map(normalizeProcess);
|
|
99
|
+
case "describe": return (await callbackCall(pm2, "describe", [missing(input.target, "target")])).map(normalizeProcess);
|
|
100
|
+
case "ping": return { connected: true, processes: (await callbackCall(pm2, "list")).length };
|
|
101
|
+
case "logs": return (await callbackCall(pm2, "describe", [missing(input.target, "target")])).map(normalizeProcess);
|
|
102
|
+
case "bus": return callbackCall(pm2, "launchBus");
|
|
103
|
+
case "start": return callbackCall(pm2, "start", [missing(input.script, "script"), input.options || {}]);
|
|
104
|
+
case "stop": return callbackCall(pm2, "stop", [missing(input.target, "target")]);
|
|
105
|
+
case "restart": return callbackCall(pm2, "restart", [missing(input.target, "target"), input.options || {}]);
|
|
106
|
+
case "reload": return callbackCall(pm2, "reload", [missing(input.target, "target"), input.options || {}]);
|
|
107
|
+
case "delete": return callbackCall(pm2, "delete", [missing(input.target, "target")]);
|
|
108
|
+
case "scale": return callbackCall(pm2, "scale", [missing(input.target, "target"), missing(input.instances, "instances")]);
|
|
109
|
+
case "reset": return callbackCall(pm2, "reset", [missing(input.target, "target")]);
|
|
110
|
+
case "dump": return callbackCall(pm2, "dump");
|
|
111
|
+
case "resurrect": return callbackCall(pm2, "resurrect");
|
|
112
|
+
case "kill_daemon": return callbackCall(pm2, "killDaemon");
|
|
113
|
+
case "startup": return callbackCall(pm2, "startup", [input.platform, input.options || {}]);
|
|
114
|
+
case "unstartup": return callbackCall(pm2, "uninstallStartup", [input.platform]);
|
|
115
|
+
case "update": return callbackCall(pm2, "update");
|
|
116
|
+
case "deep_update": return callbackCall(pm2, "deepUpdate");
|
|
117
|
+
case "send_data": return callbackCall(pm2, "sendDataToProcessId", [missing(input.target, "target"), missing(input.packet, "packet")]);
|
|
118
|
+
case "send_signal": return callbackCall(pm2, "sendSignalToProcessName", [missing(input.target, "target"), input.signal || "SIGINT"]);
|
|
119
|
+
case "trigger": return callbackCall(pm2, "trigger", [missing(input.target, "target"), missing(input.action, "action"), input.params || {}]);
|
|
120
|
+
default: throw new Error(`unimplemented PM2 operation: ${operation}`);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isReadOperation(operation) {
|
|
127
|
+
return READ_OPERATIONS.has(operation);
|
|
128
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function createSerializedExecutor() {
|
|
2
|
+
let tail = Promise.resolve();
|
|
3
|
+
return function execute(task) {
|
|
4
|
+
if (typeof task !== "function") throw new Error("task must be a function");
|
|
5
|
+
const run = tail.then(task, task);
|
|
6
|
+
tail = run.catch(() => {});
|
|
7
|
+
return run;
|
|
8
|
+
};
|
|
9
|
+
}
|