@rynx-ai/cli 0.1.11-beta.4 → 0.1.11-beta.40

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,53 @@
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 declare const RYNX_SERVICE_ENV_KEYS = "RYNX_SERVICE_ENV_KEYS";
5
+ export interface LinuxSystemdServiceState {
6
+ loadState: string;
7
+ activeState: string;
8
+ subState: string;
9
+ type: string;
10
+ mainPid: number;
11
+ killMode: string;
12
+ killSignal: string;
13
+ restartKillSignal: string;
14
+ finalKillSignal: string;
15
+ sendSigkill: string;
16
+ workingDirectory: string;
17
+ pidFile: string;
18
+ environment: string;
19
+ execStart: string;
20
+ execStop: string;
21
+ }
22
+ export interface LinuxPm2GodProcess {
23
+ pid: number;
24
+ cgroup: string;
25
+ startIdentity: string;
26
+ }
27
+ export type LinuxPm2GodOwnership = {
28
+ kind: "absent";
29
+ } | {
30
+ kind: "owned";
31
+ processes: LinuxPm2GodProcess[];
32
+ } | {
33
+ kind: "external";
34
+ processes: LinuxPm2GodProcess[];
35
+ };
36
+ export interface LinuxPm2InspectionDeps {
37
+ procEntries?: () => string[];
38
+ readText?: (file: string) => string;
39
+ statUid?: (file: string) => number;
40
+ currentUid?: number;
41
+ }
42
+ export declare function renderSystemdUnit(input: AutostartRenderInput): string;
43
+ export declare function linuxUserSystemdAvailable(): boolean;
44
+ export declare function inspectLinuxAutostart(): AutostartState;
45
+ export declare function enableLinuxAutostart(): void;
46
+ export declare function disableLinuxAutostart(): void;
47
+ /** Refresh an existing autostart unit without changing enablement. */
48
+ export declare function refreshLinuxServiceIfPresent(): boolean;
49
+ export declare function assertLinuxSystemdInvocation(): void;
50
+ export declare function parseLinuxSystemdShow(output: string): LinuxSystemdServiceState;
51
+ export declare function inspectLinuxPm2GodOwnership(home: string, deps?: LinuxPm2InspectionDeps): LinuxPm2GodOwnership;
52
+ export declare function scanLinuxPm2GodPids(home: string, deps?: LinuxPm2InspectionDeps): number[];
53
+ export declare function linuxSystemdCgroupForPid(pid: number, readText?: (file: string) => string): string;
@@ -0,0 +1,589 @@
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
+ export const RYNX_SERVICE_ENV_KEYS = "RYNX_SERVICE_ENV_KEYS";
10
+ const COMMAND_TIMEOUT_MS = 5_000;
11
+ const AUTOMATIC_SERVICE_ENVIRONMENT = new Set([
12
+ "ALL_PROXY",
13
+ "AWS_CA_BUNDLE",
14
+ "CURL_CA_BUNDLE",
15
+ "GIT_SSL_CAINFO",
16
+ "HTTPS_PROXY",
17
+ "HTTP_PROXY",
18
+ "NODE_EXTRA_CA_CERTS",
19
+ "NO_PROXY",
20
+ "REQUESTS_CA_BUNDLE",
21
+ "SSL_CERT_DIR",
22
+ "SSL_CERT_FILE",
23
+ "all_proxy",
24
+ "https_proxy",
25
+ "http_proxy",
26
+ "no_proxy",
27
+ ]);
28
+ const BLOCKED_SERVICE_ENVIRONMENT = new Set([
29
+ "PATH",
30
+ "INVOCATION_ID",
31
+ "JOURNAL_STREAM",
32
+ "LISTEN_FDS",
33
+ "LISTEN_FDNAMES",
34
+ "LISTEN_PID",
35
+ "MAINPID",
36
+ "MANAGERPID",
37
+ "NOTIFY_SOCKET",
38
+ "ELECTRON_RUN_AS_NODE",
39
+ "NODE_CHANNEL_FD",
40
+ "NODE_UNIQUE_ID",
41
+ "PM2_HOME",
42
+ "RYNX_DAEMON_LIFECYCLE",
43
+ "RYNX_HOME",
44
+ "RYNX_REFRESH_LOGIN_SHELL_PATH",
45
+ RYNX_SERVICE_ENV_KEYS,
46
+ RYNX_SYSTEMD_SERVICE_ENV,
47
+ "SYSTEMD_EXEC_PID",
48
+ "SYSTEMD_INVOCATION_ID",
49
+ "WATCHDOG_PID",
50
+ "WATCHDOG_USEC",
51
+ ]);
52
+ const TRANSIENT_SERVICE_ENVIRONMENT = new Set([
53
+ "_",
54
+ "CI",
55
+ "CLICOLOR",
56
+ "CLICOLOR_FORCE",
57
+ "CODEX_CI",
58
+ "COLORTERM",
59
+ "FORCE_COLOR",
60
+ "GH_PAGER",
61
+ "GIT_PAGER",
62
+ "NO_COLOR",
63
+ "OLDPWD",
64
+ "PAGER",
65
+ "PWD",
66
+ "SHLVL",
67
+ "SSH_CLIENT",
68
+ "SSH_CONNECTION",
69
+ "SSH_TTY",
70
+ "TERM",
71
+ "TERM_PROGRAM",
72
+ "TERM_PROGRAM_VERSION",
73
+ "TERM_SESSION_ID",
74
+ "TERMINFO",
75
+ "TMUX",
76
+ "TMUX_PANE",
77
+ ]);
78
+ export function renderSystemdUnit(input) {
79
+ const pidFile = path.join(input.dataDir, "pm2", "pm2.pid");
80
+ const inheritedEnvironment = renderInheritedEnvironment(input.environment);
81
+ return `[Unit]
82
+ Description=Rynx local control plane
83
+ After=network-online.target
84
+ Wants=network-online.target
85
+
86
+ [Service]
87
+ Type=forking
88
+ PIDFile=${systemdPath(pidFile)}
89
+ KillMode=process
90
+ KillSignal=SIGCONT
91
+ RestartKillSignal=SIGCONT
92
+ FinalKillSignal=SIGCONT
93
+ SendSIGKILL=no
94
+ TimeoutStartSec=60
95
+ TimeoutStopSec=45
96
+ WorkingDirectory=${systemdPath(input.dataDir)}
97
+ ${inheritedEnvironment}
98
+ Environment=${systemdQuote(`PATH=${input.pathEnv}`)}
99
+ Environment=${systemdQuote(`RYNX_HOME=${input.dataDir}`)}
100
+ Environment=${systemdQuote(`${RYNX_SYSTEMD_SERVICE_ENV}=${RYNX_SYSTEMD_SERVICE}`)}
101
+ ExecStart=${systemdQuote(input.nodePath)} ${systemdQuote(input.cliPath)} start --systemd-service
102
+ ExecStop=${systemdQuote(input.nodePath)} ${systemdQuote(input.cliPath)} stop --systemd-service
103
+
104
+ [Install]
105
+ WantedBy=default.target
106
+ `;
107
+ }
108
+ export function linuxUserSystemdAvailable() {
109
+ return process.platform === "linux" &&
110
+ run("systemctl", ["--user", "show-environment"]).status === 0;
111
+ }
112
+ export function inspectLinuxAutostart() {
113
+ const context = currentContext();
114
+ if (!linuxUserSystemdAvailable()) {
115
+ return {
116
+ supported: false,
117
+ registered: existsSync(context.unitPath),
118
+ manager: "systemd-user",
119
+ registrationPath: context.unitPath,
120
+ detail: "user systemd is unavailable in this login session",
121
+ };
122
+ }
123
+ const lingerEnabled = inspectLinuxLinger();
124
+ const active = run("systemctl", ["--user", "is-active", RYNX_SYSTEMD_SERVICE]).status === 0;
125
+ let detail;
126
+ if (active) {
127
+ try {
128
+ assertRunningServiceOwnership(context);
129
+ }
130
+ catch (error) {
131
+ detail = errorMessage(error);
132
+ }
133
+ }
134
+ return {
135
+ supported: true,
136
+ registered: run("systemctl", ["--user", "is-enabled", RYNX_SYSTEMD_SERVICE]).status === 0,
137
+ active,
138
+ ...(lingerEnabled === undefined
139
+ ? {}
140
+ : {
141
+ lingerEnabled,
142
+ ...(!lingerEnabled
143
+ ? { lingerEnableCommand: `sudo loginctl enable-linger ${userInfo().username}` }
144
+ : {}),
145
+ }),
146
+ manager: "systemd-user",
147
+ registrationPath: context.unitPath,
148
+ ...(detail ? { detail } : {}),
149
+ };
150
+ }
151
+ export function enableLinuxAutostart() {
152
+ assertUserSystemdAvailable();
153
+ const context = currentContext();
154
+ syncLinuxService(context);
155
+ requireSuccess(run("systemctl", ["--user", "enable", RYNX_SYSTEMD_SERVICE]), "systemctl --user enable");
156
+ }
157
+ export function disableLinuxAutostart() {
158
+ assertUserSystemdAvailable();
159
+ const context = currentContext();
160
+ const disabled = run("systemctl", ["--user", "disable", RYNX_SYSTEMD_SERVICE]);
161
+ if (disabled.status !== 0 && existsSync(context.unitPath)) {
162
+ requireSuccess(disabled, "systemctl --user disable");
163
+ }
164
+ rmSync(context.unitPath, { force: true });
165
+ requireSuccess(run("systemctl", ["--user", "daemon-reload"]), "systemctl --user daemon-reload");
166
+ }
167
+ /** Refresh an existing autostart unit without changing enablement. */
168
+ export function refreshLinuxServiceIfPresent() {
169
+ if (process.platform !== "linux" || !linuxUserSystemdAvailable())
170
+ return false;
171
+ const context = currentContext();
172
+ if (!existsSync(context.unitPath))
173
+ return false;
174
+ return syncLinuxService(context);
175
+ }
176
+ export function assertLinuxSystemdInvocation() {
177
+ if (process.platform !== "linux") {
178
+ throw new Error("--systemd-service is only valid on Linux");
179
+ }
180
+ if (process.env[RYNX_SYSTEMD_SERVICE_ENV] !== RYNX_SYSTEMD_SERVICE) {
181
+ throw new Error("--systemd-service requires the Rynx systemd environment");
182
+ }
183
+ const cgroup = linuxSystemdCgroupForPid(process.pid);
184
+ if (!cgroupHasService(cgroup)) {
185
+ throw new Error(`--systemd-service requires the ${RYNX_SYSTEMD_SERVICE} cgroup`);
186
+ }
187
+ }
188
+ export function parseLinuxSystemdShow(output) {
189
+ const values = new Map();
190
+ for (const line of output.split(/\r?\n/)) {
191
+ const separator = line.indexOf("=");
192
+ if (separator <= 0)
193
+ continue;
194
+ values.set(line.slice(0, separator), line.slice(separator + 1));
195
+ }
196
+ const parsedMainPid = Number.parseInt(values.get("MainPID") ?? "0", 10);
197
+ return {
198
+ loadState: values.get("LoadState") ?? "",
199
+ activeState: values.get("ActiveState") ?? "",
200
+ subState: values.get("SubState") ?? "",
201
+ type: values.get("Type") ?? "",
202
+ mainPid: Number.isSafeInteger(parsedMainPid) && parsedMainPid > 1
203
+ ? parsedMainPid
204
+ : 0,
205
+ killMode: values.get("KillMode") ?? "",
206
+ killSignal: values.get("KillSignal") ?? "",
207
+ restartKillSignal: values.get("RestartKillSignal") ?? "",
208
+ finalKillSignal: values.get("FinalKillSignal") ?? "",
209
+ sendSigkill: values.get("SendSIGKILL") ?? "",
210
+ workingDirectory: values.get("WorkingDirectory") ?? "",
211
+ pidFile: values.get("PIDFile") ?? "",
212
+ environment: values.get("Environment") ?? "",
213
+ execStart: values.get("ExecStart") ?? "",
214
+ execStop: values.get("ExecStop") ?? "",
215
+ };
216
+ }
217
+ export function inspectLinuxPm2GodOwnership(home, deps = {}) {
218
+ const readText = deps.readText ?? ((file) => readFileSync(file, "utf8"));
219
+ const entries = scanLinuxPm2GodPids(home, deps);
220
+ const processes = [];
221
+ for (const pid of entries) {
222
+ try {
223
+ const startBefore = linuxProcStartIdentity(readText(`/proc/${pid}/stat`));
224
+ const cmdline = readText(`/proc/${pid}/cmdline`).replaceAll("\u0000", " ").trim();
225
+ const cgroup = linuxSystemdCgroupForPid(pid, readText);
226
+ const startAfter = linuxProcStartIdentity(readText(`/proc/${pid}/stat`));
227
+ if (!startBefore || startBefore !== startAfter) {
228
+ throw new Error(`PM2 supervisor pid ${pid} changed during inspection`);
229
+ }
230
+ if (!isDedicatedPm2God(cmdline, home))
231
+ continue;
232
+ processes.push({ pid, cgroup, startIdentity: startBefore });
233
+ }
234
+ catch (error) {
235
+ if (!processDisappeared(error))
236
+ throw procReadError(`/proc/${pid}`, error);
237
+ }
238
+ }
239
+ if (processes.length === 0)
240
+ return { kind: "absent" };
241
+ const external = processes.filter((entry) => !cgroupHasService(entry.cgroup));
242
+ return external.length === 0
243
+ ? { kind: "owned", processes }
244
+ : { kind: "external", processes };
245
+ }
246
+ export function scanLinuxPm2GodPids(home, deps = {}) {
247
+ const procEntries = deps.procEntries ?? (() => readdirSync("/proc"));
248
+ const readText = deps.readText ?? ((file) => readFileSync(file, "utf8"));
249
+ const statUid = deps.statUid ?? ((file) => statSync(file).uid);
250
+ const currentUid = deps.currentUid ?? process.getuid?.();
251
+ let entries;
252
+ try {
253
+ entries = procEntries();
254
+ }
255
+ catch (error) {
256
+ throw new Error(`cannot inspect /proc: ${errorMessage(error)}`);
257
+ }
258
+ const pids = [];
259
+ for (const entry of entries) {
260
+ if (!/^\d+$/.test(entry))
261
+ continue;
262
+ const pid = Number(entry);
263
+ if (!Number.isSafeInteger(pid) || pid <= 1)
264
+ continue;
265
+ if (currentUid !== undefined) {
266
+ try {
267
+ if (statUid(`/proc/${pid}`) !== currentUid)
268
+ continue;
269
+ }
270
+ catch (error) {
271
+ if (processDisappeared(error))
272
+ continue;
273
+ throw procReadError(`/proc/${pid}`, error);
274
+ }
275
+ }
276
+ try {
277
+ const cmdline = readText(`/proc/${pid}/cmdline`).replaceAll("\u0000", " ").trim();
278
+ if (isDedicatedPm2God(cmdline, home))
279
+ pids.push(pid);
280
+ }
281
+ catch (error) {
282
+ if (!processDisappeared(error)) {
283
+ throw procReadError(`/proc/${pid}/cmdline`, error);
284
+ }
285
+ }
286
+ }
287
+ return [...new Set(pids)].sort((left, right) => left - right);
288
+ }
289
+ export function linuxSystemdCgroupForPid(pid, readText = (file) => readFileSync(file, "utf8")) {
290
+ return normalizedCgroupPath(readText(`/proc/${pid}/cgroup`)) ?? "(unreadable)";
291
+ }
292
+ function currentContext() {
293
+ const dataDir = rynxHome();
294
+ const homeDir = homedir();
295
+ const pm2Home = path.join(dataDir, "pm2");
296
+ return {
297
+ homeDir,
298
+ dataDir,
299
+ logDir: path.join(dataDir, "logs"),
300
+ cliPath: fileURLToPath(new URL("./cli.js", import.meta.url)),
301
+ environment: process.env,
302
+ nodePath: process.execPath,
303
+ pathEnv: process.env.PATH || defaultLinuxPath(),
304
+ pm2Home,
305
+ pidFile: path.join(pm2Home, "pm2.pid"),
306
+ unitPath: path.join(homeDir, ".config", "systemd", "user", RYNX_SYSTEMD_SERVICE),
307
+ };
308
+ }
309
+ function syncLinuxService(context) {
310
+ mkdirSync(context.dataDir, { recursive: true, mode: 0o700 });
311
+ mkdirSync(context.logDir, { recursive: true, mode: 0o700 });
312
+ const content = renderSystemdUnit(context);
313
+ const previous = existsSync(context.unitPath)
314
+ ? readFileSync(context.unitPath, "utf8")
315
+ : undefined;
316
+ const changed = previous !== content;
317
+ if (changed)
318
+ replaceFileAtomically(context.unitPath, content);
319
+ try {
320
+ requireSuccess(run("systemctl", ["--user", "daemon-reload"]), "systemctl --user daemon-reload");
321
+ assertStaticServiceDefinition(context, inspectLinuxSystemdService());
322
+ return changed;
323
+ }
324
+ catch (error) {
325
+ if (changed) {
326
+ if (previous === undefined)
327
+ rmSync(context.unitPath, { force: true });
328
+ else
329
+ replaceFileAtomically(context.unitPath, previous);
330
+ const rollback = run("systemctl", ["--user", "daemon-reload"]);
331
+ if (rollback.status !== 0) {
332
+ throw new AggregateError([error, new Error(`systemd rollback failed: ${commandDetail(rollback)}`)], "could not update or restore the Rynx systemd service");
333
+ }
334
+ }
335
+ throw error;
336
+ }
337
+ }
338
+ function inspectLinuxSystemdService() {
339
+ const output = systemctlUser([
340
+ "show",
341
+ RYNX_SYSTEMD_SERVICE,
342
+ "--property=LoadState",
343
+ "--property=ActiveState",
344
+ "--property=SubState",
345
+ "--property=Type",
346
+ "--property=MainPID",
347
+ "--property=KillMode",
348
+ "--property=KillSignal",
349
+ "--property=RestartKillSignal",
350
+ "--property=FinalKillSignal",
351
+ "--property=SendSIGKILL",
352
+ "--property=WorkingDirectory",
353
+ "--property=PIDFile",
354
+ "--property=Environment",
355
+ "--property=ExecStart",
356
+ "--property=ExecStop",
357
+ ]);
358
+ return parseLinuxSystemdShow(output);
359
+ }
360
+ function assertStaticServiceDefinition(context, state) {
361
+ const errors = [];
362
+ if (state.loadState !== "loaded")
363
+ errors.push(`LoadState=${state.loadState || "(empty)"}`);
364
+ if (state.type !== "forking")
365
+ errors.push(`Type=${state.type || "(empty)"}`);
366
+ if (state.pidFile !== context.pidFile)
367
+ errors.push(`PIDFile=${state.pidFile || "(empty)"}`);
368
+ if (state.workingDirectory !== context.dataDir) {
369
+ errors.push(`WorkingDirectory=${state.workingDirectory || "(empty)"}`);
370
+ }
371
+ if (state.killMode !== "process")
372
+ errors.push(`KillMode=${state.killMode || "(empty)"}`);
373
+ if (!signalMatches(state.killSignal, "18", "SIGCONT")) {
374
+ errors.push(`KillSignal=${state.killSignal || "(empty)"}`);
375
+ }
376
+ if (!signalMatches(state.restartKillSignal, "18", "SIGCONT")) {
377
+ errors.push(`RestartKillSignal=${state.restartKillSignal || "(empty)"}`);
378
+ }
379
+ if (!signalMatches(state.finalKillSignal, "18", "SIGCONT")) {
380
+ errors.push(`FinalKillSignal=${state.finalKillSignal || "(empty)"}`);
381
+ }
382
+ if (state.sendSigkill !== "no")
383
+ errors.push(`SendSIGKILL=${state.sendSigkill || "(empty)"}`);
384
+ for (const expected of [
385
+ `RYNX_HOME=${context.dataDir}`,
386
+ `${RYNX_SYSTEMD_SERVICE_ENV}=${RYNX_SYSTEMD_SERVICE}`,
387
+ `PATH=${context.pathEnv}`,
388
+ ]) {
389
+ if (!state.environment.includes(expected))
390
+ errors.push(`Environment missing ${expected}`);
391
+ }
392
+ for (const [name, value, action] of [
393
+ ["ExecStart", state.execStart, "start"],
394
+ ["ExecStop", state.execStop, "stop"],
395
+ ]) {
396
+ if (!value.includes(context.nodePath) ||
397
+ !value.includes(context.cliPath) ||
398
+ !value.includes(action) ||
399
+ !value.includes("--systemd-service")) {
400
+ errors.push(`${name}=${value || "(empty)"}`);
401
+ }
402
+ }
403
+ if (errors.length > 0) {
404
+ throw new Error(`${RYNX_SYSTEMD_SERVICE} effective configuration is invalid: ${errors.join("; ")}`);
405
+ }
406
+ }
407
+ function assertRunningServiceOwnership(context) {
408
+ const ownership = inspectLinuxPm2GodOwnership(context.pm2Home);
409
+ assertSingleGod(ownership);
410
+ if (ownership.kind !== "owned") {
411
+ throw new Error(ownership.kind === "absent"
412
+ ? "systemd started without a PM2 supervisor"
413
+ : `PM2 supervisor is outside ${RYNX_SYSTEMD_SERVICE}: ${describeOwnership(ownership)}`);
414
+ }
415
+ const process = ownership.processes[0];
416
+ const state = inspectLinuxSystemdService();
417
+ assertStaticServiceDefinition(context, state);
418
+ if (state.activeState !== "active" ||
419
+ state.subState !== "running" ||
420
+ state.mainPid !== process.pid) {
421
+ throw new Error(`${RYNX_SYSTEMD_SERVICE} does not own the PM2 supervisor: ` +
422
+ `ActiveState=${state.activeState}, SubState=${state.subState}, ` +
423
+ `MainPID=${state.mainPid}, PM2=${process.pid}`);
424
+ }
425
+ return process;
426
+ }
427
+ function assertSingleGod(ownership) {
428
+ if (ownership.kind !== "absent" && ownership.processes.length !== 1) {
429
+ throw new Error(`multiple PM2 supervisors use this RYNX_HOME: ${describeOwnership(ownership)}`);
430
+ }
431
+ }
432
+ function describeOwnership(ownership) {
433
+ if (ownership.kind === "absent")
434
+ return "absent";
435
+ return ownership.processes
436
+ .map((entry) => `${entry.pid}@${entry.cgroup}`)
437
+ .join(", ");
438
+ }
439
+ function inspectLinuxLinger() {
440
+ const result = run("loginctl", [
441
+ "show-user",
442
+ userInfo().username,
443
+ "--property=Linger",
444
+ ]);
445
+ if (result.status !== 0)
446
+ return undefined;
447
+ const value = result.stdout?.trim();
448
+ if (value === "Linger=yes")
449
+ return true;
450
+ if (value === "Linger=no")
451
+ return false;
452
+ return undefined;
453
+ }
454
+ function normalizedCgroupPath(raw) {
455
+ const trimmed = raw.trim();
456
+ if (!trimmed)
457
+ return undefined;
458
+ if (trimmed.startsWith("/"))
459
+ return trimmed;
460
+ let unified;
461
+ for (const line of trimmed.split(/\r?\n/)) {
462
+ const first = line.indexOf(":");
463
+ const second = first < 0 ? -1 : line.indexOf(":", first + 1);
464
+ if (second < 0)
465
+ continue;
466
+ const hierarchy = line.slice(0, first);
467
+ const controllers = line.slice(first + 1, second);
468
+ const cgroup = line.slice(second + 1).trim();
469
+ if (!cgroup)
470
+ continue;
471
+ if (controllers.split(",").includes("name=systemd"))
472
+ return cgroup;
473
+ if (hierarchy === "0" && controllers === "")
474
+ unified = cgroup;
475
+ }
476
+ return unified;
477
+ }
478
+ function cgroupHasService(cgroup) {
479
+ return cgroup?.split("/").includes(RYNX_SYSTEMD_SERVICE) === true;
480
+ }
481
+ function linuxProcStartIdentity(raw) {
482
+ const closeParen = raw.lastIndexOf(")");
483
+ if (closeParen < 0)
484
+ return undefined;
485
+ return raw.slice(closeParen + 2).trim().split(/\s+/)[19];
486
+ }
487
+ function isDedicatedPm2God(cmdline, home) {
488
+ return cmdline.includes("PM2 v") && cmdline.includes(`God Daemon (${home})`);
489
+ }
490
+ function processDisappeared(error) {
491
+ const code = error?.code;
492
+ return code === "ENOENT" || code === "ESRCH" || code === "ENOTDIR";
493
+ }
494
+ function procReadError(file, error) {
495
+ return new Error(`cannot inspect ${file}: ${errorMessage(error)}`);
496
+ }
497
+ function assertUserSystemdAvailable() {
498
+ if (!linuxUserSystemdAvailable()) {
499
+ throw new Error("user systemd is unavailable in this login session");
500
+ }
501
+ }
502
+ function systemctlUser(args, timeout = COMMAND_TIMEOUT_MS) {
503
+ const result = run("systemctl", ["--user", ...args], timeout);
504
+ requireSuccess(result, `systemctl --user ${args.join(" ")}`);
505
+ return result.stdout ?? "";
506
+ }
507
+ function replaceFileAtomically(file, content) {
508
+ mkdirSync(path.dirname(file), { recursive: true });
509
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
510
+ try {
511
+ writeFileSync(temporary, content, {
512
+ encoding: "utf8",
513
+ flag: "wx",
514
+ // The generated unit contains the caller's environment and may include
515
+ // runtime credentials. Keep the snapshot private to the owning user.
516
+ mode: 0o600,
517
+ });
518
+ renameSync(temporary, file);
519
+ }
520
+ finally {
521
+ rmSync(temporary, { force: true });
522
+ }
523
+ }
524
+ function run(command, args, timeout = COMMAND_TIMEOUT_MS) {
525
+ return spawnSync(command, [...args], {
526
+ encoding: "utf8",
527
+ stdio: "pipe",
528
+ timeout,
529
+ killSignal: "SIGTERM",
530
+ });
531
+ }
532
+ function requireSuccess(result, description) {
533
+ if (result.status === 0)
534
+ return;
535
+ throw new Error(`${description} failed: ${commandDetail(result)}`);
536
+ }
537
+ function commandDetail(result) {
538
+ return result.error?.message
539
+ || result.stderr?.trim()
540
+ || result.stdout?.trim()
541
+ || `exit ${result.status ?? "unknown"}`;
542
+ }
543
+ function errorMessage(error) {
544
+ return error instanceof Error ? error.message : String(error);
545
+ }
546
+ function signalMatches(value, number, name) {
547
+ return value === number || value === name;
548
+ }
549
+ function defaultLinuxPath() {
550
+ return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
551
+ }
552
+ function renderInheritedEnvironment(env) {
553
+ if (!env)
554
+ return "";
555
+ const requested = new Set((env[RYNX_SERVICE_ENV_KEYS] ?? "")
556
+ .split(/[\s,]+/)
557
+ .filter((name) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)));
558
+ return Object.entries(env)
559
+ .filter(([name, value]) => value !== undefined &&
560
+ /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
561
+ (AUTOMATIC_SERVICE_ENVIRONMENT.has(name) || requested.has(name)) &&
562
+ !name.startsWith("npm_") &&
563
+ !name.startsWith("RYNX_RUNNER_") &&
564
+ !BLOCKED_SERVICE_ENVIRONMENT.has(name) &&
565
+ !TRANSIENT_SERVICE_ENVIRONMENT.has(name))
566
+ .sort(([left], [right]) => left.localeCompare(right))
567
+ .map(([name, value]) => `Environment=${systemdQuote(`${name}=${value}`)}`)
568
+ .join("\n");
569
+ }
570
+ function systemdQuote(value) {
571
+ return `"${value
572
+ .replaceAll("\\", "\\\\")
573
+ .replaceAll("\"", "\\\"")
574
+ .replaceAll("%", "%%")
575
+ .replaceAll("\t", "\\t")
576
+ .replaceAll("\n", "\\n")
577
+ .replaceAll("\r", "\\r")}"`;
578
+ }
579
+ function systemdPath(value) {
580
+ return value
581
+ .replaceAll("%", "%%")
582
+ .replaceAll("\\", "\\x5c")
583
+ .replaceAll(" ", "\\x20")
584
+ .replaceAll("\t", "\\t")
585
+ .replaceAll("\n", "\\n")
586
+ .replaceAll("\r", "\\r")
587
+ .replaceAll('"', "\\x22")
588
+ .replaceAll("'", "\\x27");
589
+ }
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]\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
@@ -7,12 +7,13 @@ General:
7
7
  Setup:
8
8
  setup [--non-interactive] [--default-runtime <codex|traex|claude>]
9
9
  [--host <host>] [--port <port>] [--log-level <level>]
10
- [--install-browser|--skip-browser] [--json]
10
+ [--install-browser|--skip-browser] [--json|--result-file <path>]
11
11
  initialize configuration and local dependencies
12
12
  doctor read-only health check
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: