@rome-os/node 0.1.0 → 0.1.1
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/README.md +27 -5
- package/dist/entry.js +123 -48
- package/dist/help.js +40 -7
- package/dist/index.d.ts +2 -5
- package/dist/index.js +2 -5
- package/package.json +7 -6
- package/dist/actions.d.ts +0 -20
- package/dist/actions.js +0 -18
- package/dist/client.d.ts +0 -29
- package/dist/client.js +0 -143
- package/dist/cloud.d.ts +0 -7
- package/dist/cloud.js +0 -53
- package/dist/connect.d.ts +0 -2
- package/dist/connect.js +0 -114
- package/dist/connector.d.ts +0 -24
- package/dist/connector.js +0 -148
- package/dist/daemon.d.ts +0 -11
- package/dist/daemon.js +0 -208
- package/dist/executor.d.ts +0 -5
- package/dist/executor.js +0 -121
- package/dist/local.d.ts +0 -9
- package/dist/local.js +0 -33
- package/dist/login.d.ts +0 -6
- package/dist/login.js +0 -100
- package/dist/protocol.d.ts +0 -30
- package/dist/protocol.js +0 -40
- package/dist/socket.d.ts +0 -2
- package/dist/socket.js +0 -27
- package/dist/storage.d.ts +0 -2
- package/dist/storage.js +0 -37
package/README.md
CHANGED
|
@@ -8,8 +8,9 @@ Build an installation archive from this repository:
|
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
pnpm --filter @rome-os/node build
|
|
11
|
+
pnpm --filter @rome-os/node-core pack --pack-destination /tmp
|
|
11
12
|
pnpm --filter @rome-os/node pack --pack-destination /tmp
|
|
12
|
-
npm install -g /tmp/rome-os-node-0.1.0.tgz
|
|
13
|
+
npm install -g /tmp/rome-os-node-core-0.1.0.tgz /tmp/rome-os-node-0.1.0.tgz
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
On the target computer:
|
|
@@ -22,7 +23,17 @@ Open the printed authorization URL in a browser on that computer and approve it.
|
|
|
22
23
|
The browser callback uses a temporary loopback listener. A remote browser needs
|
|
23
24
|
a tunnel to that listener. Ctrl+C disconnects and preserves the credential.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
Rome instances prepare caller authorization automatically at startup and after
|
|
27
|
+
Cloud enrollment. In a server environment that supplies `ROME_INSTANCE_TOKEN`, run:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
rome-node auth --server --cloud https://romeos.cc
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This exchanges the Instance Token with Cloud and stores only the communication
|
|
34
|
+
token. Existing caller credentials are reused. The CLI does not read Rome's database.
|
|
35
|
+
|
|
36
|
+
For manual caller configuration, pass an issued communication token through stdin:
|
|
26
37
|
|
|
27
38
|
```sh
|
|
28
39
|
rome-node auth --cloud https://romeos.cc < /path/to/private-token-file
|
|
@@ -49,10 +60,21 @@ Gateway connection. `rome-node daemon status` reports its PID, and
|
|
|
49
60
|
daemon before changing credentials. `rome-node daemon serve` runs in the
|
|
50
61
|
foreground for diagnosis. Set `ROME_NODE_CONFIG_DIR` to isolate configurations.
|
|
51
62
|
|
|
63
|
+
`rome-node watch` subscribes to caller connection changes and prints one JSON
|
|
64
|
+
event per line. It starts the daemon if needed and begins with a snapshot.
|
|
65
|
+
After a connection loss it waits for the daemon and subscribes again; it does
|
|
66
|
+
not restart a stopped daemon. Ctrl+C closes only the watcher. These events
|
|
67
|
+
report the caller's Gateway connection, not remote device presence.
|
|
68
|
+
|
|
52
69
|
Commands print JSON to stdout and diagnostics to stderr. Errors and nonzero
|
|
53
|
-
remote exit codes produce a nonzero local exit status.
|
|
70
|
+
remote exit codes produce a nonzero local exit status. Output is collected in memory
|
|
71
|
+
and returned in full after program exit. Action message limits follow the WebSocket library and infrastructure.
|
|
72
|
+
The local daemon disconnects event observers that exceed its send-buffer limit. A lost response reports
|
|
73
|
+
`unknown_outcome`. Never automatically retry an unknown outcome.
|
|
74
|
+
An authorized device is
|
|
54
75
|
not necessarily online. Use `describe` to query its current platform and actions.
|
|
55
76
|
|
|
56
|
-
The
|
|
57
|
-
|
|
77
|
+
The CLI uses [`@rome-os/node-core`](../rome-node-core/README.md) for authorization,
|
|
78
|
+
its shared caller daemon, and the computer host. The package also re-exports
|
|
79
|
+
the Gateway client, socket adapter, and action response contract for compatibility. See [the device architecture and operating guide](../../docs/rome-node.md)
|
|
58
80
|
for credential boundaries, file operations, output limits, and disconnection behavior.
|
package/dist/entry.js
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import {
|
|
3
|
-
import { isRecord,
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { cloudOrigin, gatewayConfig } from "./cloud.js";
|
|
7
|
-
import { callDaemon, daemonStatus, serveDaemon, startDaemon, stopDaemon } from "./daemon.js";
|
|
8
|
-
import { validId } from "./protocol.js";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isRecord, cloudOrigin, validId } from "@rome-os/node-core";
|
|
4
|
+
import { authorizeServer, configureCaller } from "@rome-os/node-core/auth";
|
|
5
|
+
import { createNodeClient, CallerConfigurationError, DeviceActionError, nodeConfigFromEnvironment, readOptionalCallerCredential, daemonStatus, startDaemon, stopDaemon, } from "@rome-os/node-core/client";
|
|
9
6
|
import { commandHelp } from "./help.js";
|
|
7
|
+
async function withShutdown(run) {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const stop = () => controller.abort();
|
|
10
|
+
process.once("SIGINT", stop);
|
|
11
|
+
process.once("SIGTERM", stop);
|
|
12
|
+
try {
|
|
13
|
+
await run(controller.signal);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (!controller.signal.aborted)
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
process.removeListener("SIGINT", stop);
|
|
21
|
+
process.removeListener("SIGTERM", stop);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
10
24
|
async function main() {
|
|
11
25
|
const { values, positionals } = parseArgs({
|
|
12
26
|
allowPositionals: true,
|
|
@@ -16,6 +30,7 @@ async function main() {
|
|
|
16
30
|
cloud: { type: "string" },
|
|
17
31
|
name: { type: "string" },
|
|
18
32
|
args: { type: "string" },
|
|
33
|
+
server: { type: "boolean" },
|
|
19
34
|
},
|
|
20
35
|
});
|
|
21
36
|
if (values.help || positionals.length === 0 || positionals[0] === "help") {
|
|
@@ -23,10 +38,26 @@ async function main() {
|
|
|
23
38
|
process.stdout.write(commandHelp(explicitTopic ? positionals.slice(1) : positionals, explicitTopic));
|
|
24
39
|
return;
|
|
25
40
|
}
|
|
41
|
+
const config = nodeConfigFromEnvironment(process.env);
|
|
42
|
+
if (values.server && !(positionals[0] === "auth" && positionals.length === 1))
|
|
43
|
+
throw new Error("--server is only supported by rome-node auth.");
|
|
44
|
+
if (positionals[0] === "auth" && positionals[1] === "status" && positionals.length === 2) {
|
|
45
|
+
const credential = await readOptionalCallerCredential(config);
|
|
46
|
+
process.stdout.write(`${JSON.stringify(credential ? { configured: true, cloudUrl: credential.cloudUrl } : { configured: false })}\n`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
26
49
|
if (positionals[0] === "auth" && positionals.length === 1) {
|
|
50
|
+
const cloudUrl = cloudOrigin(values.cloud ?? "https://romeos.cc");
|
|
51
|
+
if (values.server) {
|
|
52
|
+
await authorizeServer(cloudUrl, process.env.ROME_INSTANCE_TOKEN, config, () => {
|
|
53
|
+
process.stderr.write("Rome Cloud is temporarily unavailable. Retrying server authorization.\n");
|
|
54
|
+
});
|
|
55
|
+
process.stdout.write(`${JSON.stringify({ configured: true })}\n`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
27
58
|
if (process.stdin.isTTY)
|
|
28
59
|
throw new Error("Pass the communication token through stdin, not command arguments.");
|
|
29
|
-
if (await daemonStatus())
|
|
60
|
+
if (await daemonStatus(config))
|
|
30
61
|
throw new Error("Stop the CLI daemon before changing credentials: rome-node daemon stop");
|
|
31
62
|
const chunks = [];
|
|
32
63
|
let size = 0;
|
|
@@ -38,11 +69,7 @@ async function main() {
|
|
|
38
69
|
chunks.push(buffer);
|
|
39
70
|
}
|
|
40
71
|
const token = Buffer.concat(chunks).toString("utf8").trim();
|
|
41
|
-
|
|
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 });
|
|
72
|
+
await configureCaller(cloudUrl, token, config);
|
|
46
73
|
process.stdout.write(`${JSON.stringify({ configured: true })}\n`);
|
|
47
74
|
return;
|
|
48
75
|
}
|
|
@@ -51,36 +78,68 @@ async function main() {
|
|
|
51
78
|
throw new Error("Invalid daemon command.");
|
|
52
79
|
const operation = positionals[1] ?? "start";
|
|
53
80
|
if (operation === "serve") {
|
|
54
|
-
await
|
|
81
|
+
const { serveDaemon } = await import("@rome-os/node-core/daemon");
|
|
82
|
+
await withShutdown((signal) => serveDaemon(config, signal));
|
|
55
83
|
return;
|
|
56
84
|
}
|
|
57
85
|
if (operation === "stop") {
|
|
58
|
-
await stopDaemon();
|
|
86
|
+
await stopDaemon(config);
|
|
59
87
|
process.stdout.write(`${JSON.stringify({ running: false })}\n`);
|
|
60
88
|
return;
|
|
61
89
|
}
|
|
62
90
|
if (operation !== "start" && operation !== "status")
|
|
63
91
|
throw new Error("Unknown daemon command.");
|
|
64
|
-
const state = operation === "start" ? await startDaemon() : await daemonStatus();
|
|
92
|
+
const state = operation === "start" ? await startDaemon(config) : await daemonStatus(config);
|
|
65
93
|
process.stdout.write(`${JSON.stringify(state ? { running: true, pid: state.pid } : { running: false })}\n`);
|
|
66
94
|
return;
|
|
67
95
|
}
|
|
96
|
+
if (positionals[0] === "watch" && positionals.length === 1) {
|
|
97
|
+
await withShutdown(async (signal) => {
|
|
98
|
+
const client = createNodeClient(config);
|
|
99
|
+
let finish;
|
|
100
|
+
const stopped = new Promise((resolve) => {
|
|
101
|
+
finish = resolve;
|
|
102
|
+
});
|
|
103
|
+
const stop = () => {
|
|
104
|
+
client.disconnect();
|
|
105
|
+
finish();
|
|
106
|
+
};
|
|
107
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
108
|
+
try {
|
|
109
|
+
if (signal.aborted)
|
|
110
|
+
return;
|
|
111
|
+
await client.subscribe("connection", (event) => process.stdout.write(`${JSON.stringify(event)}\n`));
|
|
112
|
+
await stopped;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
signal.removeEventListener("abort", stop);
|
|
116
|
+
client.disconnect();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
68
121
|
if (positionals[0] === "connect" && positionals.length === 1) {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
122
|
+
const { connectHost, defaultDeviceName, loginDevice, createFileCredentialStore } = await import("@rome-os/node-core/host");
|
|
123
|
+
const name = values.name ?? defaultDeviceName();
|
|
124
|
+
await withShutdown((signal) => connectHost({
|
|
125
|
+
cloudUrl: values.cloud ?? "https://romeos.cc",
|
|
126
|
+
name,
|
|
127
|
+
signal,
|
|
128
|
+
credentials: createFileCredentialStore(join(config.directory, "credential.json")),
|
|
129
|
+
authorize: (origin, deviceName, abort) => loginDevice(origin, deviceName, abort, (url) => {
|
|
130
|
+
process.stderr.write(`Open this URL in your browser to authorize ${deviceName}:\n${url}\n`);
|
|
131
|
+
}),
|
|
132
|
+
onEvent: (event) => {
|
|
133
|
+
if (event.type === "authorization_required")
|
|
134
|
+
process.stderr.write("While connect is running, Rome instances in your account can execute programs and read or change files as your OS user.\n");
|
|
135
|
+
else if (event.type === "retrying")
|
|
136
|
+
process.stderr.write("Rome Cloud is unavailable; keeping credentials and retrying.\n");
|
|
137
|
+
else if (event.type === "device")
|
|
138
|
+
process.stderr.write(`Device: ${event.name}\nDevice ID: ${event.deviceId}\n`);
|
|
139
|
+
else
|
|
140
|
+
process.stderr.write(`Connection: ${event.status}\n`);
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
84
143
|
return;
|
|
85
144
|
}
|
|
86
145
|
if (positionals[0] !== "device")
|
|
@@ -102,26 +161,42 @@ async function main() {
|
|
|
102
161
|
throw new Error("--args must contain valid JSON.");
|
|
103
162
|
}
|
|
104
163
|
}
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
164
|
+
const client = createNodeClient(config);
|
|
165
|
+
try {
|
|
166
|
+
if (listing) {
|
|
167
|
+
process.stdout.write(`${JSON.stringify(await client.listDevices())}\n`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const result = command === "describe"
|
|
171
|
+
? await client.describe(deviceId)
|
|
172
|
+
: await client.run(deviceId, action, args);
|
|
173
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
174
|
+
if (!result.ok ||
|
|
175
|
+
(isRecord(result.result) &&
|
|
176
|
+
Object.hasOwn(result.result, "exitCode") &&
|
|
177
|
+
result.result.exitCode !== 0))
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
client.disconnect();
|
|
112
182
|
}
|
|
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
183
|
}
|
|
124
184
|
main().catch((error) => {
|
|
185
|
+
if (error instanceof DeviceActionError) {
|
|
186
|
+
process.stdout.write(`${JSON.stringify(error.response)}\n`);
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (error instanceof CallerConfigurationError) {
|
|
191
|
+
const messages = {
|
|
192
|
+
not_configured: "Configure a communication token with rome-node auth --cloud <origin> using stdin first.",
|
|
193
|
+
daemon_running: "Stop the CLI daemon before changing credentials: rome-node daemon stop",
|
|
194
|
+
instance_token_required: "Server authorization requires ROME_INSTANCE_TOKEN in the environment.",
|
|
195
|
+
};
|
|
196
|
+
process.stderr.write(`${messages[error.code]}\n`);
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
125
200
|
process.stderr.write(`${error instanceof Error ? error.message : "Rome Node failed."}\n`);
|
|
126
201
|
process.exitCode = 1;
|
|
127
202
|
});
|
package/dist/help.js
CHANGED
|
@@ -6,10 +6,12 @@ Usage:
|
|
|
6
6
|
|
|
7
7
|
Commands:
|
|
8
8
|
connect Keep this computer online as a remote executor.
|
|
9
|
-
auth Configure
|
|
9
|
+
auth Configure caller authorization through stdin or server mode.
|
|
10
|
+
auth status Report local caller configuration without revealing the token.
|
|
10
11
|
device List authorized devices (they may be offline).
|
|
11
12
|
device describe Query a device's platform and supported actions.
|
|
12
13
|
device run Execute an action on a device.
|
|
14
|
+
watch Stream caller connection snapshots and changes as JSON lines.
|
|
13
15
|
daemon Manage the caller's background connection.
|
|
14
16
|
|
|
15
17
|
Agent workflow:
|
|
@@ -23,6 +25,19 @@ Use -h or --help on any command. No arguments also prints this overview.
|
|
|
23
25
|
Set ROME_NODE_CONFIG_DIR to use a separate configuration directory.
|
|
24
26
|
`;
|
|
25
27
|
const commands = {
|
|
28
|
+
watch: `Usage: rome-node watch
|
|
29
|
+
|
|
30
|
+
Subscribe to caller connection state as newline-delimited JSON.
|
|
31
|
+
Starts the daemon if needed, but does not open its Gateway connection.
|
|
32
|
+
Each connected event includes {transport:"connected",daemon:{pid,protocolVersion,connection}}.
|
|
33
|
+
Local disconnection emits {transport:"disconnected",daemon:null,reason:"connection_lost"}.
|
|
34
|
+
The daemon connection and its Gateway connection are separate states.
|
|
35
|
+
|
|
36
|
+
Reconnection restores the subscription and emits a fresh snapshot, not missed history.
|
|
37
|
+
A watcher does not restart an explicitly stopped daemon. Another command can start it.
|
|
38
|
+
Ctrl+C exits the watcher without stopping the daemon or remote operations.
|
|
39
|
+
These events do not indicate whether individual remote devices are online.
|
|
40
|
+
`,
|
|
26
41
|
connect: `Usage: rome-node connect [--cloud <origin>] [--name <name>]
|
|
27
42
|
|
|
28
43
|
Run this on the computer that will execute programs.
|
|
@@ -37,22 +52,39 @@ Options:
|
|
|
37
52
|
Example:
|
|
38
53
|
rome-node connect --name "My computer"
|
|
39
54
|
`,
|
|
40
|
-
auth: `Usage:
|
|
55
|
+
auth: `Usage:
|
|
56
|
+
rome-node auth [--cloud <origin>] < token-file
|
|
57
|
+
rome-node auth --server [--cloud <origin>]
|
|
41
58
|
|
|
42
59
|
Read an issued communication token (romedev_...) from stdin, validate it with
|
|
43
|
-
Cloud, and save it privately for device commands.
|
|
44
|
-
be exchanged through Cloud. This command does not perform that exchange.
|
|
60
|
+
Cloud, and save it privately for device commands.
|
|
45
61
|
Stop a running caller daemon before changing credentials.
|
|
46
62
|
|
|
63
|
+
With --server, reuse existing caller credentials for this Cloud origin. If missing,
|
|
64
|
+
read ROME_INSTANCE_TOKEN from the environment, exchange it with Cloud, and save
|
|
65
|
+
only the communication token. This does not start a daemon or read Rome's database.
|
|
66
|
+
Transient Cloud failures get up to three attempts with delays of one and five seconds.
|
|
67
|
+
An issued token is reused across validation retries. Existing credentials for
|
|
68
|
+
another Cloud origin are preserved and require manual configuration.
|
|
69
|
+
|
|
47
70
|
Options:
|
|
48
71
|
--cloud <origin> Cloud origin (default: https://romeos.cc).
|
|
72
|
+
--server Authorize using ROME_INSTANCE_TOKEN from the environment.
|
|
49
73
|
|
|
50
74
|
Example:
|
|
51
75
|
rome-node daemon stop
|
|
52
76
|
rome-node auth --cloud https://romeos.cc < /path/to/private-token-file
|
|
77
|
+
rome-node auth --server --cloud https://romeos.cc
|
|
53
78
|
|
|
54
79
|
Output: {"configured":true}
|
|
55
|
-
Pass tokens through stdin. Never put
|
|
80
|
+
Pass communication tokens through stdin. Never put tokens in command arguments or print them.
|
|
81
|
+
`,
|
|
82
|
+
"auth status": `Usage: rome-node auth status
|
|
83
|
+
|
|
84
|
+
Report local caller configuration without contacting Cloud or starting the daemon.
|
|
85
|
+
Output: {"configured":true,"cloudUrl":"https://romeos.cc"} or {"configured":false}.
|
|
86
|
+
This checks the local credential format, not its validity at Cloud.
|
|
87
|
+
The token is never printed.
|
|
56
88
|
`,
|
|
57
89
|
device: `Usage:
|
|
58
90
|
rome-node device
|
|
@@ -112,8 +144,9 @@ Output:
|
|
|
112
144
|
Check exec's exitCode even when ok is true. The local exit status is 1 for
|
|
113
145
|
action failures, nonzero remote exit codes, or signal termination, otherwise 0.
|
|
114
146
|
Local setup and argument errors print diagnostics to stderr and exit 1.
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
Output is collected in memory and returned in full after program exit.
|
|
148
|
+
The CLI does not truncate output or impose message or send-buffer size limits.
|
|
149
|
+
Transport limits still apply. Connection loss returns unknown_outcome.
|
|
117
150
|
|
|
118
151
|
The caller waits up to 60 seconds for a response. unknown_outcome means execution
|
|
119
152
|
may have started or completed. A local timeout does not cancel the remote program.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export {
|
|
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";
|
|
1
|
+
export * from "@rome-os/node-core";
|
|
2
|
+
export { createNodeSocket } from "@rome-os/node-core/socket";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export {
|
|
3
|
-
export { CloudError, cloudOrigin, cloudRequest, gatewayConfig } from "./cloud.js";
|
|
4
|
-
export { actionError, isRecord, parseResponse, } from "./actions.js";
|
|
5
|
-
export { createNodeSocket } from "./socket.js";
|
|
1
|
+
export * from "@rome-os/node-core";
|
|
2
|
+
export { createNodeSocket } from "@rome-os/node-core/socket";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rome-os/node",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Standalone CLI for Rome remote devices",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -22,21 +22,22 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"
|
|
25
|
+
"@rome-os/node-core": "^0.1.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@rstest/core": "^0.11.11",
|
|
29
29
|
"@types/node": "25.2.3",
|
|
30
30
|
"@types/ws": "^8.18.0",
|
|
31
|
-
"typescript": "^5.9.3"
|
|
31
|
+
"typescript": "^5.9.3",
|
|
32
|
+
"ws": "^8.18.0"
|
|
32
33
|
},
|
|
33
34
|
"repository": {
|
|
34
35
|
"type": "git",
|
|
35
36
|
"url": "git+https://github.com/rome-os/rome.git",
|
|
36
|
-
"directory": "packages/rome-node"
|
|
37
|
+
"directory": "packages/rome-node-cli"
|
|
37
38
|
},
|
|
38
39
|
"scripts": {
|
|
39
|
-
"build": "tsc -p tsconfig.build.json",
|
|
40
|
+
"build": "pnpm --filter @rome-os/node-core build && node -e \"require('node:fs').rmSync('dist', {recursive:true, force:true})\" && tsc -p tsconfig.build.json",
|
|
40
41
|
"typecheck": "tsc --noEmit",
|
|
41
42
|
"test": "pnpm build && ../../scripts/test-env.sh rstest"
|
|
42
43
|
}
|
package/dist/actions.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export interface ActionRequest {
|
|
2
|
-
type: "request";
|
|
3
|
-
action: string;
|
|
4
|
-
args: unknown;
|
|
5
|
-
}
|
|
6
|
-
export type ActionResponse = {
|
|
7
|
-
type: "response";
|
|
8
|
-
ok: true;
|
|
9
|
-
result: unknown;
|
|
10
|
-
} | {
|
|
11
|
-
type: "response";
|
|
12
|
-
ok: false;
|
|
13
|
-
error: {
|
|
14
|
-
code: string;
|
|
15
|
-
message: string;
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
export declare function actionError(code: string, message: string): ActionResponse;
|
|
19
|
-
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
20
|
-
export declare function parseResponse(value: unknown): ActionResponse | null;
|
package/dist/actions.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export function actionError(code, message) {
|
|
2
|
-
return { type: "response", ok: false, error: { code, message } };
|
|
3
|
-
}
|
|
4
|
-
export function isRecord(value) {
|
|
5
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
|
-
}
|
|
7
|
-
export function parseResponse(value) {
|
|
8
|
-
if (!isRecord(value) || value.type !== "response")
|
|
9
|
-
return null;
|
|
10
|
-
if (value.ok === true && Object.hasOwn(value, "result"))
|
|
11
|
-
return { type: "response", ok: true, result: value.result };
|
|
12
|
-
if (value.ok === false &&
|
|
13
|
-
isRecord(value.error) &&
|
|
14
|
-
typeof value.error.code === "string" &&
|
|
15
|
-
typeof value.error.message === "string")
|
|
16
|
-
return actionError(value.error.code, value.error.message);
|
|
17
|
-
return null;
|
|
18
|
-
}
|
package/dist/client.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { type GatewayMessage, type OutboundEnvelope } from "./protocol.js";
|
|
2
|
-
export interface ClientSocket {
|
|
3
|
-
readyState: number;
|
|
4
|
-
bufferedAmount: number;
|
|
5
|
-
send(data: string): void;
|
|
6
|
-
close(code?: number, reason?: string): void;
|
|
7
|
-
addEventListener(type: "open", listener: () => void): void;
|
|
8
|
-
addEventListener(type: "message", listener: (event: {
|
|
9
|
-
data: unknown;
|
|
10
|
-
}) => void): void;
|
|
11
|
-
addEventListener(type: "close", listener: (event: {
|
|
12
|
-
code: number;
|
|
13
|
-
}) => void): void;
|
|
14
|
-
addEventListener(type: "error", listener: () => void): void;
|
|
15
|
-
}
|
|
16
|
-
export type ConnectionStatus = "connecting" | "online" | "retrying" | "revoked" | "superseded" | "stopped";
|
|
17
|
-
export interface GatewayClientOptions {
|
|
18
|
-
gatewayUrl: string;
|
|
19
|
-
deviceToken: string;
|
|
20
|
-
createSocket(url: string, authorization: string): ClientSocket;
|
|
21
|
-
onMessage(message: GatewayMessage): void;
|
|
22
|
-
onStatus?(status: ConnectionStatus): void;
|
|
23
|
-
random?(): number;
|
|
24
|
-
beforeConnect?(): Promise<string | null>;
|
|
25
|
-
}
|
|
26
|
-
export declare function connectGateway(options: GatewayClientOptions): {
|
|
27
|
-
send(envelope: OutboundEnvelope): boolean;
|
|
28
|
-
stop(): void;
|
|
29
|
-
};
|
package/dist/client.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { byteLength, CLOSE, gatewayMessage, MAX_CLIENT_BUFFER_BYTES, MAX_MESSAGE_BYTES, outboundEnvelope, } from "./protocol.js";
|
|
2
|
-
export function connectGateway(options) {
|
|
3
|
-
let url = new URL(options.gatewayUrl);
|
|
4
|
-
if (url.protocol !== "wss:" || url.username || url.password || url.search || url.hash)
|
|
5
|
-
throw new Error("A credential-free WSS URL is required");
|
|
6
|
-
if (!/^(?:romedev_|romemob_)[A-Za-z0-9_-]{43}$/.test(options.deviceToken))
|
|
7
|
-
throw new Error("Invalid device token format");
|
|
8
|
-
let stopped = false;
|
|
9
|
-
let socket = null;
|
|
10
|
-
let attempt = 0;
|
|
11
|
-
let timer;
|
|
12
|
-
let connectingTimer;
|
|
13
|
-
let stableTimer;
|
|
14
|
-
const status = (value) => options.onStatus?.(value);
|
|
15
|
-
const schedule = () => {
|
|
16
|
-
if (stopped || timer)
|
|
17
|
-
return;
|
|
18
|
-
status("retrying");
|
|
19
|
-
const delay = Math.min(60_000, 1000 * 2 ** Math.min(attempt++, 6));
|
|
20
|
-
timer = setTimeout(() => {
|
|
21
|
-
timer = undefined;
|
|
22
|
-
void open();
|
|
23
|
-
}, delay * (0.75 + (options.random ?? Math.random)() * 0.5));
|
|
24
|
-
};
|
|
25
|
-
const open = async () => {
|
|
26
|
-
if (stopped)
|
|
27
|
-
return;
|
|
28
|
-
status("connecting");
|
|
29
|
-
let current;
|
|
30
|
-
try {
|
|
31
|
-
if (options.beforeConnect) {
|
|
32
|
-
const next = await options.beforeConnect();
|
|
33
|
-
if (stopped)
|
|
34
|
-
return;
|
|
35
|
-
if (next === null) {
|
|
36
|
-
stopped = true;
|
|
37
|
-
status("revoked");
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
url = new URL(next);
|
|
41
|
-
if (url.protocol !== "wss:" || url.username || url.password || url.search || url.hash)
|
|
42
|
-
throw new Error("A credential-free WSS URL is required");
|
|
43
|
-
}
|
|
44
|
-
current = options.createSocket(url.toString(), `Bearer ${options.deviceToken}`);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
schedule();
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
socket = current;
|
|
51
|
-
let finished = false;
|
|
52
|
-
const finish = (code) => {
|
|
53
|
-
if (finished || socket !== current)
|
|
54
|
-
return;
|
|
55
|
-
finished = true;
|
|
56
|
-
clearTimeout(connectingTimer);
|
|
57
|
-
clearTimeout(stableTimer);
|
|
58
|
-
socket = null;
|
|
59
|
-
if (code === CLOSE.revoked || code === CLOSE.superseded) {
|
|
60
|
-
stopped = true;
|
|
61
|
-
status(code === CLOSE.revoked ? "revoked" : "superseded");
|
|
62
|
-
}
|
|
63
|
-
else
|
|
64
|
-
schedule();
|
|
65
|
-
};
|
|
66
|
-
connectingTimer = setTimeout(() => {
|
|
67
|
-
finish();
|
|
68
|
-
current.close();
|
|
69
|
-
}, 10_000);
|
|
70
|
-
current.addEventListener("open", () => {
|
|
71
|
-
if (stopped || socket !== current) {
|
|
72
|
-
current.close();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
clearTimeout(connectingTimer);
|
|
76
|
-
stableTimer = setTimeout(() => {
|
|
77
|
-
attempt = 0;
|
|
78
|
-
}, 30_000);
|
|
79
|
-
status("online");
|
|
80
|
-
});
|
|
81
|
-
current.addEventListener("message", ({ data }) => {
|
|
82
|
-
if (stopped ||
|
|
83
|
-
socket !== current ||
|
|
84
|
-
typeof data !== "string" ||
|
|
85
|
-
byteLength(data) > MAX_MESSAGE_BYTES)
|
|
86
|
-
return;
|
|
87
|
-
let message;
|
|
88
|
-
try {
|
|
89
|
-
message = gatewayMessage(JSON.parse(data));
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
if (message)
|
|
95
|
-
options.onMessage(message);
|
|
96
|
-
});
|
|
97
|
-
current.addEventListener("close", ({ code }) => finish(code));
|
|
98
|
-
current.addEventListener("error", () => {
|
|
99
|
-
finish();
|
|
100
|
-
current.close();
|
|
101
|
-
});
|
|
102
|
-
};
|
|
103
|
-
void open();
|
|
104
|
-
return {
|
|
105
|
-
send(envelope) {
|
|
106
|
-
if (stopped || !socket || socket.readyState !== 1 || !outboundEnvelope(envelope))
|
|
107
|
-
return false;
|
|
108
|
-
let text;
|
|
109
|
-
try {
|
|
110
|
-
text = JSON.stringify(envelope);
|
|
111
|
-
if (!outboundEnvelope(JSON.parse(text)))
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
const size = byteLength(text);
|
|
118
|
-
if (size > MAX_MESSAGE_BYTES)
|
|
119
|
-
return false;
|
|
120
|
-
if (socket.bufferedAmount + size > MAX_CLIENT_BUFFER_BYTES) {
|
|
121
|
-
socket.close(CLOSE.outputLimit, "outbound buffer limit");
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
try {
|
|
125
|
-
socket.send(text);
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
stop() {
|
|
133
|
-
stopped = true;
|
|
134
|
-
clearTimeout(timer);
|
|
135
|
-
clearTimeout(connectingTimer);
|
|
136
|
-
clearTimeout(stableTimer);
|
|
137
|
-
const previous = socket;
|
|
138
|
-
socket = null;
|
|
139
|
-
previous?.close(1000, "client stopped");
|
|
140
|
-
status("stopped");
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
}
|
package/dist/cloud.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export declare class CloudError extends Error {
|
|
2
|
-
readonly code: string;
|
|
3
|
-
constructor(code: string);
|
|
4
|
-
}
|
|
5
|
-
export declare function cloudOrigin(value: string): string;
|
|
6
|
-
export declare function cloudRequest(origin: string, path: string, token: string, method?: string): Promise<unknown>;
|
|
7
|
-
export declare function gatewayConfig(origin: string, token: string): Promise<string>;
|