@swipt/cli 0.1.0-nightly.43 → 0.1.0-nightly.45

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 CHANGED
@@ -33,7 +33,16 @@ export class RpcError extends Error {
33
33
  }
34
34
  }
35
35
 
36
- const httpStatusCodes = { 400: Code.Internal, 401: Code.Unauthenticated, 403: Code.Unauthenticated, 404: Code.Unimplemented, 429: Code.Unavailable, 502: Code.Unavailable, 503: Code.Unavailable, 504: Code.Unavailable };
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(Code.Unavailable, cause?.message || cause?.code || error?.message || String(error));
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.slice(separator + 1).trim();
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 = this.pending.length === 0 ? Buffer.from(chunk) : Buffer.concat([this.pending, Buffer.from(chunk)]);
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(Code.Internal, `the server sent a message of ${length} bytes`);
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((this.pending[0] & 0x80) !== 0 ? { trailers: parseMetadata(body) } : { message: fromBinary(this.schema, body) });
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(`${connection.base}/${method.parent.typeName}/${method.name}`, {
130
- method: "POST",
131
- headers: requestHeaders(connection),
132
- body: frame(method.input, input),
133
- signal,
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(httpStatusCodes[response.status] ?? Code.Unknown, `HTTP ${response.status}`);
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(`${connection.webSocketBase}/${method.parent.typeName}/${method.name}`, [webSocketSubprotocol]);
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 = () => fail(new RpcError(Code.Canceled, "the call was canceled"));
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("error", () => reject(new RpcError(Code.Unavailable, `cannot connect to ${connection.webSocketBase}`)), { once: true });
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(Code.Unavailable, event.reason || "the connection closed before the response");
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(Code.Unavailable, `cannot connect to ${connection.webSocketBase}`);
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(Buffer.from(`${Object.entries(headers).map(([key, value]) => `${key}: ${value}\r\n`).join("")}`));
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(Buffer.concat([Buffer.from([0]), frame(method.input, message)]));
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 = method.methodKind === "client_streaming" || method.methodKind === "bidi_streaming";
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)(connection, method, input, { signal: options.signal });
282
- return method.methodKind === "unary" || method.methodKind === "client_streaming" ? single(call) : call;
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 = { token, base: url, webSocketBase: url.replace(/^http/, "ws") };
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 => error instanceof RpcError && error.code === Code.Unauthenticated;
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 { DeviceConnectionProblem, DeviceConnectionProblemSchema, DeviceConnectionStateSchema } from "./proto/common_pb.js";
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(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "swipt", "devices.json");
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`, { mode: 0o600 });
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) => Object.keys(stored).find(it => sameName(it, 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 (parsed.hostname === "" || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
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 { url: `${parsed.protocol}//${parsed.host}${parsed.pathname.replace(/\/+$/, "")}`, token };
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({ socket: dgram.createSocket("udp4"), bind: false });
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, { signal: AbortSignal.any([signal, enough.signal]) });
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 => `${found.name} requires a token: swipt device add ${withToken(found.url)}`;
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 = (await discover(context.signal, stored, services => services.some(it => sameName(it.name, name)))).find(it => sameName(it.name, name));
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(`there is no device ${name}; "swipt device list" lists them`);
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('there is no device; turn on Swipt › Developer › Local Network, or connect one with "swipt device add URL"');
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(`there is more than one device: ${names.join(", ")}; choose one with -d or SWIPT_DEVICE`);
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(DeviceConnectionStateSchema, connection?.state ?? 0, "DEVICE_CONNECTION_STATE_");
178
- if (connection && connection.problem !== DeviceConnectionProblem.UNSPECIFIED) {
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 [`${info.appVersion} (${info.appBuild})`, connectionText(info.deviceConnection)];
230
+ return [
231
+ `${info.appVersion} (${info.appBuild})`,
232
+ connectionText(info.deviceConnection),
233
+ ];
191
234
  } catch (error) {
192
- return [error instanceof RpcError && error.code === Code.Unauthenticated ? "requires a token" : "offline"];
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(Object.values(stored).map(url => [url, probe(url, deadline)]));
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('no device publishes Swipt on the local network; turn on Swipt › Developer › Local Network, or connect one with "swipt device add URL"');
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(names.map(async name => [name, parseURL(stored[name]).url, ...(await (probes.get(stored[name]) ?? probe(stored[name], late)))]));
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({}, { signal: context.signal });
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);
package/coredevice.mjs ADDED
@@ -0,0 +1,68 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { label, sameName } from "./connection.mjs";
7
+
8
+ export function devicectl(args, { parse = false } = {}) {
9
+ const output = parse
10
+ ? path.join(os.tmpdir(), `devicectl-${process.pid}.json`)
11
+ : undefined;
12
+ const result = spawnSync(
13
+ "xcrun",
14
+ ["devicectl", ...args, ...(parse ? ["--json-output", output] : [])],
15
+ { encoding: "utf8" },
16
+ );
17
+ if (result.error) {
18
+ throw result.error;
19
+ }
20
+ if (result.status !== 0) {
21
+ const text = `${result.stdout}${result.stderr}`;
22
+ if (text.includes("4016")) {
23
+ throw new Error(
24
+ "CoreDevice cannot reach the device while its loopback VPN is on; turn off Swipt's VPN on the device, then run this again",
25
+ );
26
+ }
27
+ throw new Error(text.trim() || `devicectl exited with ${result.status}`);
28
+ }
29
+ if (!parse) {
30
+ return undefined;
31
+ }
32
+ const parsed = JSON.parse(fs.readFileSync(output, "utf8"));
33
+ fs.rmSync(output);
34
+ return parsed;
35
+ }
36
+
37
+ export function coreDevice(given) {
38
+ const devices = devicectl(["list", "devices"], { parse: true })
39
+ .result.devices.filter(
40
+ (device) =>
41
+ device.hardwareProperties.platform === "iOS" &&
42
+ device.hardwareProperties.reality === "physical",
43
+ )
44
+ .map((device) => ({
45
+ name: device.deviceProperties.name,
46
+ udid: device.hardwareProperties.udid,
47
+ }));
48
+ const matches = given
49
+ ? devices.filter(
50
+ (device) =>
51
+ sameName(device.udid, given) ||
52
+ sameName(label(device.name), label(given)),
53
+ )
54
+ : devices;
55
+ if (matches.length !== 1) {
56
+ const found = matches.length === 0 ? "no" : "more than one";
57
+ throw new Error(
58
+ given
59
+ ? `${found} iOS device is named ${given}`
60
+ : `${found} iOS device is paired; choose one with -d NAME|UDID`,
61
+ );
62
+ }
63
+ return matches[0];
64
+ }
65
+
66
+ export const apps = (udid) =>
67
+ devicectl(["device", "info", "apps", "--device", udid], { parse: true })
68
+ .result.apps;