@swipt/cli 0.1.0-nightly.47 → 0.1.0-nightly.49
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 +103 -40
- package/output.mjs +61 -0
- package/package.json +6 -4
- package/sideload/trust.js +188 -174
- package/sideload.mjs +597 -183
- package/swipt.mjs +20 -10
package/sideload.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync, spawn
|
|
1
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import { X509Certificate, createHash } from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
@@ -7,12 +7,23 @@ import path from "node:path";
|
|
|
7
7
|
import readline from "node:readline";
|
|
8
8
|
import { Readable } from "node:stream";
|
|
9
9
|
import { pipeline } from "node:stream/promises";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
|
|
12
|
+
import password from "@inquirer/password";
|
|
13
|
+
import select from "@inquirer/select";
|
|
10
14
|
|
|
11
15
|
import { UsageError, open } from "./client.mjs";
|
|
12
|
-
import {
|
|
13
|
-
|
|
16
|
+
import {
|
|
17
|
+
apps,
|
|
18
|
+
coreDevice,
|
|
19
|
+
devicectl,
|
|
20
|
+
provisioningProfiles,
|
|
21
|
+
} from "./coredevice.mjs";
|
|
22
|
+
import { choose, interactive, printScriptError, step } from "./output.mjs";
|
|
14
23
|
import { LogLevel } from "./proto/common_pb.js";
|
|
15
24
|
|
|
25
|
+
const exec = promisify(execFile);
|
|
26
|
+
|
|
16
27
|
const bundleId = "io.nekohasekai.swipt";
|
|
17
28
|
const repository = "swipt-automation/swipt";
|
|
18
29
|
const teamPattern = /^[A-Z0-9]{10}$/;
|
|
@@ -21,6 +32,10 @@ const sideloadedPattern = new RegExp(
|
|
|
21
32
|
);
|
|
22
33
|
|
|
23
34
|
const accountsHint = "Xcode › Settings › Accounts";
|
|
35
|
+
const xcodeProfiles = "Library/Developer/Xcode/UserData/Provisioning Profiles";
|
|
36
|
+
|
|
37
|
+
const expiresSoon = ({ creation, expiration }) =>
|
|
38
|
+
expiration - Date.now() < (expiration - creation) / 2;
|
|
24
39
|
|
|
25
40
|
const passcodeTypes = ["pin", "numeric", "alphanumeric"];
|
|
26
41
|
|
|
@@ -71,22 +86,33 @@ async function github(url, accept, signal) {
|
|
|
71
86
|
}
|
|
72
87
|
if (!response.ok) {
|
|
73
88
|
let message = `GitHub answered ${response.status} for ${url}`;
|
|
74
|
-
if ([401, 403,
|
|
89
|
+
if ([401, 403, 429].includes(response.status)) {
|
|
75
90
|
message += `; ${process.env.GITHUB_TOKEN ? "GITHUB_TOKEN" : "set GITHUB_TOKEN to a token that"} must read the repository and stay under the rate limit`;
|
|
76
91
|
}
|
|
77
|
-
|
|
92
|
+
const error = new Error(message);
|
|
93
|
+
error.status = response.status;
|
|
94
|
+
throw error;
|
|
78
95
|
}
|
|
79
96
|
return response;
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
async function findRelease(nightly, signal) {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
const read = async (name) =>
|
|
101
|
+
(
|
|
102
|
+
await github(
|
|
103
|
+
`https://api.github.com/repos/${repository}/releases/${name}`,
|
|
104
|
+
"application/vnd.github+json",
|
|
105
|
+
signal,
|
|
106
|
+
)
|
|
107
|
+
).json();
|
|
108
|
+
const release = nightly
|
|
109
|
+
? await read("tags/nightly")
|
|
110
|
+
: await read("latest").catch((error) => {
|
|
111
|
+
if (error.status !== 404) {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
return read("tags/nightly");
|
|
115
|
+
});
|
|
90
116
|
const asset = (name) => {
|
|
91
117
|
const found = release.assets.find((it) => it.name === name);
|
|
92
118
|
if (!found) {
|
|
@@ -105,15 +131,31 @@ async function findRelease(nightly, signal) {
|
|
|
105
131
|
};
|
|
106
132
|
}
|
|
107
133
|
|
|
108
|
-
async function download(url, file, signal) {
|
|
134
|
+
async function download(url, file, signal, update) {
|
|
109
135
|
const response = await github(url, "application/octet-stream", signal);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
const total = Number(response.headers.get("content-length"));
|
|
137
|
+
const megabytes = (bytes) => (bytes / 1_000_000).toFixed(1);
|
|
138
|
+
let received = 0;
|
|
139
|
+
await pipeline(
|
|
140
|
+
Readable.fromWeb(response.body),
|
|
141
|
+
async function* (source) {
|
|
142
|
+
for await (const chunk of source) {
|
|
143
|
+
received += chunk.length;
|
|
144
|
+
update(
|
|
145
|
+
total > 0
|
|
146
|
+
? `${megabytes(received)} / ${megabytes(total)} MB ${Math.floor((received / total) * 100)}%`
|
|
147
|
+
: `${megabytes(received)} MB`,
|
|
148
|
+
);
|
|
149
|
+
yield chunk;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
fs.createWriteStream(file),
|
|
153
|
+
{ signal },
|
|
154
|
+
);
|
|
113
155
|
}
|
|
114
156
|
|
|
115
|
-
function unpack(ipa, directory) {
|
|
116
|
-
|
|
157
|
+
async function unpack(ipa, directory) {
|
|
158
|
+
await exec("unzip", ["-q", ipa, "-d", directory]);
|
|
117
159
|
const payload = path.join(directory, "Payload");
|
|
118
160
|
const app = fs.readdirSync(payload).find((name) => name.endsWith(".app"));
|
|
119
161
|
if (app === undefined) {
|
|
@@ -155,6 +197,7 @@ const pbxproj = (id, team) => `// !$*UTF8*$!
|
|
|
155
197
|
CODE_SIGN_STYLE = Automatic;
|
|
156
198
|
DEVELOPMENT_TEAM = "${team}";
|
|
157
199
|
GENERATE_INFOPLIST_FILE = YES;
|
|
200
|
+
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
|
158
201
|
PRODUCT_BUNDLE_IDENTIFIER = "${id}";
|
|
159
202
|
PRODUCT_NAME = Stub;
|
|
160
203
|
SDKROOT = iphoneos;
|
|
@@ -199,9 +242,20 @@ function explain(output, team) {
|
|
|
199
242
|
return errors || output.trim();
|
|
200
243
|
}
|
|
201
244
|
|
|
202
|
-
function
|
|
245
|
+
function profileField(file, key) {
|
|
246
|
+
return execFileSync("plutil", ["-extract", key, "raw", "-o", "-", "-"], {
|
|
247
|
+
input: execFileSync("security", ["cms", "-D", "-i", file], {
|
|
248
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
249
|
+
}),
|
|
250
|
+
encoding: "utf8",
|
|
251
|
+
}).trim();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function provision(work, id, team, device, update) {
|
|
203
255
|
const stub = path.join(work, "Stub");
|
|
204
256
|
const products = path.join(stub, "Products");
|
|
257
|
+
const app = path.join(products, "Stub.app");
|
|
258
|
+
const profile = path.join(app, "embedded.mobileprovision");
|
|
205
259
|
fs.mkdirSync(path.join(stub, "Stub.xcodeproj"), { recursive: true });
|
|
206
260
|
fs.mkdirSync(path.join(stub, "Stub"));
|
|
207
261
|
fs.writeFileSync(
|
|
@@ -212,34 +266,65 @@ function provision(work, id, team, device) {
|
|
|
212
266
|
path.join(stub, "Stub.xcodeproj/project.pbxproj"),
|
|
213
267
|
pbxproj(id, team),
|
|
214
268
|
);
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
269
|
+
const build = async () => {
|
|
270
|
+
const running = exec(
|
|
271
|
+
"xcodebuild",
|
|
272
|
+
[
|
|
273
|
+
"build",
|
|
274
|
+
"-project",
|
|
275
|
+
path.join(stub, "Stub.xcodeproj"),
|
|
276
|
+
"-scheme",
|
|
277
|
+
"Stub",
|
|
278
|
+
"-configuration",
|
|
279
|
+
"Release",
|
|
280
|
+
"-destination",
|
|
281
|
+
`id=${device.udid}`,
|
|
282
|
+
`CONFIGURATION_BUILD_DIR=${products}`,
|
|
283
|
+
`OBJROOT=${path.join(stub, "Objects")}`,
|
|
284
|
+
"-allowProvisioningUpdates",
|
|
285
|
+
"-allowProvisioningDeviceRegistration",
|
|
286
|
+
],
|
|
287
|
+
{ maxBuffer: Infinity },
|
|
288
|
+
);
|
|
289
|
+
running.child.stdout.on("data", (chunk) => {
|
|
290
|
+
const line = String(chunk)
|
|
291
|
+
.split("\n")
|
|
292
|
+
.findLast((it) => /^[A-Z]\w* /.test(it));
|
|
293
|
+
if (line) {
|
|
294
|
+
update(line.split(" ")[0]);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
try {
|
|
298
|
+
await running;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
if (error.code === "ENOENT") {
|
|
301
|
+
throw new Error("xcodebuild is missing; install Xcode");
|
|
302
|
+
}
|
|
303
|
+
if (typeof error.code !== "number") {
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
throw new Error(explain(`${error.stdout}${error.stderr}`, team));
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
await build();
|
|
310
|
+
if (
|
|
311
|
+
expiresSoon({
|
|
312
|
+
creation: Date.parse(profileField(profile, "CreationDate")),
|
|
313
|
+
expiration: Date.parse(profileField(profile, "ExpirationDate")),
|
|
314
|
+
})
|
|
315
|
+
) {
|
|
316
|
+
// Xcode keeps a valid cached profile and creates a new one once it is removed.
|
|
317
|
+
fs.rmSync(
|
|
318
|
+
path.join(
|
|
319
|
+
os.homedir(),
|
|
320
|
+
xcodeProfiles,
|
|
321
|
+
`${profileField(profile, "UUID")}.mobileprovision`,
|
|
322
|
+
),
|
|
323
|
+
{ force: true },
|
|
324
|
+
);
|
|
325
|
+
update("renewing the profile");
|
|
326
|
+
await build();
|
|
241
327
|
}
|
|
242
|
-
const app = path.join(products, "Stub.app");
|
|
243
328
|
const certificates = path.join(work, "certificate");
|
|
244
329
|
const stdio = ["ignore", "pipe", "ignore"];
|
|
245
330
|
execFileSync(
|
|
@@ -260,11 +345,11 @@ function provision(work, id, team, device) {
|
|
|
260
345
|
.digest("hex")
|
|
261
346
|
.toUpperCase(),
|
|
262
347
|
entitlements,
|
|
263
|
-
profile
|
|
348
|
+
profile,
|
|
264
349
|
};
|
|
265
350
|
}
|
|
266
351
|
|
|
267
|
-
function resign(app, id, signing) {
|
|
352
|
+
async function resign(app, id, signing, update) {
|
|
268
353
|
const info = path.join(app, "Info.plist");
|
|
269
354
|
execFileSync("plutil", [
|
|
270
355
|
"-replace",
|
|
@@ -306,10 +391,12 @@ function resign(app, id, signing) {
|
|
|
306
391
|
const frameworks = path.join(app, "Frameworks");
|
|
307
392
|
if (fs.existsSync(frameworks)) {
|
|
308
393
|
for (const name of fs.readdirSync(frameworks)) {
|
|
309
|
-
|
|
394
|
+
update(name);
|
|
395
|
+
await exec("codesign", [...codesign, path.join(frameworks, name)]);
|
|
310
396
|
}
|
|
311
397
|
}
|
|
312
|
-
|
|
398
|
+
update(path.basename(app));
|
|
399
|
+
await exec("codesign", [
|
|
313
400
|
...codesign,
|
|
314
401
|
"--entitlements",
|
|
315
402
|
signing.entitlements,
|
|
@@ -350,13 +437,81 @@ function xcodeTeams() {
|
|
|
350
437
|
return [...teams].map(([id, name]) => ({ id, name }));
|
|
351
438
|
}
|
|
352
439
|
|
|
440
|
+
function xcodeSignedIn() {
|
|
441
|
+
try {
|
|
442
|
+
const lists = JSON.parse(
|
|
443
|
+
execFileSync(
|
|
444
|
+
"plutil",
|
|
445
|
+
[
|
|
446
|
+
"-extract",
|
|
447
|
+
"DVTDeveloperAccountManagerAppleIDLists",
|
|
448
|
+
"json",
|
|
449
|
+
"-o",
|
|
450
|
+
"-",
|
|
451
|
+
"-",
|
|
452
|
+
],
|
|
453
|
+
{
|
|
454
|
+
input: execFileSync("defaults", [
|
|
455
|
+
"export",
|
|
456
|
+
"com.apple.dt.Xcode",
|
|
457
|
+
"-",
|
|
458
|
+
]),
|
|
459
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
460
|
+
encoding: "utf8",
|
|
461
|
+
},
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
return Object.values(lists).some((accounts) => accounts.length > 0);
|
|
465
|
+
} catch {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
353
470
|
const teamList = (teams) =>
|
|
354
471
|
teams
|
|
355
472
|
.map((team) => ` ${team.id}${team.name ? ` ${team.name}` : ""}`)
|
|
356
473
|
.join("\n");
|
|
357
474
|
|
|
358
|
-
|
|
359
|
-
|
|
475
|
+
const teamChoice = ({ id, name }) => ({
|
|
476
|
+
name: name ? `${name} ${id}` : id,
|
|
477
|
+
short: name ?? id,
|
|
478
|
+
value: id,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const installHint = '; install it with "swipt sideload install"';
|
|
482
|
+
|
|
483
|
+
async function sideloadedDevices(devices, hint = "") {
|
|
484
|
+
const sideloaded = await step(
|
|
485
|
+
"looking for Swipt on the devices",
|
|
486
|
+
() =>
|
|
487
|
+
Promise.all(
|
|
488
|
+
devices.map((device) =>
|
|
489
|
+
apps(device.udid).then(
|
|
490
|
+
(listed) =>
|
|
491
|
+
listed.some((app) =>
|
|
492
|
+
sideloadedPattern.test(app.bundleIdentifier),
|
|
493
|
+
),
|
|
494
|
+
() => true,
|
|
495
|
+
),
|
|
496
|
+
),
|
|
497
|
+
),
|
|
498
|
+
{ transient: true },
|
|
499
|
+
);
|
|
500
|
+
const found = devices.filter((_, index) => sideloaded[index]);
|
|
501
|
+
if (found.length === 0) {
|
|
502
|
+
throw new Error(`Swipt is not sideloaded on any online iOS device${hint}`);
|
|
503
|
+
}
|
|
504
|
+
return found;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function locate(context, device, fresh = false) {
|
|
508
|
+
const { options } = context;
|
|
509
|
+
const listed = await step(
|
|
510
|
+
`reading apps on ${device.name}`,
|
|
511
|
+
() => apps(device.udid),
|
|
512
|
+
{ transient: true },
|
|
513
|
+
);
|
|
514
|
+
const found = listed
|
|
360
515
|
.map((app) => ({
|
|
361
516
|
...app,
|
|
362
517
|
team: sideloadedPattern.exec(app.bundleIdentifier)?.[1],
|
|
@@ -374,98 +529,252 @@ function locate(options, device, fresh = false) {
|
|
|
374
529
|
};
|
|
375
530
|
}
|
|
376
531
|
if (found.length > 1) {
|
|
377
|
-
|
|
378
|
-
|
|
532
|
+
const known = xcodeTeams();
|
|
533
|
+
const teams = found.map((app) => ({
|
|
534
|
+
id: app.team,
|
|
535
|
+
name: known.find((team) => team.id === app.team)?.name,
|
|
536
|
+
}));
|
|
537
|
+
const team = await choose(
|
|
538
|
+
context,
|
|
539
|
+
"Choose a team",
|
|
540
|
+
teams.map(teamChoice),
|
|
541
|
+
new Error(
|
|
542
|
+
`Swipt is sideloaded on ${device.name} by more than one team; choose one with --team ID:\n${teamList(teams)}`,
|
|
543
|
+
),
|
|
379
544
|
);
|
|
545
|
+
return { team, current: found.find((app) => app.team === team) };
|
|
380
546
|
}
|
|
381
547
|
if (found.length === 0 && fresh) {
|
|
382
548
|
const teams = xcodeTeams();
|
|
383
|
-
if (teams.length
|
|
549
|
+
if (teams.length === 0) {
|
|
384
550
|
throw new Error(
|
|
385
|
-
|
|
386
|
-
? `Xcode has no development certificate;
|
|
387
|
-
: `choose a team with --team ID
|
|
551
|
+
xcodeSignedIn()
|
|
552
|
+
? `Xcode has no development certificate; add one at ${accountsHint} › Manage Certificates, or choose a team with --team ID`
|
|
553
|
+
: `Xcode has no account; sign in at ${accountsHint}, or choose a team with --team ID`,
|
|
388
554
|
);
|
|
389
555
|
}
|
|
390
|
-
|
|
556
|
+
const team =
|
|
557
|
+
teams.length === 1
|
|
558
|
+
? teams[0].id
|
|
559
|
+
: await choose(
|
|
560
|
+
context,
|
|
561
|
+
"Choose a team",
|
|
562
|
+
teams.map(teamChoice),
|
|
563
|
+
new Error(`choose a team with --team ID:\n${teamList(teams)}`),
|
|
564
|
+
);
|
|
565
|
+
return { team, current: undefined };
|
|
391
566
|
}
|
|
392
567
|
return { team: found[0]?.team, current: found[0] };
|
|
393
568
|
}
|
|
394
569
|
|
|
395
570
|
const buildText = (app) => `Swipt ${app.version} (${app.bundleVersion})`;
|
|
396
571
|
|
|
572
|
+
async function signingPeriod(device, id, team) {
|
|
573
|
+
const names = [id, "*"].map((it) => `iOS Team Provisioning Profile: ${it}`);
|
|
574
|
+
const profiles = await step(
|
|
575
|
+
`reading the signing on ${device.name}`,
|
|
576
|
+
() => provisioningProfiles(device.udid),
|
|
577
|
+
{ transient: true },
|
|
578
|
+
);
|
|
579
|
+
return profiles
|
|
580
|
+
.filter(
|
|
581
|
+
(profile) =>
|
|
582
|
+
profile.teamIdentifier === team && names.includes(profile.name),
|
|
583
|
+
)
|
|
584
|
+
.map((profile) => ({
|
|
585
|
+
creation: Date.parse(profile.creationDate),
|
|
586
|
+
expiration: Date.parse(profile.expirationDate),
|
|
587
|
+
}))
|
|
588
|
+
.reduce((latest, it) => (it.expiration > latest.expiration ? it : latest), {
|
|
589
|
+
creation: 0,
|
|
590
|
+
expiration: 0,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const dateText = (time) => new Date(time).toLocaleString();
|
|
595
|
+
|
|
596
|
+
async function askPasscode(context) {
|
|
597
|
+
const type = await select(
|
|
598
|
+
{
|
|
599
|
+
message:
|
|
600
|
+
"Set up auto-unlock? Swipt can then unlock this device by itself",
|
|
601
|
+
choices: [
|
|
602
|
+
{ name: "4 or 6 digit passcode", short: "4 or 6 digits", value: "pin" },
|
|
603
|
+
{ name: "Custom numeric passcode", short: "numeric", value: "numeric" },
|
|
604
|
+
{
|
|
605
|
+
name: "Custom alphanumeric passcode",
|
|
606
|
+
short: "alphanumeric",
|
|
607
|
+
value: "alphanumeric",
|
|
608
|
+
},
|
|
609
|
+
{ name: "No Thanks", value: null },
|
|
610
|
+
],
|
|
611
|
+
},
|
|
612
|
+
{ output: process.stderr, signal: context.signal },
|
|
613
|
+
);
|
|
614
|
+
if (type === null) {
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
const options = (passcode) => ({ passcode, "passcode-type": type });
|
|
618
|
+
return passcodeOf(
|
|
619
|
+
options(
|
|
620
|
+
await password(
|
|
621
|
+
{
|
|
622
|
+
message: "Device passcode",
|
|
623
|
+
validate(passcode) {
|
|
624
|
+
try {
|
|
625
|
+
passcodeOf(options(passcode));
|
|
626
|
+
return passcode !== "" || "enter the passcode";
|
|
627
|
+
} catch (error) {
|
|
628
|
+
return error.message;
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
{ output: process.stderr, signal: context.signal },
|
|
633
|
+
),
|
|
634
|
+
),
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
397
638
|
async function deploy(context, onlyNewer) {
|
|
398
|
-
const device = coreDevice(
|
|
399
|
-
|
|
639
|
+
const device = await coreDevice(
|
|
640
|
+
context,
|
|
641
|
+
onlyNewer
|
|
642
|
+
? { filter: (devices) => sideloadedDevices(devices, installHint) }
|
|
643
|
+
: undefined,
|
|
644
|
+
);
|
|
645
|
+
const { team, current } = await locate(context, device, !onlyNewer);
|
|
400
646
|
if (current === undefined && onlyNewer) {
|
|
401
647
|
throw new Error(
|
|
402
|
-
`Swipt is not sideloaded on ${device.name}${team === undefined ? "" : ` by team ${team}`}
|
|
648
|
+
`Swipt is not sideloaded on ${device.name}${team === undefined ? "" : ` by team ${team}`}${installHint}`,
|
|
403
649
|
);
|
|
404
650
|
}
|
|
405
651
|
const passcode =
|
|
406
|
-
current === undefined
|
|
407
|
-
|
|
408
|
-
|
|
652
|
+
current === undefined &&
|
|
653
|
+
interactive(context) &&
|
|
654
|
+
context.options.passcode === undefined &&
|
|
655
|
+
context.options["passcode-type"] === undefined &&
|
|
656
|
+
!context.options["no-setup"]
|
|
657
|
+
? await askPasscode(context)
|
|
658
|
+
: passcodeOf(context.options);
|
|
659
|
+
const release = await step(
|
|
660
|
+
"checking the latest release",
|
|
661
|
+
() => findRelease(context.options.nightly, context.signal),
|
|
662
|
+
{ transient: true },
|
|
663
|
+
);
|
|
664
|
+
const id = `${bundleId}.${team}`;
|
|
665
|
+
if (onlyNewer) {
|
|
409
666
|
const installed = Number(current.bundleVersion);
|
|
410
|
-
|
|
667
|
+
const signing = await signingPeriod(device, id, team);
|
|
668
|
+
if (installed >= release.build && !expiresSoon(signing)) {
|
|
411
669
|
console.log(
|
|
412
|
-
`${buildText(current)} on ${device.name} is already ${release.tag}`,
|
|
670
|
+
`${buildText(current)} on ${device.name} is ${installed === release.build ? `already ${release.tag}` : `newer than ${release.tag} (${release.build})`}; its signing expires ${dateText(signing.expiration)}`,
|
|
413
671
|
);
|
|
672
|
+
if (
|
|
673
|
+
!context.options["no-setup"] &&
|
|
674
|
+
!context.options["no-setup-pair"] &&
|
|
675
|
+
(await pair(device, id))
|
|
676
|
+
) {
|
|
677
|
+
await openConfigured(context, device, id, passcode);
|
|
678
|
+
}
|
|
414
679
|
return;
|
|
415
680
|
}
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
`${buildText(current)} on ${device.name}
|
|
681
|
+
if (installed > release.build) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`${buildText(current)} on ${device.name} needs new signing, but ${release.tag} (${release.build}) is older; use --nightly, or downgrade with "swipt sideload install"`,
|
|
419
684
|
);
|
|
420
|
-
return;
|
|
421
685
|
}
|
|
422
686
|
}
|
|
423
|
-
const id = `${bundleId}.${team}`;
|
|
424
687
|
const work = fs.mkdtempSync(path.join(os.tmpdir(), "swipt-sideload-"));
|
|
425
688
|
try {
|
|
426
|
-
|
|
427
|
-
`
|
|
689
|
+
const signing = await step(
|
|
690
|
+
`signing ${id} with team ${team} through Xcode`,
|
|
691
|
+
(update) => provision(work, id, team, device, update),
|
|
428
692
|
);
|
|
429
693
|
const ipa = path.join(work, "Swipt.ipa");
|
|
430
|
-
await
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
694
|
+
await step(
|
|
695
|
+
`downloading Swipt ${release.version} (${release.build}) from ${release.tag}`,
|
|
696
|
+
(update) => download(release.ipa, ipa, context.signal, update),
|
|
697
|
+
);
|
|
698
|
+
const app = await step("re-signing Swipt", async (update) => {
|
|
699
|
+
const unpacked = await unpack(ipa, work);
|
|
700
|
+
await resign(unpacked, id, signing, update);
|
|
701
|
+
return unpacked;
|
|
702
|
+
});
|
|
703
|
+
await step(`installing on ${device.name}`, () =>
|
|
704
|
+
devicectl(["device", "install", "app", "--device", device.udid, app]),
|
|
705
|
+
);
|
|
706
|
+
if (current === undefined && passcode !== undefined) {
|
|
707
|
+
await step("setting up auto-unlock", () =>
|
|
708
|
+
setUpAutoUnlock(device, id, work, passcode),
|
|
709
|
+
);
|
|
442
710
|
}
|
|
443
711
|
} finally {
|
|
444
712
|
fs.rmSync(work, { recursive: true, force: true });
|
|
445
713
|
}
|
|
446
|
-
const result = locate({ team }, device).current;
|
|
714
|
+
const result = (await locate({ options: { team } }, device)).current;
|
|
447
715
|
if (result === undefined || Number(result.bundleVersion) !== release.build) {
|
|
448
716
|
throw new Error(
|
|
449
717
|
`${device.name} ${result === undefined ? `does not list ${id}` : `has ${buildText(result)}`} after the install`,
|
|
450
718
|
);
|
|
451
719
|
}
|
|
720
|
+
const action =
|
|
721
|
+
current === undefined
|
|
722
|
+
? "installed"
|
|
723
|
+
: Number(current.bundleVersion) === release.build
|
|
724
|
+
? "re-signed"
|
|
725
|
+
: "updated";
|
|
452
726
|
console.log(
|
|
453
|
-
`${buildText(result)} is ${
|
|
727
|
+
`${buildText(result)} is ${action} on ${device.name}; its signing expires ${dateText((await signingPeriod(device, id, team)).expiration)}`,
|
|
454
728
|
);
|
|
729
|
+
if (context.options["no-setup"]) {
|
|
730
|
+
await launch(device, id);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (!context.options["no-setup-pair"]) {
|
|
734
|
+
await pair(device, id);
|
|
735
|
+
}
|
|
736
|
+
await openConfigured(context, device, id, passcode);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const launchCommand = (device, id, args = []) => [
|
|
740
|
+
"device",
|
|
741
|
+
"process",
|
|
742
|
+
"launch",
|
|
743
|
+
"--terminate-existing",
|
|
744
|
+
"--device",
|
|
745
|
+
device.udid,
|
|
746
|
+
id,
|
|
747
|
+
...(args.length > 0 ? ["--", ...args] : []),
|
|
748
|
+
];
|
|
749
|
+
|
|
750
|
+
const publishArgument = "--publish-local-network";
|
|
751
|
+
const setupArgument = "--setup";
|
|
752
|
+
|
|
753
|
+
async function openConfigured(context, device, id, passcode) {
|
|
455
754
|
try {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
755
|
+
await trustAndOpen(context, device, id, passcode);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (error.message !== serverHint) {
|
|
758
|
+
throw error;
|
|
759
|
+
}
|
|
760
|
+
console.error(`${serverHint}; its developer is not trusted automatically`);
|
|
761
|
+
await launch(
|
|
762
|
+
device,
|
|
463
763
|
id,
|
|
464
|
-
|
|
764
|
+
context.options["no-setup-api"] ? [] : [publishArgument],
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function launch(device, id, args) {
|
|
770
|
+
try {
|
|
771
|
+
await step(`opening Swipt on ${device.name}`, () =>
|
|
772
|
+
devicectl(launchCommand(device, id, args)),
|
|
773
|
+
);
|
|
465
774
|
} catch (error) {
|
|
466
775
|
if (/trusted by the user/.test(error.message)) {
|
|
467
776
|
console.log(
|
|
468
|
-
`
|
|
777
|
+
`its developer is not trusted yet; run "swipt sideload install -d ${device.udid} --team ${id.slice(bundleId.length + 1)}" to trust it and open Swipt automatically, or trust it in Settings › General › VPN & Device Management on ${device.name}`,
|
|
469
778
|
);
|
|
470
779
|
} else if (/BSErrorCodeDescription = Locked/.test(error.message)) {
|
|
471
780
|
console.log(`it opens once ${device.name} is unlocked`);
|
|
@@ -480,9 +789,14 @@ export const install = (context) => deploy(context, false);
|
|
|
480
789
|
export const update = (context) => deploy(context, true);
|
|
481
790
|
|
|
482
791
|
const serverHint =
|
|
483
|
-
|
|
792
|
+
'swipt-server is missing; install it with "npm install -g @swipt/cli --include=optional"';
|
|
484
793
|
|
|
485
|
-
function serverPath() {
|
|
794
|
+
async function serverPath() {
|
|
795
|
+
const root = path.dirname(import.meta.dirname);
|
|
796
|
+
if (fs.existsSync(path.join(root, "cmd/swipt-server"))) {
|
|
797
|
+
await exec("make", ["server"], { cwd: root });
|
|
798
|
+
return path.join(root, "build/swipt-server");
|
|
799
|
+
}
|
|
486
800
|
let packaged;
|
|
487
801
|
try {
|
|
488
802
|
packaged = path.join(
|
|
@@ -508,7 +822,7 @@ const schema = [
|
|
|
508
822
|
"INSERT INTO grdb_migrations VALUES ('0.1.0')",
|
|
509
823
|
];
|
|
510
824
|
|
|
511
|
-
function setUpAutoUnlock(device, id, work, passcode) {
|
|
825
|
+
async function setUpAutoUnlock(device, id, work, passcode) {
|
|
512
826
|
const database = path.join(work, "Swipt.sqlite");
|
|
513
827
|
const preferences = {
|
|
514
828
|
serverAutoUnlocks: true,
|
|
@@ -528,7 +842,7 @@ function setUpAutoUnlock(device, id, work, passcode) {
|
|
|
528
842
|
.map((statement) => `${statement};\n`)
|
|
529
843
|
.join(""),
|
|
530
844
|
});
|
|
531
|
-
devicectl([
|
|
845
|
+
await devicectl([
|
|
532
846
|
"device",
|
|
533
847
|
"copy",
|
|
534
848
|
"to",
|
|
@@ -545,46 +859,94 @@ function setUpAutoUnlock(device, id, work, passcode) {
|
|
|
545
859
|
]);
|
|
546
860
|
}
|
|
547
861
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
{
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
862
|
+
const pairingDirectory = "Library/Application Support";
|
|
863
|
+
const pairingFile = "PairingRecord.plist";
|
|
864
|
+
|
|
865
|
+
async function hasPairing(device, id) {
|
|
866
|
+
try {
|
|
867
|
+
const { result } = await devicectl(
|
|
868
|
+
[
|
|
869
|
+
"device",
|
|
870
|
+
"info",
|
|
871
|
+
"files",
|
|
872
|
+
"--device",
|
|
873
|
+
device.udid,
|
|
874
|
+
"--domain-type",
|
|
875
|
+
"appDataContainer",
|
|
876
|
+
"--domain-identifier",
|
|
877
|
+
id,
|
|
878
|
+
"--subdirectory",
|
|
879
|
+
pairingDirectory,
|
|
880
|
+
],
|
|
881
|
+
{ parse: true },
|
|
882
|
+
);
|
|
883
|
+
return result.files.some((file) => file.name === pairingFile);
|
|
884
|
+
} catch {
|
|
885
|
+
return false;
|
|
558
886
|
}
|
|
559
|
-
|
|
560
|
-
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function pair(device, id) {
|
|
890
|
+
const paired = await step(
|
|
891
|
+
`checking the pairing of ${device.name}`,
|
|
892
|
+
() => hasPairing(device, id),
|
|
893
|
+
{ transient: true },
|
|
894
|
+
);
|
|
895
|
+
if (paired) {
|
|
896
|
+
return false;
|
|
561
897
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
)
|
|
566
|
-
|
|
898
|
+
const work = fs.mkdtempSync(path.join(os.tmpdir(), "swipt-pairing-"));
|
|
899
|
+
let warning;
|
|
900
|
+
try {
|
|
901
|
+
await step(`pairing with ${device.name}`, async () => {
|
|
902
|
+
const record = path.join(work, pairingFile);
|
|
903
|
+
try {
|
|
904
|
+
await exec(await serverPath(), [
|
|
905
|
+
"pair",
|
|
906
|
+
"-d",
|
|
907
|
+
device.udid,
|
|
908
|
+
"-o",
|
|
909
|
+
record,
|
|
910
|
+
]);
|
|
911
|
+
} catch (error) {
|
|
912
|
+
if (typeof error.code === "number") {
|
|
913
|
+
warning = `pairing failed, so Swipt pairs in Settings instead: ${error.stderr.trim()}`;
|
|
914
|
+
} else if (error.code === "ENOENT") {
|
|
915
|
+
warning = `${serverHint}; Swipt pairs in Settings instead`;
|
|
916
|
+
}
|
|
917
|
+
throw error;
|
|
918
|
+
}
|
|
919
|
+
await devicectl([
|
|
920
|
+
"device",
|
|
921
|
+
"copy",
|
|
922
|
+
"to",
|
|
923
|
+
"--device",
|
|
924
|
+
device.udid,
|
|
925
|
+
"--source",
|
|
926
|
+
record,
|
|
927
|
+
"--destination",
|
|
928
|
+
`${pairingDirectory}/${pairingFile}`,
|
|
929
|
+
"--domain-type",
|
|
930
|
+
"appDataContainer",
|
|
931
|
+
"--domain-identifier",
|
|
932
|
+
id,
|
|
933
|
+
]);
|
|
934
|
+
});
|
|
935
|
+
} catch (error) {
|
|
936
|
+
if (warning === undefined) {
|
|
937
|
+
throw error;
|
|
938
|
+
}
|
|
939
|
+
console.error(warning);
|
|
940
|
+
return false;
|
|
941
|
+
} finally {
|
|
942
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
567
943
|
}
|
|
568
|
-
|
|
569
|
-
"device",
|
|
570
|
-
"copy",
|
|
571
|
-
"to",
|
|
572
|
-
"--device",
|
|
573
|
-
device.udid,
|
|
574
|
-
"--source",
|
|
575
|
-
record,
|
|
576
|
-
"--destination",
|
|
577
|
-
"Library/Application Support/PairingRecord.plist",
|
|
578
|
-
"--domain-type",
|
|
579
|
-
"appDataContainer",
|
|
580
|
-
"--domain-identifier",
|
|
581
|
-
id,
|
|
582
|
-
]);
|
|
944
|
+
return true;
|
|
583
945
|
}
|
|
584
946
|
|
|
585
|
-
function serve(udid, passcode, signal) {
|
|
947
|
+
async function serve(udid, passcode, signal) {
|
|
586
948
|
const server = spawn(
|
|
587
|
-
serverPath(),
|
|
949
|
+
await serverPath(),
|
|
588
950
|
[
|
|
589
951
|
"-d",
|
|
590
952
|
udid,
|
|
@@ -631,66 +993,118 @@ function serve(udid, passcode, signal) {
|
|
|
631
993
|
};
|
|
632
994
|
}
|
|
633
995
|
|
|
634
|
-
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
const { team, current } = locate(context.options, device);
|
|
638
|
-
if (current === undefined) {
|
|
639
|
-
throw new Error(
|
|
640
|
-
`Swipt is not sideloaded on ${device.name}${team === undefined ? "" : ` by team ${team}`}`,
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
const server = serve(device.udid, passcode, context.signal);
|
|
996
|
+
async function trustAndOpen(context, device, id, passcode) {
|
|
997
|
+
const lines = [];
|
|
998
|
+
let failure;
|
|
644
999
|
try {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
break;
|
|
1000
|
+
await step(`opening Swipt on ${device.name}`, async (update) => {
|
|
1001
|
+
update("starting swipt-server");
|
|
1002
|
+
const waiting = passcode === undefined && process.stderr.isTTY;
|
|
1003
|
+
let server;
|
|
1004
|
+
let url;
|
|
1005
|
+
while (url === undefined) {
|
|
1006
|
+
server = await serve(device.udid, passcode, context.signal);
|
|
1007
|
+
try {
|
|
1008
|
+
url = await server.url;
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
if (!/is connected to this Mac/.test(error.message)) {
|
|
1011
|
+
throw error;
|
|
1012
|
+
}
|
|
1013
|
+
if (!waiting) {
|
|
1014
|
+
throw new Error(
|
|
1015
|
+
`${device.name} is not reachable; unlock it or pass --passcode, then run this again`,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
update("unlock the device to continue");
|
|
665
1019
|
}
|
|
666
|
-
case "error":
|
|
667
|
-
failure = response.part.value;
|
|
668
|
-
break;
|
|
669
1020
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1021
|
+
try {
|
|
1022
|
+
const client = open({ url, token: "" });
|
|
1023
|
+
const run = async (args) => {
|
|
1024
|
+
for await (const response of client.repl.execute(
|
|
1025
|
+
{
|
|
1026
|
+
code: fs.readFileSync(
|
|
1027
|
+
new URL("sideload/trust.js", import.meta.url),
|
|
1028
|
+
"utf8",
|
|
1029
|
+
),
|
|
1030
|
+
args,
|
|
1031
|
+
program: true,
|
|
1032
|
+
},
|
|
1033
|
+
{ signal: context.signal },
|
|
1034
|
+
)) {
|
|
1035
|
+
switch (response.part.case) {
|
|
1036
|
+
case "console": {
|
|
1037
|
+
const output = response.part.value;
|
|
1038
|
+
if (output.level === LogLevel.DEBUG) {
|
|
1039
|
+
update(output.text);
|
|
1040
|
+
} else {
|
|
1041
|
+
lines.push(output);
|
|
1042
|
+
}
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
case "error":
|
|
1046
|
+
failure = response.part.value;
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (failure) {
|
|
1051
|
+
throw new Error(failure.message);
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
const runArguments = (mode) => [id, mode, waiting ? "wait" : ""];
|
|
1055
|
+
await run(runArguments("trust"));
|
|
1056
|
+
update("opening Swipt");
|
|
1057
|
+
await devicectl(
|
|
1058
|
+
launchCommand(device, id, [
|
|
1059
|
+
setupArgument,
|
|
1060
|
+
...(context.options["no-setup-api"] ? [] : [publishArgument]),
|
|
1061
|
+
]),
|
|
1062
|
+
);
|
|
1063
|
+
await run(runArguments("prompts"));
|
|
1064
|
+
} finally {
|
|
1065
|
+
await server.stop();
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
if (failure === undefined) {
|
|
1070
|
+
throw error;
|
|
673
1071
|
}
|
|
674
1072
|
} finally {
|
|
675
|
-
|
|
1073
|
+
for (const output of lines) {
|
|
1074
|
+
(output.level === LogLevel.ERROR || output.level === LogLevel.WARN
|
|
1075
|
+
? console.error
|
|
1076
|
+
: console.log)(output.text);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (failure) {
|
|
1080
|
+
if (/\bdevice is locked\b/.test(failure.message)) {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
passcode === undefined
|
|
1083
|
+
? `${device.name} is locked; unlock it or pass --passcode, then run this again`
|
|
1084
|
+
: `${device.name} did not unlock; check --passcode and --passcode-type, then run this again`,
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
throw printScriptError(failure);
|
|
676
1088
|
}
|
|
677
1089
|
}
|
|
678
1090
|
|
|
679
|
-
export function uninstall(context) {
|
|
680
|
-
const device = coreDevice(context
|
|
681
|
-
const { team, current } = locate(context
|
|
1091
|
+
export async function uninstall(context) {
|
|
1092
|
+
const device = await coreDevice(context, { filter: sideloadedDevices });
|
|
1093
|
+
const { team, current } = await locate(context, device);
|
|
682
1094
|
if (current === undefined) {
|
|
683
1095
|
throw new Error(
|
|
684
1096
|
`Swipt is not sideloaded on ${device.name}${team === undefined ? "" : ` by team ${team}`}`,
|
|
685
1097
|
);
|
|
686
1098
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1099
|
+
await step(`removing Swipt from ${device.name}`, () =>
|
|
1100
|
+
devicectl([
|
|
1101
|
+
"device",
|
|
1102
|
+
"uninstall",
|
|
1103
|
+
"app",
|
|
1104
|
+
"--device",
|
|
1105
|
+
device.udid,
|
|
1106
|
+
current.bundleIdentifier,
|
|
1107
|
+
]),
|
|
1108
|
+
);
|
|
695
1109
|
console.log(`removed ${buildText(current)} from ${device.name}`);
|
|
696
1110
|
}
|