@lifeaitools/clauth 1.30.25 → 1.31.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.
Files changed (39) hide show
  1. package/.clauth-skill/SKILL.md +0 -31
  2. package/.clauth-skill/references/keys-guide.md +270 -270
  3. package/.clauth-skill/references/operator-guide.md +0 -27
  4. package/README.md +2 -48
  5. package/cli/api.js +238 -238
  6. package/cli/commands/agent-pool.js +51 -15
  7. package/cli/commands/install.js +396 -396
  8. package/cli/commands/login.js +135 -0
  9. package/cli/commands/login.test.js +73 -0
  10. package/cli/commands/serve.js +9 -815
  11. package/cli/commands/uninstall.js +164 -164
  12. package/cli/commands/watchdog.js +1 -1
  13. package/cli/index.js +29 -20
  14. package/cli/supervisor-registry.js +1 -6
  15. package/cli/watchdog-registry.js +2 -30
  16. package/cli/watchdog-registry.test.js +5 -28
  17. package/cli/webdav-service.js +339 -339
  18. package/install.ps1 +102 -102
  19. package/install.sh +49 -49
  20. package/package.json +4 -6
  21. package/scripts/bin/bootstrap-linux +0 -0
  22. package/scripts/bin/bootstrap-macos +0 -0
  23. package/scripts/bin/bootstrap-win.exe +0 -0
  24. package/scripts/bootstrap.cjs +121 -121
  25. package/scripts/build.mjs +66 -0
  26. package/scripts/build.sh +5 -45
  27. package/scripts/postinstall.js +189 -189
  28. package/supabase/functions/auth-vault/index.ts +350 -350
  29. package/supabase/migrations/001_clauth_schema.sql +94 -94
  30. package/supabase/migrations/002_vault_helpers.sql +90 -90
  31. package/supabase/migrations/20260317_lockout.sql +26 -26
  32. package/cli/commands/ops-install.js +0 -211
  33. package/cli/commands/ops.js +0 -69
  34. package/cli/ops/coolify-adapter.js +0 -80
  35. package/cli/ops/deployment-adapter.js +0 -63
  36. package/cli/ops/job-store.js +0 -116
  37. package/cli/ops/operation-policy.js +0 -51
  38. package/cli/ops/pm2-adapter.js +0 -128
  39. package/cli/ops/serialized-executor.js +0 -9
@@ -1,69 +0,0 @@
1
- const DEFAULT_LOCAL_CLAUTH = "http://127.0.0.1:52437";
2
-
3
- function endpointFrom(opts) {
4
- const approved = process.env.CLAUTH_OPS_APPROVED_ORIGIN;
5
- if (!approved) throw new Error("Ops endpoint unavailable: CLAUTH_OPS_APPROVED_ORIGIN is required");
6
- const approvedUrl = new URL(approved);
7
- const requested = opts.endpoint ? new URL(opts.endpoint) : approvedUrl;
8
- if (approvedUrl.protocol !== "https:" || requested.origin !== approvedUrl.origin) throw new Error("Ops endpoint is not the approved Vultr origin");
9
- return approvedUrl.toString().replace(/\/$/, "");
10
- }
11
-
12
- async function bearer(service = process.env.CLAUTH_OPS_TOKEN_SERVICE || "vultr-ops-api-token") {
13
- const response = await fetch(`${process.env.CLAUTH_LOCAL_URL || DEFAULT_LOCAL_CLAUTH}/v/${encodeURIComponent(service)}`);
14
- if (!response.ok) throw new Error(`${service} unavailable from local clauth`);
15
- const token = (await response.text()).trim();
16
- if (!token) throw new Error("vultr-ops-api-token is empty");
17
- return token;
18
- }
19
-
20
- export async function requestOps({ endpoint, token, method, path, body }) {
21
- const response = await fetch(`${endpoint}${path}`, {
22
- method,
23
- headers: { Authorization: `Bearer ${token}`, Accept: "application/json", ...(body ? { "Content-Type": "application/json" } : {}) },
24
- ...(body ? { body: JSON.stringify(body) } : {}),
25
- });
26
- const payload = await response.json().catch(() => ({ error: "invalid_json_response" }));
27
- if (!response.ok) throw new Error(payload.error || `ops HTTP ${response.status}`);
28
- return payload;
29
- }
30
-
31
- function print(value) {
32
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
33
- }
34
-
35
- export async function runOps(action, opts = {}) {
36
- const endpoint = endpointFrom(opts);
37
- const token = await bearer(opts.tokenService);
38
- const request = (method, path, body) => requestOps({ endpoint, token, method, path, body });
39
- if (action === "catalog") return print(await request("GET", "/v1/ops/catalog"));
40
- if (action === "list") return print(await request("GET", "/v1/ops/processes"));
41
- if (action === "describe") {
42
- if (!opts.target) throw new Error("--target is required for describe");
43
- return print(await request("GET", `/v1/ops/processes/${encodeURIComponent(opts.target)}`));
44
- }
45
- if (action === "run") {
46
- if (!opts.operation) throw new Error("--operation is required for run");
47
- const input = {
48
- ...(opts.target ? { target: opts.target } : {}),
49
- ...(opts.script ? { script: opts.script } : {}),
50
- ...(opts.instances ? { instances: Number(opts.instances) } : {}),
51
- ...(opts.argsJson ? { args: JSON.parse(opts.argsJson) } : {}),
52
- ...(opts.optionsJson ? { options: JSON.parse(opts.optionsJson) } : {}),
53
- };
54
- return print(await request("POST", "/v1/ops/operations", { operation: opts.operation, input }));
55
- }
56
- if (action === "promote") {
57
- if (!opts.application) throw new Error("--application is required for promote");
58
- return print(await request("POST", "/v1/ops/promotions", { application_uuid: opts.application }));
59
- }
60
- if (action === "deploy") {
61
- if (!opts.application) throw new Error("--application is required for deploy");
62
- return print(await request("POST", "/v1/ops/deployments", { application: opts.application, ...(opts.ref ? { ref: opts.ref } : {}) }));
63
- }
64
- if (action === "job") {
65
- if (!opts.job) throw new Error("--job is required for job");
66
- return print(await request("GET", `/v1/ops/jobs/${encodeURIComponent(opts.job)}`));
67
- }
68
- throw new Error(`unknown ops action: ${action}`);
69
- }
@@ -1,80 +0,0 @@
1
- const TERMINAL = new Map([
2
- ["finished", "succeeded"], ["success", "succeeded"], ["successful", "succeeded"],
3
- ["failed", "failed"], ["error", "failed"], ["cancelled", "failed"],
4
- ]);
5
-
6
- function required(value, name) {
7
- if (!value) throw new Error(`${name} is required`);
8
- return value;
9
- }
10
-
11
- export function normalizeCoolifyState(value) {
12
- const raw = String(value || "unknown").toLowerCase();
13
- return TERMINAL.get(raw) || (raw.includes("progress") || raw.includes("queue") || raw.includes("running") ? "running" : "unknown");
14
- }
15
-
16
- /**
17
- * Extract the deployment UUID from a Coolify deploy response.
18
- *
19
- * Coolify's `/api/v1/deploy` does NOT return the UUID at the top level — it
20
- * answers with a `deployments` ARRAY:
21
- *
22
- * { "deployments": [ { "message": "...", "resource_uuid": "...",
23
- * "deployment_uuid": "ha9xjqcldy7duf2diyjdc20p" } ] }
24
- *
25
- * Reading only `body.deployment_uuid` therefore yields undefined, the caller
26
- * concludes the promotion never started, and the job is marked failed — while
27
- * Coolify is in fact building. That happened on a real life.ai promote:
28
- * deployment ha9xjqcldy7duf2diyjdc20p was created at the same second the job
29
- * reported failure. A failed job invites a retry, so the bug turns one
30
- * production deploy into several.
31
- *
32
- * Accepts the array shape first, then the flat shapes, so a future/simplified
33
- * response still resolves.
34
- */
35
- export function deploymentUuidFrom(body) {
36
- if (!body || typeof body !== "object") return null;
37
- const list = Array.isArray(body.deployments) ? body.deployments : [];
38
- for (const entry of list) {
39
- if (!entry || typeof entry !== "object") continue;
40
- const nested = entry.deployment_uuid || entry.uuid || entry.id;
41
- if (typeof nested === "string" && nested) return nested;
42
- }
43
- const flat = body.deployment_uuid || body.uuid || body.id;
44
- return typeof flat === "string" && flat ? flat : null;
45
- }
46
-
47
- export function createCoolifyAdapter({ baseUrl, getToken, fetchImpl = globalThis.fetch } = {}) {
48
- const api = String(required(baseUrl, "baseUrl")).replace(/\/$/, "");
49
- if (typeof getToken !== "function") throw new Error("getToken is required");
50
- async function request(path, options = {}) {
51
- const token = await getToken();
52
- if (!token) throw new Error("coolify credential unavailable");
53
- const headers = new Headers(options.headers || {});
54
- headers.set("Accept", "application/json");
55
- headers.set("Authorization", `Bearer ${token}`);
56
- const response = await fetchImpl(`${api}${path}`, {
57
- ...options,
58
- headers,
59
- });
60
- if (!response.ok) throw new Error(`Coolify HTTP ${response.status}`);
61
- return response.json();
62
- }
63
- return {
64
- inspect(applicationUuid) { return request(`/api/v1/applications/${encodeURIComponent(required(applicationUuid, "applicationUuid"))}`); },
65
- async promote(applicationUuid, options = {}) {
66
- const uuid = encodeURIComponent(required(applicationUuid, "applicationUuid"));
67
- return request(`/api/v1/deploy?uuid=${uuid}&force=true`, options);
68
- },
69
- async deployment(deploymentUuid) { return request(`/api/v1/deployments/${encodeURIComponent(required(deploymentUuid, "deploymentUuid"))}`); },
70
- async poll(deploymentUuid, { attempts = 30, delay = async () => {} } = {}) {
71
- for (let attempt = 1; attempt <= attempts; attempt++) {
72
- const deployment = await this.deployment(deploymentUuid);
73
- const state = normalizeCoolifyState(deployment.status || deployment.state);
74
- if (state === "succeeded" || state === "failed") return { state, deployment, attempts: attempt };
75
- await delay(attempt);
76
- }
77
- return { state: "timed_out", deployment: null, attempts };
78
- },
79
- };
80
- }
@@ -1,63 +0,0 @@
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
- }
@@ -1,116 +0,0 @@
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
- }
@@ -1,51 +0,0 @@
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
- }
@@ -1,128 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
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
- }