@opentrust/cli 7.3.18 → 7.3.21
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/dist/commands/connect.d.ts +2 -0
- package/dist/commands/connect.js +192 -0
- package/dist/commands/init.js +3 -3
- package/dist/index.js +2 -0
- package/dist/lib/command-handler.d.ts +7 -0
- package/dist/lib/command-handler.js +141 -0
- package/dist/lib/dashboard-client.d.ts +41 -0
- package/dist/lib/dashboard-client.js +93 -0
- package/dist/lib/host-reporter.d.ts +9 -0
- package/dist/lib/host-reporter.js +59 -0
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { DashboardClient, loadConfig, saveConfig, clearConfig, } from "../lib/dashboard-client.js";
|
|
2
|
+
import { getHostInfo, getServicesSnapshot, getSystemMetadata } from "../lib/host-reporter.js";
|
|
3
|
+
import { executeHostCommand } from "../lib/command-handler.js";
|
|
4
|
+
import { projectMode, projectRoot } from "../lib/paths.js";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
const HEARTBEAT_INTERVAL = 30_000;
|
|
10
|
+
const POLL_INTERVAL = 10_000;
|
|
11
|
+
const PID_FILE = path.join(os.homedir(), ".opentrust", "connect.pid");
|
|
12
|
+
export function registerConnectCommand(program) {
|
|
13
|
+
program
|
|
14
|
+
.command("connect")
|
|
15
|
+
.description("Connect this host to an OpenTrust Dashboard for remote management")
|
|
16
|
+
.option("--dashboard-url <url>", "Dashboard URL (e.g. http://host:53667)")
|
|
17
|
+
.option("--api-key <key>", "Core API key (sk-og-...)")
|
|
18
|
+
.option("--daemon", "Run in background as a daemon process")
|
|
19
|
+
.action(async (opts) => {
|
|
20
|
+
let config = loadConfig();
|
|
21
|
+
if (opts.dashboardUrl && opts.apiKey) {
|
|
22
|
+
config = {
|
|
23
|
+
dashboardUrl: opts.dashboardUrl,
|
|
24
|
+
apiKey: opts.apiKey,
|
|
25
|
+
hostId: config?.hostId,
|
|
26
|
+
};
|
|
27
|
+
saveConfig(config);
|
|
28
|
+
console.log("Configuration saved to ~/.opentrust/client.json");
|
|
29
|
+
}
|
|
30
|
+
if (!config?.dashboardUrl || !config?.apiKey) {
|
|
31
|
+
console.error("No connection configured.");
|
|
32
|
+
console.error("");
|
|
33
|
+
console.error("Usage:");
|
|
34
|
+
console.error(" opentrust connect --dashboard-url http://host:53667 --api-key sk-og-xxx");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (opts.daemon) {
|
|
38
|
+
launchDaemon(config);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
await runDaemon(config);
|
|
42
|
+
});
|
|
43
|
+
program
|
|
44
|
+
.command("disconnect")
|
|
45
|
+
.description("Disconnect this host from Dashboard and clear configuration")
|
|
46
|
+
.action(async () => {
|
|
47
|
+
const config = loadConfig();
|
|
48
|
+
if (config?.dashboardUrl && config?.apiKey) {
|
|
49
|
+
try {
|
|
50
|
+
const client = new DashboardClient(config);
|
|
51
|
+
await client.disconnect();
|
|
52
|
+
console.log("Host unregistered from Dashboard.");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Dashboard may be unreachable; still clear local config
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
stopDaemon();
|
|
59
|
+
clearConfig();
|
|
60
|
+
console.log("Configuration cleared. Host disconnected.");
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function launchDaemon(config) {
|
|
64
|
+
if (fs.existsSync(PID_FILE)) {
|
|
65
|
+
const existingPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
66
|
+
try {
|
|
67
|
+
process.kill(existingPid, 0);
|
|
68
|
+
console.log(`Connect daemon already running (PID ${existingPid}).`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// stale pid file
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const logFile = path.join(os.homedir(), ".opentrust", "logs", "connect.log");
|
|
76
|
+
const logDir = path.dirname(logFile);
|
|
77
|
+
if (!fs.existsSync(logDir))
|
|
78
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
79
|
+
const logFd = fs.openSync(logFile, "a");
|
|
80
|
+
const child = spawn(process.execPath, [process.argv[1], "connect"], {
|
|
81
|
+
cwd: process.cwd(),
|
|
82
|
+
env: process.env,
|
|
83
|
+
stdio: ["ignore", logFd, logFd],
|
|
84
|
+
detached: true,
|
|
85
|
+
});
|
|
86
|
+
if (child.pid) {
|
|
87
|
+
fs.writeFileSync(PID_FILE, String(child.pid), "utf-8");
|
|
88
|
+
child.unref();
|
|
89
|
+
fs.closeSync(logFd);
|
|
90
|
+
console.log(`Connect daemon started (PID ${child.pid}).`);
|
|
91
|
+
console.log(`Logs: ${logFile}`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
fs.closeSync(logFd);
|
|
95
|
+
console.error("Failed to launch daemon.");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function stopDaemon() {
|
|
100
|
+
if (!fs.existsSync(PID_FILE))
|
|
101
|
+
return;
|
|
102
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
103
|
+
try {
|
|
104
|
+
process.kill(pid, "SIGTERM");
|
|
105
|
+
console.log(`Daemon stopped (PID ${pid}).`);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// already dead
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
fs.unlinkSync(PID_FILE);
|
|
112
|
+
}
|
|
113
|
+
catch { }
|
|
114
|
+
}
|
|
115
|
+
async function runDaemon(config) {
|
|
116
|
+
const client = new DashboardClient(config);
|
|
117
|
+
const log = (msg) => console.log(`[${new Date().toISOString()}] ${msg}`);
|
|
118
|
+
// Register
|
|
119
|
+
log("Registering with Dashboard...");
|
|
120
|
+
const hostInfo = getHostInfo();
|
|
121
|
+
const services = await getServicesSnapshot();
|
|
122
|
+
const metadata = getSystemMetadata();
|
|
123
|
+
try {
|
|
124
|
+
const hostId = await client.register({
|
|
125
|
+
...hostInfo,
|
|
126
|
+
projectMode,
|
|
127
|
+
projectRoot,
|
|
128
|
+
services,
|
|
129
|
+
metadata,
|
|
130
|
+
});
|
|
131
|
+
config.hostId = hostId;
|
|
132
|
+
saveConfig(config);
|
|
133
|
+
log(`Registered as host ${hostId}`);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
log(`Registration failed: ${err.message}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
// Heartbeat
|
|
140
|
+
const heartbeat = async () => {
|
|
141
|
+
try {
|
|
142
|
+
const svc = await getServicesSnapshot();
|
|
143
|
+
const meta = getSystemMetadata();
|
|
144
|
+
await client.heartbeat({ services: svc, metadata: meta });
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
log(`Heartbeat failed: ${err.message}`);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const heartbeatTimer = setInterval(heartbeat, HEARTBEAT_INTERVAL);
|
|
151
|
+
// Command polling
|
|
152
|
+
const poll = async () => {
|
|
153
|
+
try {
|
|
154
|
+
const cmds = await client.fetchPendingCommands();
|
|
155
|
+
for (const cmd of cmds) {
|
|
156
|
+
log(`Executing command: ${cmd.type} (${cmd.id})`);
|
|
157
|
+
await client.ackCommand(cmd.id, "running").catch(() => { });
|
|
158
|
+
const result = executeHostCommand(cmd);
|
|
159
|
+
await client
|
|
160
|
+
.ackCommand(cmd.id, result.success ? "completed" : "failed", {
|
|
161
|
+
output: result.output,
|
|
162
|
+
error: result.error,
|
|
163
|
+
})
|
|
164
|
+
.catch(() => { });
|
|
165
|
+
log(`Command ${cmd.id}: ${result.success ? "completed" : "failed"}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Dashboard may be unreachable
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const pollTimer = setInterval(poll, POLL_INTERVAL);
|
|
173
|
+
poll();
|
|
174
|
+
// Graceful shutdown
|
|
175
|
+
const shutdown = async () => {
|
|
176
|
+
log("Shutting down...");
|
|
177
|
+
clearInterval(heartbeatTimer);
|
|
178
|
+
clearInterval(pollTimer);
|
|
179
|
+
try {
|
|
180
|
+
await client.heartbeat({ services: await getServicesSnapshot(), metadata: { ...getSystemMetadata(), shuttingDown: true } });
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
try {
|
|
184
|
+
fs.unlinkSync(PID_FILE);
|
|
185
|
+
}
|
|
186
|
+
catch { }
|
|
187
|
+
process.exit(0);
|
|
188
|
+
};
|
|
189
|
+
process.on("SIGTERM", shutdown);
|
|
190
|
+
process.on("SIGINT", shutdown);
|
|
191
|
+
log("Connected. Listening for commands... (Ctrl+C to stop)");
|
|
192
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -13,9 +13,9 @@ const SCAFFOLD_PKG = {
|
|
|
13
13
|
status: "opentrust status",
|
|
14
14
|
},
|
|
15
15
|
dependencies: {
|
|
16
|
-
"@opentrust/core": "^7.3.
|
|
17
|
-
"@opentrust/gateway": "^7.3.
|
|
18
|
-
"@opentrust/dashboard": "^7.3.
|
|
16
|
+
"@opentrust/core": "^7.3.21",
|
|
17
|
+
"@opentrust/gateway": "^7.3.21",
|
|
18
|
+
"@opentrust/dashboard": "^7.3.21",
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
21
|
const ENV_TEMPLATE = `# OpenTrust Configuration
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerStatusCommand } from "./commands/status.js";
|
|
|
10
10
|
import { registerSetupCommand } from "./commands/setup.js";
|
|
11
11
|
import { registerLogsCommand } from "./commands/logs.js";
|
|
12
12
|
import { registerUpgradeCommand } from "./commands/upgrade.js";
|
|
13
|
+
import { registerConnectCommand } from "./commands/connect.js";
|
|
13
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
15
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
15
16
|
const program = new Command();
|
|
@@ -24,4 +25,5 @@ registerStopCommand(program);
|
|
|
24
25
|
registerStatusCommand(program);
|
|
25
26
|
registerLogsCommand(program);
|
|
26
27
|
registerUpgradeCommand(program);
|
|
28
|
+
registerConnectCommand(program);
|
|
27
29
|
program.parse();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { startService, stopService, getStatus, SERVICES } from "./process-manager.js";
|
|
5
|
+
import { paths, projectRoot, projectMode } from "./paths.js";
|
|
6
|
+
const SERVICE_KEYS = Object.keys(SERVICES);
|
|
7
|
+
export function executeHostCommand(cmd) {
|
|
8
|
+
const payload = cmd.payload ?? {};
|
|
9
|
+
switch (cmd.type) {
|
|
10
|
+
case "start_service":
|
|
11
|
+
return handleStartService(payload);
|
|
12
|
+
case "stop_service":
|
|
13
|
+
return handleStopService(payload);
|
|
14
|
+
case "restart_service":
|
|
15
|
+
return handleRestartService(payload);
|
|
16
|
+
case "upgrade":
|
|
17
|
+
return handleUpgrade(payload);
|
|
18
|
+
case "fetch_logs":
|
|
19
|
+
return handleFetchLogs(payload);
|
|
20
|
+
case "update_env":
|
|
21
|
+
return handleUpdateEnv(payload);
|
|
22
|
+
default:
|
|
23
|
+
return { success: false, error: `Unknown command type: ${cmd.type}` };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function handleStartService(payload) {
|
|
27
|
+
const service = payload.service;
|
|
28
|
+
if (!service)
|
|
29
|
+
return { success: false, error: "Missing service in payload" };
|
|
30
|
+
const keys = service === "all" ? SERVICE_KEYS : [service];
|
|
31
|
+
const results = [];
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
if (!SERVICES[key]) {
|
|
34
|
+
results.push(`${key}: unknown service`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const ok = startService(key);
|
|
38
|
+
results.push(`${key}: ${ok ? "started" : "failed to start"}`);
|
|
39
|
+
}
|
|
40
|
+
return { success: true, output: results.join("\n") };
|
|
41
|
+
}
|
|
42
|
+
function handleStopService(payload) {
|
|
43
|
+
const service = payload.service;
|
|
44
|
+
if (!service)
|
|
45
|
+
return { success: false, error: "Missing service in payload" };
|
|
46
|
+
const keys = service === "all" ? SERVICE_KEYS : [service];
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
if (!SERVICES[key]) {
|
|
50
|
+
results.push(`${key}: unknown service`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const ok = stopService(key);
|
|
54
|
+
results.push(`${key}: ${ok ? "stopped" : "was not running"}`);
|
|
55
|
+
}
|
|
56
|
+
return { success: true, output: results.join("\n") };
|
|
57
|
+
}
|
|
58
|
+
function handleRestartService(payload) {
|
|
59
|
+
const service = payload.service;
|
|
60
|
+
if (!service)
|
|
61
|
+
return { success: false, error: "Missing service in payload" };
|
|
62
|
+
const keys = service === "all" ? SERVICE_KEYS : [service];
|
|
63
|
+
const results = [];
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
if (!SERVICES[key]) {
|
|
66
|
+
results.push(`${key}: unknown service`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const status = getStatus(key);
|
|
70
|
+
if (status.running)
|
|
71
|
+
stopService(key);
|
|
72
|
+
const ok = startService(key);
|
|
73
|
+
results.push(`${key}: ${ok ? "restarted" : "failed to restart"}`);
|
|
74
|
+
}
|
|
75
|
+
return { success: true, output: results.join("\n") };
|
|
76
|
+
}
|
|
77
|
+
function handleUpgrade(payload) {
|
|
78
|
+
const version = payload.version || "latest";
|
|
79
|
+
try {
|
|
80
|
+
const results = [];
|
|
81
|
+
const cliOut = execSync(`npm install -g @opentrust/cli@${version}`, {
|
|
82
|
+
encoding: "utf-8",
|
|
83
|
+
timeout: 120_000,
|
|
84
|
+
});
|
|
85
|
+
results.push(`CLI: upgraded to ${version}`);
|
|
86
|
+
if (projectMode === "npm") {
|
|
87
|
+
const pkgs = ["@opentrust/core", "@opentrust/gateway", "@opentrust/dashboard"].map((p) => `${p}@${version}`);
|
|
88
|
+
execSync(`npm install ${pkgs.join(" ")}`, {
|
|
89
|
+
encoding: "utf-8",
|
|
90
|
+
cwd: projectRoot,
|
|
91
|
+
timeout: 120_000,
|
|
92
|
+
});
|
|
93
|
+
results.push(`Packages: upgraded to ${version}`);
|
|
94
|
+
}
|
|
95
|
+
return { success: true, output: results.join("\n") };
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
return { success: false, error: (err.stderr || err.message || String(err)).slice(0, 1000) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function handleFetchLogs(payload) {
|
|
102
|
+
const service = payload.service;
|
|
103
|
+
const lines = payload.lines || 200;
|
|
104
|
+
if (!service || !SERVICE_KEYS.includes(service)) {
|
|
105
|
+
return { success: false, error: `Invalid service. Must be one of: ${SERVICE_KEYS.join(", ")}` };
|
|
106
|
+
}
|
|
107
|
+
const logFile = path.join(paths.logs, `${service}.log`);
|
|
108
|
+
if (!fs.existsSync(logFile)) {
|
|
109
|
+
return { success: true, output: "(no log file)" };
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const content = fs.readFileSync(logFile, "utf-8");
|
|
113
|
+
const allLines = content.split("\n");
|
|
114
|
+
const tail = allLines.slice(-lines).join("\n");
|
|
115
|
+
return { success: true, output: tail.slice(-50_000) };
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return { success: false, error: err.message };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function handleUpdateEnv(payload) {
|
|
122
|
+
const key = payload.key;
|
|
123
|
+
const value = payload.value;
|
|
124
|
+
if (!key)
|
|
125
|
+
return { success: false, error: "Missing key in payload" };
|
|
126
|
+
const envFile = path.join(projectRoot, ".env");
|
|
127
|
+
let content = "";
|
|
128
|
+
if (fs.existsSync(envFile)) {
|
|
129
|
+
content = fs.readFileSync(envFile, "utf-8");
|
|
130
|
+
}
|
|
131
|
+
const lines = content.split("\n");
|
|
132
|
+
const idx = lines.findIndex((l) => l.startsWith(`${key}=`));
|
|
133
|
+
if (idx >= 0) {
|
|
134
|
+
lines[idx] = `${key}=${value}`;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
lines.push(`${key}=${value}`);
|
|
138
|
+
}
|
|
139
|
+
fs.writeFileSync(envFile, lines.join("\n"), "utf-8");
|
|
140
|
+
return { success: true, output: `${key}=${value}` };
|
|
141
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ClientConfig {
|
|
2
|
+
dashboardUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
hostId?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface HostCommand {
|
|
7
|
+
id: string;
|
|
8
|
+
hostId: string;
|
|
9
|
+
type: string;
|
|
10
|
+
payload: Record<string, unknown> | null;
|
|
11
|
+
status: string;
|
|
12
|
+
createdAt: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function loadConfig(): ClientConfig | null;
|
|
15
|
+
export declare function saveConfig(config: ClientConfig): void;
|
|
16
|
+
export declare function clearConfig(): void;
|
|
17
|
+
export declare class DashboardClient {
|
|
18
|
+
private baseUrl;
|
|
19
|
+
private apiKey;
|
|
20
|
+
hostId: string | undefined;
|
|
21
|
+
constructor(config: ClientConfig);
|
|
22
|
+
private request;
|
|
23
|
+
register(data: {
|
|
24
|
+
hostname: string;
|
|
25
|
+
os: string;
|
|
26
|
+
arch: string;
|
|
27
|
+
ip: string;
|
|
28
|
+
cliVersion: string;
|
|
29
|
+
projectMode: string;
|
|
30
|
+
projectRoot: string;
|
|
31
|
+
services?: Record<string, unknown>;
|
|
32
|
+
metadata?: Record<string, unknown>;
|
|
33
|
+
}): Promise<string>;
|
|
34
|
+
heartbeat(data: {
|
|
35
|
+
services?: Record<string, unknown>;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
fetchPendingCommands(): Promise<HostCommand[]>;
|
|
39
|
+
ackCommand(id: string, status: "running" | "completed" | "failed", result?: Record<string, unknown>): Promise<void>;
|
|
40
|
+
disconnect(): Promise<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), ".opentrust");
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "client.json");
|
|
6
|
+
export function loadConfig() {
|
|
7
|
+
try {
|
|
8
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
9
|
+
return null;
|
|
10
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function saveConfig(config) {
|
|
17
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
18
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
20
|
+
}
|
|
21
|
+
export function clearConfig() {
|
|
22
|
+
try {
|
|
23
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
}
|
|
27
|
+
export class DashboardClient {
|
|
28
|
+
baseUrl;
|
|
29
|
+
apiKey;
|
|
30
|
+
hostId;
|
|
31
|
+
constructor(config) {
|
|
32
|
+
this.baseUrl = config.dashboardUrl.replace(/\/+$/, "");
|
|
33
|
+
this.apiKey = config.apiKey;
|
|
34
|
+
this.hostId = config.hostId;
|
|
35
|
+
}
|
|
36
|
+
async request(path, options) {
|
|
37
|
+
const { timeoutMs = 15_000, ...init } = options ?? {};
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
42
|
+
...init,
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
47
|
+
...init?.headers,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const text = await res.text().catch(() => "");
|
|
52
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
53
|
+
}
|
|
54
|
+
return (await res.json());
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async register(data) {
|
|
61
|
+
const resp = await this.request("/api/hosts/register", { method: "POST", body: JSON.stringify(data), timeoutMs: 20_000 });
|
|
62
|
+
this.hostId = resp.data.id;
|
|
63
|
+
return resp.data.id;
|
|
64
|
+
}
|
|
65
|
+
async heartbeat(data) {
|
|
66
|
+
if (!this.hostId)
|
|
67
|
+
return;
|
|
68
|
+
await this.request(`/api/hosts/${this.hostId}/heartbeat`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify(data),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async fetchPendingCommands() {
|
|
74
|
+
if (!this.hostId)
|
|
75
|
+
return [];
|
|
76
|
+
const resp = await this.request(`/api/hosts/commands/pending?hostId=${this.hostId}`);
|
|
77
|
+
return resp.data;
|
|
78
|
+
}
|
|
79
|
+
async ackCommand(id, status, result) {
|
|
80
|
+
await this.request(`/api/hosts/commands/${id}/ack`, {
|
|
81
|
+
method: "PUT",
|
|
82
|
+
body: JSON.stringify({ status, result }),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async disconnect() {
|
|
86
|
+
if (!this.hostId)
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
await this.request(`/api/hosts/${this.hostId}`, { method: "DELETE" });
|
|
90
|
+
}
|
|
91
|
+
catch { }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function getHostInfo(): {
|
|
2
|
+
hostname: string;
|
|
3
|
+
os: string;
|
|
4
|
+
arch: string;
|
|
5
|
+
ip: string;
|
|
6
|
+
cliVersion: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function getServicesSnapshot(): Promise<Record<string, unknown>>;
|
|
9
|
+
export declare function getSystemMetadata(): Record<string, unknown>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getStatus, checkHealth, getAllServiceKeys, SERVICES } from "./process-manager.js";
|
|
5
|
+
function getLocalIp() {
|
|
6
|
+
const interfaces = os.networkInterfaces();
|
|
7
|
+
for (const name of Object.keys(interfaces)) {
|
|
8
|
+
for (const iface of interfaces[name] ?? []) {
|
|
9
|
+
if (iface.family === "IPv4" && !iface.internal)
|
|
10
|
+
return iface.address;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return "127.0.0.1";
|
|
14
|
+
}
|
|
15
|
+
function getCliVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "../../package.json");
|
|
18
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf-8")).version ?? "unknown";
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function getHostInfo() {
|
|
25
|
+
return {
|
|
26
|
+
hostname: os.hostname(),
|
|
27
|
+
os: `${os.platform()} ${os.release()}`,
|
|
28
|
+
arch: os.arch(),
|
|
29
|
+
ip: getLocalIp(),
|
|
30
|
+
cliVersion: getCliVersion(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function getServicesSnapshot() {
|
|
34
|
+
const result = {};
|
|
35
|
+
for (const key of getAllServiceKeys()) {
|
|
36
|
+
const svc = SERVICES[key];
|
|
37
|
+
const status = getStatus(key);
|
|
38
|
+
const healthy = status.running ? await checkHealth(key) : false;
|
|
39
|
+
result[key] = {
|
|
40
|
+
name: svc.name,
|
|
41
|
+
port: svc.port,
|
|
42
|
+
running: status.running,
|
|
43
|
+
pid: status.pid ?? null,
|
|
44
|
+
healthy,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
export function getSystemMetadata() {
|
|
50
|
+
const cpus = os.cpus();
|
|
51
|
+
return {
|
|
52
|
+
cpuModel: cpus[0]?.model ?? "unknown",
|
|
53
|
+
cpuCount: cpus.length,
|
|
54
|
+
totalMemMB: Math.round(os.totalmem() / 1048576),
|
|
55
|
+
freeMemMB: Math.round(os.freemem() / 1048576),
|
|
56
|
+
uptime: os.uptime(),
|
|
57
|
+
nodeVersion: process.version,
|
|
58
|
+
};
|
|
59
|
+
}
|