@swipt/cli 0.1.0-nightly.47 → 0.1.0-nightly.48

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/coredevice.mjs CHANGED
@@ -1,30 +1,44 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { execFile } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { promisify } from "node:util";
5
6
 
6
7
  import { label, sameName } from "./connection.mjs";
8
+ import { choose, step } from "./output.mjs";
7
9
 
8
- export function devicectl(args, { parse = false } = {}) {
10
+ const exec = promisify(execFile);
11
+
12
+ const xcodeMissing =
13
+ "Xcode is missing; install it from the App Store, open it once, and select it with xcode-select";
14
+
15
+ export async function devicectl(args, { parse = false } = {}) {
9
16
  const output = parse
10
17
  ? path.join(os.tmpdir(), `devicectl-${process.pid}.json`)
11
18
  : 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}`;
19
+ try {
20
+ await exec(
21
+ "xcrun",
22
+ ["devicectl", ...args, ...(parse ? ["--json-output", output] : [])],
23
+ { maxBuffer: Infinity },
24
+ );
25
+ } catch (error) {
26
+ if (error.code === "ENOENT") {
27
+ throw new Error(xcodeMissing);
28
+ }
29
+ if (typeof error.code !== "number") {
30
+ throw error;
31
+ }
32
+ const text = `${error.stdout}${error.stderr}`;
33
+ if (/^(xcrun|xcode-select): /m.test(text)) {
34
+ throw new Error(`${xcodeMissing} (${text.trim()})`);
35
+ }
22
36
  if (text.includes("4016")) {
23
37
  throw new Error(
24
38
  "CoreDevice cannot reach the device while its loopback VPN is on; turn off Swipt's VPN on the device, then run this again",
25
39
  );
26
40
  }
27
- throw new Error(text.trim() || `devicectl exited with ${result.status}`);
41
+ throw new Error(text.trim() || `devicectl exited with ${error.code}`);
28
42
  }
29
43
  if (!parse) {
30
44
  return undefined;
@@ -34,41 +48,85 @@ export function devicectl(args, { parse = false } = {}) {
34
48
  return parsed;
35
49
  }
36
50
 
37
- export function coreDevice(given, { required = false } = {}) {
38
- const devices = devicectl(["list", "devices"], { parse: true })
39
- .result.devices.filter(
51
+ const reachability =
52
+ "unlock the device and connect it with USB or the same network as this Mac";
53
+
54
+ const deviceLine = (device) =>
55
+ ` ${device.name} ${device.udid}${device.offline ? " (offline)" : ""}`;
56
+
57
+ export async function coreDevice(context, { required = true } = {}) {
58
+ const given = context.options.device;
59
+ const listed = await step(
60
+ "looking for devices",
61
+ () => devicectl(["list", "devices"], { parse: true }),
62
+ { transient: true },
63
+ );
64
+ const paired = listed.result.devices
65
+ .filter(
40
66
  (device) =>
41
67
  device.hardwareProperties.platform === "iOS" &&
42
- device.hardwareProperties.reality === "physical",
68
+ ["physical", undefined].includes(device.hardwareProperties.reality),
43
69
  )
44
70
  .map((device) => ({
45
71
  name: device.deviceProperties.name,
46
72
  udid: device.hardwareProperties.udid,
73
+ offline: device.connectionProperties.tunnelState === "unavailable",
47
74
  }));
48
- const matches = given
49
- ? devices.filter(
50
- (device) =>
51
- sameName(device.udid, given) ||
52
- sameName(label(device.name), label(given)),
53
- )
54
- : required
55
- ? []
56
- : devices;
57
- if (matches.length !== 1) {
58
- const reason = !given
59
- ? "choose a device with -d NAME|UDID"
60
- : matches.length === 0
61
- ? `no iOS device is named ${given}`
62
- : `more than one iOS device is named ${given}; choose one by UDID`;
75
+ const named = (device) =>
76
+ sameName(device.udid, given) || sameName(label(device.name), label(given));
77
+ const devices = paired.filter((device) => !device.offline);
78
+ const matches = given ? devices.filter(named) : devices;
79
+ if ((given || !required) && matches.length === 1) {
80
+ return matches[0];
81
+ }
82
+ if (paired.length === 0) {
83
+ throw new Error(
84
+ "no iOS device is paired with this Mac; connect one with USB, unlock it, and trust this Mac",
85
+ );
86
+ }
87
+ const offline = given && paired.find(named);
88
+ if (matches.length === 0 && offline) {
89
+ throw new Error(`${offline.name} is offline; ${reachability}`);
90
+ }
91
+ if (devices.length === 0) {
63
92
  throw new Error(
64
- devices.length === 0
65
- ? `${reason}; no iOS device is paired with this Mac`
66
- : `${reason}; paired iOS devices:\n${devices.map((device) => ` ${device.name} ${device.udid}`).join("\n")}`,
93
+ `every paired iOS device is offline; ${reachability}:\n${paired.map(deviceLine).join("\n")}`,
67
94
  );
68
95
  }
69
- return matches[0];
96
+ const reason = !given
97
+ ? "choose a device with -d NAME|UDID"
98
+ : matches.length === 0
99
+ ? `no iOS device is named ${given}`
100
+ : `more than one iOS device is named ${given}; choose one by UDID`;
101
+ const failure = new Error(
102
+ `${reason}; paired iOS devices:\n${paired.map(deviceLine).join("\n")}`,
103
+ );
104
+ if (matches.length === 0) {
105
+ throw failure;
106
+ }
107
+ return choose(
108
+ context,
109
+ "Choose a device",
110
+ matches.map((device) => ({
111
+ name: `${device.name} ${device.udid}`,
112
+ short: device.name,
113
+ value: device,
114
+ })),
115
+ failure,
116
+ );
70
117
  }
71
118
 
72
- export const apps = (udid) =>
73
- devicectl(["device", "info", "apps", "--device", udid], { parse: true })
74
- .result.apps;
119
+ export const apps = async (udid) =>
120
+ (
121
+ await devicectl(["device", "info", "apps", "--device", udid], {
122
+ parse: true,
123
+ })
124
+ ).result.apps;
125
+
126
+ export const provisioningProfiles = async (udid) =>
127
+ (
128
+ await devicectl(
129
+ ["device", "profile", "list", "--type", "provisioning", "--device", udid],
130
+ { parse: true },
131
+ )
132
+ ).result.provisioningProfiles;
package/output.mjs CHANGED
@@ -1,10 +1,71 @@
1
+ import { styleText } from "node:util";
2
+
1
3
  import { timestampDate } from "@bufbuild/protobuf/wkt";
4
+ import select from "@inquirer/select";
2
5
 
3
6
  import { LogLevel, LogLevelSchema } from "./proto/common_pb.js";
4
7
  import { ServiceStatus, ServiceStatusSchema } from "./proto/swiplets_pb.js";
5
8
 
6
9
  export class FalseResult extends Error {}
7
10
 
11
+ export const interactive = (context) =>
12
+ context.options.device === undefined &&
13
+ context.options.team === undefined &&
14
+ process.stdin.isTTY &&
15
+ process.stderr.isTTY;
16
+
17
+ export function choose(context, message, choices, failure) {
18
+ if (!interactive(context)) {
19
+ throw failure;
20
+ }
21
+ return select(
22
+ { message, choices },
23
+ { output: process.stderr, signal: context.signal },
24
+ );
25
+ }
26
+
27
+ const spinner = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
28
+
29
+ export async function step(label, run, { transient = false } = {}) {
30
+ const stream = process.stderr;
31
+ if (!stream.isTTY) {
32
+ console.error(label);
33
+ return run(() => {});
34
+ }
35
+ const style = (format, text) => styleText(format, text, { stream });
36
+ const started = Date.now();
37
+ const elapsed = () => {
38
+ const seconds = Math.floor((Date.now() - started) / 1000);
39
+ return seconds > 0 ? style("dim", ` ${seconds}s`) : "";
40
+ };
41
+ const draw = (line) => stream.write(`\r\x1b[?7l\x1b[2K${line}\x1b[?7h`);
42
+ let detail = "";
43
+ let frame = 0;
44
+ const render = () => {
45
+ frame = (frame + 1) % spinner.length;
46
+ draw(
47
+ `${style("cyan", spinner[frame])} ${label}${detail && style("dim", ` ${detail}`)}${elapsed()}`,
48
+ );
49
+ };
50
+ stream.write("\x1b[?25l");
51
+ render();
52
+ const timer = setInterval(render, 80);
53
+ try {
54
+ const result = await run((text) => {
55
+ detail = text;
56
+ });
57
+ clearInterval(timer);
58
+ draw(transient ? "" : `${style("green", "✔")} ${label}${elapsed()}\n`);
59
+ return result;
60
+ } catch (error) {
61
+ clearInterval(timer);
62
+ draw(`${style("red", "✖")} ${label}${elapsed()}\n`);
63
+ throw error;
64
+ } finally {
65
+ stream.write("\x1b[?25h");
66
+ }
67
+ }
68
+
8
69
  export const enumText = (schema, value, prefix) =>
9
70
  schema.value[value].name
10
71
  .slice(prefix.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swipt/cli",
3
- "version": "0.1.0-nightly.47",
3
+ "version": "0.1.0-nightly.48",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,6 +25,8 @@
25
25
  "@babel/runtime": "^7.29.10",
26
26
  "@bufbuild/protobuf": "^2.16.0",
27
27
  "@expo/env": "^2.5.3",
28
+ "@inquirer/password": "^5.2.5",
29
+ "@inquirer/select": "^5.2.8",
28
30
  "babel-preset-expo": "~58.0.23",
29
31
  "bonjour-service": "^1.4.4",
30
32
  "esbuild": "^0.28.2",
@@ -32,11 +34,11 @@
32
34
  "react-native-worklets": "0.13.0"
33
35
  },
34
36
  "peerDependencies": {
35
- "@swipt/api": "0.1.0-nightly.47"
37
+ "@swipt/api": "0.1.0-nightly.48"
36
38
  },
37
39
  "optionalDependencies": {
38
- "@swipt/server-darwin-arm64": "0.1.0-nightly.47",
39
- "@swipt/server-darwin-x64": "0.1.0-nightly.47"
40
+ "@swipt/server-darwin-arm64": "0.1.0-nightly.48",
41
+ "@swipt/server-darwin-x64": "0.1.0-nightly.48"
40
42
  },
41
43
  "devDependencies": {
42
44
  "@bufbuild/protoc-gen-es": "^2.16.0"