@rome-os/node 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.
package/dist/daemon.js ADDED
@@ -0,0 +1,208 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { createServer } from "node:http";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { actionError, isRecord } from "./actions.js";
6
+ import { DeviceConnector } from "./connector.js";
7
+ import { configRoot, daemonPort, daemonStatePath, readCallerCredential } from "./local.js";
8
+ import { MAX_MESSAGE_BYTES } from "./protocol.js";
9
+ import { readPrivateJson, writePrivateJson } from "./storage.js";
10
+ async function readDaemonState() {
11
+ const value = await readPrivateJson(daemonStatePath());
12
+ if (!isRecord(value) ||
13
+ typeof value.token !== "string" ||
14
+ !/^romenode_[A-Za-z0-9_-]{43}$/.test(value.token) ||
15
+ value.port !== daemonPort() ||
16
+ typeof value.pid !== "number" ||
17
+ !Number.isSafeInteger(value.pid) ||
18
+ value.pid <= 0)
19
+ return null;
20
+ return { token: value.token, port: daemonPort(), pid: value.pid };
21
+ }
22
+ async function request(state, path, body) {
23
+ return fetch(`http://127.0.0.1:${state.port}${path}`, {
24
+ method: body === undefined ? "GET" : "POST",
25
+ redirect: "error",
26
+ signal: AbortSignal.timeout(path === "/status" ? 1000 : 85_000),
27
+ headers: { authorization: `Bearer ${state.token}`, "content-type": "application/json" },
28
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
29
+ });
30
+ }
31
+ export async function daemonStatus() {
32
+ const state = await readDaemonState();
33
+ if (!state)
34
+ return null;
35
+ try {
36
+ const response = await request(state, "/status");
37
+ const value = await response.json();
38
+ return response.ok && isRecord(value) && value.pid === state.pid ? state : null;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ export async function startDaemon() {
45
+ const current = await daemonStatus();
46
+ if (current)
47
+ return current;
48
+ await readCallerCredential();
49
+ const child = spawn(process.execPath, [process.argv[1], "daemon", "serve"], {
50
+ detached: true,
51
+ stdio: "ignore",
52
+ windowsHide: true,
53
+ env: { ...process.env, ROME_NODE_CONFIG_DIR: configRoot() },
54
+ });
55
+ let failed = false;
56
+ child.on("error", () => {
57
+ failed = true;
58
+ });
59
+ child.unref();
60
+ const deadline = Date.now() + 5000;
61
+ while (Date.now() < deadline && !failed) {
62
+ const state = await daemonStatus();
63
+ if (state)
64
+ return state;
65
+ await delay(50);
66
+ }
67
+ throw new Error("Could not start the CLI daemon. Run rome-node daemon serve to inspect the error. If its port is occupied, set ROME_NODE_DAEMON_PORT.");
68
+ }
69
+ export async function stopDaemon() {
70
+ const state = await daemonStatus();
71
+ if (!state)
72
+ return;
73
+ const response = await request(state, "/stop", {});
74
+ if (!response.ok)
75
+ throw new Error("Could not stop the CLI daemon.");
76
+ const deadline = Date.now() + 5000;
77
+ while (Date.now() < deadline) {
78
+ try {
79
+ const status = await request(state, "/status");
80
+ if (status.status === 401)
81
+ return;
82
+ }
83
+ catch {
84
+ return;
85
+ }
86
+ await delay(50);
87
+ }
88
+ throw new Error("The CLI daemon has not stopped yet.");
89
+ }
90
+ export async function callDaemon(path, body) {
91
+ const state = await startDaemon();
92
+ try {
93
+ return await request(state, path, body);
94
+ }
95
+ catch {
96
+ throw new Error("CLI daemon connection failed. Execution outcome may be unknown; do not automatically retry.");
97
+ }
98
+ }
99
+ function json(response, status, value) {
100
+ response.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
101
+ response.end(JSON.stringify(value));
102
+ }
103
+ async function readBody(request) {
104
+ let size = 0;
105
+ const chunks = [];
106
+ for await (const chunk of request) {
107
+ const buffer = Buffer.from(chunk);
108
+ size += buffer.length;
109
+ if (size > MAX_MESSAGE_BYTES)
110
+ throw new Error("message_too_large");
111
+ chunks.push(buffer);
112
+ }
113
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
114
+ }
115
+ export async function serveDaemon() {
116
+ const credential = await readCallerCredential();
117
+ const connector = new DeviceConnector({ credential });
118
+ const state = {
119
+ port: daemonPort(),
120
+ pid: process.pid,
121
+ token: `romenode_${randomBytes(32).toString("base64url")}`,
122
+ };
123
+ let closing = false;
124
+ const server = createServer((req, res) => {
125
+ void handle(req, res).catch(() => {
126
+ if (!res.headersSent)
127
+ json(res, 503, actionError("gateway_unavailable", "The device service is unavailable."));
128
+ else
129
+ res.destroy();
130
+ });
131
+ });
132
+ server.requestTimeout = 10_000;
133
+ server.headersTimeout = 10_000;
134
+ const close = () => {
135
+ if (closing)
136
+ return;
137
+ closing = true;
138
+ connector.stop();
139
+ server.close();
140
+ server.closeIdleConnections();
141
+ const timer = setTimeout(() => server.closeAllConnections(), 1000);
142
+ timer.unref();
143
+ };
144
+ async function handle(req, res) {
145
+ const actual = Buffer.from(req.headers.authorization ?? "");
146
+ const expected = Buffer.from(`Bearer ${state.token}`);
147
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
148
+ json(res, 401, actionError("unauthorized", "A CLI daemon credential is required."));
149
+ return;
150
+ }
151
+ if (closing) {
152
+ json(res, 503, actionError("daemon_stopped", "The CLI daemon is stopping."));
153
+ }
154
+ else if (req.method === "GET" && req.url === "/status") {
155
+ json(res, 200, { pid: state.pid });
156
+ }
157
+ else if (req.method === "POST" && req.url === "/stop") {
158
+ json(res, 200, { stopped: true });
159
+ close();
160
+ }
161
+ else if (req.method === "GET" && req.url === "/devices") {
162
+ json(res, 200, await connector.list());
163
+ }
164
+ else if (req.method === "POST" && req.url === "/run") {
165
+ if (Number(req.headers["content-length"]) > MAX_MESSAGE_BYTES) {
166
+ json(res, 413, actionError("message_too_large", "The request exceeds 128 KiB."));
167
+ req.resume();
168
+ return;
169
+ }
170
+ let body;
171
+ try {
172
+ body = await readBody(req);
173
+ }
174
+ catch (error) {
175
+ const tooLarge = error instanceof Error && error.message === "message_too_large";
176
+ json(res, tooLarge ? 413 : 400, actionError(tooLarge ? "message_too_large" : "invalid_request", "Invalid device request."));
177
+ return;
178
+ }
179
+ if (!isRecord(body) || typeof body.deviceId !== "string" || typeof body.action !== "string") {
180
+ json(res, 400, actionError("invalid_request", "A device ID and action are required."));
181
+ return;
182
+ }
183
+ json(res, 200, await connector.run(body.deviceId, body.action, body.args ?? {}));
184
+ }
185
+ else {
186
+ json(res, 404, actionError("not_found", "Unknown CLI daemon operation."));
187
+ }
188
+ }
189
+ await new Promise((resolve, reject) => {
190
+ server.once("error", reject);
191
+ server.listen(state.port, "127.0.0.1", () => {
192
+ server.removeListener("error", reject);
193
+ resolve();
194
+ });
195
+ });
196
+ const finished = new Promise((resolve) => server.once("close", resolve));
197
+ process.once("SIGINT", close);
198
+ process.once("SIGTERM", close);
199
+ try {
200
+ await writePrivateJson(daemonStatePath(), state);
201
+ await finished;
202
+ }
203
+ finally {
204
+ close();
205
+ process.removeListener("SIGINT", close);
206
+ process.removeListener("SIGTERM", close);
207
+ }
208
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/entry.js ADDED
@@ -0,0 +1,127 @@
1
+ import { parseArgs } from "node:util";
2
+ import { connectComputer, defaultDeviceName } from "./connect.js";
3
+ import { isRecord, parseResponse } from "./actions.js";
4
+ import { callerCredentialPath } from "./local.js";
5
+ import { writePrivateJson } from "./storage.js";
6
+ import { cloudOrigin, gatewayConfig } from "./cloud.js";
7
+ import { callDaemon, daemonStatus, serveDaemon, startDaemon, stopDaemon } from "./daemon.js";
8
+ import { validId } from "./protocol.js";
9
+ import { commandHelp } from "./help.js";
10
+ async function main() {
11
+ const { values, positionals } = parseArgs({
12
+ allowPositionals: true,
13
+ strict: true,
14
+ options: {
15
+ help: { type: "boolean", short: "h" },
16
+ cloud: { type: "string" },
17
+ name: { type: "string" },
18
+ args: { type: "string" },
19
+ },
20
+ });
21
+ if (values.help || positionals.length === 0 || positionals[0] === "help") {
22
+ const explicitTopic = positionals[0] === "help";
23
+ process.stdout.write(commandHelp(explicitTopic ? positionals.slice(1) : positionals, explicitTopic));
24
+ return;
25
+ }
26
+ if (positionals[0] === "auth" && positionals.length === 1) {
27
+ if (process.stdin.isTTY)
28
+ throw new Error("Pass the communication token through stdin, not command arguments.");
29
+ if (await daemonStatus())
30
+ throw new Error("Stop the CLI daemon before changing credentials: rome-node daemon stop");
31
+ const chunks = [];
32
+ let size = 0;
33
+ for await (const chunk of process.stdin) {
34
+ const buffer = Buffer.from(chunk);
35
+ size += buffer.length;
36
+ if (size > 1024)
37
+ throw new Error("Invalid communication token.");
38
+ chunks.push(buffer);
39
+ }
40
+ const token = Buffer.concat(chunks).toString("utf8").trim();
41
+ if (!/^romedev_[A-Za-z0-9_-]{43}$/.test(token))
42
+ throw new Error("Invalid communication token.");
43
+ const cloudUrl = cloudOrigin(values.cloud ?? "https://romeos.cc");
44
+ await gatewayConfig(cloudUrl, token);
45
+ await writePrivateJson(callerCredentialPath(), { cloudUrl, token });
46
+ process.stdout.write(`${JSON.stringify({ configured: true })}\n`);
47
+ return;
48
+ }
49
+ if (positionals[0] === "daemon") {
50
+ if (positionals.length > 2)
51
+ throw new Error("Invalid daemon command.");
52
+ const operation = positionals[1] ?? "start";
53
+ if (operation === "serve") {
54
+ await serveDaemon();
55
+ return;
56
+ }
57
+ if (operation === "stop") {
58
+ await stopDaemon();
59
+ process.stdout.write(`${JSON.stringify({ running: false })}\n`);
60
+ return;
61
+ }
62
+ if (operation !== "start" && operation !== "status")
63
+ throw new Error("Unknown daemon command.");
64
+ const state = operation === "start" ? await startDaemon() : await daemonStatus();
65
+ process.stdout.write(`${JSON.stringify(state ? { running: true, pid: state.pid } : { running: false })}\n`);
66
+ return;
67
+ }
68
+ if (positionals[0] === "connect" && positionals.length === 1) {
69
+ const controller = new AbortController();
70
+ const stop = () => controller.abort();
71
+ process.once("SIGINT", stop);
72
+ process.once("SIGTERM", stop);
73
+ try {
74
+ await connectComputer(values.cloud ?? "https://romeos.cc", values.name ?? defaultDeviceName(), controller.signal);
75
+ }
76
+ catch (error) {
77
+ if (!controller.signal.aborted)
78
+ throw error;
79
+ }
80
+ finally {
81
+ process.removeListener("SIGINT", stop);
82
+ process.removeListener("SIGTERM", stop);
83
+ }
84
+ return;
85
+ }
86
+ if (positionals[0] !== "device")
87
+ throw new Error("Unknown command. Run rome-node --help.");
88
+ const [, command, deviceId, action] = positionals;
89
+ const listing = positionals.length === 1;
90
+ if (!listing &&
91
+ !(command === "describe" && positionals.length === 3) &&
92
+ !(command === "run" && positionals.length === 4))
93
+ throw new Error("Invalid device command. Run rome-node --help.");
94
+ if (!listing && !validId(deviceId))
95
+ throw new Error("Invalid device ID.");
96
+ let args = {};
97
+ if (values.args) {
98
+ try {
99
+ args = JSON.parse(values.args);
100
+ }
101
+ catch {
102
+ throw new Error("--args must contain valid JSON.");
103
+ }
104
+ }
105
+ const response = await callDaemon(listing ? "/devices" : "/run", listing
106
+ ? undefined
107
+ : { deviceId, action: command === "describe" ? "system.info" : action, args });
108
+ const body = await response.json();
109
+ if (listing && response.ok && isRecord(body) && Array.isArray(body.items)) {
110
+ process.stdout.write(`${JSON.stringify({ items: body.items })}\n`);
111
+ return;
112
+ }
113
+ const result = parseResponse(body);
114
+ if (!result)
115
+ throw new Error("Invalid response from the CLI daemon.");
116
+ process.stdout.write(`${JSON.stringify(result)}\n`);
117
+ if (!result.ok ||
118
+ !response.ok ||
119
+ (isRecord(result.result) &&
120
+ Object.hasOwn(result.result, "exitCode") &&
121
+ result.result.exitCode !== 0))
122
+ process.exitCode = 1;
123
+ }
124
+ main().catch((error) => {
125
+ process.stderr.write(`${error instanceof Error ? error.message : "Rome Node failed."}\n`);
126
+ process.exitCode = 1;
127
+ });
@@ -0,0 +1,5 @@
1
+ import { type InboundEnvelope, type OutboundEnvelope } from "./protocol.js";
2
+ export declare function createExecutor(name: string, send: (message: OutboundEnvelope) => boolean, secrets?: string[]): {
3
+ receive(message: InboundEnvelope): Promise<void>;
4
+ disconnect(): void;
5
+ };
@@ -0,0 +1,121 @@
1
+ import { spawn } from "node:child_process";
2
+ import { platform } from "node:os";
3
+ import { actionError, isRecord } from "./actions.js";
4
+ import { byteLength, MAX_MESSAGE_BYTES, } from "./protocol.js";
5
+ export function createExecutor(name, send, secrets = []) {
6
+ const children = new Set();
7
+ let generation = 0;
8
+ const handlers = {
9
+ "system.info": async () => ({
10
+ type: "response",
11
+ ok: true,
12
+ result: {
13
+ name,
14
+ platform: platform() === "darwin" ? "macos" : platform() === "win32" ? "windows" : platform(),
15
+ actions: ["system.info", "exec"],
16
+ },
17
+ }),
18
+ exec: async (input) => {
19
+ if (!isRecord(input) ||
20
+ typeof input.command !== "string" ||
21
+ !input.command ||
22
+ input.command.includes("\0") ||
23
+ (input.args !== undefined &&
24
+ (!Array.isArray(input.args) ||
25
+ !input.args.every((arg) => typeof arg === "string" && !arg.includes("\0")))) ||
26
+ (input.cwd !== undefined && (typeof input.cwd !== "string" || input.cwd.includes("\0"))))
27
+ return actionError("invalid_args", "exec requires command, optional string args[], and optional cwd.");
28
+ if (children.size >= 8)
29
+ return actionError("busy", "The computer is already running eight programs.");
30
+ const command = input.command;
31
+ const args = (input.args ?? []);
32
+ const cwd = input.cwd;
33
+ return new Promise((resolve) => {
34
+ const environment = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(?:ROME_.*(?:TOKEN|SECRET|CREDENTIAL)|NODE_OPTIONS)$/i.test(key)));
35
+ let child;
36
+ try {
37
+ child = spawn(command, args, {
38
+ cwd,
39
+ env: environment,
40
+ shell: false,
41
+ windowsHide: true,
42
+ stdio: ["ignore", "pipe", "pipe"],
43
+ });
44
+ }
45
+ catch {
46
+ resolve(actionError("exec_failed", "Could not start the program."));
47
+ return;
48
+ }
49
+ children.add(child);
50
+ const output = { stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
51
+ const truncated = { stdout: false, stderr: false };
52
+ for (const stream of ["stdout", "stderr"]) {
53
+ child[stream]?.on("data", (chunk) => {
54
+ const remaining = 48 * 1024 - output[stream].length;
55
+ if (chunk.length > remaining)
56
+ truncated[stream] = true;
57
+ output[stream] = Buffer.concat([output[stream], chunk.subarray(0, remaining)]);
58
+ });
59
+ }
60
+ child.once("error", () => {
61
+ children.delete(child);
62
+ resolve(actionError("exec_failed", "Could not start the program. Check the executable and working directory."));
63
+ });
64
+ child.once("close", (exitCode, signal) => {
65
+ children.delete(child);
66
+ const redact = (value) => secrets.reduce((text, secret) => text.replaceAll(secret, "[redacted]"), value.toString("utf8"));
67
+ resolve({
68
+ type: "response",
69
+ ok: true,
70
+ result: {
71
+ exitCode,
72
+ signal,
73
+ stdout: redact(output.stdout),
74
+ stderr: redact(output.stderr),
75
+ truncated,
76
+ },
77
+ });
78
+ });
79
+ });
80
+ },
81
+ };
82
+ return {
83
+ async receive(message) {
84
+ const request = message.payload;
85
+ if (!isRecord(request) || request.type !== "request" || typeof request.action !== "string")
86
+ return;
87
+ const current = generation;
88
+ const handler = Object.hasOwn(handlers, request.action)
89
+ ? handlers[request.action]
90
+ : undefined;
91
+ const payload = handler
92
+ ? await handler(request.args).catch(() => actionError("exec_failed", "The action failed."))
93
+ : actionError("unsupported_action", "The computer does not support this action.");
94
+ const reply = { id: message.id, to: message.from, payload };
95
+ if (payload.ok && isRecord(payload.result) && isRecord(payload.result.truncated)) {
96
+ const result = payload.result;
97
+ while (byteLength(JSON.stringify(reply)) > MAX_MESSAGE_BYTES) {
98
+ for (const stream of ["stdout", "stderr"]) {
99
+ if (typeof result[stream] === "string" && result[stream].length > 0) {
100
+ result[stream] = result[stream].slice(0, Math.floor(result[stream].length * 0.75));
101
+ result.truncated[stream] = true;
102
+ }
103
+ }
104
+ }
105
+ }
106
+ if (current === generation)
107
+ send(reply);
108
+ },
109
+ disconnect() {
110
+ generation++;
111
+ for (const child of children) {
112
+ child.kill("SIGTERM");
113
+ const timer = setTimeout(() => {
114
+ if (child.exitCode === null && child.signalCode === null)
115
+ child.kill("SIGKILL");
116
+ }, 1000);
117
+ timer.unref();
118
+ }
119
+ },
120
+ };
121
+ }
package/dist/help.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function commandHelp(positionals: string[], explicitTopic: boolean): string;
package/dist/help.js ADDED
@@ -0,0 +1,156 @@
1
+ const overview = `Rome Node — run programs on authorized remote computers.
2
+
3
+ Usage:
4
+ rome-node help [command ...]
5
+ rome-node <command> --help
6
+
7
+ Commands:
8
+ connect Keep this computer online as a remote executor.
9
+ auth Configure the caller's communication token through stdin.
10
+ device List authorized devices (they may be offline).
11
+ device describe Query a device's platform and supported actions.
12
+ device run Execute an action on a device.
13
+ daemon Manage the caller's background connection.
14
+
15
+ Agent workflow:
16
+ 1. Run rome-node device and choose a device ID from items.
17
+ 2. Run rome-node device describe <device-id> to check its platform and actions.
18
+ 3. Run rome-node help device run for argument formats and result handling.
19
+
20
+ Device commands require caller auth and start the daemon automatically.
21
+ Help works offline without credentials and does not start the daemon.
22
+ Use -h or --help on any command. No arguments also prints this overview.
23
+ Set ROME_NODE_CONFIG_DIR to use a separate configuration directory.
24
+ `;
25
+ const commands = {
26
+ connect: `Usage: rome-node connect [--cloud <origin>] [--name <name>]
27
+
28
+ Run this on the computer that will execute programs.
29
+ Open the printed browser URL and approve access when authorization is needed.
30
+ The process stays in the foreground. Ctrl+C disconnects and preserves authorization.
31
+ Programs run as the OS user who starts connect.
32
+
33
+ Options:
34
+ --cloud <origin> Cloud origin (default: https://romeos.cc).
35
+ --name <name> Name for browser authorization (default: computer hostname).
36
+
37
+ Example:
38
+ rome-node connect --name "My computer"
39
+ `,
40
+ auth: `Usage: rome-node auth [--cloud <origin>] < token-file
41
+
42
+ Read an issued communication token (romedev_...) from stdin, validate it with
43
+ Cloud, and save it privately for device commands. An Instance Token must first
44
+ be exchanged through Cloud. This command does not perform that exchange.
45
+ Stop a running caller daemon before changing credentials.
46
+
47
+ Options:
48
+ --cloud <origin> Cloud origin (default: https://romeos.cc).
49
+
50
+ Example:
51
+ rome-node daemon stop
52
+ rome-node auth --cloud https://romeos.cc < /path/to/private-token-file
53
+
54
+ Output: {"configured":true}
55
+ Pass tokens through stdin. Never put them in command arguments or print them.
56
+ `,
57
+ device: `Usage:
58
+ rome-node device
59
+ rome-node device describe <device-id>
60
+ rome-node device run <device-id> <action> [--args <json>]
61
+
62
+ With no subcommand, list authorized devices as {"items":[...]}.
63
+ Use an item's id for subsequent commands. Listed devices may be offline.
64
+ describe queries the target's current platform and actions through Gateway.
65
+ run invokes one of those actions. The caller daemon starts automatically.
66
+
67
+ Examples:
68
+ rome-node device
69
+ rome-node device describe <device-id>
70
+ rome-node help device run
71
+ `,
72
+ "device describe": `Usage: rome-node device describe <device-id>
73
+
74
+ Invoke system.info on an online target. Use an ID from rome-node device.
75
+ The result includes name, platform, and actions. Platform-specific commands and
76
+ paths must match the target computer, which may differ from the caller.
77
+
78
+ Example result:
79
+ {"type":"response","ok":true,"result":{"name":"My computer","platform":"macos","actions":["system.info","exec"]}}
80
+
81
+ Failures return {"type":"response","ok":false,"error":{"code":"...","message":"..."}}.
82
+ The local exit status is 0 on success or 1 on failure.
83
+ `,
84
+ "device run": `Usage: rome-node device run <device-id> <action> [--args <json>]
85
+
86
+ Use an ID from rome-node device. Run device describe first to discover actions.
87
+ --args is a JSON object for the action (default: {}).
88
+
89
+ Computer actions:
90
+ system.info No arguments. Returns name, platform, and actions.
91
+ exec Run a program on the target computer.
92
+
93
+ exec arguments:
94
+ command Required nonempty executable name or path.
95
+ args Optional array of string arguments (default: []).
96
+ cwd Optional working directory on the target (default: executor cwd).
97
+
98
+ Programs receive no interactive stdin. No shell is added automatically.
99
+ Invoke a shell explicitly for pipelines, redirection, or shell builtins.
100
+ Examples below use POSIX caller-shell quoting:
101
+ rome-node device run <device-id> system.info
102
+ rome-node device run <device-id> exec --args '{"command":"git","args":["status"],"cwd":"/workspace"}'
103
+ rome-node device run <device-id> exec --args '{"command":"cat","args":["/workspace/README.md"]}'
104
+ rome-node device run <device-id> exec --args '{"command":"sh","args":["-c","ls /workspace | head"]}'
105
+ rome-node device run <device-id> exec --args '{"command":"powershell.exe","args":["-NoProfile","-Command","Get-ChildItem -LiteralPath C:/Work"]}'
106
+
107
+ Output:
108
+ Success: {"type":"response","ok":true,"result":{...}}
109
+ Failure: {"type":"response","ok":false,"error":{"code":"...","message":"..."}}
110
+ exec result: {exitCode, signal, stdout, stderr, truncated: {stdout, stderr}}
111
+
112
+ Check exec's exitCode even when ok is true. The local exit status is 1 for
113
+ action failures, nonzero remote exit codes, or signal termination, otherwise 0.
114
+ Local setup and argument errors print diagnostics to stderr and exit 1.
115
+ Each output stream retains at most 48 KiB, with further truncation to fit a frame.
116
+ Check truncated before treating output or file contents as complete.
117
+
118
+ The caller waits up to 60 seconds for a response. unknown_outcome means execution
119
+ may have started or completed. A local timeout does not cancel the remote program.
120
+ Never automatically retry an unknown outcome. Requests are never replayed.
121
+ `,
122
+ daemon: `Usage: rome-node daemon [start|status|stop|serve]
123
+
124
+ start Start the background caller daemon if needed (default).
125
+ status Report {"running":true,"pid":...} or {"running":false} without starting it.
126
+ stop Close the daemon and its Gateway connection. Print {"running":false}.
127
+ serve Run the daemon in the foreground for diagnosis. Ctrl+C stops it.
128
+
129
+ Device commands start the daemon automatically and share its connection.
130
+ start and serve require caller auth. A later device command restarts a stopped
131
+ or crashed daemon. Interrupted operations are never replayed.
132
+ Stop the daemon before changing credentials with rome-node auth.
133
+
134
+ Environment:
135
+ ROME_NODE_CONFIG_DIR Configuration directory shared by caller and daemon.
136
+ ROME_NODE_DAEMON_PORT Override the derived loopback port (1024-65535).
137
+ Use the same value for caller and daemon.
138
+ `,
139
+ };
140
+ export function commandHelp(positionals, explicitTopic) {
141
+ if (positionals.length === 0)
142
+ return overview;
143
+ const topic = positionals.join(" ");
144
+ if (Object.hasOwn(commands, topic))
145
+ return commands[topic];
146
+ if (positionals[0] === "daemon" &&
147
+ positionals.length === 2 &&
148
+ ["start", "status", "stop", "serve"].includes(positionals[1]))
149
+ return commands.daemon;
150
+ if (!explicitTopic) {
151
+ const command = positionals.slice(0, 2).join(" ");
152
+ if (Object.hasOwn(commands, command))
153
+ return commands[command];
154
+ }
155
+ throw new Error(`Unknown help topic: ${topic}. Run rome-node help.`);
156
+ }
@@ -0,0 +1,5 @@
1
+ export { connectGateway, type GatewayClientOptions, type ConnectionStatus, type ClientSocket, } from "./client.js";
2
+ export { MAX_MESSAGE_BYTES, byteLength, validId, type GatewayMessage, type InboundEnvelope, type OutboundEnvelope, } from "./protocol.js";
3
+ export { CloudError, cloudOrigin, cloudRequest, gatewayConfig } from "./cloud.js";
4
+ export { actionError, isRecord, parseResponse, type ActionResponse, type ActionRequest, } from "./actions.js";
5
+ export { createNodeSocket } from "./socket.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { connectGateway, } from "./client.js";
2
+ export { MAX_MESSAGE_BYTES, byteLength, validId, } from "./protocol.js";
3
+ export { CloudError, cloudOrigin, cloudRequest, gatewayConfig } from "./cloud.js";
4
+ export { actionError, isRecord, parseResponse, } from "./actions.js";
5
+ export { createNodeSocket } from "./socket.js";
@@ -0,0 +1,9 @@
1
+ export declare function configRoot(): string;
2
+ export declare const callerCredentialPath: () => string;
3
+ export declare const daemonStatePath: () => string;
4
+ export declare function daemonPort(): number;
5
+ export interface CallerCredential {
6
+ cloudUrl: string;
7
+ token: string;
8
+ }
9
+ export declare function readCallerCredential(): Promise<CallerCredential>;