@the-seeker/server-agent 0.1.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.
@@ -0,0 +1,21 @@
1
+ import type { Detection, InstallEnvironment } from "./detect.js";
2
+ export declare const LAUNCHD_LABEL = "io.theseeker.agent";
3
+ export declare const PLIST_FILE_MODE = 420;
4
+ export declare const LAUNCHD_DIR_MODE = 493;
5
+ export declare const LOG_FILE_NAME = "agent.log";
6
+ export declare const ERROR_LOG_FILE_NAME = "agent.error.log";
7
+ export declare function launchAgentsDirectory(home: string): string;
8
+ export declare function plistPath(home: string): string;
9
+ export declare function logDirectory(home: string): string;
10
+ export interface PlistOptions {
11
+ readonly nodePath: string;
12
+ readonly binPath: string;
13
+ readonly home: string;
14
+ }
15
+ export declare function buildPlist(options: PlistOptions): string;
16
+ export interface LaunchdInstallOptions {
17
+ readonly detection: Detection;
18
+ }
19
+ export declare function installLaunchd(environment: InstallEnvironment, options: LaunchdInstallOptions): Promise<number>;
20
+ export declare function uninstallLaunchd(environment: InstallEnvironment, detection: Detection): Promise<number>;
21
+ export declare function statusLaunchd(environment: InstallEnvironment, detection: Detection): Promise<number>;
@@ -0,0 +1,113 @@
1
+ import { join } from "node:path";
2
+ import { skipsServiceManager } from "./detect.js";
3
+ export const LAUNCHD_LABEL = "io.theseeker.agent";
4
+ export const PLIST_FILE_MODE = 0o644;
5
+ export const LAUNCHD_DIR_MODE = 0o755;
6
+ export const LOG_FILE_NAME = "agent.log";
7
+ export const ERROR_LOG_FILE_NAME = "agent.error.log";
8
+ export function launchAgentsDirectory(home) {
9
+ return join(home, "Library", "LaunchAgents");
10
+ }
11
+ export function plistPath(home) {
12
+ return join(launchAgentsDirectory(home), `${LAUNCHD_LABEL}.plist`);
13
+ }
14
+ export function logDirectory(home) {
15
+ return join(home, "Library", "Logs", "theseeker-agent");
16
+ }
17
+ function escapeXml(value) {
18
+ return value.split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;");
19
+ }
20
+ export function buildPlist(options) {
21
+ const logs = logDirectory(options.home);
22
+ const programArguments = [options.nodePath, options.binPath, "run"].map((argument) => ` <string>${escapeXml(argument)}</string>`);
23
+ return [
24
+ '<?xml version="1.0" encoding="UTF-8"?>',
25
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
26
+ '<plist version="1.0">',
27
+ "<dict>",
28
+ " <key>Label</key>",
29
+ ` <string>${LAUNCHD_LABEL}</string>`,
30
+ " <key>ProgramArguments</key>",
31
+ " <array>",
32
+ ...programArguments,
33
+ " </array>",
34
+ " <key>RunAtLoad</key>",
35
+ " <true/>",
36
+ " <key>KeepAlive</key>",
37
+ " <true/>",
38
+ " <key>StandardOutPath</key>",
39
+ ` <string>${escapeXml(join(logs, LOG_FILE_NAME))}</string>`,
40
+ " <key>StandardErrorPath</key>",
41
+ ` <string>${escapeXml(join(logs, ERROR_LOG_FILE_NAME))}</string>`,
42
+ "</dict>",
43
+ "</plist>",
44
+ "",
45
+ ].join("\n");
46
+ }
47
+ function serviceTarget(uid) {
48
+ return `gui/${uid}/${LAUNCHD_LABEL}`;
49
+ }
50
+ async function bootout(environment, uid, path) {
51
+ if (skipsServiceManager(environment)) {
52
+ environment.print(`skipped: launchctl bootout ${serviceTarget(uid)}`);
53
+ return;
54
+ }
55
+ const result = await environment.exec("launchctl", ["bootout", serviceTarget(uid)]);
56
+ if (result.code !== 0)
57
+ await environment.exec("launchctl", ["unload", "-w", path]);
58
+ }
59
+ export async function installLaunchd(environment, options) {
60
+ const home = options.detection.userHome;
61
+ const path = plistPath(home);
62
+ const plist = buildPlist({ nodePath: options.detection.nodePath, binPath: options.detection.binPath, home });
63
+ await environment.fs.mkdir(launchAgentsDirectory(home), LAUNCHD_DIR_MODE);
64
+ await environment.fs.mkdir(logDirectory(home), LAUNCHD_DIR_MODE);
65
+ const existing = await environment.fs.read(path);
66
+ await environment.fs.write(path, plist, PLIST_FILE_MODE);
67
+ environment.print(`${existing === null ? "wrote" : "rewrote"} launch agent: ${path}`);
68
+ await bootout(environment, environment.uid, path);
69
+ if (skipsServiceManager(environment))
70
+ environment.print(`skipped: launchctl bootstrap gui/${environment.uid} ${path}`);
71
+ else {
72
+ const bootstrap = await environment.exec("launchctl", ["bootstrap", `gui/${environment.uid}`, path]);
73
+ if (bootstrap.code !== 0) {
74
+ const legacy = await environment.exec("launchctl", ["load", "-w", path]);
75
+ if (legacy.code !== 0) {
76
+ environment.print(`launchctl bootstrap failed (exit ${bootstrap.code}) ${bootstrap.stderr.trim()}`);
77
+ return 1;
78
+ }
79
+ }
80
+ environment.print(`loaded: ${serviceTarget(environment.uid)}`);
81
+ }
82
+ environment.print(`logs: ${join(logDirectory(home), LOG_FILE_NAME)}`);
83
+ environment.print("note: a LaunchAgent is bound to the GUI login session — it runs while this user is logged in, not at boot.");
84
+ return 0;
85
+ }
86
+ export async function uninstallLaunchd(environment, detection) {
87
+ const path = plistPath(detection.userHome);
88
+ await bootout(environment, environment.uid, path);
89
+ if (await environment.fs.exists(path)) {
90
+ await environment.fs.remove(path);
91
+ environment.print(`removed launch agent: ${path}`);
92
+ }
93
+ else
94
+ environment.print(`launch agent not present: ${path}`);
95
+ environment.print(`kept logs: ${logDirectory(detection.userHome)}`);
96
+ return 0;
97
+ }
98
+ export async function statusLaunchd(environment, detection) {
99
+ const path = plistPath(detection.userHome);
100
+ const installed = await environment.fs.exists(path);
101
+ environment.print(`launch agent: ${path} ${installed ? "present" : "missing"}`);
102
+ if (!installed)
103
+ return 1;
104
+ if (!skipsServiceManager(environment)) {
105
+ const print = await environment.exec("launchctl", ["print", serviceTarget(environment.uid)]);
106
+ environment.print(`state: ${print.code === 0 ? "loaded" : "not loaded"} (${serviceTarget(environment.uid)})`);
107
+ }
108
+ const logPath = join(logDirectory(detection.userHome), LOG_FILE_NAME);
109
+ const contents = await environment.fs.read(logPath);
110
+ environment.print(`--- last 20 lines of ${logPath} ---`);
111
+ environment.print(contents === null ? "(no log file yet)" : contents.trimEnd().split("\n").slice(-20).join("\n") || "(empty)");
112
+ return 0;
113
+ }
@@ -0,0 +1,28 @@
1
+ import type { Detection, InstallEnvironment } from "./detect.js";
2
+ export declare const SERVICE_NAME = "theseeker-agent";
3
+ export declare const UNIT_PATH = "/etc/systemd/system/theseeker-agent.service";
4
+ export declare const UNIT_FILE_MODE = 420;
5
+ export declare const SUPPLEMENTARY_GROUPS: readonly ["adm", "systemd-journal"];
6
+ export interface UnitOptions {
7
+ readonly user: string;
8
+ readonly group: string;
9
+ readonly nodePath: string;
10
+ readonly binPath: string;
11
+ readonly pm2Home: string;
12
+ readonly addGroups: boolean;
13
+ }
14
+ /** `ProtectSystem=strict` still hides `/home`; the agent reads PM2 logs there, so a `/home` PM2_HOME disables the guard. */
15
+ export declare function protectHomeValue(pm2Home: string): "read-only" | "false";
16
+ export declare function buildUnitFile(options: UnitOptions): string;
17
+ export declare function unitOptionsFrom(detection: Detection, addGroups: boolean): UnitOptions;
18
+ /** Printed verbatim when the installer is not root: it writes nothing and exits 2. */
19
+ export declare function manualInstallCommands(endpoint: string, user: string, addGroups: boolean): readonly string[];
20
+ export declare function manualGroupCommand(user: string): readonly string[];
21
+ export interface SystemdInstallOptions {
22
+ readonly detection: Detection;
23
+ readonly addGroups: boolean;
24
+ readonly endpoint: string;
25
+ }
26
+ export declare function installSystemd(environment: InstallEnvironment, options: SystemdInstallOptions): Promise<number>;
27
+ export declare function uninstallSystemd(environment: InstallEnvironment): Promise<number>;
28
+ export declare function statusSystemd(environment: InstallEnvironment): Promise<number>;
@@ -0,0 +1,122 @@
1
+ import { SYSTEM_CONFIG_DIR } from "../config.js";
2
+ import { skipsServiceManager } from "./detect.js";
3
+ export const SERVICE_NAME = "theseeker-agent";
4
+ export const UNIT_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
5
+ export const UNIT_FILE_MODE = 0o644;
6
+ export const SUPPLEMENTARY_GROUPS = ["adm", "systemd-journal"];
7
+ /** `ProtectSystem=strict` still hides `/home`; the agent reads PM2 logs there, so a `/home` PM2_HOME disables the guard. */
8
+ export function protectHomeValue(pm2Home) {
9
+ return pm2Home.startsWith("/home/") || pm2Home === "/home" ? "false" : "read-only";
10
+ }
11
+ export function buildUnitFile(options) {
12
+ const lines = [
13
+ "[Unit]",
14
+ "Description=The Seeker server monitoring agent",
15
+ "Documentation=https://www.npmjs.com/package/@the-seeker/server-agent",
16
+ "After=network-online.target",
17
+ "Wants=network-online.target",
18
+ "",
19
+ "[Service]",
20
+ "Type=simple",
21
+ `User=${options.user}`,
22
+ `Group=${options.group}`,
23
+ ];
24
+ if (options.addGroups)
25
+ lines.push(`SupplementaryGroups=${SUPPLEMENTARY_GROUPS.join(" ")}`);
26
+ lines.push(`Environment=PM2_HOME=${options.pm2Home}`, `ExecStart=${options.nodePath} ${options.binPath} run`, "Restart=always", "RestartSec=5", "NoNewPrivileges=true", "ProtectSystem=strict", `ProtectHome=${protectHomeValue(options.pm2Home)}`, `ReadWritePaths=${SYSTEM_CONFIG_DIR}`, "", "[Install]", "WantedBy=multi-user.target", "");
27
+ return lines.join("\n");
28
+ }
29
+ export function unitOptionsFrom(detection, addGroups) {
30
+ return {
31
+ user: detection.user,
32
+ group: detection.group,
33
+ nodePath: detection.nodePath,
34
+ binPath: detection.binPath,
35
+ pm2Home: detection.pm2Home,
36
+ addGroups,
37
+ };
38
+ }
39
+ /** Printed verbatim when the installer is not root: it writes nothing and exits 2. */
40
+ export function manualInstallCommands(endpoint, user, addGroups) {
41
+ const extra = addGroups ? " --add-groups" : "";
42
+ return [
43
+ "systemd unit installation requires root.",
44
+ "Run the same command again with sudo:",
45
+ "",
46
+ ` sudo ${SERVICE_NAME} install --token <sa_...> --endpoint ${endpoint} --user ${user}${extra}`,
47
+ "",
48
+ "It writes:",
49
+ ` ${UNIT_PATH}`,
50
+ ` ${SYSTEM_CONFIG_DIR}/config.json (0600, owned by ${user})`,
51
+ "and then runs:",
52
+ " sudo systemctl daemon-reload",
53
+ ` sudo systemctl enable --now ${SERVICE_NAME}`,
54
+ ];
55
+ }
56
+ export function manualGroupCommand(user) {
57
+ return [
58
+ `note: SupplementaryGroups was omitted. Without ${SUPPLEMENTARY_GROUPS.join(" and ")} membership the agent`,
59
+ " can only read journal entries and log files its own user may read (systemd unit status still works).",
60
+ " To grant broader log access run, then restart the service:",
61
+ ` sudo usermod -aG ${SUPPLEMENTARY_GROUPS.join(",")} ${user}`,
62
+ ` sudo systemctl restart ${SERVICE_NAME}`,
63
+ ];
64
+ }
65
+ async function runSystemctl(environment, args) {
66
+ if (skipsServiceManager(environment)) {
67
+ environment.print(`skipped: systemctl ${args.join(" ")}`);
68
+ return true;
69
+ }
70
+ const result = await environment.exec("systemctl", args);
71
+ if (result.code !== 0)
72
+ environment.print(`systemctl ${args.join(" ")} failed (exit ${result.code}) ${result.stderr.trim()}`);
73
+ return result.code === 0;
74
+ }
75
+ export async function installSystemd(environment, options) {
76
+ const unit = buildUnitFile(unitOptionsFrom(options.detection, options.addGroups));
77
+ const existing = await environment.fs.read(UNIT_PATH);
78
+ if (existing === unit)
79
+ environment.print(`unit unchanged: ${UNIT_PATH}`);
80
+ else {
81
+ await environment.fs.write(UNIT_PATH, unit, UNIT_FILE_MODE);
82
+ environment.print(`${existing === null ? "wrote" : "rewrote"} unit: ${UNIT_PATH}`);
83
+ }
84
+ if (!(await runSystemctl(environment, ["daemon-reload"])))
85
+ return 1;
86
+ if (!(await runSystemctl(environment, ["enable", "--now", SERVICE_NAME])))
87
+ return 1;
88
+ environment.print(`service enabled: ${SERVICE_NAME}`);
89
+ if (!options.addGroups)
90
+ for (const line of manualGroupCommand(options.detection.user))
91
+ environment.print(line);
92
+ if (protectHomeValue(options.detection.pm2Home) === "false") {
93
+ environment.print(`note: ProtectHome=false because PM2_HOME (${options.detection.pm2Home}) lives under /home and must stay readable.`);
94
+ }
95
+ return 0;
96
+ }
97
+ export async function uninstallSystemd(environment) {
98
+ await runSystemctl(environment, ["disable", "--now", SERVICE_NAME]);
99
+ if (await environment.fs.exists(UNIT_PATH)) {
100
+ await environment.fs.remove(UNIT_PATH);
101
+ environment.print(`removed unit: ${UNIT_PATH}`);
102
+ }
103
+ else
104
+ environment.print(`unit not present: ${UNIT_PATH}`);
105
+ await runSystemctl(environment, ["daemon-reload"]);
106
+ return 0;
107
+ }
108
+ export async function statusSystemd(environment) {
109
+ const installed = await environment.fs.exists(UNIT_PATH);
110
+ environment.print(`unit: ${UNIT_PATH} ${installed ? "present" : "missing"}`);
111
+ if (!installed)
112
+ return 1;
113
+ if (skipsServiceManager(environment))
114
+ return 0;
115
+ const active = await environment.exec("systemctl", ["is-active", SERVICE_NAME]);
116
+ const enabled = await environment.exec("systemctl", ["is-enabled", SERVICE_NAME]);
117
+ environment.print(`state: ${active.stdout.trim() || "unknown"} (${enabled.stdout.trim() || "unknown"})`);
118
+ const logs = await environment.exec("journalctl", ["-u", SERVICE_NAME, "-n", "20", "--no-pager"]);
119
+ environment.print("--- last 20 log lines ---");
120
+ environment.print(logs.stdout.trim() || logs.stderr.trim() || "(no log lines)");
121
+ return active.stdout.trim() === "active" ? 0 : 1;
122
+ }