@rome-os/node 0.1.1 → 0.1.3
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 +38 -3
- package/dist/copy.d.ts +7 -0
- package/dist/copy.js +43 -0
- package/dist/entry.js +82 -8
- package/dist/help.js +69 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,6 +23,16 @@ Open the printed authorization URL in a browser on that computer and approve it.
|
|
|
23
23
|
The browser callback uses a temporary loopback listener. A remote browser needs
|
|
24
24
|
a tunnel to that listener. Ctrl+C disconnects and preserves the credential.
|
|
25
25
|
|
|
26
|
+
Over SSH or on a headless computer, use device authorization:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
rome-node connect --device-code --name "Remote Linux" --cloud https://romeos.cc
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Open the printed URL on another device and approve the displayed user code.
|
|
33
|
+
The CLI prints the expiry and waits for approval without a callback listener or SSH tunnel.
|
|
34
|
+
Ctrl+C cancels the authorization. Both modes reuse stored credentials when valid.
|
|
35
|
+
|
|
26
36
|
Rome instances prepare caller authorization automatically at startup and after
|
|
27
37
|
Cloud enrollment. In a server environment that supplies `ROME_INSTANCE_TOKEN`, run:
|
|
28
38
|
|
|
@@ -50,6 +60,29 @@ rome-node device describe <device-id>
|
|
|
50
60
|
rome-node device run <device-id> exec --args '{"command":"git","args":["status"],"cwd":"/workspace"}'
|
|
51
61
|
```
|
|
52
62
|
|
|
63
|
+
Copy a file to or from a device:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
rome-node cp ./video.mp4 <device-id>:/tmp/video.mp4
|
|
67
|
+
rome-node cp <device-id>:/tmp/video.mp4 ./video.mp4
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The caller daemon and `rome-node connect` read and write the files with Node
|
|
71
|
+
streams. Each copy writes its own hidden part file next to the destination.
|
|
72
|
+
The destination changes only after its size and SHA-256 checksum match and the
|
|
73
|
+
data is flushed to disk. A replaced file keeps its permission bits.
|
|
74
|
+
A failed copy leaves it unchanged and is safe to run again. To copy between two
|
|
75
|
+
devices, copy to this computer first and then to the second device. Progress
|
|
76
|
+
goes to stderr, and `{"bytes":...,"ms":...,"sha256":"..."}` goes to stdout. The
|
|
77
|
+
device needs a rome-node version with file transfer support.
|
|
78
|
+
|
|
79
|
+
To pass raw bytes to a program and read its raw stdout, add `--input` or `--output`
|
|
80
|
+
to `device run`:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
rome-node device run <device-id> exec --args '{"command":"gzip","args":["-c"]}' --input data.bin --output data.bin.gz
|
|
84
|
+
```
|
|
85
|
+
|
|
53
86
|
Use `rome-node <command> --help` or `-h` for command-specific help.
|
|
54
87
|
Help includes action arguments, examples, output fields, and failure handling.
|
|
55
88
|
It works offline without credentials and does not start the daemon.
|
|
@@ -66,10 +99,12 @@ After a connection loss it waits for the daemon and subscribes again; it does
|
|
|
66
99
|
not restart a stopped daemon. Ctrl+C closes only the watcher. These events
|
|
67
100
|
report the caller's Gateway connection, not remote device presence.
|
|
68
101
|
|
|
69
|
-
Commands print JSON to stdout and diagnostics to stderr.
|
|
102
|
+
Commands print JSON to stdout and diagnostics to stderr. With `--input` and no
|
|
103
|
+
`--output <file>`, `device run` prints the program's raw stdout and moves the JSON
|
|
104
|
+
response to stderr. Errors and nonzero
|
|
70
105
|
remote exit codes produce a nonzero local exit status. Output is collected in memory
|
|
71
106
|
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
|
|
107
|
+
The local daemon disconnects event observers that exceed its unsent event limit. A lost response reports
|
|
73
108
|
`unknown_outcome`. Never automatically retry an unknown outcome.
|
|
74
109
|
An authorized device is
|
|
75
110
|
not necessarily online. Use `describe` to query its current platform and actions.
|
|
@@ -77,4 +112,4 @@ not necessarily online. Use `describe` to query its current platform and actions
|
|
|
77
112
|
The CLI uses [`@rome-os/node-core`](../rome-node-core/README.md) for authorization,
|
|
78
113
|
its shared caller daemon, and the computer host. The package also re-exports
|
|
79
114
|
the Gateway client, socket adapter, and action response contract for compatibility. See [the device architecture and operating guide](../../docs/rome-node.md)
|
|
80
|
-
for credential boundaries, file
|
|
115
|
+
for credential boundaries, binary frames, file copies, output limits, and disconnection behavior.
|
package/dist/copy.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CopyRequest } from "@rome-os/node-core/client";
|
|
2
|
+
export interface CopyLocation {
|
|
3
|
+
deviceId?: string;
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseLocation(value: string): CopyLocation;
|
|
7
|
+
export declare function copyRequest(source: string, destination: string, cwd: string): CopyRequest;
|
package/dist/copy.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { isUuid } from "@rome-os/node-core";
|
|
3
|
+
export function parseLocation(value) {
|
|
4
|
+
const separator = value.indexOf(":");
|
|
5
|
+
const prefix = value.slice(0, separator);
|
|
6
|
+
if (separator === 36 && isUuid(prefix)) {
|
|
7
|
+
if (separator === value.length - 1)
|
|
8
|
+
throw new Error("A path is required after <device-id>:.");
|
|
9
|
+
return { deviceId: prefix.toLowerCase(), path: value.slice(separator + 1) };
|
|
10
|
+
}
|
|
11
|
+
if (!value)
|
|
12
|
+
throw new Error("A path is required.");
|
|
13
|
+
return { path: value };
|
|
14
|
+
}
|
|
15
|
+
export function copyRequest(source, destination, cwd) {
|
|
16
|
+
const from = parseLocation(source);
|
|
17
|
+
const to = parseLocation(destination);
|
|
18
|
+
if (from.deviceId && to.deviceId)
|
|
19
|
+
throw new Error("Copy between two devices in two steps through this computer: " +
|
|
20
|
+
"rome-node cp <device-a>:<path> <local-file>, then rome-node cp <local-file> <device-b>:<path>.");
|
|
21
|
+
if (!from.deviceId && !to.deviceId)
|
|
22
|
+
throw new Error("One side must be a device path: <device-id>:<path>.");
|
|
23
|
+
let target = to.path;
|
|
24
|
+
if (/[\\/]$/.test(target)) {
|
|
25
|
+
const name = from.path.split(/[\\/]/).filter(Boolean).pop();
|
|
26
|
+
if (!name)
|
|
27
|
+
throw new Error("The source path has no file name.");
|
|
28
|
+
target += name;
|
|
29
|
+
}
|
|
30
|
+
return from.deviceId
|
|
31
|
+
? {
|
|
32
|
+
direction: "pull",
|
|
33
|
+
deviceId: from.deviceId,
|
|
34
|
+
remotePath: from.path,
|
|
35
|
+
localPath: resolve(cwd, target),
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
direction: "push",
|
|
39
|
+
deviceId: to.deviceId,
|
|
40
|
+
remotePath: target,
|
|
41
|
+
localPath: resolve(cwd, from.path),
|
|
42
|
+
};
|
|
43
|
+
}
|
package/dist/entry.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { isRecord, cloudOrigin, validId } from "@rome-os/node-core";
|
|
4
5
|
import { authorizeServer, configureCaller } from "@rome-os/node-core/auth";
|
|
5
6
|
import { createNodeClient, CallerConfigurationError, DeviceActionError, nodeConfigFromEnvironment, readOptionalCallerCredential, daemonStatus, startDaemon, stopDaemon, } from "@rome-os/node-core/client";
|
|
6
7
|
import { commandHelp } from "./help.js";
|
|
8
|
+
import { copyRequest } from "./copy.js";
|
|
9
|
+
const failed = (response) => !response.ok ||
|
|
10
|
+
(isRecord(response.result) &&
|
|
11
|
+
Object.hasOwn(response.result, "exitCode") &&
|
|
12
|
+
response.result.exitCode !== 0);
|
|
13
|
+
async function readInput(path) {
|
|
14
|
+
if (path !== "-")
|
|
15
|
+
return readFile(path);
|
|
16
|
+
const chunks = [];
|
|
17
|
+
for await (const chunk of process.stdin)
|
|
18
|
+
chunks.push(Buffer.from(chunk));
|
|
19
|
+
return Buffer.concat(chunks);
|
|
20
|
+
}
|
|
21
|
+
function write(stream, data) {
|
|
22
|
+
return new Promise((resolve, reject) => stream.write(data, (error) => (error ? reject(error) : resolve())));
|
|
23
|
+
}
|
|
24
|
+
function progressPrinter() {
|
|
25
|
+
const started = Date.now();
|
|
26
|
+
let last = 0;
|
|
27
|
+
const mib = (bytes) => (bytes / 1024 / 1024).toFixed(1);
|
|
28
|
+
return (progress, final = false) => {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
if (!final && now - last < 1000 && progress.bytes !== progress.total)
|
|
31
|
+
return;
|
|
32
|
+
last = now;
|
|
33
|
+
const percent = progress.total ? Math.floor((progress.bytes / progress.total) * 100) : 100;
|
|
34
|
+
const ms = now - started;
|
|
35
|
+
const rate = ms ? mib((progress.bytes / ms) * 1000) : "0.0";
|
|
36
|
+
const line = `${mib(progress.bytes)} / ${mib(progress.total)} MiB (${percent}%) ${rate} MiB/s`;
|
|
37
|
+
process.stderr.write(process.stderr.isTTY ? `\r${line}\x1b[K${final ? "\n" : ""}` : `${line}\n`);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
7
40
|
async function withShutdown(run) {
|
|
8
41
|
const controller = new AbortController();
|
|
9
42
|
const stop = () => controller.abort();
|
|
@@ -30,7 +63,10 @@ async function main() {
|
|
|
30
63
|
cloud: { type: "string" },
|
|
31
64
|
name: { type: "string" },
|
|
32
65
|
args: { type: "string" },
|
|
66
|
+
input: { type: "string" },
|
|
67
|
+
output: { type: "string" },
|
|
33
68
|
server: { type: "boolean" },
|
|
69
|
+
"device-code": { type: "boolean" },
|
|
34
70
|
},
|
|
35
71
|
});
|
|
36
72
|
if (values.help || positionals.length === 0 || positionals[0] === "help") {
|
|
@@ -41,6 +77,27 @@ async function main() {
|
|
|
41
77
|
const config = nodeConfigFromEnvironment(process.env);
|
|
42
78
|
if (values.server && !(positionals[0] === "auth" && positionals.length === 1))
|
|
43
79
|
throw new Error("--server is only supported by rome-node auth.");
|
|
80
|
+
if (values["device-code"] && !(positionals[0] === "connect" && positionals.length === 1))
|
|
81
|
+
throw new Error("--device-code is only supported by rome-node connect.");
|
|
82
|
+
if ((values.input !== undefined || values.output !== undefined) &&
|
|
83
|
+
!(positionals[0] === "device" && positionals[1] === "run"))
|
|
84
|
+
throw new Error("--input and --output are only supported by rome-node device run.");
|
|
85
|
+
if (positionals[0] === "cp") {
|
|
86
|
+
if (positionals.length !== 3)
|
|
87
|
+
throw new Error("Usage: rome-node cp <src> <dst>");
|
|
88
|
+
const request = copyRequest(positionals[1], positionals[2], process.cwd());
|
|
89
|
+
const client = createNodeClient(config);
|
|
90
|
+
const print = progressPrinter();
|
|
91
|
+
try {
|
|
92
|
+
const summary = await client.copy(request, { onProgress: (progress) => print(progress) });
|
|
93
|
+
print({ bytes: summary.bytes, total: summary.bytes }, true);
|
|
94
|
+
process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
client.disconnect();
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
44
101
|
if (positionals[0] === "auth" && positionals[1] === "status" && positionals.length === 2) {
|
|
45
102
|
const credential = await readOptionalCallerCredential(config);
|
|
46
103
|
process.stdout.write(`${JSON.stringify(credential ? { configured: true, cloudUrl: credential.cloudUrl } : { configured: false })}\n`);
|
|
@@ -119,16 +176,23 @@ async function main() {
|
|
|
119
176
|
return;
|
|
120
177
|
}
|
|
121
178
|
if (positionals[0] === "connect" && positionals.length === 1) {
|
|
122
|
-
const { connectHost, defaultDeviceName, loginDevice, createFileCredentialStore } = await import("@rome-os/node-core/host");
|
|
179
|
+
const { connectHost, defaultDeviceName, loginDevice, loginDeviceCode, createFileCredentialStore, } = await import("@rome-os/node-core/host");
|
|
123
180
|
const name = values.name ?? defaultDeviceName();
|
|
124
181
|
await withShutdown((signal) => connectHost({
|
|
125
182
|
cloudUrl: values.cloud ?? "https://romeos.cc",
|
|
126
183
|
name,
|
|
127
184
|
signal,
|
|
128
185
|
credentials: createFileCredentialStore(join(config.directory, "credential.json")),
|
|
129
|
-
authorize: (origin, deviceName, abort) =>
|
|
130
|
-
|
|
131
|
-
|
|
186
|
+
authorize: (origin, deviceName, abort) => values["device-code"]
|
|
187
|
+
? loginDeviceCode(origin, deviceName, abort, (prompt) => {
|
|
188
|
+
process.stderr.write(`Open this URL in a browser on another device:\n${prompt.verificationUri}\n` +
|
|
189
|
+
`User code: ${prompt.userCode}\n` +
|
|
190
|
+
`Or open: ${prompt.verificationUriComplete}\n` +
|
|
191
|
+
`Expires in ${prompt.expiresIn} seconds. Waiting for approval. Ctrl+C cancels.\n`);
|
|
192
|
+
})
|
|
193
|
+
: loginDevice(origin, deviceName, abort, (url) => {
|
|
194
|
+
process.stderr.write(`Open this URL in your browser to authorize ${deviceName}:\n${url}\n`);
|
|
195
|
+
}),
|
|
132
196
|
onEvent: (event) => {
|
|
133
197
|
if (event.type === "authorization_required")
|
|
134
198
|
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");
|
|
@@ -167,14 +231,24 @@ async function main() {
|
|
|
167
231
|
process.stdout.write(`${JSON.stringify(await client.listDevices())}\n`);
|
|
168
232
|
return;
|
|
169
233
|
}
|
|
234
|
+
if (values.input !== undefined || values.output !== undefined) {
|
|
235
|
+
const output = values.output ?? "-";
|
|
236
|
+
const input = values.input === undefined ? undefined : await readInput(values.input);
|
|
237
|
+
const { response, body } = await client.runBinary(deviceId, action, args, input);
|
|
238
|
+
if (output === "-")
|
|
239
|
+
await write(process.stdout, body);
|
|
240
|
+
else if (response.ok)
|
|
241
|
+
await writeFile(output, body);
|
|
242
|
+
await write(output === "-" ? process.stderr : process.stdout, `${JSON.stringify(response)}\n`);
|
|
243
|
+
if (failed(response))
|
|
244
|
+
process.exitCode = 1;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
170
247
|
const result = command === "describe"
|
|
171
248
|
? await client.describe(deviceId)
|
|
172
249
|
: await client.run(deviceId, action, args);
|
|
173
250
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
174
|
-
if (
|
|
175
|
-
(isRecord(result.result) &&
|
|
176
|
-
Object.hasOwn(result.result, "exitCode") &&
|
|
177
|
-
result.result.exitCode !== 0))
|
|
251
|
+
if (failed(result))
|
|
178
252
|
process.exitCode = 1;
|
|
179
253
|
}
|
|
180
254
|
finally {
|
package/dist/help.js
CHANGED
|
@@ -11,6 +11,7 @@ Commands:
|
|
|
11
11
|
device List authorized devices (they may be offline).
|
|
12
12
|
device describe Query a device's platform and supported actions.
|
|
13
13
|
device run Execute an action on a device.
|
|
14
|
+
cp Copy a file to or from a device.
|
|
14
15
|
watch Stream caller connection snapshots and changes as JSON lines.
|
|
15
16
|
daemon Manage the caller's background connection.
|
|
16
17
|
|
|
@@ -38,19 +39,25 @@ A watcher does not restart an explicitly stopped daemon. Another command can sta
|
|
|
38
39
|
Ctrl+C exits the watcher without stopping the daemon or remote operations.
|
|
39
40
|
These events do not indicate whether individual remote devices are online.
|
|
40
41
|
`,
|
|
41
|
-
connect: `Usage: rome-node connect [--cloud <origin>] [--name <name>]
|
|
42
|
+
connect: `Usage: rome-node connect [--device-code] [--cloud <origin>] [--name <name>]
|
|
42
43
|
|
|
43
44
|
Run this on the computer that will execute programs.
|
|
44
45
|
Open the printed browser URL and approve access when authorization is needed.
|
|
46
|
+
By default, the browser returns to a loopback listener on this computer.
|
|
47
|
+
Use --device-code over SSH or on a headless computer to approve from another device.
|
|
48
|
+
This mode prints a URL and user code without opening a browser or callback listener.
|
|
49
|
+
Stored credentials are reused in either mode. SSH is not detected automatically.
|
|
45
50
|
The process stays in the foreground. Ctrl+C disconnects and preserves authorization.
|
|
46
51
|
Programs run as the OS user who starts connect.
|
|
47
52
|
|
|
48
53
|
Options:
|
|
49
54
|
--cloud <origin> Cloud origin (default: https://romeos.cc).
|
|
50
55
|
--name <name> Name for browser authorization (default: computer hostname).
|
|
56
|
+
--device-code Authorize from another device without a callback or SSH tunnel.
|
|
51
57
|
|
|
52
58
|
Example:
|
|
53
59
|
rome-node connect --name "My computer"
|
|
60
|
+
rome-node connect --device-code --name "Remote Linux" --cloud https://romeos.cc
|
|
54
61
|
`,
|
|
55
62
|
auth: `Usage:
|
|
56
63
|
rome-node auth [--cloud <origin>] < token-file
|
|
@@ -89,7 +96,7 @@ The token is never printed.
|
|
|
89
96
|
device: `Usage:
|
|
90
97
|
rome-node device
|
|
91
98
|
rome-node device describe <device-id>
|
|
92
|
-
rome-node device run <device-id> <action> [--args <json>]
|
|
99
|
+
rome-node device run <device-id> <action> [--args <json>] [--input <file|->] [--output <file|->]
|
|
93
100
|
|
|
94
101
|
With no subcommand, list authorized devices as {"items":[...]}.
|
|
95
102
|
Use an item's id for subsequent commands. Listed devices may be offline.
|
|
@@ -113,7 +120,9 @@ Example result:
|
|
|
113
120
|
Failures return {"type":"response","ok":false,"error":{"code":"...","message":"..."}}.
|
|
114
121
|
The local exit status is 0 on success or 1 on failure.
|
|
115
122
|
`,
|
|
116
|
-
"device run": `Usage:
|
|
123
|
+
"device run": `Usage:
|
|
124
|
+
rome-node device run <device-id> <action> [--args <json>]
|
|
125
|
+
rome-node device run <device-id> <action> [--args <json>] [--input <file|->] [--output <file|->]
|
|
117
126
|
|
|
118
127
|
Use an ID from rome-node device. Run device describe first to discover actions.
|
|
119
128
|
--args is a JSON object for the action (default: {}).
|
|
@@ -127,7 +136,7 @@ exec arguments:
|
|
|
127
136
|
args Optional array of string arguments (default: []).
|
|
128
137
|
cwd Optional working directory on the target (default: executor cwd).
|
|
129
138
|
|
|
130
|
-
Programs receive no
|
|
139
|
+
Programs receive no stdin unless --input is given. No shell is added automatically.
|
|
131
140
|
Invoke a shell explicitly for pipelines, redirection, or shell builtins.
|
|
132
141
|
Examples below use POSIX caller-shell quoting:
|
|
133
142
|
rome-node device run <device-id> system.info
|
|
@@ -145,12 +154,66 @@ Check exec's exitCode even when ok is true. The local exit status is 1 for
|
|
|
145
154
|
action failures, nonzero remote exit codes, or signal termination, otherwise 0.
|
|
146
155
|
Local setup and argument errors print diagnostics to stderr and exit 1.
|
|
147
156
|
Output is collected in memory and returned in full after program exit.
|
|
148
|
-
|
|
149
|
-
Transport limits still apply. Connection loss returns unknown_outcome.
|
|
157
|
+
In text mode, the CLI does not truncate output or impose message or send-buffer
|
|
158
|
+
size limits. Transport limits still apply. Connection loss returns unknown_outcome.
|
|
159
|
+
|
|
160
|
+
Raw bytes (--input or --output):
|
|
161
|
+
The request travels as a binary frame. Bytes are never JSON or base64 encoded.
|
|
162
|
+
--input <file|-> Send the file, or this command's stdin, as the program's stdin.
|
|
163
|
+
The program then reads end of file. Without --input, stdin is empty.
|
|
164
|
+
--output <file|-> Write the program's stdout bytes to the file, or to stdout (default: -).
|
|
165
|
+
The file is written only when ok is true.
|
|
166
|
+
The JSON response goes to stdout, or to stderr when the output is -.
|
|
167
|
+
exec result: {exitCode, signal, stderr, truncated}. stdout is the raw output.
|
|
168
|
+
Secrets in stdout are replaced by the same number of * bytes.
|
|
169
|
+
Each request and each response is one frame of at most 32 MiB. Larger input
|
|
170
|
+
fails with message_too_large before anything is sent. Larger output fails with
|
|
171
|
+
output_too_large after the program ran. Use rome-node cp for large files.
|
|
172
|
+
A target without binary frame support (no frameVersion in system.info) drops
|
|
173
|
+
the request, and the caller reports unknown_outcome after 60 seconds.
|
|
174
|
+
device run does not check frameVersion. rome-node cp checks transferVersion
|
|
175
|
+
first and fails at once with unsupported_device.
|
|
176
|
+
Example:
|
|
177
|
+
rome-node device run <device-id> exec --args '{"command":"gzip","args":["-c"]}' --input data.bin --output data.bin.gz
|
|
150
178
|
|
|
151
179
|
The caller waits up to 60 seconds for a response. unknown_outcome means execution
|
|
152
180
|
may have started or completed. A local timeout does not cancel the remote program.
|
|
153
181
|
Never automatically retry an unknown outcome. Requests are never replayed.
|
|
182
|
+
`,
|
|
183
|
+
cp: `Usage: rome-node cp <src> <dst>
|
|
184
|
+
|
|
185
|
+
Copy one regular file between this computer and a device. Exactly one side is
|
|
186
|
+
<device-id>:<path>, and the other is a local path. A device path that is not
|
|
187
|
+
absolute starts from the working directory of rome-node connect on that device.
|
|
188
|
+
A destination ending in / or \\ receives the source file name.
|
|
189
|
+
|
|
190
|
+
rome-node cp ./video.mp4 <device-id>:/tmp/video.mp4
|
|
191
|
+
rome-node cp <device-id>:/tmp/video.mp4 ./video.mp4
|
|
192
|
+
rome-node cp ./video.mp4 '<device-id>:C:/Users/me/'
|
|
193
|
+
|
|
194
|
+
To copy between two devices, copy in two steps through this computer:
|
|
195
|
+
rome-node cp <device-a>:/data/video.mp4 ./video.mp4
|
|
196
|
+
rome-node cp ./video.mp4 <device-b>:/data/video.mp4
|
|
197
|
+
|
|
198
|
+
The caller daemon reads or writes the local file, and rome-node connect reads
|
|
199
|
+
or writes the device file. Programs and shells are not involved. The source
|
|
200
|
+
must be a regular file. FIFOs, devices, and directories fail with not_a_file.
|
|
201
|
+
|
|
202
|
+
Each copy writes its own .<name>.<uuid>.rome-part next to <dst>, created with
|
|
203
|
+
mode 0600. It replaces <dst> only after the size and SHA-256 checksum match the
|
|
204
|
+
source and the data is flushed to disk. A replaced file keeps its permission
|
|
205
|
+
bits, and a new file gets the directory's default mode.
|
|
206
|
+
|
|
207
|
+
A failed or interrupted copy (including Ctrl+C) leaves <dst> unchanged and
|
|
208
|
+
deletes its part file. There is no resume or automatic retry. Run the copy
|
|
209
|
+
again. A copy aborts after 60 seconds without progress. The device must run a
|
|
210
|
+
rome-node version that reports transferVersion in system.info.
|
|
211
|
+
|
|
212
|
+
Output:
|
|
213
|
+
Progress (bytes, percent, throughput) goes to stderr.
|
|
214
|
+
Success prints {"bytes":...,"ms":...,"sha256":"..."} to stdout and exits 0.
|
|
215
|
+
A copy failure prints {"type":"response","ok":false,"error":{...}} to stdout
|
|
216
|
+
and exits 1. Argument errors print to stderr and exit 1.
|
|
154
217
|
`,
|
|
155
218
|
daemon: `Usage: rome-node daemon [start|status|stop|serve]
|
|
156
219
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rome-os/node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Standalone CLI for Rome remote devices",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rome-os/node-core": "^0.1.
|
|
25
|
+
"@rome-os/node-core": "^0.1.2"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@rstest/core": "^0.11.11",
|