@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 +97 -39
- package/output.mjs +61 -0
- package/package.json +6 -4
- package/sideload/trust.js +188 -174
- package/sideload.mjs +565 -182
- 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,55 @@ 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
|
+
async function locate(context, device, fresh = false) {
|
|
482
|
+
const { options } = context;
|
|
483
|
+
const listed = await step(
|
|
484
|
+
`reading apps on ${device.name}`,
|
|
485
|
+
() => apps(device.udid),
|
|
486
|
+
{ transient: true },
|
|
487
|
+
);
|
|
488
|
+
const found = listed
|
|
360
489
|
.map((app) => ({
|
|
361
490
|
...app,
|
|
362
491
|
team: sideloadedPattern.exec(app.bundleIdentifier)?.[1],
|
|
@@ -374,98 +503,247 @@ function locate(options, device, fresh = false) {
|
|
|
374
503
|
};
|
|
375
504
|
}
|
|
376
505
|
if (found.length > 1) {
|
|
377
|
-
|
|
378
|
-
|
|
506
|
+
const known = xcodeTeams();
|
|
507
|
+
const teams = found.map((app) => ({
|
|
508
|
+
id: app.team,
|
|
509
|
+
name: known.find((team) => team.id === app.team)?.name,
|
|
510
|
+
}));
|
|
511
|
+
const team = await choose(
|
|
512
|
+
context,
|
|
513
|
+
"Choose a team",
|
|
514
|
+
teams.map(teamChoice),
|
|
515
|
+
new Error(
|
|
516
|
+
`Swipt is sideloaded on ${device.name} by more than one team; choose one with --team ID:\n${teamList(teams)}`,
|
|
517
|
+
),
|
|
379
518
|
);
|
|
519
|
+
return { team, current: found.find((app) => app.team === team) };
|
|
380
520
|
}
|
|
381
521
|
if (found.length === 0 && fresh) {
|
|
382
522
|
const teams = xcodeTeams();
|
|
383
|
-
if (teams.length
|
|
523
|
+
if (teams.length === 0) {
|
|
384
524
|
throw new Error(
|
|
385
|
-
|
|
386
|
-
? `Xcode has no development certificate;
|
|
387
|
-
: `choose a team with --team ID
|
|
525
|
+
xcodeSignedIn()
|
|
526
|
+
? `Xcode has no development certificate; add one at ${accountsHint} › Manage Certificates, or choose a team with --team ID`
|
|
527
|
+
: `Xcode has no account; sign in at ${accountsHint}, or choose a team with --team ID`,
|
|
388
528
|
);
|
|
389
529
|
}
|
|
390
|
-
|
|
530
|
+
const team =
|
|
531
|
+
teams.length === 1
|
|
532
|
+
? teams[0].id
|
|
533
|
+
: await choose(
|
|
534
|
+
context,
|
|
535
|
+
"Choose a team",
|
|
536
|
+
teams.map(teamChoice),
|
|
537
|
+
new Error(`choose a team with --team ID:\n${teamList(teams)}`),
|
|
538
|
+
);
|
|
539
|
+
return { team, current: undefined };
|
|
391
540
|
}
|
|
392
541
|
return { team: found[0]?.team, current: found[0] };
|
|
393
542
|
}
|
|
394
543
|
|
|
395
544
|
const buildText = (app) => `Swipt ${app.version} (${app.bundleVersion})`;
|
|
396
545
|
|
|
546
|
+
async function signingPeriod(device, id, team) {
|
|
547
|
+
const names = [id, "*"].map((it) => `iOS Team Provisioning Profile: ${it}`);
|
|
548
|
+
const profiles = await step(
|
|
549
|
+
`reading the signing on ${device.name}`,
|
|
550
|
+
() => provisioningProfiles(device.udid),
|
|
551
|
+
{ transient: true },
|
|
552
|
+
);
|
|
553
|
+
return profiles
|
|
554
|
+
.filter(
|
|
555
|
+
(profile) =>
|
|
556
|
+
profile.teamIdentifier === team && names.includes(profile.name),
|
|
557
|
+
)
|
|
558
|
+
.map((profile) => ({
|
|
559
|
+
creation: Date.parse(profile.creationDate),
|
|
560
|
+
expiration: Date.parse(profile.expirationDate),
|
|
561
|
+
}))
|
|
562
|
+
.reduce((latest, it) => (it.expiration > latest.expiration ? it : latest), {
|
|
563
|
+
creation: 0,
|
|
564
|
+
expiration: 0,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const dateText = (time) => new Date(time).toLocaleString();
|
|
569
|
+
|
|
570
|
+
async function askPasscode(context) {
|
|
571
|
+
const type = await select(
|
|
572
|
+
{
|
|
573
|
+
message:
|
|
574
|
+
"Set up auto-unlock? Swipt can then unlock this device by itself",
|
|
575
|
+
choices: [
|
|
576
|
+
{ name: "4 or 6 digit passcode", short: "4 or 6 digits", value: "pin" },
|
|
577
|
+
{ name: "Custom numeric passcode", short: "numeric", value: "numeric" },
|
|
578
|
+
{
|
|
579
|
+
name: "Custom alphanumeric passcode",
|
|
580
|
+
short: "alphanumeric",
|
|
581
|
+
value: "alphanumeric",
|
|
582
|
+
},
|
|
583
|
+
{ name: "No Thanks", value: null },
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
{ output: process.stderr, signal: context.signal },
|
|
587
|
+
);
|
|
588
|
+
if (type === null) {
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
const options = (passcode) => ({ passcode, "passcode-type": type });
|
|
592
|
+
return passcodeOf(
|
|
593
|
+
options(
|
|
594
|
+
await password(
|
|
595
|
+
{
|
|
596
|
+
message: "Device passcode",
|
|
597
|
+
validate(passcode) {
|
|
598
|
+
try {
|
|
599
|
+
passcodeOf(options(passcode));
|
|
600
|
+
return passcode !== "" || "enter the passcode";
|
|
601
|
+
} catch (error) {
|
|
602
|
+
return error.message;
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
{ output: process.stderr, signal: context.signal },
|
|
607
|
+
),
|
|
608
|
+
),
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
397
612
|
async function deploy(context, onlyNewer) {
|
|
398
|
-
const device = coreDevice(context
|
|
399
|
-
const { team, current } = locate(context
|
|
613
|
+
const device = await coreDevice(context);
|
|
614
|
+
const { team, current } = await locate(context, device, !onlyNewer);
|
|
400
615
|
if (current === undefined && onlyNewer) {
|
|
401
616
|
throw new Error(
|
|
402
617
|
`Swipt is not sideloaded on ${device.name}${team === undefined ? "" : ` by team ${team}`}; install it with "swipt sideload install"`,
|
|
403
618
|
);
|
|
404
619
|
}
|
|
405
620
|
const passcode =
|
|
406
|
-
current === undefined
|
|
407
|
-
|
|
408
|
-
|
|
621
|
+
current === undefined &&
|
|
622
|
+
interactive(context) &&
|
|
623
|
+
context.options.passcode === undefined &&
|
|
624
|
+
context.options["passcode-type"] === undefined &&
|
|
625
|
+
!context.options["no-setup"]
|
|
626
|
+
? await askPasscode(context)
|
|
627
|
+
: passcodeOf(context.options);
|
|
628
|
+
const release = await step(
|
|
629
|
+
"checking the latest release",
|
|
630
|
+
() => findRelease(context.options.nightly, context.signal),
|
|
631
|
+
{ transient: true },
|
|
632
|
+
);
|
|
633
|
+
const id = `${bundleId}.${team}`;
|
|
634
|
+
if (onlyNewer) {
|
|
409
635
|
const installed = Number(current.bundleVersion);
|
|
410
|
-
|
|
636
|
+
const signing = await signingPeriod(device, id, team);
|
|
637
|
+
if (installed >= release.build && !expiresSoon(signing)) {
|
|
411
638
|
console.log(
|
|
412
|
-
`${buildText(current)} on ${device.name} is already ${release.tag}`,
|
|
639
|
+
`${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
640
|
);
|
|
641
|
+
if (
|
|
642
|
+
!context.options["no-setup"] &&
|
|
643
|
+
!context.options["no-setup-pair"] &&
|
|
644
|
+
(await pair(device, id))
|
|
645
|
+
) {
|
|
646
|
+
await openConfigured(context, device, id, passcode);
|
|
647
|
+
}
|
|
414
648
|
return;
|
|
415
649
|
}
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
`${buildText(current)} on ${device.name}
|
|
650
|
+
if (installed > release.build) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`${buildText(current)} on ${device.name} needs new signing, but ${release.tag} (${release.build}) is older; use --nightly, or downgrade with "swipt sideload install"`,
|
|
419
653
|
);
|
|
420
|
-
return;
|
|
421
654
|
}
|
|
422
655
|
}
|
|
423
|
-
const id = `${bundleId}.${team}`;
|
|
424
656
|
const work = fs.mkdtempSync(path.join(os.tmpdir(), "swipt-sideload-"));
|
|
425
657
|
try {
|
|
426
|
-
|
|
427
|
-
`
|
|
658
|
+
const signing = await step(
|
|
659
|
+
`signing ${id} with team ${team} through Xcode`,
|
|
660
|
+
(update) => provision(work, id, team, device, update),
|
|
428
661
|
);
|
|
429
662
|
const ipa = path.join(work, "Swipt.ipa");
|
|
430
|
-
await
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
663
|
+
await step(
|
|
664
|
+
`downloading Swipt ${release.version} (${release.build}) from ${release.tag}`,
|
|
665
|
+
(update) => download(release.ipa, ipa, context.signal, update),
|
|
666
|
+
);
|
|
667
|
+
const app = await step("re-signing Swipt", async (update) => {
|
|
668
|
+
const unpacked = await unpack(ipa, work);
|
|
669
|
+
await resign(unpacked, id, signing, update);
|
|
670
|
+
return unpacked;
|
|
671
|
+
});
|
|
672
|
+
await step(`installing on ${device.name}`, () =>
|
|
673
|
+
devicectl(["device", "install", "app", "--device", device.udid, app]),
|
|
674
|
+
);
|
|
675
|
+
if (current === undefined && passcode !== undefined) {
|
|
676
|
+
await step("setting up auto-unlock", () =>
|
|
677
|
+
setUpAutoUnlock(device, id, work, passcode),
|
|
678
|
+
);
|
|
442
679
|
}
|
|
443
680
|
} finally {
|
|
444
681
|
fs.rmSync(work, { recursive: true, force: true });
|
|
445
682
|
}
|
|
446
|
-
const result = locate({ team }, device).current;
|
|
683
|
+
const result = (await locate({ options: { team } }, device)).current;
|
|
447
684
|
if (result === undefined || Number(result.bundleVersion) !== release.build) {
|
|
448
685
|
throw new Error(
|
|
449
686
|
`${device.name} ${result === undefined ? `does not list ${id}` : `has ${buildText(result)}`} after the install`,
|
|
450
687
|
);
|
|
451
688
|
}
|
|
689
|
+
const action =
|
|
690
|
+
current === undefined
|
|
691
|
+
? "installed"
|
|
692
|
+
: Number(current.bundleVersion) === release.build
|
|
693
|
+
? "re-signed"
|
|
694
|
+
: "updated";
|
|
452
695
|
console.log(
|
|
453
|
-
`${buildText(result)} is ${
|
|
696
|
+
`${buildText(result)} is ${action} on ${device.name}; its signing expires ${dateText((await signingPeriod(device, id, team)).expiration)}`,
|
|
454
697
|
);
|
|
698
|
+
if (context.options["no-setup"]) {
|
|
699
|
+
await launch(device, id);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (!context.options["no-setup-pair"]) {
|
|
703
|
+
await pair(device, id);
|
|
704
|
+
}
|
|
705
|
+
await openConfigured(context, device, id, passcode);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const launchCommand = (device, id, args = []) => [
|
|
709
|
+
"device",
|
|
710
|
+
"process",
|
|
711
|
+
"launch",
|
|
712
|
+
"--terminate-existing",
|
|
713
|
+
"--device",
|
|
714
|
+
device.udid,
|
|
715
|
+
id,
|
|
716
|
+
...(args.length > 0 ? ["--", ...args] : []),
|
|
717
|
+
];
|
|
718
|
+
|
|
719
|
+
const publishArgument = "--publish-local-network";
|
|
720
|
+
const setupArgument = "--setup";
|
|
721
|
+
|
|
722
|
+
async function openConfigured(context, device, id, passcode) {
|
|
455
723
|
try {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
724
|
+
await trustAndOpen(context, device, id, passcode);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
if (error.message !== serverHint) {
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
console.error(`${serverHint}; its developer is not trusted automatically`);
|
|
730
|
+
await launch(
|
|
731
|
+
device,
|
|
463
732
|
id,
|
|
464
|
-
|
|
733
|
+
context.options["no-setup-api"] ? [] : [publishArgument],
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function launch(device, id, args) {
|
|
739
|
+
try {
|
|
740
|
+
await step(`opening Swipt on ${device.name}`, () =>
|
|
741
|
+
devicectl(launchCommand(device, id, args)),
|
|
742
|
+
);
|
|
465
743
|
} catch (error) {
|
|
466
744
|
if (/trusted by the user/.test(error.message)) {
|
|
467
745
|
console.log(
|
|
468
|
-
`
|
|
746
|
+
`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
747
|
);
|
|
470
748
|
} else if (/BSErrorCodeDescription = Locked/.test(error.message)) {
|
|
471
749
|
console.log(`it opens once ${device.name} is unlocked`);
|
|
@@ -480,9 +758,14 @@ export const install = (context) => deploy(context, false);
|
|
|
480
758
|
export const update = (context) => deploy(context, true);
|
|
481
759
|
|
|
482
760
|
const serverHint =
|
|
483
|
-
|
|
761
|
+
'swipt-server is missing; install it with "npm install -g @swipt/cli --include=optional"';
|
|
484
762
|
|
|
485
|
-
function serverPath() {
|
|
763
|
+
async function serverPath() {
|
|
764
|
+
const root = path.dirname(import.meta.dirname);
|
|
765
|
+
if (fs.existsSync(path.join(root, "cmd/swipt-server"))) {
|
|
766
|
+
await exec("make", ["server"], { cwd: root });
|
|
767
|
+
return path.join(root, "build/swipt-server");
|
|
768
|
+
}
|
|
486
769
|
let packaged;
|
|
487
770
|
try {
|
|
488
771
|
packaged = path.join(
|
|
@@ -508,7 +791,7 @@ const schema = [
|
|
|
508
791
|
"INSERT INTO grdb_migrations VALUES ('0.1.0')",
|
|
509
792
|
];
|
|
510
793
|
|
|
511
|
-
function setUpAutoUnlock(device, id, work, passcode) {
|
|
794
|
+
async function setUpAutoUnlock(device, id, work, passcode) {
|
|
512
795
|
const database = path.join(work, "Swipt.sqlite");
|
|
513
796
|
const preferences = {
|
|
514
797
|
serverAutoUnlocks: true,
|
|
@@ -528,7 +811,7 @@ function setUpAutoUnlock(device, id, work, passcode) {
|
|
|
528
811
|
.map((statement) => `${statement};\n`)
|
|
529
812
|
.join(""),
|
|
530
813
|
});
|
|
531
|
-
devicectl([
|
|
814
|
+
await devicectl([
|
|
532
815
|
"device",
|
|
533
816
|
"copy",
|
|
534
817
|
"to",
|
|
@@ -545,46 +828,94 @@ function setUpAutoUnlock(device, id, work, passcode) {
|
|
|
545
828
|
]);
|
|
546
829
|
}
|
|
547
830
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
{
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
831
|
+
const pairingDirectory = "Library/Application Support";
|
|
832
|
+
const pairingFile = "PairingRecord.plist";
|
|
833
|
+
|
|
834
|
+
async function hasPairing(device, id) {
|
|
835
|
+
try {
|
|
836
|
+
const { result } = await devicectl(
|
|
837
|
+
[
|
|
838
|
+
"device",
|
|
839
|
+
"info",
|
|
840
|
+
"files",
|
|
841
|
+
"--device",
|
|
842
|
+
device.udid,
|
|
843
|
+
"--domain-type",
|
|
844
|
+
"appDataContainer",
|
|
845
|
+
"--domain-identifier",
|
|
846
|
+
id,
|
|
847
|
+
"--subdirectory",
|
|
848
|
+
pairingDirectory,
|
|
849
|
+
],
|
|
850
|
+
{ parse: true },
|
|
851
|
+
);
|
|
852
|
+
return result.files.some((file) => file.name === pairingFile);
|
|
853
|
+
} catch {
|
|
854
|
+
return false;
|
|
558
855
|
}
|
|
559
|
-
|
|
560
|
-
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function pair(device, id) {
|
|
859
|
+
const paired = await step(
|
|
860
|
+
`checking the pairing of ${device.name}`,
|
|
861
|
+
() => hasPairing(device, id),
|
|
862
|
+
{ transient: true },
|
|
863
|
+
);
|
|
864
|
+
if (paired) {
|
|
865
|
+
return false;
|
|
561
866
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
)
|
|
566
|
-
|
|
867
|
+
const work = fs.mkdtempSync(path.join(os.tmpdir(), "swipt-pairing-"));
|
|
868
|
+
let warning;
|
|
869
|
+
try {
|
|
870
|
+
await step(`pairing with ${device.name}`, async () => {
|
|
871
|
+
const record = path.join(work, pairingFile);
|
|
872
|
+
try {
|
|
873
|
+
await exec(await serverPath(), [
|
|
874
|
+
"pair",
|
|
875
|
+
"-d",
|
|
876
|
+
device.udid,
|
|
877
|
+
"-o",
|
|
878
|
+
record,
|
|
879
|
+
]);
|
|
880
|
+
} catch (error) {
|
|
881
|
+
if (typeof error.code === "number") {
|
|
882
|
+
warning = `pairing failed, so Swipt pairs in Settings instead: ${error.stderr.trim()}`;
|
|
883
|
+
} else if (error.code === "ENOENT") {
|
|
884
|
+
warning = `${serverHint}; Swipt pairs in Settings instead`;
|
|
885
|
+
}
|
|
886
|
+
throw error;
|
|
887
|
+
}
|
|
888
|
+
await devicectl([
|
|
889
|
+
"device",
|
|
890
|
+
"copy",
|
|
891
|
+
"to",
|
|
892
|
+
"--device",
|
|
893
|
+
device.udid,
|
|
894
|
+
"--source",
|
|
895
|
+
record,
|
|
896
|
+
"--destination",
|
|
897
|
+
`${pairingDirectory}/${pairingFile}`,
|
|
898
|
+
"--domain-type",
|
|
899
|
+
"appDataContainer",
|
|
900
|
+
"--domain-identifier",
|
|
901
|
+
id,
|
|
902
|
+
]);
|
|
903
|
+
});
|
|
904
|
+
} catch (error) {
|
|
905
|
+
if (warning === undefined) {
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
908
|
+
console.error(warning);
|
|
909
|
+
return false;
|
|
910
|
+
} finally {
|
|
911
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
567
912
|
}
|
|
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
|
-
]);
|
|
913
|
+
return true;
|
|
583
914
|
}
|
|
584
915
|
|
|
585
|
-
function serve(udid, passcode, signal) {
|
|
916
|
+
async function serve(udid, passcode, signal) {
|
|
586
917
|
const server = spawn(
|
|
587
|
-
serverPath(),
|
|
918
|
+
await serverPath(),
|
|
588
919
|
[
|
|
589
920
|
"-d",
|
|
590
921
|
udid,
|
|
@@ -631,66 +962,118 @@ function serve(udid, passcode, signal) {
|
|
|
631
962
|
};
|
|
632
963
|
}
|
|
633
964
|
|
|
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);
|
|
965
|
+
async function trustAndOpen(context, device, id, passcode) {
|
|
966
|
+
const lines = [];
|
|
967
|
+
let failure;
|
|
644
968
|
try {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
break;
|
|
969
|
+
await step(`opening Swipt on ${device.name}`, async (update) => {
|
|
970
|
+
update("starting swipt-server");
|
|
971
|
+
const waiting = passcode === undefined && process.stderr.isTTY;
|
|
972
|
+
let server;
|
|
973
|
+
let url;
|
|
974
|
+
while (url === undefined) {
|
|
975
|
+
server = await serve(device.udid, passcode, context.signal);
|
|
976
|
+
try {
|
|
977
|
+
url = await server.url;
|
|
978
|
+
} catch (error) {
|
|
979
|
+
if (!/is connected to this Mac/.test(error.message)) {
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
if (!waiting) {
|
|
983
|
+
throw new Error(
|
|
984
|
+
`${device.name} is not reachable; unlock it or pass --passcode, then run this again`,
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
update("unlock the device to continue");
|
|
665
988
|
}
|
|
666
|
-
case "error":
|
|
667
|
-
failure = response.part.value;
|
|
668
|
-
break;
|
|
669
989
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
990
|
+
try {
|
|
991
|
+
const client = open({ url, token: "" });
|
|
992
|
+
const run = async (args) => {
|
|
993
|
+
for await (const response of client.repl.execute(
|
|
994
|
+
{
|
|
995
|
+
code: fs.readFileSync(
|
|
996
|
+
new URL("sideload/trust.js", import.meta.url),
|
|
997
|
+
"utf8",
|
|
998
|
+
),
|
|
999
|
+
args,
|
|
1000
|
+
program: true,
|
|
1001
|
+
},
|
|
1002
|
+
{ signal: context.signal },
|
|
1003
|
+
)) {
|
|
1004
|
+
switch (response.part.case) {
|
|
1005
|
+
case "console": {
|
|
1006
|
+
const output = response.part.value;
|
|
1007
|
+
if (output.level === LogLevel.DEBUG) {
|
|
1008
|
+
update(output.text);
|
|
1009
|
+
} else {
|
|
1010
|
+
lines.push(output);
|
|
1011
|
+
}
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
case "error":
|
|
1015
|
+
failure = response.part.value;
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (failure) {
|
|
1020
|
+
throw new Error(failure.message);
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
const runArguments = (mode) => [id, mode, waiting ? "wait" : ""];
|
|
1024
|
+
await run(runArguments("trust"));
|
|
1025
|
+
update("opening Swipt");
|
|
1026
|
+
await devicectl(
|
|
1027
|
+
launchCommand(device, id, [
|
|
1028
|
+
setupArgument,
|
|
1029
|
+
...(context.options["no-setup-api"] ? [] : [publishArgument]),
|
|
1030
|
+
]),
|
|
1031
|
+
);
|
|
1032
|
+
await run(runArguments("prompts"));
|
|
1033
|
+
} finally {
|
|
1034
|
+
await server.stop();
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
if (failure === undefined) {
|
|
1039
|
+
throw error;
|
|
673
1040
|
}
|
|
674
1041
|
} finally {
|
|
675
|
-
|
|
1042
|
+
for (const output of lines) {
|
|
1043
|
+
(output.level === LogLevel.ERROR || output.level === LogLevel.WARN
|
|
1044
|
+
? console.error
|
|
1045
|
+
: console.log)(output.text);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (failure) {
|
|
1049
|
+
if (/\bdevice is locked\b/.test(failure.message)) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
passcode === undefined
|
|
1052
|
+
? `${device.name} is locked; unlock it or pass --passcode, then run this again`
|
|
1053
|
+
: `${device.name} did not unlock; check --passcode and --passcode-type, then run this again`,
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
throw printScriptError(failure);
|
|
676
1057
|
}
|
|
677
1058
|
}
|
|
678
1059
|
|
|
679
|
-
export function uninstall(context) {
|
|
680
|
-
const device = coreDevice(context
|
|
681
|
-
const { team, current } = locate(context
|
|
1060
|
+
export async function uninstall(context) {
|
|
1061
|
+
const device = await coreDevice(context);
|
|
1062
|
+
const { team, current } = await locate(context, device);
|
|
682
1063
|
if (current === undefined) {
|
|
683
1064
|
throw new Error(
|
|
684
1065
|
`Swipt is not sideloaded on ${device.name}${team === undefined ? "" : ` by team ${team}`}`,
|
|
685
1066
|
);
|
|
686
1067
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1068
|
+
await step(`removing Swipt from ${device.name}`, () =>
|
|
1069
|
+
devicectl([
|
|
1070
|
+
"device",
|
|
1071
|
+
"uninstall",
|
|
1072
|
+
"app",
|
|
1073
|
+
"--device",
|
|
1074
|
+
device.udid,
|
|
1075
|
+
current.bundleIdentifier,
|
|
1076
|
+
]),
|
|
1077
|
+
);
|
|
695
1078
|
console.log(`removed ${buildText(current)} from ${device.name}`);
|
|
696
1079
|
}
|