@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 +103 -30
- package/connection.mjs +86 -26
- package/coredevice.mjs +68 -0
- package/device.mjs +274 -90
- package/ignore.mjs +17 -5
- package/metro.mjs +235 -65
- package/output.mjs +32 -7
- package/package.json +8 -3
- package/project.mjs +394 -106
- package/proto/system_pb.js +1 -1
- package/sideload/trust.js +235 -0
- package/sideload.mjs +649 -0
- package/swipt.mjs +374 -52
package/sideload.mjs
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import readline from "node:readline";
|
|
8
|
+
import { Readable } from "node:stream";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
10
|
+
|
|
11
|
+
import { UsageError, open } from "./client.mjs";
|
|
12
|
+
import { apps, coreDevice, devicectl } from "./coredevice.mjs";
|
|
13
|
+
import { printScriptError } from "./output.mjs";
|
|
14
|
+
import { LogLevel } from "./proto/common_pb.js";
|
|
15
|
+
|
|
16
|
+
const bundleId = "io.nekohasekai.swipt";
|
|
17
|
+
const repository = "swipt-automation/swipt";
|
|
18
|
+
const teamPattern = /^[A-Z0-9]{10}$/;
|
|
19
|
+
const sideloadedPattern = new RegExp(
|
|
20
|
+
`^${bundleId.replaceAll(".", "\\.")}\\.([A-Z0-9]{10})$`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const accountsHint = "Xcode › Settings › Accounts";
|
|
24
|
+
|
|
25
|
+
const passcodeTypes = ["pin", "numeric", "alphanumeric"];
|
|
26
|
+
|
|
27
|
+
function passcodeOf(options) {
|
|
28
|
+
const type = options["passcode-type"] ?? "pin";
|
|
29
|
+
if (!passcodeTypes.includes(type)) {
|
|
30
|
+
throw new UsageError(
|
|
31
|
+
`invalid argument ${JSON.stringify(type)} for --passcode-type: it is ${passcodeTypes.join(", ")}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const passcode = options.passcode ?? process.env.SWIPT_PASSCODE;
|
|
35
|
+
if (!passcode) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const digits = /^\d+$/.test(passcode);
|
|
39
|
+
if (type === "pin" && !(digits && [4, 6].includes(passcode.length))) {
|
|
40
|
+
throw new UsageError(
|
|
41
|
+
"a pin passcode is 4 or 6 digits; a custom one needs --passcode-type numeric or alphanumeric",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (type === "numeric" && !digits) {
|
|
45
|
+
throw new UsageError("a numeric passcode is digits only");
|
|
46
|
+
}
|
|
47
|
+
if (!/^[\x20-\x7e]{1,64}$/.test(passcode)) {
|
|
48
|
+
throw new UsageError("a passcode is up to 64 printable ASCII characters");
|
|
49
|
+
}
|
|
50
|
+
return { passcode, type };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function headers(accept) {
|
|
54
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
55
|
+
return {
|
|
56
|
+
Accept: accept,
|
|
57
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
58
|
+
"User-Agent": "swipt-cli",
|
|
59
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function github(url, accept, signal) {
|
|
64
|
+
let response = await fetch(url, {
|
|
65
|
+
headers: headers(accept),
|
|
66
|
+
signal,
|
|
67
|
+
redirect: "manual",
|
|
68
|
+
});
|
|
69
|
+
if (response.status >= 300 && response.status < 400) {
|
|
70
|
+
response = await fetch(response.headers.get("location"), { signal });
|
|
71
|
+
}
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
let message = `GitHub answered ${response.status} for ${url}`;
|
|
74
|
+
if ([401, 403, 404, 429].includes(response.status)) {
|
|
75
|
+
message += `; ${process.env.GITHUB_TOKEN || process.env.GH_TOKEN ? "GITHUB_TOKEN" : "set GITHUB_TOKEN to a token that"} must read the repository and stay under the rate limit`;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(message);
|
|
78
|
+
}
|
|
79
|
+
return response;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function findRelease(nightly, signal) {
|
|
83
|
+
const release = await (
|
|
84
|
+
await github(
|
|
85
|
+
`https://api.github.com/repos/${repository}/releases/${nightly ? "tags/nightly" : "latest"}`,
|
|
86
|
+
"application/vnd.github+json",
|
|
87
|
+
signal,
|
|
88
|
+
)
|
|
89
|
+
).json();
|
|
90
|
+
const asset = (name) => {
|
|
91
|
+
const found = release.assets.find((it) => it.name === name);
|
|
92
|
+
if (!found) {
|
|
93
|
+
throw new Error(`release ${release.tag_name} has no ${name}`);
|
|
94
|
+
}
|
|
95
|
+
return found.url;
|
|
96
|
+
};
|
|
97
|
+
const metadata = await (
|
|
98
|
+
await github(asset("metadata.json"), "application/octet-stream", signal)
|
|
99
|
+
).json();
|
|
100
|
+
return {
|
|
101
|
+
tag: release.tag_name,
|
|
102
|
+
version: metadata.version,
|
|
103
|
+
build: Number(metadata.build),
|
|
104
|
+
ipa: asset("Swipt.ipa"),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function download(url, file, signal) {
|
|
109
|
+
const response = await github(url, "application/octet-stream", signal);
|
|
110
|
+
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(file), {
|
|
111
|
+
signal,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function unpack(ipa, directory) {
|
|
116
|
+
execFileSync("unzip", ["-q", ipa, "-d", directory]);
|
|
117
|
+
const payload = path.join(directory, "Payload");
|
|
118
|
+
const app = fs.readdirSync(payload).find((name) => name.endsWith(".app"));
|
|
119
|
+
if (app === undefined) {
|
|
120
|
+
throw new Error("the IPA has no app in Payload");
|
|
121
|
+
}
|
|
122
|
+
return path.join(payload, app);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const pbxproj = (id, team) => `// !$*UTF8*$!
|
|
126
|
+
{
|
|
127
|
+
archiveVersion = 1;
|
|
128
|
+
classes = {
|
|
129
|
+
};
|
|
130
|
+
objectVersion = 56;
|
|
131
|
+
objects = {
|
|
132
|
+
A1 = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = "<group>"; };
|
|
133
|
+
A2 = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stub.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
134
|
+
A3 = {isa = PBXBuildFile; fileRef = A1; };
|
|
135
|
+
B1 = {isa = PBXGroup; children = (B2, B3); sourceTree = "<group>"; };
|
|
136
|
+
B2 = {isa = PBXGroup; children = (A1); path = Stub; sourceTree = "<group>"; };
|
|
137
|
+
B3 = {isa = PBXGroup; children = (A2); name = Products; sourceTree = "<group>"; };
|
|
138
|
+
C1 = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (A3); runOnlyForDeploymentPostprocessing = 0; };
|
|
139
|
+
D1 = {
|
|
140
|
+
isa = PBXNativeTarget;
|
|
141
|
+
buildConfigurationList = E1;
|
|
142
|
+
buildPhases = (C1);
|
|
143
|
+
buildRules = ();
|
|
144
|
+
dependencies = ();
|
|
145
|
+
name = Stub;
|
|
146
|
+
productName = Stub;
|
|
147
|
+
productReference = A2;
|
|
148
|
+
productType = "com.apple.product-type.application";
|
|
149
|
+
};
|
|
150
|
+
E1 = {isa = XCConfigurationList; buildConfigurations = (F1); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; };
|
|
151
|
+
E2 = {isa = XCConfigurationList; buildConfigurations = (F2); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; };
|
|
152
|
+
F1 = {
|
|
153
|
+
isa = XCBuildConfiguration;
|
|
154
|
+
buildSettings = {
|
|
155
|
+
CODE_SIGN_STYLE = Automatic;
|
|
156
|
+
DEVELOPMENT_TEAM = "${team}";
|
|
157
|
+
GENERATE_INFOPLIST_FILE = YES;
|
|
158
|
+
PRODUCT_BUNDLE_IDENTIFIER = "${id}";
|
|
159
|
+
PRODUCT_NAME = Stub;
|
|
160
|
+
SDKROOT = iphoneos;
|
|
161
|
+
TARGETED_DEVICE_FAMILY = "1,2";
|
|
162
|
+
};
|
|
163
|
+
name = Release;
|
|
164
|
+
};
|
|
165
|
+
F2 = {isa = XCBuildConfiguration; buildSettings = {}; name = Release; };
|
|
166
|
+
G1 = {
|
|
167
|
+
isa = PBXProject;
|
|
168
|
+
buildConfigurationList = E2;
|
|
169
|
+
compatibilityVersion = "Xcode 14.0";
|
|
170
|
+
mainGroup = B1;
|
|
171
|
+
productRefGroup = B3;
|
|
172
|
+
projectDirPath = "";
|
|
173
|
+
projectRoot = "";
|
|
174
|
+
targets = (D1);
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
rootObject = G1;
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
function explain(output, team) {
|
|
182
|
+
const errors = [
|
|
183
|
+
...new Set(
|
|
184
|
+
output
|
|
185
|
+
.split("\n")
|
|
186
|
+
.filter((line) => /^(xcodebuild: )?error: /.test(line))
|
|
187
|
+
.map((line) => line.replace(/^(xcodebuild: )?error: /, "")),
|
|
188
|
+
),
|
|
189
|
+
].join("; ");
|
|
190
|
+
if (/No Accounts?\b/.test(output)) {
|
|
191
|
+
return `Xcode has no account for team ${team}; sign in with its Apple Account at ${accountsHint}`;
|
|
192
|
+
}
|
|
193
|
+
if (/signing certificate|certificateType/i.test(output)) {
|
|
194
|
+
return `Xcode could not make a development certificate for team ${team}; add one at ${accountsHint} › Manage Certificates (${errors})`;
|
|
195
|
+
}
|
|
196
|
+
if (/requires Xcode|xcode-select/.test(output)) {
|
|
197
|
+
return "xcodebuild needs Xcode; install it and select it with xcode-select";
|
|
198
|
+
}
|
|
199
|
+
return errors || output.trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function provision(work, id, team, device) {
|
|
203
|
+
const stub = path.join(work, "Stub");
|
|
204
|
+
const products = path.join(stub, "Products");
|
|
205
|
+
fs.mkdirSync(path.join(stub, "Stub.xcodeproj"), { recursive: true });
|
|
206
|
+
fs.mkdirSync(path.join(stub, "Stub"));
|
|
207
|
+
fs.writeFileSync(
|
|
208
|
+
path.join(stub, "Stub/main.c"),
|
|
209
|
+
"int main(void) { return 0; }\n",
|
|
210
|
+
);
|
|
211
|
+
fs.writeFileSync(
|
|
212
|
+
path.join(stub, "Stub.xcodeproj/project.pbxproj"),
|
|
213
|
+
pbxproj(id, team),
|
|
214
|
+
);
|
|
215
|
+
const result = spawnSync(
|
|
216
|
+
"xcodebuild",
|
|
217
|
+
[
|
|
218
|
+
"build",
|
|
219
|
+
"-project",
|
|
220
|
+
path.join(stub, "Stub.xcodeproj"),
|
|
221
|
+
"-target",
|
|
222
|
+
"Stub",
|
|
223
|
+
"-configuration",
|
|
224
|
+
"Release",
|
|
225
|
+
"-destination",
|
|
226
|
+
`id=${device.udid}`,
|
|
227
|
+
`CONFIGURATION_BUILD_DIR=${products}`,
|
|
228
|
+
`OBJROOT=${path.join(stub, "Objects")}`,
|
|
229
|
+
"-allowProvisioningUpdates",
|
|
230
|
+
"-allowProvisioningDeviceRegistration",
|
|
231
|
+
],
|
|
232
|
+
{ encoding: "utf8" },
|
|
233
|
+
);
|
|
234
|
+
if (result.error) {
|
|
235
|
+
throw result.error.code === "ENOENT"
|
|
236
|
+
? new Error("xcodebuild is missing; install Xcode")
|
|
237
|
+
: result.error;
|
|
238
|
+
}
|
|
239
|
+
if (result.status !== 0) {
|
|
240
|
+
throw new Error(explain(`${result.stdout}${result.stderr}`, team));
|
|
241
|
+
}
|
|
242
|
+
const app = path.join(products, "Stub.app");
|
|
243
|
+
const certificates = path.join(work, "certificate");
|
|
244
|
+
const stdio = ["ignore", "pipe", "ignore"];
|
|
245
|
+
execFileSync(
|
|
246
|
+
"codesign",
|
|
247
|
+
["-d", `--extract-certificates=${certificates}`, app],
|
|
248
|
+
{ stdio },
|
|
249
|
+
);
|
|
250
|
+
const entitlements = path.join(work, "entitlements.plist");
|
|
251
|
+
fs.writeFileSync(
|
|
252
|
+
entitlements,
|
|
253
|
+
execFileSync("codesign", ["-d", "--entitlements", "-", "--xml", app], {
|
|
254
|
+
stdio,
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
return {
|
|
258
|
+
identity: createHash("sha1")
|
|
259
|
+
.update(fs.readFileSync(`${certificates}0`))
|
|
260
|
+
.digest("hex")
|
|
261
|
+
.toUpperCase(),
|
|
262
|
+
entitlements,
|
|
263
|
+
profile: path.join(app, "embedded.mobileprovision"),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resign(app, id, signing) {
|
|
268
|
+
const info = path.join(app, "Info.plist");
|
|
269
|
+
execFileSync("plutil", [
|
|
270
|
+
"-replace",
|
|
271
|
+
"CFBundleIdentifier",
|
|
272
|
+
"-string",
|
|
273
|
+
id,
|
|
274
|
+
info,
|
|
275
|
+
]);
|
|
276
|
+
let permitted;
|
|
277
|
+
try {
|
|
278
|
+
permitted = JSON.parse(
|
|
279
|
+
execFileSync("plutil", [
|
|
280
|
+
"-extract",
|
|
281
|
+
"BGTaskSchedulerPermittedIdentifiers",
|
|
282
|
+
"json",
|
|
283
|
+
"-o",
|
|
284
|
+
"-",
|
|
285
|
+
info,
|
|
286
|
+
]),
|
|
287
|
+
);
|
|
288
|
+
} catch {
|
|
289
|
+
permitted = undefined;
|
|
290
|
+
}
|
|
291
|
+
if (permitted !== undefined) {
|
|
292
|
+
execFileSync("plutil", [
|
|
293
|
+
"-replace",
|
|
294
|
+
"BGTaskSchedulerPermittedIdentifiers",
|
|
295
|
+
"-json",
|
|
296
|
+
JSON.stringify(
|
|
297
|
+
permitted.map((it) =>
|
|
298
|
+
it.startsWith(`${bundleId}.`) ? id + it.slice(bundleId.length) : it,
|
|
299
|
+
),
|
|
300
|
+
),
|
|
301
|
+
info,
|
|
302
|
+
]);
|
|
303
|
+
}
|
|
304
|
+
fs.copyFileSync(signing.profile, path.join(app, "embedded.mobileprovision"));
|
|
305
|
+
const codesign = ["--force", "--sign", signing.identity, "--timestamp=none"];
|
|
306
|
+
const frameworks = path.join(app, "Frameworks");
|
|
307
|
+
if (fs.existsSync(frameworks)) {
|
|
308
|
+
for (const name of fs.readdirSync(frameworks)) {
|
|
309
|
+
execFileSync("codesign", [...codesign, path.join(frameworks, name)]);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
execFileSync("codesign", [
|
|
313
|
+
...codesign,
|
|
314
|
+
"--entitlements",
|
|
315
|
+
signing.entitlements,
|
|
316
|
+
app,
|
|
317
|
+
]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function locate(options, device) {
|
|
321
|
+
const found = apps(device.udid)
|
|
322
|
+
.map((app) => ({
|
|
323
|
+
...app,
|
|
324
|
+
team: sideloadedPattern.exec(app.bundleIdentifier)?.[1],
|
|
325
|
+
}))
|
|
326
|
+
.filter((app) => app.team !== undefined);
|
|
327
|
+
if (options.team !== undefined) {
|
|
328
|
+
if (!teamPattern.test(options.team)) {
|
|
329
|
+
throw new UsageError(
|
|
330
|
+
`invalid argument ${JSON.stringify(options.team)} for --team: it is the 10-character team ID shown in ${accountsHint}`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
team: options.team,
|
|
335
|
+
current: found.find((app) => app.team === options.team),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (found.length > 1) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Swipt is sideloaded on ${device.name} by more than one team (${found.map((app) => app.team).join(", ")}); choose one with --team`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return { team: found[0]?.team, current: found[0] };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const buildText = (app) => `Swipt ${app.version} (${app.bundleVersion})`;
|
|
347
|
+
|
|
348
|
+
async function deploy(context, onlyNewer) {
|
|
349
|
+
const passcode = passcodeOf(context.options);
|
|
350
|
+
const device = coreDevice(context.options.device ?? process.env.SWIPT_DEVICE);
|
|
351
|
+
const { team, current } = locate(context.options, device);
|
|
352
|
+
if (team === undefined) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
onlyNewer
|
|
355
|
+
? `Swipt is not sideloaded on ${device.name}; install it with "swipt sideload install"`
|
|
356
|
+
: `Swipt is not sideloaded on ${device.name} yet; choose the team with --team ID, as shown in ${accountsHint}`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const release = await findRelease(context.options.nightly, context.signal);
|
|
360
|
+
if (current !== undefined) {
|
|
361
|
+
const installed = Number(current.bundleVersion);
|
|
362
|
+
if (installed === release.build) {
|
|
363
|
+
console.log(
|
|
364
|
+
`${buildText(current)} on ${device.name} is already ${release.tag}`,
|
|
365
|
+
);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (onlyNewer && installed > release.build) {
|
|
369
|
+
console.log(
|
|
370
|
+
`${buildText(current)} on ${device.name} is newer than ${release.tag} (${release.build})`,
|
|
371
|
+
);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const id = `${bundleId}.${team}`;
|
|
376
|
+
const work = fs.mkdtempSync(path.join(os.tmpdir(), "swipt-sideload-"));
|
|
377
|
+
try {
|
|
378
|
+
console.error(
|
|
379
|
+
`downloading Swipt ${release.version} (${release.build}) from ${release.tag}`,
|
|
380
|
+
);
|
|
381
|
+
const ipa = path.join(work, "Swipt.ipa");
|
|
382
|
+
await download(release.ipa, ipa, context.signal);
|
|
383
|
+
const app = unpack(ipa, work);
|
|
384
|
+
console.error(`signing ${id} with team ${team} through Xcode`);
|
|
385
|
+
resign(app, id, provision(work, id, team, device));
|
|
386
|
+
console.error(`installing on ${device.name}`);
|
|
387
|
+
devicectl(["device", "install", "app", "--device", device.udid, app]);
|
|
388
|
+
if (current === undefined) {
|
|
389
|
+
console.error(`pairing with ${device.name}`);
|
|
390
|
+
pair(device, id, work);
|
|
391
|
+
if (passcode !== undefined) {
|
|
392
|
+
setUpAutoUnlock(device, id, work, passcode);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
} finally {
|
|
396
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
397
|
+
}
|
|
398
|
+
const result = locate({ team }, device).current;
|
|
399
|
+
if (result === undefined || Number(result.bundleVersion) !== release.build) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`${device.name} ${result === undefined ? `does not list ${id}` : `has ${buildText(result)}`} after the install`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
console.log(
|
|
405
|
+
`${buildText(result)} is ${current === undefined ? "installed" : "updated"} on ${device.name}`,
|
|
406
|
+
);
|
|
407
|
+
try {
|
|
408
|
+
devicectl([
|
|
409
|
+
"device",
|
|
410
|
+
"process",
|
|
411
|
+
"launch",
|
|
412
|
+
"--terminate-existing",
|
|
413
|
+
"--device",
|
|
414
|
+
device.udid,
|
|
415
|
+
id,
|
|
416
|
+
]);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (/trusted by the user/.test(error.message)) {
|
|
419
|
+
console.log(
|
|
420
|
+
`it opens once the team's developer app is trusted in Settings › General › VPN & Device Management on ${device.name}`,
|
|
421
|
+
);
|
|
422
|
+
} else if (/BSErrorCodeDescription = Locked/.test(error.message)) {
|
|
423
|
+
console.log(`it opens once ${device.name} is unlocked`);
|
|
424
|
+
} else {
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export const install = (context) => deploy(context, false);
|
|
431
|
+
|
|
432
|
+
export const update = (context) => deploy(context, true);
|
|
433
|
+
|
|
434
|
+
const serverHint =
|
|
435
|
+
"swipt-server is missing; install @swipt/cli with its optional dependencies, or name it with SWIPT_SERVER_PATH";
|
|
436
|
+
|
|
437
|
+
function serverPath() {
|
|
438
|
+
if (process.env.SWIPT_SERVER_PATH) {
|
|
439
|
+
return process.env.SWIPT_SERVER_PATH;
|
|
440
|
+
}
|
|
441
|
+
let packaged;
|
|
442
|
+
try {
|
|
443
|
+
packaged = path.join(
|
|
444
|
+
path.dirname(
|
|
445
|
+
createRequire(import.meta.url).resolve(
|
|
446
|
+
`@swipt/server-${process.platform}-${process.arch}/package.json`,
|
|
447
|
+
),
|
|
448
|
+
),
|
|
449
|
+
"swipt-server",
|
|
450
|
+
);
|
|
451
|
+
} catch {
|
|
452
|
+
return "swipt-server";
|
|
453
|
+
}
|
|
454
|
+
return fs.existsSync(packaged) ? packaged : "swipt-server";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const schema = [
|
|
458
|
+
"CREATE TABLE grdb_migrations (identifier TEXT NOT NULL PRIMARY KEY)",
|
|
459
|
+
'CREATE TABLE "preferences" ("name" TEXT PRIMARY KEY NOT NULL, "data" BLOB NOT NULL)',
|
|
460
|
+
'CREATE TABLE "swiplets" ("id" TEXT PRIMARY KEY NOT NULL, "position" INTEGER NOT NULL, "etag" TEXT NOT NULL)',
|
|
461
|
+
'CREATE TABLE "repositories" ("url" TEXT PRIMARY KEY NOT NULL, "position" INTEGER NOT NULL, "name" TEXT NOT NULL, "localizedNames" TEXT, "iconColor" TEXT, "iconSymbol" TEXT, "image" BLOB, "etag" TEXT NOT NULL)',
|
|
462
|
+
'CREATE TABLE "repositorySwiplets" ("repository" TEXT NOT NULL REFERENCES "repositories"("url") ON DELETE CASCADE, "position" INTEGER NOT NULL, "id" TEXT NOT NULL, "name" TEXT NOT NULL, "localizedNames" TEXT NOT NULL, "description" TEXT NOT NULL, "version" TEXT NOT NULL, "versionCode" INTEGER NOT NULL, "iconColor" TEXT, "iconSymbol" TEXT, "url" TEXT NOT NULL, "sha256" TEXT NOT NULL, PRIMARY KEY ("repository", "id"))',
|
|
463
|
+
"INSERT INTO grdb_migrations VALUES ('0.1.0')",
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
function setUpAutoUnlock(device, id, work, passcode) {
|
|
467
|
+
const database = path.join(work, "Swipt.sqlite");
|
|
468
|
+
const preferences = {
|
|
469
|
+
serverAutoUnlocks: true,
|
|
470
|
+
serverAutoUnlockPasscodeType: passcodeTypes.indexOf(passcode.type),
|
|
471
|
+
serverAutoUnlockPasscode: passcode.passcode,
|
|
472
|
+
autoUnlockSetUp: true,
|
|
473
|
+
};
|
|
474
|
+
const quote = (text) => `'${text.replaceAll("'", "''")}'`;
|
|
475
|
+
execFileSync("sqlite3", [database], {
|
|
476
|
+
input: [
|
|
477
|
+
...schema,
|
|
478
|
+
...Object.entries(preferences).map(
|
|
479
|
+
([name, value]) =>
|
|
480
|
+
`INSERT INTO preferences VALUES (${quote(name)}, CAST(${quote(JSON.stringify(value))} AS BLOB))`,
|
|
481
|
+
),
|
|
482
|
+
]
|
|
483
|
+
.map((statement) => `${statement};\n`)
|
|
484
|
+
.join(""),
|
|
485
|
+
});
|
|
486
|
+
devicectl([
|
|
487
|
+
"device",
|
|
488
|
+
"copy",
|
|
489
|
+
"to",
|
|
490
|
+
"--device",
|
|
491
|
+
device.udid,
|
|
492
|
+
"--source",
|
|
493
|
+
database,
|
|
494
|
+
"--destination",
|
|
495
|
+
"Library/Application Support/Swipt.sqlite",
|
|
496
|
+
"--domain-type",
|
|
497
|
+
"appDataContainer",
|
|
498
|
+
"--domain-identifier",
|
|
499
|
+
id,
|
|
500
|
+
]);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function pair(device, id, work) {
|
|
504
|
+
const record = path.join(work, "PairingRecord.plist");
|
|
505
|
+
const result = spawnSync(
|
|
506
|
+
serverPath(),
|
|
507
|
+
["pair", "-d", device.udid, "-o", record],
|
|
508
|
+
{ encoding: "utf8", stdio: ["ignore", "ignore", "pipe"] },
|
|
509
|
+
);
|
|
510
|
+
if (result.error?.code === "ENOENT") {
|
|
511
|
+
console.error(`${serverHint}; Swipt pairs in Settings instead`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (result.error) {
|
|
515
|
+
throw result.error;
|
|
516
|
+
}
|
|
517
|
+
if (result.status !== 0) {
|
|
518
|
+
console.error(
|
|
519
|
+
`pairing failed, so Swipt pairs in Settings instead: ${result.stderr.trim()}`,
|
|
520
|
+
);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
devicectl([
|
|
524
|
+
"device",
|
|
525
|
+
"copy",
|
|
526
|
+
"to",
|
|
527
|
+
"--device",
|
|
528
|
+
device.udid,
|
|
529
|
+
"--source",
|
|
530
|
+
record,
|
|
531
|
+
"--destination",
|
|
532
|
+
"Library/Application Support/PairingRecord.plist",
|
|
533
|
+
"--domain-type",
|
|
534
|
+
"appDataContainer",
|
|
535
|
+
"--domain-identifier",
|
|
536
|
+
id,
|
|
537
|
+
]);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function serve(udid, passcode, signal) {
|
|
541
|
+
const server = spawn(
|
|
542
|
+
serverPath(),
|
|
543
|
+
["-d", udid, ...(passcode ? ["--passcode-type", passcode.type] : [])],
|
|
544
|
+
{
|
|
545
|
+
env: {
|
|
546
|
+
...process.env,
|
|
547
|
+
...(passcode ? { SWIPT_PASSCODE: passcode.passcode } : {}),
|
|
548
|
+
},
|
|
549
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
550
|
+
signal,
|
|
551
|
+
},
|
|
552
|
+
);
|
|
553
|
+
let logs = "";
|
|
554
|
+
server.stderr.on("data", (chunk) => {
|
|
555
|
+
logs += chunk;
|
|
556
|
+
});
|
|
557
|
+
const exited = new Promise((resolve) => server.once("close", resolve));
|
|
558
|
+
const url = new Promise((resolve, reject) => {
|
|
559
|
+
server.once("error", (error) =>
|
|
560
|
+
reject(error.code === "ENOENT" ? new Error(serverHint) : error),
|
|
561
|
+
);
|
|
562
|
+
exited.then((code) =>
|
|
563
|
+
reject(
|
|
564
|
+
new Error(
|
|
565
|
+
`swipt-server exited with ${code ?? "a signal"}${logs.trim() === "" ? "" : `: ${logs.trim()}`}`,
|
|
566
|
+
),
|
|
567
|
+
),
|
|
568
|
+
);
|
|
569
|
+
readline.createInterface({ input: server.stdout }).on("line", (line) => {
|
|
570
|
+
const found = /^Swipt API: (\S+)/.exec(line);
|
|
571
|
+
if (found) {
|
|
572
|
+
const parsed = new URL(found[1]);
|
|
573
|
+
parsed.hostname = "127.0.0.1";
|
|
574
|
+
resolve(parsed.origin);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
return {
|
|
579
|
+
url,
|
|
580
|
+
stop() {
|
|
581
|
+
server.kill("SIGTERM");
|
|
582
|
+
return exited;
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export async function start(context) {
|
|
588
|
+
const passcode = passcodeOf(context.options);
|
|
589
|
+
const device = coreDevice(context.options.device ?? process.env.SWIPT_DEVICE);
|
|
590
|
+
const { team, current } = locate(context.options, device);
|
|
591
|
+
if (current === undefined) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`Swipt is not sideloaded on ${device.name}${team === undefined ? "" : ` by team ${team}`}`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const server = serve(device.udid, passcode, context.signal);
|
|
597
|
+
try {
|
|
598
|
+
const client = open({ url: await server.url, token: "" });
|
|
599
|
+
let failure;
|
|
600
|
+
for await (const response of client.repl.execute(
|
|
601
|
+
{
|
|
602
|
+
code: fs.readFileSync(
|
|
603
|
+
new URL("sideload/trust.js", import.meta.url),
|
|
604
|
+
"utf8",
|
|
605
|
+
),
|
|
606
|
+
args: [current.bundleIdentifier],
|
|
607
|
+
program: true,
|
|
608
|
+
},
|
|
609
|
+
{ signal: context.signal },
|
|
610
|
+
)) {
|
|
611
|
+
switch (response.part.case) {
|
|
612
|
+
case "console": {
|
|
613
|
+
const output = response.part.value;
|
|
614
|
+
(output.level === LogLevel.ERROR || output.level === LogLevel.WARN
|
|
615
|
+
? console.error
|
|
616
|
+
: console.log)(output.text);
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
case "error":
|
|
620
|
+
failure = response.part.value;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (failure) {
|
|
625
|
+
throw printScriptError(failure);
|
|
626
|
+
}
|
|
627
|
+
} finally {
|
|
628
|
+
await server.stop();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function uninstall(context) {
|
|
633
|
+
const device = coreDevice(context.options.device ?? process.env.SWIPT_DEVICE);
|
|
634
|
+
const { team, current } = locate(context.options, device);
|
|
635
|
+
if (current === undefined) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
`Swipt is not sideloaded on ${device.name}${team === undefined ? "" : ` by team ${team}`}`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
devicectl([
|
|
641
|
+
"device",
|
|
642
|
+
"uninstall",
|
|
643
|
+
"app",
|
|
644
|
+
"--device",
|
|
645
|
+
device.udid,
|
|
646
|
+
current.bundleIdentifier,
|
|
647
|
+
]);
|
|
648
|
+
console.log(`removed ${buildText(current)} from ${device.name}`);
|
|
649
|
+
}
|