@swipt/cli 0.1.0-nightly.43 → 0.1.0-nightly.44
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 +103 -30
- package/connection.mjs +86 -26
- package/device.mjs +268 -90
- package/ignore.mjs +17 -5
- package/metro.mjs +235 -65
- package/output.mjs +32 -7
- package/package.json +2 -2
- package/project.mjs +394 -106
- package/swipt.mjs +298 -52
package/client.mjs
CHANGED
|
@@ -33,7 +33,16 @@ export class RpcError extends Error {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const httpStatusCodes = {
|
|
36
|
+
const httpStatusCodes = {
|
|
37
|
+
400: Code.Internal,
|
|
38
|
+
401: Code.Unauthenticated,
|
|
39
|
+
403: Code.Unauthenticated,
|
|
40
|
+
404: Code.Unimplemented,
|
|
41
|
+
429: Code.Unavailable,
|
|
42
|
+
502: Code.Unavailable,
|
|
43
|
+
503: Code.Unavailable,
|
|
44
|
+
504: Code.Unavailable,
|
|
45
|
+
};
|
|
37
46
|
|
|
38
47
|
function statusMessage(text) {
|
|
39
48
|
if (text === undefined || text === null) {
|
|
@@ -46,7 +55,7 @@ function statusMessage(text) {
|
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
const transportError = error => {
|
|
58
|
+
const transportError = (error) => {
|
|
50
59
|
if (error instanceof RpcError) {
|
|
51
60
|
return error;
|
|
52
61
|
}
|
|
@@ -54,7 +63,10 @@ const transportError = error => {
|
|
|
54
63
|
return new RpcError(Code.Canceled, "the call was canceled");
|
|
55
64
|
}
|
|
56
65
|
const cause = error?.cause;
|
|
57
|
-
return new RpcError(
|
|
66
|
+
return new RpcError(
|
|
67
|
+
Code.Unavailable,
|
|
68
|
+
cause?.message || cause?.code || error?.message || String(error),
|
|
69
|
+
);
|
|
58
70
|
};
|
|
59
71
|
|
|
60
72
|
function frame(schema, init) {
|
|
@@ -71,7 +83,9 @@ function parseMetadata(body) {
|
|
|
71
83
|
for (const line of Buffer.from(body).toString().split("\r\n")) {
|
|
72
84
|
const separator = line.indexOf(":");
|
|
73
85
|
if (separator > 0) {
|
|
74
|
-
headers[line.slice(0, separator).trim().toLowerCase()] = line
|
|
86
|
+
headers[line.slice(0, separator).trim().toLowerCase()] = line
|
|
87
|
+
.slice(separator + 1)
|
|
88
|
+
.trim();
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
return headers;
|
|
@@ -85,18 +99,28 @@ class FrameReader {
|
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
push(chunk) {
|
|
88
|
-
this.pending =
|
|
102
|
+
this.pending =
|
|
103
|
+
this.pending.length === 0
|
|
104
|
+
? Buffer.from(chunk)
|
|
105
|
+
: Buffer.concat([this.pending, Buffer.from(chunk)]);
|
|
89
106
|
const frames = [];
|
|
90
107
|
while (this.pending.length >= 5) {
|
|
91
108
|
const length = this.pending.readUInt32BE(1);
|
|
92
109
|
if (length > largestMessage) {
|
|
93
|
-
throw new RpcError(
|
|
110
|
+
throw new RpcError(
|
|
111
|
+
Code.Internal,
|
|
112
|
+
`the server sent a message of ${length} bytes`,
|
|
113
|
+
);
|
|
94
114
|
}
|
|
95
115
|
if (this.pending.length < 5 + length) {
|
|
96
116
|
break;
|
|
97
117
|
}
|
|
98
118
|
const body = this.pending.subarray(5, 5 + length);
|
|
99
|
-
frames.push(
|
|
119
|
+
frames.push(
|
|
120
|
+
(this.pending[0] & 0x80) !== 0
|
|
121
|
+
? { trailers: parseMetadata(body) }
|
|
122
|
+
: { message: fromBinary(this.schema, body) },
|
|
123
|
+
);
|
|
100
124
|
this.pending = this.pending.subarray(5 + length);
|
|
101
125
|
}
|
|
102
126
|
return frames;
|
|
@@ -126,19 +150,25 @@ function requestHeaders(connection) {
|
|
|
126
150
|
async function* invokeHTTP(connection, method, input, { signal }) {
|
|
127
151
|
let response;
|
|
128
152
|
try {
|
|
129
|
-
response = await fetch(
|
|
130
|
-
method
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
response = await fetch(
|
|
154
|
+
`${connection.base}/${method.parent.typeName}/${method.name}`,
|
|
155
|
+
{
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: requestHeaders(connection),
|
|
158
|
+
body: frame(method.input, input),
|
|
159
|
+
signal,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
135
162
|
} catch (error) {
|
|
136
163
|
throw transportError(error);
|
|
137
164
|
}
|
|
138
165
|
const headers = Object.fromEntries(response.headers);
|
|
139
166
|
if (response.status !== 200) {
|
|
140
167
|
await response.body?.cancel();
|
|
141
|
-
throw new RpcError(
|
|
168
|
+
throw new RpcError(
|
|
169
|
+
httpStatusCodes[response.status] ?? Code.Unknown,
|
|
170
|
+
`HTTP ${response.status}`,
|
|
171
|
+
);
|
|
142
172
|
}
|
|
143
173
|
if (checkStatus(headers)) {
|
|
144
174
|
await response.body?.cancel();
|
|
@@ -166,7 +196,10 @@ async function* invokeHTTP(connection, method, input, { signal }) {
|
|
|
166
196
|
}
|
|
167
197
|
|
|
168
198
|
async function* invokeWebSocket(connection, method, input, { signal }) {
|
|
169
|
-
const socket = new WebSocket(
|
|
199
|
+
const socket = new WebSocket(
|
|
200
|
+
`${connection.webSocketBase}/${method.parent.typeName}/${method.name}`,
|
|
201
|
+
[webSocketSubprotocol],
|
|
202
|
+
);
|
|
170
203
|
socket.binaryType = "arraybuffer";
|
|
171
204
|
const reader = new FrameReader(method.output);
|
|
172
205
|
const messages = [];
|
|
@@ -179,17 +212,28 @@ async function* invokeWebSocket(connection, method, input, { signal }) {
|
|
|
179
212
|
wake = () => {};
|
|
180
213
|
current();
|
|
181
214
|
};
|
|
182
|
-
const fail = error => {
|
|
215
|
+
const fail = (error) => {
|
|
183
216
|
failure ??= error;
|
|
184
217
|
socket.close();
|
|
185
218
|
notify();
|
|
186
219
|
};
|
|
187
|
-
const abort = () =>
|
|
220
|
+
const abort = () =>
|
|
221
|
+
fail(new RpcError(Code.Canceled, "the call was canceled"));
|
|
188
222
|
const opened = new Promise((resolve, reject) => {
|
|
189
223
|
socket.addEventListener("open", resolve, { once: true });
|
|
190
|
-
socket.addEventListener(
|
|
224
|
+
socket.addEventListener(
|
|
225
|
+
"error",
|
|
226
|
+
() =>
|
|
227
|
+
reject(
|
|
228
|
+
new RpcError(
|
|
229
|
+
Code.Unavailable,
|
|
230
|
+
`cannot connect to ${connection.webSocketBase}`,
|
|
231
|
+
),
|
|
232
|
+
),
|
|
233
|
+
{ once: true },
|
|
234
|
+
);
|
|
191
235
|
});
|
|
192
|
-
socket.addEventListener("message", event => {
|
|
236
|
+
socket.addEventListener("message", (event) => {
|
|
193
237
|
try {
|
|
194
238
|
for (const found of reader.push(new Uint8Array(event.data))) {
|
|
195
239
|
if (found.message) {
|
|
@@ -207,21 +251,33 @@ async function* invokeWebSocket(connection, method, input, { signal }) {
|
|
|
207
251
|
}
|
|
208
252
|
notify();
|
|
209
253
|
});
|
|
210
|
-
socket.addEventListener("close", event => {
|
|
254
|
+
socket.addEventListener("close", (event) => {
|
|
211
255
|
if (!closed) {
|
|
212
|
-
failure ??= new RpcError(
|
|
256
|
+
failure ??= new RpcError(
|
|
257
|
+
Code.Unavailable,
|
|
258
|
+
event.reason || "the connection closed before the response",
|
|
259
|
+
);
|
|
213
260
|
}
|
|
214
261
|
closed = true;
|
|
215
262
|
notify();
|
|
216
263
|
});
|
|
217
264
|
socket.addEventListener("error", () => {
|
|
218
|
-
failure ??= new RpcError(
|
|
265
|
+
failure ??= new RpcError(
|
|
266
|
+
Code.Unavailable,
|
|
267
|
+
`cannot connect to ${connection.webSocketBase}`,
|
|
268
|
+
);
|
|
219
269
|
});
|
|
220
270
|
signal?.addEventListener("abort", abort, { once: true });
|
|
221
271
|
try {
|
|
222
272
|
await opened;
|
|
223
273
|
const headers = requestHeaders(connection);
|
|
224
|
-
socket.send(
|
|
274
|
+
socket.send(
|
|
275
|
+
Buffer.from(
|
|
276
|
+
`${Object.entries(headers)
|
|
277
|
+
.map(([key, value]) => `${key}: ${value}\r\n`)
|
|
278
|
+
.join("")}`,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
225
281
|
const sender = (async () => {
|
|
226
282
|
try {
|
|
227
283
|
const body = method.methodKind === "client_streaming" ? input : [input];
|
|
@@ -229,7 +285,9 @@ async function* invokeWebSocket(connection, method, input, { signal }) {
|
|
|
229
285
|
if (closed) {
|
|
230
286
|
return;
|
|
231
287
|
}
|
|
232
|
-
socket.send(
|
|
288
|
+
socket.send(
|
|
289
|
+
Buffer.concat([Buffer.from([0]), frame(method.input, message)]),
|
|
290
|
+
);
|
|
233
291
|
}
|
|
234
292
|
if (!closed) {
|
|
235
293
|
socket.send(Buffer.from([1]));
|
|
@@ -246,7 +304,7 @@ async function* invokeWebSocket(connection, method, input, { signal }) {
|
|
|
246
304
|
if (closed) {
|
|
247
305
|
break;
|
|
248
306
|
}
|
|
249
|
-
await new Promise(resolve => {
|
|
307
|
+
await new Promise((resolve) => {
|
|
250
308
|
wake = resolve;
|
|
251
309
|
});
|
|
252
310
|
}
|
|
@@ -276,17 +334,31 @@ async function single(iterable) {
|
|
|
276
334
|
function bind(service, connection) {
|
|
277
335
|
const client = {};
|
|
278
336
|
for (const method of service.methods) {
|
|
279
|
-
const streamsInput =
|
|
337
|
+
const streamsInput =
|
|
338
|
+
method.methodKind === "client_streaming" ||
|
|
339
|
+
method.methodKind === "bidi_streaming";
|
|
280
340
|
client[method.localName] = (input, options = {}) => {
|
|
281
|
-
const call = (streamsInput ? invokeWebSocket : invokeHTTP)(
|
|
282
|
-
|
|
341
|
+
const call = (streamsInput ? invokeWebSocket : invokeHTTP)(
|
|
342
|
+
connection,
|
|
343
|
+
method,
|
|
344
|
+
input,
|
|
345
|
+
{ signal: options.signal },
|
|
346
|
+
);
|
|
347
|
+
return method.methodKind === "unary" ||
|
|
348
|
+
method.methodKind === "client_streaming"
|
|
349
|
+
? single(call)
|
|
350
|
+
: call;
|
|
283
351
|
};
|
|
284
352
|
}
|
|
285
353
|
return client;
|
|
286
354
|
}
|
|
287
355
|
|
|
288
356
|
export function open({ url, token }) {
|
|
289
|
-
const connection = {
|
|
357
|
+
const connection = {
|
|
358
|
+
token,
|
|
359
|
+
base: url,
|
|
360
|
+
webSocketBase: url.replace(/^http/, "ws"),
|
|
361
|
+
};
|
|
290
362
|
return {
|
|
291
363
|
system: bind(SystemService, connection),
|
|
292
364
|
logs: bind(LogService, connection),
|
|
@@ -295,4 +367,5 @@ export function open({ url, token }) {
|
|
|
295
367
|
};
|
|
296
368
|
}
|
|
297
369
|
|
|
298
|
-
export const unauthenticated = error =>
|
|
370
|
+
export const unauthenticated = (error) =>
|
|
371
|
+
error instanceof RpcError && error.code === Code.Unauthenticated;
|
package/connection.mjs
CHANGED
|
@@ -8,12 +8,20 @@ import Bonjour from "bonjour-service";
|
|
|
8
8
|
|
|
9
9
|
import { Code, RpcError, UsageError, open } from "./client.mjs";
|
|
10
10
|
import { columns, enumText } from "./output.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
DeviceConnectionProblem,
|
|
13
|
+
DeviceConnectionProblemSchema,
|
|
14
|
+
DeviceConnectionStateSchema,
|
|
15
|
+
} from "./proto/common_pb.js";
|
|
12
16
|
|
|
13
17
|
const browseLimit = 1500;
|
|
14
18
|
const browseRequery = 300;
|
|
15
19
|
|
|
16
|
-
const storeFile = path.join(
|
|
20
|
+
const storeFile = path.join(
|
|
21
|
+
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"),
|
|
22
|
+
"swipt",
|
|
23
|
+
"devices.json",
|
|
24
|
+
);
|
|
17
25
|
|
|
18
26
|
function load() {
|
|
19
27
|
let text;
|
|
@@ -30,22 +38,25 @@ function load() {
|
|
|
30
38
|
|
|
31
39
|
function save(devices) {
|
|
32
40
|
fs.mkdirSync(path.dirname(storeFile), { recursive: true, mode: 0o700 });
|
|
33
|
-
fs.writeFileSync(storeFile, `${JSON.stringify(devices, null, 2)}\n`, {
|
|
41
|
+
fs.writeFileSync(storeFile, `${JSON.stringify(devices, null, 2)}\n`, {
|
|
42
|
+
mode: 0o600,
|
|
43
|
+
});
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
export function label(name) {
|
|
37
47
|
const slug = [...name.replaceAll(/['’]/g, "")]
|
|
38
|
-
.map(character => (/[A-Za-z0-9]/.test(character) ? character : "-"))
|
|
48
|
+
.map((character) => (/[A-Za-z0-9]/.test(character) ? character : "-"))
|
|
39
49
|
.join("")
|
|
40
50
|
.split("-")
|
|
41
|
-
.filter(part => part !== "")
|
|
51
|
+
.filter((part) => part !== "")
|
|
42
52
|
.join("-");
|
|
43
53
|
return slug === "" ? name : slug;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
export const sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
47
57
|
|
|
48
|
-
const storedName = (stored, name) =>
|
|
58
|
+
const storedName = (stored, name) =>
|
|
59
|
+
Object.keys(stored).find((it) => sameName(it, name));
|
|
49
60
|
|
|
50
61
|
function parseURL(text) {
|
|
51
62
|
let parsed;
|
|
@@ -54,7 +65,10 @@ function parseURL(text) {
|
|
|
54
65
|
} catch {
|
|
55
66
|
throw new UsageError(`${text} is not a URL`);
|
|
56
67
|
}
|
|
57
|
-
if (
|
|
68
|
+
if (
|
|
69
|
+
parsed.hostname === "" ||
|
|
70
|
+
(parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
71
|
+
) {
|
|
58
72
|
throw new UsageError(`${text} is not an http or https URL`);
|
|
59
73
|
}
|
|
60
74
|
let token;
|
|
@@ -63,17 +77,23 @@ function parseURL(text) {
|
|
|
63
77
|
} catch {
|
|
64
78
|
token = parsed.username;
|
|
65
79
|
}
|
|
66
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
url: `${parsed.protocol}//${parsed.host}${parsed.pathname.replace(/\/+$/, "")}`,
|
|
82
|
+
token,
|
|
83
|
+
};
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
export const withToken = url => url.replace("://", "://TOKEN@");
|
|
86
|
+
export const withToken = (url) => url.replace("://", "://TOKEN@");
|
|
70
87
|
|
|
71
88
|
async function browse(signal, until) {
|
|
72
|
-
const bonjour = new Bonjour({
|
|
89
|
+
const bonjour = new Bonjour({
|
|
90
|
+
socket: dgram.createSocket("udp4"),
|
|
91
|
+
bind: false,
|
|
92
|
+
});
|
|
73
93
|
const found = new Map();
|
|
74
94
|
const enough = new AbortController();
|
|
75
95
|
try {
|
|
76
|
-
const browser = bonjour.find({ type: "swipt" }, service => {
|
|
96
|
+
const browser = bonjour.find({ type: "swipt" }, (service) => {
|
|
77
97
|
if (!service.host || found.has(service.name)) {
|
|
78
98
|
return;
|
|
79
99
|
}
|
|
@@ -89,7 +109,9 @@ async function browse(signal, until) {
|
|
|
89
109
|
});
|
|
90
110
|
const requery = setInterval(() => browser.update(), browseRequery);
|
|
91
111
|
try {
|
|
92
|
-
await sleep(browseLimit, undefined, {
|
|
112
|
+
await sleep(browseLimit, undefined, {
|
|
113
|
+
signal: AbortSignal.any([signal, enough.signal]),
|
|
114
|
+
});
|
|
93
115
|
} catch (error) {
|
|
94
116
|
if (signal.aborted) {
|
|
95
117
|
throw error;
|
|
@@ -129,7 +151,8 @@ async function discover(signal, stored, until) {
|
|
|
129
151
|
return found;
|
|
130
152
|
}
|
|
131
153
|
|
|
132
|
-
const requiresToken = found =>
|
|
154
|
+
const requiresToken = (found) =>
|
|
155
|
+
`${found.name} requires a token: swipt device add ${withToken(found.url)}`;
|
|
133
156
|
|
|
134
157
|
export async function attach(context) {
|
|
135
158
|
const given = context.options.device ?? process.env.SWIPT_DEVICE;
|
|
@@ -141,9 +164,15 @@ export async function attach(context) {
|
|
|
141
164
|
const name = label(given);
|
|
142
165
|
let known = storedName(stored, name);
|
|
143
166
|
if (known === undefined) {
|
|
144
|
-
const found = (
|
|
167
|
+
const found = (
|
|
168
|
+
await discover(context.signal, stored, (services) =>
|
|
169
|
+
services.some((it) => sameName(it.name, name)),
|
|
170
|
+
)
|
|
171
|
+
).find((it) => sameName(it.name, name));
|
|
145
172
|
if (!found) {
|
|
146
|
-
throw new Error(
|
|
173
|
+
throw new Error(
|
|
174
|
+
`there is no device ${name}; "swipt device list" lists them`,
|
|
175
|
+
);
|
|
147
176
|
}
|
|
148
177
|
if (found.authenticates) {
|
|
149
178
|
throw new Error(requiresToken(found));
|
|
@@ -158,14 +187,18 @@ export async function attach(context) {
|
|
|
158
187
|
}
|
|
159
188
|
const names = Object.keys(stored);
|
|
160
189
|
if (names.length === 0) {
|
|
161
|
-
const locked = found.filter(it => it.authenticates);
|
|
190
|
+
const locked = found.filter((it) => it.authenticates);
|
|
162
191
|
if (locked.length > 0) {
|
|
163
192
|
throw new Error(locked.map(requiresToken).join("\n"));
|
|
164
193
|
}
|
|
165
|
-
throw new Error(
|
|
194
|
+
throw new Error(
|
|
195
|
+
'there is no device; turn on Swipt › Developer › Local Network, or connect one with "swipt device add URL"',
|
|
196
|
+
);
|
|
166
197
|
}
|
|
167
198
|
if (names.length > 1) {
|
|
168
|
-
throw new Error(
|
|
199
|
+
throw new Error(
|
|
200
|
+
`there is more than one device: ${names.join(", ")}; choose one with -d or SWIPT_DEVICE`,
|
|
201
|
+
);
|
|
169
202
|
}
|
|
170
203
|
device = parseURL(stored[names[0]]);
|
|
171
204
|
}
|
|
@@ -174,8 +207,15 @@ export async function attach(context) {
|
|
|
174
207
|
}
|
|
175
208
|
|
|
176
209
|
function connectionText(connection) {
|
|
177
|
-
let text = enumText(
|
|
178
|
-
|
|
210
|
+
let text = enumText(
|
|
211
|
+
DeviceConnectionStateSchema,
|
|
212
|
+
connection?.state ?? 0,
|
|
213
|
+
"DEVICE_CONNECTION_STATE_",
|
|
214
|
+
);
|
|
215
|
+
if (
|
|
216
|
+
connection &&
|
|
217
|
+
connection.problem !== DeviceConnectionProblem.UNSPECIFIED
|
|
218
|
+
) {
|
|
179
219
|
text += `, ${enumText(DeviceConnectionProblemSchema, connection.problem, "DEVICE_CONNECTION_PROBLEM_")}`;
|
|
180
220
|
}
|
|
181
221
|
if (connection?.message) {
|
|
@@ -187,31 +227,51 @@ function connectionText(connection) {
|
|
|
187
227
|
async function probe(url, signal) {
|
|
188
228
|
try {
|
|
189
229
|
const info = await open(parseURL(url)).system.getInfo({}, { signal });
|
|
190
|
-
return [
|
|
230
|
+
return [
|
|
231
|
+
`${info.appVersion} (${info.appBuild})`,
|
|
232
|
+
connectionText(info.deviceConnection),
|
|
233
|
+
];
|
|
191
234
|
} catch (error) {
|
|
192
|
-
return [
|
|
235
|
+
return [
|
|
236
|
+
error instanceof RpcError && error.code === Code.Unauthenticated
|
|
237
|
+
? "requires a token"
|
|
238
|
+
: "offline",
|
|
239
|
+
];
|
|
193
240
|
}
|
|
194
241
|
}
|
|
195
242
|
|
|
196
243
|
export async function list({ signal }) {
|
|
197
244
|
const stored = load();
|
|
198
245
|
const deadline = AbortSignal.any([signal, AbortSignal.timeout(browseLimit)]);
|
|
199
|
-
const probes = new Map(
|
|
246
|
+
const probes = new Map(
|
|
247
|
+
Object.values(stored).map((url) => [url, probe(url, deadline)]),
|
|
248
|
+
);
|
|
200
249
|
await discover(signal, stored);
|
|
201
250
|
const names = Object.keys(stored).sort((a, b) => a.localeCompare(b));
|
|
202
251
|
if (names.length === 0) {
|
|
203
|
-
console.error(
|
|
252
|
+
console.error(
|
|
253
|
+
'no device publishes Swipt on the local network; turn on Swipt › Developer › Local Network, or connect one with "swipt device add URL"',
|
|
254
|
+
);
|
|
204
255
|
return;
|
|
205
256
|
}
|
|
206
257
|
const late = AbortSignal.any([signal, AbortSignal.timeout(browseLimit)]);
|
|
207
|
-
const rows = await Promise.all(
|
|
258
|
+
const rows = await Promise.all(
|
|
259
|
+
names.map(async (name) => [
|
|
260
|
+
name,
|
|
261
|
+
parseURL(stored[name]).url,
|
|
262
|
+
...(await (probes.get(stored[name]) ?? probe(stored[name], late))),
|
|
263
|
+
]),
|
|
264
|
+
);
|
|
208
265
|
console.log(columns(rows));
|
|
209
266
|
}
|
|
210
267
|
|
|
211
268
|
export async function add(context) {
|
|
212
269
|
const device = parseURL(context.args[0]);
|
|
213
270
|
context.target.url = device.url;
|
|
214
|
-
const info = await open(device).system.getInfo(
|
|
271
|
+
const info = await open(device).system.getInfo(
|
|
272
|
+
{},
|
|
273
|
+
{ signal: context.signal },
|
|
274
|
+
);
|
|
215
275
|
const name = label(info.deviceName) || new URL(device.url).hostname;
|
|
216
276
|
const stored = load();
|
|
217
277
|
const previous = storedName(stored, name);
|