@rynx-ai/cli 0.1.11-beta.32 → 0.1.11-beta.34

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,26 @@
1
+ export { renderSystemdUnit } from "./systemd-service.js";
2
+ export type AutostartManager = "launchd" | "systemd-user" | "unsupported";
3
+ export interface AutostartState {
4
+ supported: boolean;
5
+ registered: boolean;
6
+ active?: boolean;
7
+ lingerEnabled?: boolean;
8
+ lingerEnableCommand?: string;
9
+ manager: AutostartManager;
10
+ registrationPath?: string;
11
+ detail?: string;
12
+ }
13
+ export interface AutostartRenderInput {
14
+ cliPath: string;
15
+ dataDir: string;
16
+ logDir: string;
17
+ nodePath: string;
18
+ pathEnv: string;
19
+ }
20
+ export declare function autostartPlatformLabel(platform?: NodeJS.Platform): string | undefined;
21
+ export declare function inspectAutostart(): AutostartState;
22
+ export declare function enableAutostart(): AutostartState;
23
+ export declare function disableAutostart(): AutostartState;
24
+ /** Refresh paths in an existing registration without enabling a new one. */
25
+ export declare function refreshAutostart(): boolean;
26
+ export declare function renderLaunchAgent(input: AutostartRenderInput): string;
@@ -0,0 +1,229 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { homedir, userInfo } from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { rynxHome } from "@rynx-ai/core";
7
+ import { disableLinuxAutostart, enableLinuxAutostart, inspectLinuxAutostart, refreshLinuxServiceIfPresent, } from "./systemd-service.js";
8
+ export { renderSystemdUnit } from "./systemd-service.js";
9
+ const LAUNCHD_LABEL = "ai.rynx.daemon";
10
+ const COMMAND_TIMEOUT_MS = 5_000;
11
+ export function autostartPlatformLabel(platform = process.platform) {
12
+ if (platform === "darwin")
13
+ return "macOS LaunchAgent";
14
+ if (platform === "linux")
15
+ return "Linux user systemd";
16
+ return undefined;
17
+ }
18
+ export function inspectAutostart() {
19
+ if (process.platform !== "darwin" && process.platform !== "linux") {
20
+ return unsupportedState();
21
+ }
22
+ const context = currentContext();
23
+ return process.platform === "darwin" ? inspectMac(context) : inspectLinuxAutostart();
24
+ }
25
+ export function enableAutostart() {
26
+ if (process.platform !== "darwin" && process.platform !== "linux") {
27
+ throw new Error(`Rynx autostart is not supported on ${process.platform}`);
28
+ }
29
+ const context = currentContext();
30
+ if (process.platform === "darwin")
31
+ enableMac(context);
32
+ else
33
+ enableLinuxAutostart();
34
+ return inspectAutostart();
35
+ }
36
+ export function disableAutostart() {
37
+ if (process.platform !== "darwin" && process.platform !== "linux") {
38
+ throw new Error(`Rynx autostart is not supported on ${process.platform}`);
39
+ }
40
+ const context = currentContext();
41
+ if (process.platform === "darwin")
42
+ disableMac(context);
43
+ else
44
+ disableLinuxAutostart();
45
+ return inspectAutostart();
46
+ }
47
+ /** Refresh paths in an existing registration without enabling a new one. */
48
+ export function refreshAutostart() {
49
+ if (process.platform === "darwin") {
50
+ const context = currentContext();
51
+ const registrationPath = launchAgentPath(context);
52
+ if (!existsSync(registrationPath))
53
+ return false;
54
+ return syncMacRegistration(context);
55
+ }
56
+ if (process.platform === "linux")
57
+ return refreshLinuxServiceIfPresent();
58
+ return false;
59
+ }
60
+ function unsupportedState() {
61
+ return {
62
+ supported: false,
63
+ registered: false,
64
+ manager: "unsupported",
65
+ detail: `Rynx autostart is not supported on ${process.platform}`,
66
+ };
67
+ }
68
+ export function renderLaunchAgent(input) {
69
+ const outLog = path.join(input.logDir, "autostart-out.log");
70
+ const errLog = path.join(input.logDir, "autostart-err.log");
71
+ return `<?xml version="1.0" encoding="UTF-8"?>
72
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
73
+ <plist version="1.0">
74
+ <dict>
75
+ <key>Label</key>
76
+ <string>${escapeXml(LAUNCHD_LABEL)}</string>
77
+ <key>ProgramArguments</key>
78
+ <array>
79
+ <string>${escapeXml(input.nodePath)}</string>
80
+ <string>${escapeXml(input.cliPath)}</string>
81
+ <string>start</string>
82
+ </array>
83
+ <key>RunAtLoad</key>
84
+ <true/>
85
+ <key>KeepAlive</key>
86
+ <false/>
87
+ <key>WorkingDirectory</key>
88
+ <string>${escapeXml(input.dataDir)}</string>
89
+ <key>EnvironmentVariables</key>
90
+ <dict>
91
+ <key>PATH</key>
92
+ <string>${escapeXml(input.pathEnv)}</string>
93
+ <key>RYNX_HOME</key>
94
+ <string>${escapeXml(input.dataDir)}</string>
95
+ </dict>
96
+ <key>StandardOutPath</key>
97
+ <string>${escapeXml(outLog)}</string>
98
+ <key>StandardErrorPath</key>
99
+ <string>${escapeXml(errLog)}</string>
100
+ </dict>
101
+ </plist>
102
+ `;
103
+ }
104
+ function currentContext() {
105
+ const dataDir = rynxHome();
106
+ const homeDir = homedir();
107
+ return {
108
+ homeDir,
109
+ dataDir,
110
+ logDir: path.join(dataDir, "logs"),
111
+ cliPath: fileURLToPath(new URL("./cli.js", import.meta.url)),
112
+ nodePath: process.execPath,
113
+ pathEnv: process.env.PATH || defaultPath(),
114
+ };
115
+ }
116
+ function defaultPath() {
117
+ if (process.platform === "darwin") {
118
+ return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
119
+ }
120
+ return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
121
+ }
122
+ function launchAgentPath(context) {
123
+ return path.join(context.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
124
+ }
125
+ function inspectMac(context) {
126
+ const registrationPath = launchAgentPath(context);
127
+ return {
128
+ supported: true,
129
+ registered: existsSync(registrationPath),
130
+ active: launchctlLoaded(),
131
+ manager: "launchd",
132
+ registrationPath,
133
+ };
134
+ }
135
+ function enableMac(context) {
136
+ mkdirSync(context.logDir, { recursive: true, mode: 0o700 });
137
+ syncMacRegistration(context);
138
+ }
139
+ function syncMacRegistration(context) {
140
+ const registrationPath = launchAgentPath(context);
141
+ const previous = existsSync(registrationPath)
142
+ ? readFileSync(registrationPath, "utf8")
143
+ : undefined;
144
+ const content = renderLaunchAgent(context);
145
+ if (previous === content)
146
+ return false;
147
+ const wasLoaded = launchctlLoaded();
148
+ replaceFileAtomically(registrationPath, content);
149
+ if (!wasLoaded)
150
+ return true;
151
+ if (!launchctlBootout(registrationPath)) {
152
+ restoreRegistrationFile(registrationPath, previous);
153
+ throw new Error(`launchctl could not unload ${LAUNCHD_LABEL}; the previous file was restored`);
154
+ }
155
+ if (launchctlBootstrap(registrationPath))
156
+ return true;
157
+ restoreRegistrationFile(registrationPath, previous);
158
+ if (previous !== undefined && launchctlBootstrap(registrationPath)) {
159
+ throw new Error(`launchctl could not load the updated registration; the previous job was restored`);
160
+ }
161
+ throw new Error(`launchctl could not load the updated registration and could not restore the previous job`);
162
+ }
163
+ function disableMac(context) {
164
+ const registrationPath = launchAgentPath(context);
165
+ if (launchctlLoaded() && !launchctlBootout(registrationPath)) {
166
+ throw new Error(`launchctl could not unload ${LAUNCHD_LABEL}`);
167
+ }
168
+ rmSync(registrationPath, { force: true });
169
+ }
170
+ function launchctlLoaded() {
171
+ const uid = userInfo().uid;
172
+ return run("launchctl", ["print", `gui/${uid}/${LAUNCHD_LABEL}`]).status === 0;
173
+ }
174
+ function launchctlBootstrap(registrationPath) {
175
+ const uid = userInfo().uid;
176
+ const modern = run("launchctl", [
177
+ "bootstrap",
178
+ `gui/${uid}`,
179
+ registrationPath,
180
+ ]);
181
+ if (modern.status === 0)
182
+ return true;
183
+ return run("launchctl", ["load", "-w", registrationPath]).status === 0;
184
+ }
185
+ function launchctlBootout(registrationPath) {
186
+ const uid = userInfo().uid;
187
+ const modern = run("launchctl", [
188
+ "bootout",
189
+ `gui/${uid}/${LAUNCHD_LABEL}`,
190
+ ]);
191
+ if (modern.status === 0)
192
+ return true;
193
+ return run("launchctl", ["unload", "-w", registrationPath]).status === 0;
194
+ }
195
+ function run(command, args) {
196
+ return spawnSync(command, [...args], {
197
+ encoding: "utf8",
198
+ stdio: "pipe",
199
+ timeout: COMMAND_TIMEOUT_MS,
200
+ killSignal: "SIGTERM",
201
+ });
202
+ }
203
+ function escapeXml(value) {
204
+ return value
205
+ .replaceAll("&", "&amp;")
206
+ .replaceAll("<", "&lt;")
207
+ .replaceAll(">", "&gt;");
208
+ }
209
+ function replaceFileAtomically(file, content) {
210
+ mkdirSync(path.dirname(file), { recursive: true });
211
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
212
+ try {
213
+ writeFileSync(temporary, content, {
214
+ encoding: "utf8",
215
+ flag: "wx",
216
+ mode: 0o644,
217
+ });
218
+ renameSync(temporary, file);
219
+ }
220
+ finally {
221
+ rmSync(temporary, { force: true });
222
+ }
223
+ }
224
+ function restoreRegistrationFile(file, previous) {
225
+ if (previous === undefined)
226
+ rmSync(file, { force: true });
227
+ else
228
+ replaceFileAtomically(file, previous);
229
+ }
@@ -0,0 +1 @@
1
+ export declare function runAutostartCommand(args: readonly string[]): Promise<number>;
@@ -0,0 +1,58 @@
1
+ import { autostartPlatformLabel, disableAutostart, enableAutostart, inspectAutostart, } from "../autostart.js";
2
+ import { fail } from "./errors.js";
3
+ export async function runAutostartCommand(args) {
4
+ if (process.env.RYNX_DISTRIBUTION === "app") {
5
+ fail("Rynx App manages startup. Use the App's launch-at-login setting.");
6
+ }
7
+ const [action, ...rest] = args;
8
+ if (rest.length > 0)
9
+ fail(`autostart: unexpected argument ${rest[0]}`);
10
+ if (action === "enable") {
11
+ const state = enableAutostart();
12
+ console.log(`Rynx autostart enabled with ${autostartPlatformLabel() ?? state.manager}.`);
13
+ if (state.registrationPath)
14
+ console.log(`Registration: ${state.registrationPath}`);
15
+ console.log("Rynx will start at the next login. Start it now with `rynx start`.");
16
+ printLingerWarning(state);
17
+ return 0;
18
+ }
19
+ if (action === "disable") {
20
+ const state = disableAutostart();
21
+ console.log("Rynx autostart disabled.");
22
+ console.log("The running daemon was not stopped; use `rynx stop` if needed.");
23
+ if (state.registered) {
24
+ console.warn("An autostart registration is still present; run `rynx autostart status`.");
25
+ return 1;
26
+ }
27
+ return 0;
28
+ }
29
+ if (action === "status") {
30
+ printAutostartState(inspectAutostart());
31
+ return 0;
32
+ }
33
+ fail("Usage: rynx autostart <enable|disable|status>");
34
+ }
35
+ function printAutostartState(state) {
36
+ console.log(`manager: ${state.manager}`);
37
+ console.log(`supported: ${state.supported ? "yes" : "no"}`);
38
+ console.log(`registered: ${state.registered ? "yes" : "no"}`);
39
+ if (state.active !== undefined) {
40
+ console.log(`active: ${state.active ? "yes" : "no"}`);
41
+ }
42
+ if (state.lingerEnabled !== undefined) {
43
+ console.log(`linger: ${state.lingerEnabled ? "yes" : "no"}`);
44
+ }
45
+ if (state.registrationPath) {
46
+ console.log(`registration: ${state.registrationPath}`);
47
+ }
48
+ if (state.detail)
49
+ console.log(`detail: ${state.detail}`);
50
+ }
51
+ function printLingerWarning(state) {
52
+ if (state.manager !== "systemd-user" || state.lingerEnabled !== false)
53
+ return;
54
+ console.warn("Linger is disabled; Rynx stops when this user logs out.");
55
+ if (state.lingerEnableCommand) {
56
+ console.warn(`To keep it running across logout and start it before login, run manually: ${state.lingerEnableCommand}`);
57
+ }
58
+ }
@@ -1 +1,3 @@
1
- export declare function runLifecycleCommand(command: string, args: readonly string[]): Promise<number>;
1
+ export declare function runLifecycleCommand(command: string, args: readonly string[], options?: {
2
+ quiet?: boolean;
3
+ }): Promise<number>;
@@ -1,13 +1,40 @@
1
- import { startStandaloneDaemon, statusStandaloneDaemon, stopStandaloneDaemon, streamStandaloneDaemonLogs, } from "../standalone.js";
1
+ import { startStandaloneDaemon, standaloneDaemonRestartRequired, statusStandaloneDaemon, stopStandaloneDaemon, streamStandaloneDaemonLogs, } from "../standalone.js";
2
+ import { refreshAutostart } from "../autostart.js";
3
+ import { assertLinuxSystemdInvocation, linuxUserSystemdAvailable, runLinuxSystemdLifecycle, } from "../systemd-service.js";
2
4
  import { fail } from "./errors.js";
3
- export async function runLifecycleCommand(command, args) {
5
+ const SYSTEMD_SERVICE_FLAG = "--systemd-service";
6
+ export async function runLifecycleCommand(command, args, options = {}) {
7
+ if (args.length === 1 && args[0] === SYSTEMD_SERVICE_FLAG) {
8
+ assertLinuxSystemdInvocation();
9
+ if (command === "start")
10
+ return startStandaloneDaemon();
11
+ if (command === "stop")
12
+ return stopStandaloneDaemon();
13
+ fail(`${SYSTEMD_SERVICE_FLAG} is only valid for start and stop`);
14
+ }
4
15
  if (args.length > 0)
5
16
  fail(`${command}: unexpected argument ${args[0]}`);
17
+ if (linuxUserSystemdAvailable() &&
18
+ (command === "start" ||
19
+ command === "restart" ||
20
+ command === "stop" ||
21
+ command === "status")) {
22
+ return runLinuxSystemdLifecycle(command, {
23
+ restartRequired: standaloneDaemonRestartRequired,
24
+ start: startStandaloneDaemon,
25
+ status: statusStandaloneDaemon,
26
+ stop: stopStandaloneDaemon,
27
+ }, options);
28
+ }
6
29
  switch (command) {
7
- case "start":
8
- return startStandaloneDaemon();
9
- case "restart":
10
- return startStandaloneDaemon({ restart: true });
30
+ case "start": {
31
+ refreshAutostart();
32
+ return startStandaloneDaemon(options.quiet ? { quiet: true } : undefined);
33
+ }
34
+ case "restart": {
35
+ refreshAutostart();
36
+ return startStandaloneDaemon({ restart: true, quiet: options.quiet });
37
+ }
11
38
  case "stop":
12
39
  return stopStandaloneDaemon();
13
40
  case "status":
@@ -5,8 +5,10 @@ import * as prompts from "@clack/prompts";
5
5
  import { AGENT_RUNTIME_IDS, getRuntimeProfile, loadConfig, rynxConfigFile, } from "@rynx-ai/core";
6
6
  import { inspectDaemonDiagnostics, inspectSystemDependencies, prepareBundledPlugins, } from "@rynx-ai/daemon/setup-service";
7
7
  import { BUNDLED_TMUX_UNAVAILABLE_MESSAGE, resolveBundledTmux, } from "@rynx-ai/tmux";
8
+ import { enableAutostart, inspectAutostart, } from "../autostart.js";
8
9
  import { createProgressDisplay } from "../progress-display.js";
9
10
  import { fail } from "./errors.js";
11
+ import { runLifecycleCommand } from "./lifecycle.js";
10
12
  export async function runSetupCommand(args) {
11
13
  const options = parseSetupOptions(args);
12
14
  const progress = options.json ? undefined : createProgressDisplay();
@@ -113,6 +115,9 @@ export async function runSetupCommand(args) {
113
115
  else {
114
116
  progress?.succeed("已跳过 Rynx Browser 安装");
115
117
  }
118
+ if (interactive && process.env.RYNX_DISTRIBUTION !== "app") {
119
+ failed = await offerBackgroundServiceSetup() || failed;
120
+ }
116
121
  if (options.resultFile) {
117
122
  writeJsonAtomically(options.resultFile, result);
118
123
  }
@@ -304,19 +309,55 @@ async function collectInteractiveSetup(config) {
304
309
  if (prompts.isCancel(host))
305
310
  return cancelled();
306
311
  config.HOST = host;
307
- const logLevel = await prompts.text({
308
- message: "Log level",
309
- initialValue: String(config.LOG_LEVEL),
310
- });
311
- if (prompts.isCancel(logLevel))
312
- return cancelled();
313
- config.LOG_LEVEL = logLevel;
314
312
  return true;
315
313
  }
316
314
  function cancelled() {
317
315
  prompts.cancel("Cancelled");
318
316
  return false;
319
317
  }
318
+ async function offerBackgroundServiceSetup() {
319
+ const state = inspectAutostart();
320
+ if (!state.supported) {
321
+ prompts.log.warn(`Background service setup is unavailable${state.detail ? `: ${state.detail}` : ""}`);
322
+ return false;
323
+ }
324
+ const confirmed = await prompts.confirm({
325
+ message: state.registered
326
+ ? "Start the Rynx background service now? (Autostart is already enabled)"
327
+ : "Start Rynx now and automatically at login?",
328
+ initialValue: true,
329
+ });
330
+ if (prompts.isCancel(confirmed) || !confirmed) {
331
+ prompts.log.info("Skipped background service setup");
332
+ return false;
333
+ }
334
+ let failed = false;
335
+ if (!state.registered) {
336
+ try {
337
+ const enabled = enableAutostart();
338
+ prompts.log.success(`Autostart enabled${enabled.registrationPath ? `: ${enabled.registrationPath}` : ""}`);
339
+ if (enabled.manager === "systemd-user" && enabled.lingerEnabled === false) {
340
+ prompts.log.warn("Linger is disabled; Rynx stops when this user logs out");
341
+ if (enabled.lingerEnableCommand) {
342
+ prompts.log.info(`Optional, requires sudo and is never run by setup: ${enabled.lingerEnableCommand}`);
343
+ }
344
+ }
345
+ }
346
+ catch (error) {
347
+ failed = true;
348
+ prompts.log.warn(`Could not enable autostart: ${errorMessage(error)}`);
349
+ }
350
+ }
351
+ const startStatus = await runLifecycleCommand("start", [], { quiet: true });
352
+ if (startStatus === 0) {
353
+ prompts.log.success("Rynx background service is running");
354
+ }
355
+ else {
356
+ failed = true;
357
+ prompts.log.warn("Rynx background service failed to start; run `rynx start` for details");
358
+ }
359
+ return failed;
360
+ }
320
361
  function validateSetupConfig(config) {
321
362
  loadConfig({
322
363
  HOST: String(config.HOST),
@@ -3,7 +3,7 @@ import { closeSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join, sep } from "node:path";
5
5
  import { rynxHome } from "@rynx-ai/core";
6
- import { stopStandaloneDaemon } from "../standalone.js";
6
+ import { runLifecycleCommand } from "./lifecycle.js";
7
7
  function selfPackage() {
8
8
  const require = createRequire(import.meta.url);
9
9
  const manifest = require("../../package.json");
@@ -72,7 +72,7 @@ export async function runUpdate(options) {
72
72
  if (!releaseLock)
73
73
  return 1;
74
74
  try {
75
- if (await stopStandaloneDaemon() !== 0) {
75
+ if (await runLifecycleCommand("stop", []) !== 0) {
76
76
  console.error("update: could not stop the standalone daemon");
77
77
  return 1;
78
78
  }
package/dist/run-cli.js CHANGED
@@ -62,6 +62,10 @@ export async function runCli(argv) {
62
62
  return runBrowserCommand(argv.slice(1));
63
63
  case "cleanup":
64
64
  return runCleanupCommand(argv.slice(1));
65
+ case "autostart": {
66
+ const { runAutostartCommand } = await import("./commands/autostart.js");
67
+ return runAutostartCommand(argv.slice(1));
68
+ }
65
69
  case "setup": {
66
70
  const { runSetupCommand } = await import("./commands/setup.js");
67
71
  return runSetupCommand(argv.slice(1));
@@ -4,6 +4,7 @@ export declare function startStandaloneDaemon(options?: {
4
4
  restart?: boolean;
5
5
  quiet?: boolean;
6
6
  }): Promise<number>;
7
+ export declare function standaloneDaemonRestartRequired(): Promise<boolean>;
7
8
  /** Reuse any healthy owner; otherwise explicitly start the standalone owner. */
8
9
  export declare function ensureStandaloneDaemon(): Promise<{
9
10
  origin: string;
@@ -22,6 +22,10 @@ export async function startStandaloneDaemon(options = {}) {
22
22
  },
23
23
  });
24
24
  }
25
+ export async function standaloneDaemonRestartRequired() {
26
+ const { isDaemonStartRequired } = await import("@rynx-ai/daemon/lifecycle");
27
+ return isDaemonStartRequired();
28
+ }
25
29
  /** Reuse any healthy owner; otherwise explicitly start the standalone owner. */
26
30
  export async function ensureStandaloneDaemon() {
27
31
  const current = await resolveDaemonControlEndpoint();
@@ -0,0 +1,64 @@
1
+ import type { AutostartRenderInput, AutostartState } from "./autostart.js";
2
+ export declare const RYNX_SYSTEMD_SERVICE = "rynx.service";
3
+ export declare const RYNX_SYSTEMD_SERVICE_ENV = "RYNX_SYSTEMD_SERVICE";
4
+ export interface LinuxSystemdServiceState {
5
+ loadState: string;
6
+ activeState: string;
7
+ subState: string;
8
+ type: string;
9
+ mainPid: number;
10
+ killMode: string;
11
+ killSignal: string;
12
+ restartKillSignal: string;
13
+ finalKillSignal: string;
14
+ sendSigkill: string;
15
+ workingDirectory: string;
16
+ pidFile: string;
17
+ environment: string;
18
+ execStart: string;
19
+ execStop: string;
20
+ }
21
+ export interface LinuxPm2GodProcess {
22
+ pid: number;
23
+ cgroup: string;
24
+ startIdentity: string;
25
+ }
26
+ export type LinuxPm2GodOwnership = {
27
+ kind: "absent";
28
+ } | {
29
+ kind: "owned";
30
+ processes: LinuxPm2GodProcess[];
31
+ } | {
32
+ kind: "external";
33
+ processes: LinuxPm2GodProcess[];
34
+ };
35
+ export interface LinuxPm2InspectionDeps {
36
+ procEntries?: () => string[];
37
+ readText?: (file: string) => string;
38
+ statUid?: (file: string) => number;
39
+ currentUid?: number;
40
+ }
41
+ export interface LinuxLifecycleActions {
42
+ restartRequired: () => Promise<boolean>;
43
+ start: (options?: {
44
+ restart?: boolean;
45
+ quiet?: boolean;
46
+ }) => Promise<number>;
47
+ status: () => Promise<number>;
48
+ stop: () => Promise<number>;
49
+ }
50
+ export declare function renderSystemdUnit(input: AutostartRenderInput): string;
51
+ export declare function linuxUserSystemdAvailable(): boolean;
52
+ export declare function inspectLinuxAutostart(): AutostartState;
53
+ export declare function enableLinuxAutostart(): void;
54
+ export declare function disableLinuxAutostart(): void;
55
+ /** Refresh a registered or lifecycle-created unit without changing enablement. */
56
+ export declare function refreshLinuxServiceIfPresent(): boolean;
57
+ export declare function assertLinuxSystemdInvocation(): void;
58
+ export declare function runLinuxSystemdLifecycle(command: "start" | "restart" | "stop" | "status", actions: LinuxLifecycleActions, options?: {
59
+ quiet?: boolean;
60
+ }): Promise<number>;
61
+ export declare function parseLinuxSystemdShow(output: string): LinuxSystemdServiceState;
62
+ export declare function inspectLinuxPm2GodOwnership(home: string, deps?: LinuxPm2InspectionDeps): LinuxPm2GodOwnership;
63
+ export declare function scanLinuxPm2GodPids(home: string, deps?: LinuxPm2InspectionDeps): number[];
64
+ export declare function linuxSystemdCgroupForPid(pid: number, readText?: (file: string) => string): string;
@@ -0,0 +1,608 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { homedir, userInfo } from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { rynxHome } from "@rynx-ai/core";
7
+ export const RYNX_SYSTEMD_SERVICE = "rynx.service";
8
+ export const RYNX_SYSTEMD_SERVICE_ENV = "RYNX_SYSTEMD_SERVICE";
9
+ const COMMAND_TIMEOUT_MS = 5_000;
10
+ const LIFECYCLE_TIMEOUT_MS = 60_000;
11
+ export function renderSystemdUnit(input) {
12
+ const pidFile = path.join(input.dataDir, "pm2", "pm2.pid");
13
+ return `[Unit]
14
+ Description=Rynx local control plane
15
+ After=network-online.target
16
+ Wants=network-online.target
17
+
18
+ [Service]
19
+ Type=forking
20
+ PIDFile=${systemdPath(pidFile)}
21
+ KillMode=process
22
+ KillSignal=SIGCONT
23
+ RestartKillSignal=SIGCONT
24
+ FinalKillSignal=SIGCONT
25
+ SendSIGKILL=no
26
+ TimeoutStartSec=60
27
+ TimeoutStopSec=45
28
+ WorkingDirectory=${systemdPath(input.dataDir)}
29
+ Environment=${systemdQuote(`PATH=${input.pathEnv}`)}
30
+ Environment=${systemdQuote(`RYNX_HOME=${input.dataDir}`)}
31
+ Environment=${systemdQuote(`${RYNX_SYSTEMD_SERVICE_ENV}=${RYNX_SYSTEMD_SERVICE}`)}
32
+ ExecStart=${systemdQuote(input.nodePath)} ${systemdQuote(input.cliPath)} start --systemd-service
33
+ ExecStop=${systemdQuote(input.nodePath)} ${systemdQuote(input.cliPath)} stop --systemd-service
34
+
35
+ [Install]
36
+ WantedBy=default.target
37
+ `;
38
+ }
39
+ export function linuxUserSystemdAvailable() {
40
+ return process.platform === "linux" &&
41
+ run("systemctl", ["--user", "show-environment"]).status === 0;
42
+ }
43
+ export function inspectLinuxAutostart() {
44
+ const context = currentContext();
45
+ if (!linuxUserSystemdAvailable()) {
46
+ return {
47
+ supported: false,
48
+ registered: existsSync(context.unitPath),
49
+ manager: "systemd-user",
50
+ registrationPath: context.unitPath,
51
+ detail: "user systemd is unavailable in this login session",
52
+ };
53
+ }
54
+ const lingerEnabled = inspectLinuxLinger();
55
+ const active = run("systemctl", ["--user", "is-active", RYNX_SYSTEMD_SERVICE]).status === 0;
56
+ let detail;
57
+ if (active) {
58
+ try {
59
+ assertRunningServiceOwnership(context);
60
+ }
61
+ catch (error) {
62
+ detail = errorMessage(error);
63
+ }
64
+ }
65
+ return {
66
+ supported: true,
67
+ registered: run("systemctl", ["--user", "is-enabled", RYNX_SYSTEMD_SERVICE]).status === 0,
68
+ active,
69
+ ...(lingerEnabled === undefined
70
+ ? {}
71
+ : {
72
+ lingerEnabled,
73
+ ...(!lingerEnabled
74
+ ? { lingerEnableCommand: `sudo loginctl enable-linger ${userInfo().username}` }
75
+ : {}),
76
+ }),
77
+ manager: "systemd-user",
78
+ registrationPath: context.unitPath,
79
+ ...(detail ? { detail } : {}),
80
+ };
81
+ }
82
+ export function enableLinuxAutostart() {
83
+ assertUserSystemdAvailable();
84
+ const context = currentContext();
85
+ syncLinuxService(context);
86
+ requireSuccess(run("systemctl", ["--user", "enable", RYNX_SYSTEMD_SERVICE]), "systemctl --user enable");
87
+ }
88
+ export function disableLinuxAutostart() {
89
+ assertUserSystemdAvailable();
90
+ const context = currentContext();
91
+ const disabled = run("systemctl", ["--user", "disable", RYNX_SYSTEMD_SERVICE]);
92
+ if (disabled.status !== 0 && existsSync(context.unitPath)) {
93
+ requireSuccess(disabled, "systemctl --user disable");
94
+ }
95
+ rmSync(context.unitPath, { force: true });
96
+ requireSuccess(run("systemctl", ["--user", "daemon-reload"]), "systemctl --user daemon-reload");
97
+ }
98
+ /** Refresh a registered or lifecycle-created unit without changing enablement. */
99
+ export function refreshLinuxServiceIfPresent() {
100
+ if (process.platform !== "linux" || !linuxUserSystemdAvailable())
101
+ return false;
102
+ const context = currentContext();
103
+ if (!existsSync(context.unitPath))
104
+ return false;
105
+ return syncLinuxService(context);
106
+ }
107
+ export function assertLinuxSystemdInvocation() {
108
+ if (process.platform !== "linux") {
109
+ throw new Error("--systemd-service is only valid on Linux");
110
+ }
111
+ if (process.env[RYNX_SYSTEMD_SERVICE_ENV] !== RYNX_SYSTEMD_SERVICE) {
112
+ throw new Error("--systemd-service requires the Rynx systemd environment");
113
+ }
114
+ const cgroup = linuxSystemdCgroupForPid(process.pid);
115
+ if (!cgroupHasService(cgroup)) {
116
+ throw new Error(`--systemd-service requires the ${RYNX_SYSTEMD_SERVICE} cgroup`);
117
+ }
118
+ }
119
+ export async function runLinuxSystemdLifecycle(command, actions, options = {}) {
120
+ assertUserSystemdAvailable();
121
+ const context = currentContext();
122
+ if (command === "status") {
123
+ return statusLinuxLifecycle(context, actions);
124
+ }
125
+ if (command === "stop") {
126
+ return stopLinuxLifecycle(context, actions);
127
+ }
128
+ const unitChanged = syncLinuxService(context);
129
+ const before = inspectLinuxPm2GodOwnership(context.pm2Home);
130
+ assertSingleGod(before);
131
+ const serviceBefore = inspectLinuxSystemdService();
132
+ if (before.kind === "owned" && serviceOwnsGod(serviceBefore, before.processes[0])) {
133
+ if (command === "start") {
134
+ if (unitChanged || await actions.restartRequired()) {
135
+ systemctlUser(["restart", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
136
+ assertRunningServiceOwnership(context);
137
+ return options.quiet ? 0 : actions.status();
138
+ }
139
+ const result = await actions.start(options.quiet ? { quiet: true } : undefined);
140
+ if (result !== 0)
141
+ return result;
142
+ assertRunningServiceOwnership(context);
143
+ return 0;
144
+ }
145
+ const previous = before.processes[0];
146
+ systemctlUser(["restart", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
147
+ const current = assertRunningServiceOwnership(context);
148
+ if (current.pid === previous.pid &&
149
+ current.startIdentity === previous.startIdentity) {
150
+ throw new Error("systemd restart did not replace the PM2 supervisor generation");
151
+ }
152
+ return options.quiet ? 0 : actions.status();
153
+ }
154
+ if (before.kind !== "absent") {
155
+ assertMigratableOwnership(before);
156
+ const stopped = await actions.stop();
157
+ if (stopped !== 0)
158
+ return stopped;
159
+ const afterStop = inspectLinuxPm2GodOwnership(context.pm2Home);
160
+ if (afterStop.kind !== "absent") {
161
+ throw new Error("the previous PM2 supervisor is still running after shutdown");
162
+ }
163
+ }
164
+ if (serviceBefore.activeState !== "inactive") {
165
+ systemctlUser(["stop", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
166
+ }
167
+ run("systemctl", ["--user", "reset-failed", RYNX_SYSTEMD_SERVICE]);
168
+ systemctlUser(["start", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
169
+ assertRunningServiceOwnership(context);
170
+ return options.quiet ? 0 : actions.status();
171
+ }
172
+ export function parseLinuxSystemdShow(output) {
173
+ const values = new Map();
174
+ for (const line of output.split(/\r?\n/)) {
175
+ const separator = line.indexOf("=");
176
+ if (separator <= 0)
177
+ continue;
178
+ values.set(line.slice(0, separator), line.slice(separator + 1));
179
+ }
180
+ const parsedMainPid = Number.parseInt(values.get("MainPID") ?? "0", 10);
181
+ return {
182
+ loadState: values.get("LoadState") ?? "",
183
+ activeState: values.get("ActiveState") ?? "",
184
+ subState: values.get("SubState") ?? "",
185
+ type: values.get("Type") ?? "",
186
+ mainPid: Number.isSafeInteger(parsedMainPid) && parsedMainPid > 1
187
+ ? parsedMainPid
188
+ : 0,
189
+ killMode: values.get("KillMode") ?? "",
190
+ killSignal: values.get("KillSignal") ?? "",
191
+ restartKillSignal: values.get("RestartKillSignal") ?? "",
192
+ finalKillSignal: values.get("FinalKillSignal") ?? "",
193
+ sendSigkill: values.get("SendSIGKILL") ?? "",
194
+ workingDirectory: values.get("WorkingDirectory") ?? "",
195
+ pidFile: values.get("PIDFile") ?? "",
196
+ environment: values.get("Environment") ?? "",
197
+ execStart: values.get("ExecStart") ?? "",
198
+ execStop: values.get("ExecStop") ?? "",
199
+ };
200
+ }
201
+ export function inspectLinuxPm2GodOwnership(home, deps = {}) {
202
+ const readText = deps.readText ?? ((file) => readFileSync(file, "utf8"));
203
+ const entries = scanLinuxPm2GodPids(home, deps);
204
+ const processes = [];
205
+ for (const pid of entries) {
206
+ try {
207
+ const startBefore = linuxProcStartIdentity(readText(`/proc/${pid}/stat`));
208
+ const cmdline = readText(`/proc/${pid}/cmdline`).replaceAll("\u0000", " ").trim();
209
+ const cgroup = linuxSystemdCgroupForPid(pid, readText);
210
+ const startAfter = linuxProcStartIdentity(readText(`/proc/${pid}/stat`));
211
+ if (!startBefore || startBefore !== startAfter) {
212
+ throw new Error(`PM2 supervisor pid ${pid} changed during inspection`);
213
+ }
214
+ if (!isDedicatedPm2God(cmdline, home))
215
+ continue;
216
+ processes.push({ pid, cgroup, startIdentity: startBefore });
217
+ }
218
+ catch (error) {
219
+ if (!processDisappeared(error))
220
+ throw procReadError(`/proc/${pid}`, error);
221
+ }
222
+ }
223
+ if (processes.length === 0)
224
+ return { kind: "absent" };
225
+ const external = processes.filter((entry) => !cgroupHasService(entry.cgroup));
226
+ return external.length === 0
227
+ ? { kind: "owned", processes }
228
+ : { kind: "external", processes };
229
+ }
230
+ export function scanLinuxPm2GodPids(home, deps = {}) {
231
+ const procEntries = deps.procEntries ?? (() => readdirSync("/proc"));
232
+ const readText = deps.readText ?? ((file) => readFileSync(file, "utf8"));
233
+ const statUid = deps.statUid ?? ((file) => statSync(file).uid);
234
+ const currentUid = deps.currentUid ?? process.getuid?.();
235
+ let entries;
236
+ try {
237
+ entries = procEntries();
238
+ }
239
+ catch (error) {
240
+ throw new Error(`cannot inspect /proc: ${errorMessage(error)}`);
241
+ }
242
+ const pids = [];
243
+ for (const entry of entries) {
244
+ if (!/^\d+$/.test(entry))
245
+ continue;
246
+ const pid = Number(entry);
247
+ if (!Number.isSafeInteger(pid) || pid <= 1)
248
+ continue;
249
+ if (currentUid !== undefined) {
250
+ try {
251
+ if (statUid(`/proc/${pid}`) !== currentUid)
252
+ continue;
253
+ }
254
+ catch (error) {
255
+ if (processDisappeared(error))
256
+ continue;
257
+ throw procReadError(`/proc/${pid}`, error);
258
+ }
259
+ }
260
+ try {
261
+ const cmdline = readText(`/proc/${pid}/cmdline`).replaceAll("\u0000", " ").trim();
262
+ if (isDedicatedPm2God(cmdline, home))
263
+ pids.push(pid);
264
+ }
265
+ catch (error) {
266
+ if (!processDisappeared(error)) {
267
+ throw procReadError(`/proc/${pid}/cmdline`, error);
268
+ }
269
+ }
270
+ }
271
+ return [...new Set(pids)].sort((left, right) => left - right);
272
+ }
273
+ export function linuxSystemdCgroupForPid(pid, readText = (file) => readFileSync(file, "utf8")) {
274
+ return normalizedCgroupPath(readText(`/proc/${pid}/cgroup`)) ?? "(unreadable)";
275
+ }
276
+ function currentContext() {
277
+ const dataDir = rynxHome();
278
+ const homeDir = homedir();
279
+ const pm2Home = path.join(dataDir, "pm2");
280
+ return {
281
+ homeDir,
282
+ dataDir,
283
+ logDir: path.join(dataDir, "logs"),
284
+ cliPath: fileURLToPath(new URL("./cli.js", import.meta.url)),
285
+ nodePath: process.execPath,
286
+ pathEnv: process.env.PATH || defaultLinuxPath(),
287
+ pm2Home,
288
+ pidFile: path.join(pm2Home, "pm2.pid"),
289
+ unitPath: path.join(homeDir, ".config", "systemd", "user", RYNX_SYSTEMD_SERVICE),
290
+ };
291
+ }
292
+ function syncLinuxService(context) {
293
+ mkdirSync(context.dataDir, { recursive: true, mode: 0o700 });
294
+ mkdirSync(context.logDir, { recursive: true, mode: 0o700 });
295
+ const content = renderSystemdUnit(context);
296
+ const previous = existsSync(context.unitPath)
297
+ ? readFileSync(context.unitPath, "utf8")
298
+ : undefined;
299
+ const changed = previous !== content;
300
+ if (changed)
301
+ replaceFileAtomically(context.unitPath, content);
302
+ try {
303
+ requireSuccess(run("systemctl", ["--user", "daemon-reload"]), "systemctl --user daemon-reload");
304
+ assertStaticServiceDefinition(context, inspectLinuxSystemdService());
305
+ return changed;
306
+ }
307
+ catch (error) {
308
+ if (changed) {
309
+ if (previous === undefined)
310
+ rmSync(context.unitPath, { force: true });
311
+ else
312
+ replaceFileAtomically(context.unitPath, previous);
313
+ const rollback = run("systemctl", ["--user", "daemon-reload"]);
314
+ if (rollback.status !== 0) {
315
+ throw new AggregateError([error, new Error(`systemd rollback failed: ${commandDetail(rollback)}`)], "could not update or restore the Rynx systemd service");
316
+ }
317
+ }
318
+ throw error;
319
+ }
320
+ }
321
+ function inspectLinuxSystemdService() {
322
+ const output = systemctlUser([
323
+ "show",
324
+ RYNX_SYSTEMD_SERVICE,
325
+ "--property=LoadState",
326
+ "--property=ActiveState",
327
+ "--property=SubState",
328
+ "--property=Type",
329
+ "--property=MainPID",
330
+ "--property=KillMode",
331
+ "--property=KillSignal",
332
+ "--property=RestartKillSignal",
333
+ "--property=FinalKillSignal",
334
+ "--property=SendSIGKILL",
335
+ "--property=WorkingDirectory",
336
+ "--property=PIDFile",
337
+ "--property=Environment",
338
+ "--property=ExecStart",
339
+ "--property=ExecStop",
340
+ ]);
341
+ return parseLinuxSystemdShow(output);
342
+ }
343
+ function assertStaticServiceDefinition(context, state) {
344
+ const errors = [];
345
+ if (state.loadState !== "loaded")
346
+ errors.push(`LoadState=${state.loadState || "(empty)"}`);
347
+ if (state.type !== "forking")
348
+ errors.push(`Type=${state.type || "(empty)"}`);
349
+ if (state.pidFile !== context.pidFile)
350
+ errors.push(`PIDFile=${state.pidFile || "(empty)"}`);
351
+ if (state.workingDirectory !== context.dataDir) {
352
+ errors.push(`WorkingDirectory=${state.workingDirectory || "(empty)"}`);
353
+ }
354
+ if (state.killMode !== "process")
355
+ errors.push(`KillMode=${state.killMode || "(empty)"}`);
356
+ if (!signalMatches(state.killSignal, "18", "SIGCONT")) {
357
+ errors.push(`KillSignal=${state.killSignal || "(empty)"}`);
358
+ }
359
+ if (!signalMatches(state.restartKillSignal, "18", "SIGCONT")) {
360
+ errors.push(`RestartKillSignal=${state.restartKillSignal || "(empty)"}`);
361
+ }
362
+ if (!signalMatches(state.finalKillSignal, "18", "SIGCONT")) {
363
+ errors.push(`FinalKillSignal=${state.finalKillSignal || "(empty)"}`);
364
+ }
365
+ if (state.sendSigkill !== "no")
366
+ errors.push(`SendSIGKILL=${state.sendSigkill || "(empty)"}`);
367
+ for (const expected of [
368
+ `RYNX_HOME=${context.dataDir}`,
369
+ `${RYNX_SYSTEMD_SERVICE_ENV}=${RYNX_SYSTEMD_SERVICE}`,
370
+ `PATH=${context.pathEnv}`,
371
+ ]) {
372
+ if (!state.environment.includes(expected))
373
+ errors.push(`Environment missing ${expected}`);
374
+ }
375
+ for (const [name, value, action] of [
376
+ ["ExecStart", state.execStart, "start"],
377
+ ["ExecStop", state.execStop, "stop"],
378
+ ]) {
379
+ if (!value.includes(context.nodePath) ||
380
+ !value.includes(context.cliPath) ||
381
+ !value.includes(action) ||
382
+ !value.includes("--systemd-service")) {
383
+ errors.push(`${name}=${value || "(empty)"}`);
384
+ }
385
+ }
386
+ if (errors.length > 0) {
387
+ throw new Error(`${RYNX_SYSTEMD_SERVICE} effective configuration is invalid: ${errors.join("; ")}`);
388
+ }
389
+ }
390
+ function assertRunningServiceOwnership(context) {
391
+ const ownership = inspectLinuxPm2GodOwnership(context.pm2Home);
392
+ assertSingleGod(ownership);
393
+ if (ownership.kind !== "owned") {
394
+ throw new Error(ownership.kind === "absent"
395
+ ? "systemd started without a PM2 supervisor"
396
+ : `PM2 supervisor is outside ${RYNX_SYSTEMD_SERVICE}: ${describeOwnership(ownership)}`);
397
+ }
398
+ const process = ownership.processes[0];
399
+ const state = inspectLinuxSystemdService();
400
+ assertStaticServiceDefinition(context, state);
401
+ if (state.activeState !== "active" ||
402
+ state.subState !== "running" ||
403
+ state.mainPid !== process.pid) {
404
+ throw new Error(`${RYNX_SYSTEMD_SERVICE} does not own the PM2 supervisor: ` +
405
+ `ActiveState=${state.activeState}, SubState=${state.subState}, ` +
406
+ `MainPID=${state.mainPid}, PM2=${process.pid}`);
407
+ }
408
+ return process;
409
+ }
410
+ async function statusLinuxLifecycle(context, actions) {
411
+ const ownership = inspectLinuxPm2GodOwnership(context.pm2Home);
412
+ if (ownership.kind === "absent") {
413
+ console.log("rynx: stopped (PM2 supervisor is absent)");
414
+ return 1;
415
+ }
416
+ assertSingleGod(ownership);
417
+ const directStatus = await actions.status();
418
+ if (ownership.kind !== "owned") {
419
+ console.error(`rynx: PM2 supervisor is not owned by ${RYNX_SYSTEMD_SERVICE}: ${describeOwnership(ownership)}`);
420
+ return 2;
421
+ }
422
+ try {
423
+ assertRunningServiceOwnership(context);
424
+ }
425
+ catch (error) {
426
+ console.error(`rynx: ${errorMessage(error)}`);
427
+ return 1;
428
+ }
429
+ return directStatus;
430
+ }
431
+ async function stopLinuxLifecycle(context, actions) {
432
+ const ownership = inspectLinuxPm2GodOwnership(context.pm2Home);
433
+ assertSingleGod(ownership);
434
+ if (ownership.kind === "owned") {
435
+ const state = inspectLinuxSystemdService();
436
+ if (serviceOwnsGod(state, ownership.processes[0])) {
437
+ systemctlUser(["stop", RYNX_SYSTEMD_SERVICE], LIFECYCLE_TIMEOUT_MS);
438
+ if (inspectLinuxPm2GodOwnership(context.pm2Home).kind !== "absent") {
439
+ throw new Error("PM2 supervisor survived systemd stop");
440
+ }
441
+ console.log("rynx stopped.");
442
+ return 0;
443
+ }
444
+ }
445
+ if (ownership.kind === "external")
446
+ assertMigratableOwnership(ownership);
447
+ return actions.stop();
448
+ }
449
+ function assertSingleGod(ownership) {
450
+ if (ownership.kind !== "absent" && ownership.processes.length !== 1) {
451
+ throw new Error(`multiple PM2 supervisors use this RYNX_HOME: ${describeOwnership(ownership)}`);
452
+ }
453
+ }
454
+ function assertMigratableOwnership(ownership) {
455
+ if (ownership.kind !== "external")
456
+ return;
457
+ const externalService = ownership.processes.find((entry) => serviceOwner(entry.cgroup)?.endsWith(".service"));
458
+ if (externalService) {
459
+ throw new Error(`refusing to replace a PM2 supervisor owned by ${serviceOwner(externalService.cgroup)}`);
460
+ }
461
+ }
462
+ function serviceOwnsGod(state, process) {
463
+ return state.activeState === "active" &&
464
+ state.subState === "running" &&
465
+ state.mainPid === process.pid &&
466
+ cgroupHasService(process.cgroup);
467
+ }
468
+ function describeOwnership(ownership) {
469
+ if (ownership.kind === "absent")
470
+ return "absent";
471
+ return ownership.processes
472
+ .map((entry) => `${entry.pid}@${entry.cgroup}`)
473
+ .join(", ");
474
+ }
475
+ function serviceOwner(cgroup) {
476
+ return cgroup.split("/").reverse().find((part) => part.endsWith(".service") || part.endsWith(".scope"));
477
+ }
478
+ function inspectLinuxLinger() {
479
+ const result = run("loginctl", [
480
+ "show-user",
481
+ userInfo().username,
482
+ "--property=Linger",
483
+ ]);
484
+ if (result.status !== 0)
485
+ return undefined;
486
+ const value = result.stdout?.trim();
487
+ if (value === "Linger=yes")
488
+ return true;
489
+ if (value === "Linger=no")
490
+ return false;
491
+ return undefined;
492
+ }
493
+ function normalizedCgroupPath(raw) {
494
+ const trimmed = raw.trim();
495
+ if (!trimmed)
496
+ return undefined;
497
+ if (trimmed.startsWith("/"))
498
+ return trimmed;
499
+ let unified;
500
+ for (const line of trimmed.split(/\r?\n/)) {
501
+ const first = line.indexOf(":");
502
+ const second = first < 0 ? -1 : line.indexOf(":", first + 1);
503
+ if (second < 0)
504
+ continue;
505
+ const hierarchy = line.slice(0, first);
506
+ const controllers = line.slice(first + 1, second);
507
+ const cgroup = line.slice(second + 1).trim();
508
+ if (!cgroup)
509
+ continue;
510
+ if (controllers.split(",").includes("name=systemd"))
511
+ return cgroup;
512
+ if (hierarchy === "0" && controllers === "")
513
+ unified = cgroup;
514
+ }
515
+ return unified;
516
+ }
517
+ function cgroupHasService(cgroup) {
518
+ return cgroup?.split("/").includes(RYNX_SYSTEMD_SERVICE) === true;
519
+ }
520
+ function linuxProcStartIdentity(raw) {
521
+ const closeParen = raw.lastIndexOf(")");
522
+ if (closeParen < 0)
523
+ return undefined;
524
+ return raw.slice(closeParen + 2).trim().split(/\s+/)[19];
525
+ }
526
+ function isDedicatedPm2God(cmdline, home) {
527
+ return cmdline.includes("PM2 v") && cmdline.includes(`God Daemon (${home})`);
528
+ }
529
+ function processDisappeared(error) {
530
+ const code = error?.code;
531
+ return code === "ENOENT" || code === "ESRCH" || code === "ENOTDIR";
532
+ }
533
+ function procReadError(file, error) {
534
+ return new Error(`cannot inspect ${file}: ${errorMessage(error)}`);
535
+ }
536
+ function assertUserSystemdAvailable() {
537
+ if (!linuxUserSystemdAvailable()) {
538
+ throw new Error("user systemd is unavailable in this login session");
539
+ }
540
+ }
541
+ function systemctlUser(args, timeout = COMMAND_TIMEOUT_MS) {
542
+ const result = run("systemctl", ["--user", ...args], timeout);
543
+ requireSuccess(result, `systemctl --user ${args.join(" ")}`);
544
+ return result.stdout ?? "";
545
+ }
546
+ function replaceFileAtomically(file, content) {
547
+ mkdirSync(path.dirname(file), { recursive: true });
548
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
549
+ try {
550
+ writeFileSync(temporary, content, {
551
+ encoding: "utf8",
552
+ flag: "wx",
553
+ mode: 0o644,
554
+ });
555
+ renameSync(temporary, file);
556
+ }
557
+ finally {
558
+ rmSync(temporary, { force: true });
559
+ }
560
+ }
561
+ function run(command, args, timeout = COMMAND_TIMEOUT_MS) {
562
+ return spawnSync(command, [...args], {
563
+ encoding: "utf8",
564
+ stdio: "pipe",
565
+ timeout,
566
+ killSignal: "SIGTERM",
567
+ });
568
+ }
569
+ function requireSuccess(result, description) {
570
+ if (result.status === 0)
571
+ return;
572
+ throw new Error(`${description} failed: ${commandDetail(result)}`);
573
+ }
574
+ function commandDetail(result) {
575
+ return result.error?.message
576
+ || result.stderr?.trim()
577
+ || result.stdout?.trim()
578
+ || `exit ${result.status ?? "unknown"}`;
579
+ }
580
+ function errorMessage(error) {
581
+ return error instanceof Error ? error.message : String(error);
582
+ }
583
+ function signalMatches(value, number, name) {
584
+ return value === number || value === name;
585
+ }
586
+ function defaultLinuxPath() {
587
+ return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
588
+ }
589
+ function systemdQuote(value) {
590
+ return `"${value
591
+ .replaceAll("\\", "\\\\")
592
+ .replaceAll("\"", "\\\"")
593
+ .replaceAll("%", "%%")
594
+ .replaceAll("\t", "\\t")
595
+ .replaceAll("\n", "\\n")
596
+ .replaceAll("\r", "\\r")}"`;
597
+ }
598
+ function systemdPath(value) {
599
+ return value
600
+ .replaceAll("%", "%%")
601
+ .replaceAll("\\", "\\x5c")
602
+ .replaceAll(" ", "\\x20")
603
+ .replaceAll("\t", "\\t")
604
+ .replaceAll("\n", "\\n")
605
+ .replaceAll("\r", "\\r")
606
+ .replaceAll('"', "\\x22")
607
+ .replaceAll("'", "\\x27");
608
+ }
package/dist/usage.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const USAGE = "Usage: rynx <command>\n\nGeneral:\n version [--json] | --version\n print the installed Rynx version\n\nSetup:\n setup [--non-interactive] [--default-runtime <codex|traex|claude>]\n [--host <host>] [--port <port>] [--log-level <level>]\n [--install-browser|--skip-browser] [--json|--result-file <path>]\n initialize configuration and local dependencies\n doctor read-only health check\n\nLifecycle:\n start | restart | stop | status | logs\n update [version] [--check] [--json]\n\nPlugins:\n market list\n market add <git-or-local-source> [--alias <id>]\n market refresh [market-id]\n market remove <market-id>\n plugin list\n plugin install <source|plugin@market> [--force] [--expect-digest <sha256-...>]\n plugin update <plugin@market> [--expect-digest <sha256-...>]\n plugin enable|disable|uninstall <plugin@market>\n plugin <plugin@market> <command> invoke a plugin-owned command\n\nAgents:\n agent list\n agent show <id>\n agent add <id>\n agent rm <id>\n\nBuiltin Skills:\n skills list [--json]\n skills get <browser|emulator> [--full] [--json]\n\nMaintenance:\n cleanup sessions [--dry-run]\n\nEmulator:\n emulator <args...>\n\nRemote Runtime:\n runtime share --address <host|ws-url> [--label <label>] [--json]\n runtime add --pairing-code <rynx://...> [--name <name>]\n runtime list [--json]\n runtime test <local|daemon-id> [--json]\n runtime forget <daemon-id>\n runtime clients list [--json]\n runtime clients revoke <grant-id>\n\nSessions:\n session fork <session-id> [--title <title>] [--json]\n\nBrowser:\n browser install|update|version|clean [...]\n browser open [url] [--session <id>] [--runtime <local|daemon-id>] [--json]\n browser status|pages|close [--session <id>] [--runtime <local|daemon-id>] [--json]\n browser endpoint [--ensure] [--session <local-id>] [--json]\n browser snapshot [--session <local-id>] [--json]\n browser navigate <url> [--session <local-id>] [--json]\n browser click (--ref <ref>|--selector <css>|--x <n> --y <n>) [--session <local-id>] [--json]\n browser type (--ref <ref>|--selector <css>) --text <text> [--session <local-id>] [--json]\n browser screenshot --output <absolute-path> [--session <local-id>] [--json]\n";
1
+ export declare const USAGE = "Usage: rynx <command>\n\nGeneral:\n version [--json] | --version\n print the installed Rynx version\n\nSetup:\n setup [--non-interactive] [--default-runtime <codex|traex|claude>]\n [--host <host>] [--port <port>] [--log-level <level>]\n [--install-browser|--skip-browser] [--json|--result-file <path>]\n initialize configuration and local dependencies\n doctor read-only health check\n\nLifecycle:\n start | restart | stop | status | logs\n autostart enable|disable|status\n update [version] [--check] [--json]\n\nPlugins:\n market list\n market add <git-or-local-source> [--alias <id>]\n market refresh [market-id]\n market remove <market-id>\n plugin list\n plugin install <source|plugin@market> [--force] [--expect-digest <sha256-...>]\n plugin update <plugin@market> [--expect-digest <sha256-...>]\n plugin enable|disable|uninstall <plugin@market>\n plugin <plugin@market> <command> invoke a plugin-owned command\n\nAgents:\n agent list\n agent show <id>\n agent add <id>\n agent rm <id>\n\nBuiltin Skills:\n skills list [--json]\n skills get <browser|emulator> [--full] [--json]\n\nMaintenance:\n cleanup sessions [--dry-run]\n\nEmulator:\n emulator <args...>\n\nRemote Runtime:\n runtime share --address <host|ws-url> [--label <label>] [--json]\n runtime add --pairing-code <rynx://...> [--name <name>]\n runtime list [--json]\n runtime test <local|daemon-id> [--json]\n runtime forget <daemon-id>\n runtime clients list [--json]\n runtime clients revoke <grant-id>\n\nSessions:\n session fork <session-id> [--title <title>] [--json]\n\nBrowser:\n browser install|update|version|clean [...]\n browser open [url] [--session <id>] [--runtime <local|daemon-id>] [--json]\n browser status|pages|close [--session <id>] [--runtime <local|daemon-id>] [--json]\n browser endpoint [--ensure] [--session <local-id>] [--json]\n browser snapshot [--session <local-id>] [--json]\n browser navigate <url> [--session <local-id>] [--json]\n browser click (--ref <ref>|--selector <css>|--x <n> --y <n>) [--session <local-id>] [--json]\n browser type (--ref <ref>|--selector <css>) --text <text> [--session <local-id>] [--json]\n browser screenshot --output <absolute-path> [--session <local-id>] [--json]\n";
package/dist/usage.js CHANGED
@@ -13,6 +13,7 @@ Setup:
13
13
 
14
14
  Lifecycle:
15
15
  start | restart | stop | status | logs
16
+ autostart enable|disable|status
16
17
  update [version] [--check] [--json]
17
18
 
18
19
  Plugins:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/cli",
3
- "version": "0.1.11-beta.32",
3
+ "version": "0.1.11-beta.34",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -51,12 +51,12 @@
51
51
  "dependencies": {
52
52
  "@clack/prompts": "^1.6.0",
53
53
  "ws": "^8.21.0",
54
- "@rynx-ai/browser-cdp": "0.1.11-beta.32",
55
- "@rynx-ai/core": "0.1.11-beta.32",
56
- "@rynx-ai/daemon": "0.1.11-beta.32",
57
- "@rynx-ai/emulator": "0.1.11-beta.32",
58
- "@rynx-ai/protocol": "0.1.11-beta.32",
59
- "@rynx-ai/tmux": "0.1.11-beta.32"
54
+ "@rynx-ai/core": "0.1.11-beta.34",
55
+ "@rynx-ai/browser-cdp": "0.1.11-beta.34",
56
+ "@rynx-ai/emulator": "0.1.11-beta.34",
57
+ "@rynx-ai/daemon": "0.1.11-beta.34",
58
+ "@rynx-ai/protocol": "0.1.11-beta.34",
59
+ "@rynx-ai/tmux": "0.1.11-beta.34"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/ws": "^8.18.1"