@swipt/cli 0.1.0-nightly.33 → 0.1.0-nightly.35
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/client.mjs +12 -26
- package/connection.mjs +227 -0
- package/device.mjs +6 -35
- package/package.json +2 -2
- package/swipt.mjs +13 -31
package/client.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import dns from "node:dns";
|
|
2
|
+
|
|
1
3
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
2
4
|
|
|
3
5
|
import { LogService } from "./proto/logs_pb.js";
|
|
@@ -9,6 +11,8 @@ const largestMessage = 128 << 20;
|
|
|
9
11
|
const contentType = "application/grpc-web+proto";
|
|
10
12
|
const webSocketSubprotocol = "grpc-websockets";
|
|
11
13
|
|
|
14
|
+
dns.setDefaultResultOrder("ipv4first");
|
|
15
|
+
|
|
12
16
|
export const Code = {
|
|
13
17
|
Canceled: 1,
|
|
14
18
|
Unknown: 2,
|
|
@@ -111,23 +115,20 @@ function checkStatus(headers) {
|
|
|
111
115
|
return true;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
function requestHeaders(connection
|
|
118
|
+
function requestHeaders(connection) {
|
|
115
119
|
const headers = { "content-type": contentType, "x-grpc-web": "1" };
|
|
116
120
|
if (connection.token !== "") {
|
|
117
121
|
headers.authorization = `Bearer ${connection.token}`;
|
|
118
122
|
}
|
|
119
|
-
if (timeoutMs > 0) {
|
|
120
|
-
headers["grpc-timeout"] = `${Math.ceil(timeoutMs)}m`;
|
|
121
|
-
}
|
|
122
123
|
return headers;
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
async function* invokeHTTP(connection, method, input, { signal
|
|
126
|
+
async function* invokeHTTP(connection, method, input, { signal }) {
|
|
126
127
|
let response;
|
|
127
128
|
try {
|
|
128
129
|
response = await fetch(`${connection.base}/${method.parent.typeName}/${method.name}`, {
|
|
129
130
|
method: "POST",
|
|
130
|
-
headers: requestHeaders(connection
|
|
131
|
+
headers: requestHeaders(connection),
|
|
131
132
|
body: frame(method.input, input),
|
|
132
133
|
signal,
|
|
133
134
|
});
|
|
@@ -164,7 +165,7 @@ async function* invokeHTTP(connection, method, input, { signal, timeoutMs }) {
|
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
async function* invokeWebSocket(connection, method, input, { signal
|
|
168
|
+
async function* invokeWebSocket(connection, method, input, { signal }) {
|
|
168
169
|
const socket = new WebSocket(`${connection.webSocketBase}/${method.parent.typeName}/${method.name}`, [webSocketSubprotocol]);
|
|
169
170
|
socket.binaryType = "arraybuffer";
|
|
170
171
|
const reader = new FrameReader(method.output);
|
|
@@ -219,7 +220,7 @@ async function* invokeWebSocket(connection, method, input, { signal, timeoutMs }
|
|
|
219
220
|
signal?.addEventListener("abort", abort, { once: true });
|
|
220
221
|
try {
|
|
221
222
|
await opened;
|
|
222
|
-
const headers = requestHeaders(connection
|
|
223
|
+
const headers = requestHeaders(connection);
|
|
223
224
|
socket.send(Buffer.from(`${Object.entries(headers).map(([key, value]) => `${key}: ${value}\r\n`).join("")}`));
|
|
224
225
|
const sender = (async () => {
|
|
225
226
|
try {
|
|
@@ -277,30 +278,15 @@ function bind(service, connection) {
|
|
|
277
278
|
for (const method of service.methods) {
|
|
278
279
|
const streamsInput = method.methodKind === "client_streaming" || method.methodKind === "bidi_streaming";
|
|
279
280
|
client[method.localName] = (input, options = {}) => {
|
|
280
|
-
const call = (streamsInput ? invokeWebSocket : invokeHTTP)(connection, method, input, { signal: options.signal
|
|
281
|
+
const call = (streamsInput ? invokeWebSocket : invokeHTTP)(connection, method, input, { signal: options.signal });
|
|
281
282
|
return method.methodKind === "unary" || method.methodKind === "client_streaming" ? single(call) : call;
|
|
282
283
|
};
|
|
283
284
|
}
|
|
284
285
|
return client;
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
export function open({
|
|
288
|
-
|
|
289
|
-
token ||= process.env.SWIPT_TOKEN ?? "";
|
|
290
|
-
if (host === "") {
|
|
291
|
-
throw new UsageError('give --host or set SWIPT_HOST; "swipt devices" lists the hosts on the local network');
|
|
292
|
-
}
|
|
293
|
-
let parsed;
|
|
294
|
-
try {
|
|
295
|
-
parsed = new URL(host.includes("://") ? host : `http://${host}`);
|
|
296
|
-
} catch {
|
|
297
|
-
throw new UsageError(`--host is not a URL: ${host}`);
|
|
298
|
-
}
|
|
299
|
-
if (parsed.hostname === "" || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
300
|
-
throw new UsageError(`--host is an http or https URL: ${host}`);
|
|
301
|
-
}
|
|
302
|
-
const base = `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
303
|
-
const connection = { token, base, webSocketBase: base.replace(/^http/, "ws") };
|
|
288
|
+
export function open({ url, token }) {
|
|
289
|
+
const connection = { token, base: url, webSocketBase: url.replace(/^http/, "ws") };
|
|
304
290
|
return {
|
|
305
291
|
system: bind(SystemService, connection),
|
|
306
292
|
logs: bind(LogService, connection),
|
package/connection.mjs
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import dgram from "node:dgram";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
6
|
+
|
|
7
|
+
import Bonjour from "bonjour-service";
|
|
8
|
+
|
|
9
|
+
import { Code, RpcError, UsageError, open } from "./client.mjs";
|
|
10
|
+
import { columns } from "./output.mjs";
|
|
11
|
+
|
|
12
|
+
const browseLimit = 1500;
|
|
13
|
+
const browseRequery = 300;
|
|
14
|
+
|
|
15
|
+
const storeFile = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "swipt", "devices.json");
|
|
16
|
+
|
|
17
|
+
function load() {
|
|
18
|
+
let text;
|
|
19
|
+
try {
|
|
20
|
+
text = fs.readFileSync(storeFile, "utf8");
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error.code === "ENOENT") {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return JSON.parse(text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function save(devices) {
|
|
31
|
+
fs.mkdirSync(path.dirname(storeFile), { recursive: true, mode: 0o700 });
|
|
32
|
+
fs.writeFileSync(storeFile, `${JSON.stringify(devices, null, 2)}\n`, { mode: 0o600 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function label(name) {
|
|
36
|
+
const slug = [...name.replaceAll(/['’]/g, "")]
|
|
37
|
+
.map(character => (/[A-Za-z0-9]/.test(character) ? character : "-"))
|
|
38
|
+
.join("")
|
|
39
|
+
.split("-")
|
|
40
|
+
.filter(part => part !== "")
|
|
41
|
+
.join("-");
|
|
42
|
+
return slug === "" ? name : slug;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
46
|
+
|
|
47
|
+
const storedName = (stored, name) => Object.keys(stored).find(it => sameName(it, name));
|
|
48
|
+
|
|
49
|
+
function parseURL(text) {
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = new URL(text.includes("://") ? text : `http://${text}`);
|
|
53
|
+
} catch {
|
|
54
|
+
throw new UsageError(`${text} is not a URL`);
|
|
55
|
+
}
|
|
56
|
+
if (parsed.hostname === "" || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
57
|
+
throw new UsageError(`${text} is not an http or https URL`);
|
|
58
|
+
}
|
|
59
|
+
let token;
|
|
60
|
+
try {
|
|
61
|
+
token = decodeURIComponent(parsed.username);
|
|
62
|
+
} catch {
|
|
63
|
+
token = parsed.username;
|
|
64
|
+
}
|
|
65
|
+
return { url: `${parsed.protocol}//${parsed.host}${parsed.pathname.replace(/\/+$/, "")}`, token };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const withToken = url => url.replace("://", "://TOKEN@");
|
|
69
|
+
|
|
70
|
+
async function browse(signal, until) {
|
|
71
|
+
const bonjour = new Bonjour({ socket: dgram.createSocket("udp4"), bind: false });
|
|
72
|
+
const found = new Map();
|
|
73
|
+
const enough = new AbortController();
|
|
74
|
+
try {
|
|
75
|
+
const browser = bonjour.find({ type: "swipt" }, service => {
|
|
76
|
+
if (!service.host || found.has(service.name)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const host = service.host.replace(/\.$/, "").toLowerCase();
|
|
80
|
+
found.set(service.name, {
|
|
81
|
+
name: label(service.name),
|
|
82
|
+
url: `http://${host.includes(":") ? `[${host}]` : host}:${service.port}`,
|
|
83
|
+
authenticates: service.txt?.auth === "1",
|
|
84
|
+
});
|
|
85
|
+
if (until?.([...found.values()])) {
|
|
86
|
+
enough.abort();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const requery = setInterval(() => browser.update(), browseRequery);
|
|
90
|
+
try {
|
|
91
|
+
await sleep(browseLimit, undefined, { signal: AbortSignal.any([signal, enough.signal]) });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (signal.aborted) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
clearInterval(requery);
|
|
98
|
+
}
|
|
99
|
+
browser.stop();
|
|
100
|
+
} finally {
|
|
101
|
+
bonjour.destroy();
|
|
102
|
+
}
|
|
103
|
+
return [...found.values()];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function discover(signal, stored, until) {
|
|
107
|
+
const found = await browse(signal, until);
|
|
108
|
+
let changed = false;
|
|
109
|
+
for (const it of found) {
|
|
110
|
+
if (it.authenticates) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const name = storedName(stored, it.name);
|
|
114
|
+
if (name === undefined) {
|
|
115
|
+
stored[it.name] = it.url;
|
|
116
|
+
changed = true;
|
|
117
|
+
} else {
|
|
118
|
+
const current = parseURL(stored[name]);
|
|
119
|
+
if (current.token === "" && current.url !== it.url) {
|
|
120
|
+
stored[name] = it.url;
|
|
121
|
+
changed = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (changed) {
|
|
126
|
+
save(stored);
|
|
127
|
+
}
|
|
128
|
+
return found;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const requiresToken = found => `${found.name} requires a token: swipt connect ${withToken(found.url)}`;
|
|
132
|
+
|
|
133
|
+
export async function attach(context) {
|
|
134
|
+
const given = context.options.device ?? process.env.SWIPT_DEVICE;
|
|
135
|
+
const stored = load();
|
|
136
|
+
let device;
|
|
137
|
+
if (given && /[:/]/.test(given)) {
|
|
138
|
+
device = parseURL(given);
|
|
139
|
+
} else if (given) {
|
|
140
|
+
const name = label(given);
|
|
141
|
+
let known = storedName(stored, name);
|
|
142
|
+
if (known === undefined) {
|
|
143
|
+
const found = (await discover(context.signal, stored, services => services.some(it => sameName(it.name, name)))).find(it => sameName(it.name, name));
|
|
144
|
+
if (!found) {
|
|
145
|
+
throw new Error(`there is no device ${name}; "swipt devices" lists them`);
|
|
146
|
+
}
|
|
147
|
+
if (found.authenticates) {
|
|
148
|
+
throw new Error(requiresToken(found));
|
|
149
|
+
}
|
|
150
|
+
known = found.name;
|
|
151
|
+
}
|
|
152
|
+
device = parseURL(stored[known]);
|
|
153
|
+
} else {
|
|
154
|
+
let found = [];
|
|
155
|
+
if (Object.keys(stored).length === 0) {
|
|
156
|
+
found = await discover(context.signal, stored);
|
|
157
|
+
}
|
|
158
|
+
const names = Object.keys(stored);
|
|
159
|
+
if (names.length === 0) {
|
|
160
|
+
const locked = found.filter(it => it.authenticates);
|
|
161
|
+
if (locked.length > 0) {
|
|
162
|
+
throw new Error(locked.map(requiresToken).join("\n"));
|
|
163
|
+
}
|
|
164
|
+
throw new Error('there is no device; turn on Swipt › Developer › Local Network, or connect one with "swipt connect URL"');
|
|
165
|
+
}
|
|
166
|
+
if (names.length > 1) {
|
|
167
|
+
throw new Error(`there is more than one device: ${names.join(", ")}; choose one with -d or SWIPT_DEVICE`);
|
|
168
|
+
}
|
|
169
|
+
device = parseURL(stored[names[0]]);
|
|
170
|
+
}
|
|
171
|
+
context.target.url = device.url;
|
|
172
|
+
return open(device);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function reachable(device, signal) {
|
|
176
|
+
try {
|
|
177
|
+
await open(device).system.getInfo({}, { signal });
|
|
178
|
+
return true;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return error instanceof RpcError && error.code === Code.Unauthenticated;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function devices({ signal }) {
|
|
185
|
+
const stored = load();
|
|
186
|
+
const deadline = AbortSignal.any([signal, AbortSignal.timeout(browseLimit)]);
|
|
187
|
+
const probes = new Map(Object.entries(stored).map(([name, url]) => [name, reachable(parseURL(url), deadline)]));
|
|
188
|
+
const found = await discover(signal, stored);
|
|
189
|
+
const names = Object.keys(stored).sort((a, b) => a.localeCompare(b));
|
|
190
|
+
if (names.length === 0) {
|
|
191
|
+
console.error('no device publishes Swipt on the local network; turn on Swipt › Developer › Local Network, or connect one with "swipt connect URL"');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const rows = [];
|
|
195
|
+
for (const name of names) {
|
|
196
|
+
const { url } = parseURL(stored[name]);
|
|
197
|
+
const online = found.some(it => sameName(it.name, name) && it.url === url) || (probes.has(name) && (await probes.get(name)));
|
|
198
|
+
rows.push([name, url, online ? "" : "offline"]);
|
|
199
|
+
}
|
|
200
|
+
console.log(columns(rows));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function connect(context) {
|
|
204
|
+
const device = parseURL(context.args[0]);
|
|
205
|
+
context.target.url = device.url;
|
|
206
|
+
const info = await open(device).system.getInfo({}, { signal: context.signal });
|
|
207
|
+
const name = label(info.deviceName) || new URL(device.url).hostname;
|
|
208
|
+
const stored = load();
|
|
209
|
+
const previous = storedName(stored, name);
|
|
210
|
+
if (previous !== undefined) {
|
|
211
|
+
delete stored[previous];
|
|
212
|
+
}
|
|
213
|
+
stored[name] = context.args[0];
|
|
214
|
+
save(stored);
|
|
215
|
+
console.log("connected", name);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function disconnect({ args }) {
|
|
219
|
+
const stored = load();
|
|
220
|
+
const name = storedName(stored, label(args[0]));
|
|
221
|
+
if (name === undefined) {
|
|
222
|
+
throw new Error(`there is no device ${args[0]}`);
|
|
223
|
+
}
|
|
224
|
+
delete stored[name];
|
|
225
|
+
save(stored);
|
|
226
|
+
console.log("disconnected", name);
|
|
227
|
+
}
|
package/device.mjs
CHANGED
|
@@ -7,9 +7,9 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
7
7
|
|
|
8
8
|
import { fromJson, toJson } from "@bufbuild/protobuf";
|
|
9
9
|
import { ValueSchema, timestampFromMs } from "@bufbuild/protobuf/wkt";
|
|
10
|
-
import Bonjour from "bonjour-service";
|
|
11
10
|
|
|
12
|
-
import { UsageError,
|
|
11
|
+
import { UsageError, unauthenticated } from "./client.mjs";
|
|
12
|
+
import { attach } from "./connection.mjs";
|
|
13
13
|
import { Ignorer, relativePath } from "./ignore.mjs";
|
|
14
14
|
import { columns, dateTime, enumText, printLog, printScriptError, serviceText } from "./output.mjs";
|
|
15
15
|
import { buildProject, loadProject } from "./project.mjs";
|
|
@@ -18,10 +18,8 @@ import { ServiceStatus, ServiceStatusSchema } from "./proto/swiplets_pb.js";
|
|
|
18
18
|
import { sources } from "./sourcemap.mjs";
|
|
19
19
|
|
|
20
20
|
const uploadChunk = 256 << 10;
|
|
21
|
-
const browseLimit = 1500;
|
|
22
21
|
const rebuildDelay = 300;
|
|
23
22
|
const logReconnect = 2000;
|
|
24
|
-
const scriptGrace = 10_000;
|
|
25
23
|
|
|
26
24
|
const { version } = createRequire(import.meta.url)("./package.json");
|
|
27
25
|
|
|
@@ -41,33 +39,7 @@ function elapsed(since) {
|
|
|
41
39
|
return `${Math.floor(seconds / 60)}m${Number((seconds % 60).toFixed(3))}s`;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
const session = (context, run) =>
|
|
45
|
-
|
|
46
|
-
export async function devices({ signal }) {
|
|
47
|
-
const bonjour = new Bonjour();
|
|
48
|
-
const found = new Map();
|
|
49
|
-
try {
|
|
50
|
-
const browser = bonjour.find({ type: "swipt" }, service => {
|
|
51
|
-
if (service.host && !found.has(service.name)) {
|
|
52
|
-
found.set(service.name, { name: service.name, host: service.host.replace(/\.$/, ""), port: service.port });
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
try {
|
|
56
|
-
await sleep(browseLimit, undefined, { signal });
|
|
57
|
-
} catch {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
browser.stop();
|
|
61
|
-
} finally {
|
|
62
|
-
bonjour.destroy();
|
|
63
|
-
}
|
|
64
|
-
if (found.size === 0) {
|
|
65
|
-
console.error("no device publishes Swipt on the local network; turn on Swipt › Developer › Local Network");
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const hostPort = service => (service.host.includes(":") ? `[${service.host}]:${service.port}` : `${service.host}:${service.port}`);
|
|
69
|
-
console.log(columns([["NAME", "HOST"], ...[...found.values()].map(service => [service.name, `http://${hostPort(service)}`])]));
|
|
70
|
-
}
|
|
42
|
+
const session = (context, run) => attach(context).then(run);
|
|
71
43
|
|
|
72
44
|
export function printVersion() {
|
|
73
45
|
console.log("swipt", version, `node ${process.versions.node}`, `${process.platform}/${process.arch}`);
|
|
@@ -436,7 +408,7 @@ export async function develop(context) {
|
|
|
436
408
|
const project = loadProject(path.resolve(context.args[0] ?? "."));
|
|
437
409
|
const root = project.root;
|
|
438
410
|
sources.root = root;
|
|
439
|
-
const client =
|
|
411
|
+
const client = await attach(context);
|
|
440
412
|
const controller = new AbortController();
|
|
441
413
|
const inner = AbortSignal.any([signal, controller.signal]);
|
|
442
414
|
let fatalReject;
|
|
@@ -573,7 +545,7 @@ class ImageSink {
|
|
|
573
545
|
}
|
|
574
546
|
|
|
575
547
|
export async function js(context) {
|
|
576
|
-
const { args, options,
|
|
548
|
+
const { args, options, signal } = context;
|
|
577
549
|
if (options.api) {
|
|
578
550
|
if (args.length > 0 || options.file !== undefined || options.reset) {
|
|
579
551
|
throw new UsageError("--api takes no script and no --reset");
|
|
@@ -584,7 +556,6 @@ export async function js(context) {
|
|
|
584
556
|
return;
|
|
585
557
|
}
|
|
586
558
|
const { code, run } = readScript(args, options);
|
|
587
|
-
const signal = globals.timeoutMs > 0 ? AbortSignal.any([context.interrupt, AbortSignal.timeout(globals.timeoutMs + scriptGrace)]) : context.interrupt;
|
|
588
559
|
await session(context, async client => {
|
|
589
560
|
if (options.reset) {
|
|
590
561
|
await client.repl.reset({}, { signal });
|
|
@@ -595,7 +566,7 @@ export async function js(context) {
|
|
|
595
566
|
const script = options.file ?? "input.js";
|
|
596
567
|
const images = new ImageSink(options.output);
|
|
597
568
|
let failure;
|
|
598
|
-
for await (const response of client.repl.execute({ code
|
|
569
|
+
for await (const response of client.repl.execute({ code }, { signal })) {
|
|
599
570
|
switch (response.part.case) {
|
|
600
571
|
case "console": {
|
|
601
572
|
const output = response.part.value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swipt/cli",
|
|
3
|
-
"version": "0.1.0-nightly.
|
|
3
|
+
"version": "0.1.0-nightly.35",
|
|
4
4
|
"license": "GPL-3.0-or-later",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"esbuild": "^0.28.2"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@swipt/api": "0.1.0-nightly.
|
|
28
|
+
"@swipt/api": "0.1.0-nightly.35"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@bufbuild/protoc-gen-es": "^2.16.0"
|
package/swipt.mjs
CHANGED
|
@@ -3,15 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
|
|
5
5
|
import { Code, RpcError, UsageError } from "./client.mjs";
|
|
6
|
+
import * as connection from "./connection.mjs";
|
|
6
7
|
import * as device from "./device.mjs";
|
|
7
8
|
import { FalseResult } from "./output.mjs";
|
|
8
9
|
import { buildProject, createCommand, sourceCommand } from "./project.mjs";
|
|
9
10
|
import { sources } from "./sourcemap.mjs";
|
|
10
11
|
|
|
11
12
|
const globalOptions = {
|
|
12
|
-
|
|
13
|
-
token: { type: "string", value: "TOKEN", help: "API token (SWIPT_TOKEN)" },
|
|
14
|
-
timeout: { type: "string", value: "DURATION", help: "time limit" },
|
|
13
|
+
device: { type: "string", short: "d", value: "NAME|URL", help: "the device to use (SWIPT_DEVICE)" },
|
|
15
14
|
help: { type: "boolean", short: "h", help: "help" },
|
|
16
15
|
};
|
|
17
16
|
|
|
@@ -73,7 +72,9 @@ const commands = [
|
|
|
73
72
|
args: [0, Infinity],
|
|
74
73
|
run: ({ args }) => sourceCommand(process.cwd(), args),
|
|
75
74
|
},
|
|
76
|
-
{ name: "devices", short: "List the devices
|
|
75
|
+
{ name: "devices", short: "List the devices, adding the ones found on the local network", args: [0, 0], run: connection.devices },
|
|
76
|
+
{ name: "connect", use: "connect URL", short: "Add a device by its URL; needed when it is remote or requires a token", args: [1, 1], run: connection.connect },
|
|
77
|
+
{ name: "disconnect", use: "disconnect NAME", short: "Forget a device", args: [1, 1], run: connection.disconnect },
|
|
77
78
|
{ name: "info", short: "Show the device and its connection", args: [0, 0], run: device.info },
|
|
78
79
|
{ name: "version", short: "Print the version", args: [0, 0], run: device.printVersion },
|
|
79
80
|
{ name: "swiplets", use: "swiplets [ID]", short: "List the swiplets or show one", args: [0, 1], options: projectOption, run: device.swiplets },
|
|
@@ -168,7 +169,7 @@ function usage(trail) {
|
|
|
168
169
|
}
|
|
169
170
|
const options = { ...trail.reduce((found, it) => ({ ...found, ...it.options }), {}), ...globalOptions };
|
|
170
171
|
lines.push("", "options:");
|
|
171
|
-
lines.push(...columns(Object.entries(options).map(([option, it]) => [` --${option}${it.value ? ` ${it.value}` : ""}`, it.help])).split("\n"));
|
|
172
|
+
lines.push(...columns(Object.entries(options).map(([option, it]) => [` ${it.short ? `-${it.short}, ` : ""}--${option}${it.value ? ` ${it.value}` : ""}`, it.help])).split("\n"));
|
|
172
173
|
return lines.join("\n");
|
|
173
174
|
}
|
|
174
175
|
|
|
@@ -221,29 +222,22 @@ function resolve(argv) {
|
|
|
221
222
|
return { trail, help, args, values: parsed.values };
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
function printError(error,
|
|
225
|
+
function printError(error, target, operation) {
|
|
225
226
|
let exit = 1;
|
|
226
227
|
let message = error.message;
|
|
227
228
|
if (error instanceof UsageError) {
|
|
228
229
|
exit = 2;
|
|
229
|
-
} else if (timedOut) {
|
|
230
|
-
message = `the command ran past --timeout ${timeout}`;
|
|
231
230
|
} else if (error instanceof RpcError) {
|
|
232
231
|
switch (error.code) {
|
|
233
232
|
case Code.Unavailable:
|
|
234
233
|
exit = 5;
|
|
235
|
-
message = `Swipt at ${
|
|
234
|
+
message = `Swipt at ${target.url} is unreachable: ${message}`;
|
|
236
235
|
break;
|
|
237
236
|
case Code.Unauthenticated:
|
|
238
|
-
message +=
|
|
237
|
+
message += `; connect with the URL from Swipt › Developer: swipt connect ${connection.withToken(target.url)}`;
|
|
239
238
|
break;
|
|
240
239
|
case Code.Unimplemented:
|
|
241
|
-
message = `Swipt at ${
|
|
242
|
-
break;
|
|
243
|
-
case Code.DeadlineExceeded:
|
|
244
|
-
if (timeout) {
|
|
245
|
-
message = `the command ran past --timeout ${timeout}`;
|
|
246
|
-
}
|
|
240
|
+
message = `Swipt at ${target.url} does not have this command; update Swipt`;
|
|
247
241
|
break;
|
|
248
242
|
}
|
|
249
243
|
}
|
|
@@ -259,8 +253,7 @@ async function main() {
|
|
|
259
253
|
const interrupted = () => interrupt.abort();
|
|
260
254
|
process.once("SIGINT", interrupted);
|
|
261
255
|
process.once("SIGTERM", interrupted);
|
|
262
|
-
|
|
263
|
-
const state = { host: "", timeout: undefined, timedOut: false };
|
|
256
|
+
const target = { url: "" };
|
|
264
257
|
try {
|
|
265
258
|
const resolved = resolve(process.argv.slice(2));
|
|
266
259
|
if (resolved.help) {
|
|
@@ -269,28 +262,17 @@ async function main() {
|
|
|
269
262
|
}
|
|
270
263
|
const command = resolved.trail.at(-1);
|
|
271
264
|
const values = command.parse ? command.parse(resolved.values) : resolved.values;
|
|
272
|
-
state.host = values.host ?? process.env.SWIPT_HOST ?? "";
|
|
273
|
-
state.timeout = values.timeout;
|
|
274
|
-
const timeoutMs = parseDuration(values.timeout, "--timeout");
|
|
275
|
-
if (timeoutMs > 0) {
|
|
276
|
-
timeout = AbortSignal.timeout(timeoutMs);
|
|
277
|
-
timeout.addEventListener("abort", () => {
|
|
278
|
-
state.timedOut = true;
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
265
|
if (values.project !== undefined) {
|
|
282
266
|
sources.root = path.resolve(values.project);
|
|
283
267
|
}
|
|
284
|
-
|
|
285
|
-
const signal = timeout ? AbortSignal.any([interrupt.signal, timeout]) : interrupt.signal;
|
|
286
|
-
await command.run({ args: resolved.args, options: values, globals, signal, interrupt: interrupt.signal, printError: (error, operation) => printError(error, state, operation) });
|
|
268
|
+
await command.run({ args: resolved.args, options: values, target, signal: interrupt.signal, printError: (error, operation) => printError(error, target, operation) });
|
|
287
269
|
} catch (error) {
|
|
288
270
|
if (error instanceof FalseResult) {
|
|
289
271
|
process.exitCode = 1;
|
|
290
272
|
} else if (interrupt.signal.aborted) {
|
|
291
273
|
process.exitCode = 130;
|
|
292
274
|
} else {
|
|
293
|
-
process.exitCode = printError(error,
|
|
275
|
+
process.exitCode = printError(error, target);
|
|
294
276
|
}
|
|
295
277
|
} finally {
|
|
296
278
|
process.off("SIGINT", interrupted);
|