@jskit-ai/jskit-cli 0.2.72 → 0.2.74
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/package.json +4 -4
- package/src/server/cliRuntime/completion.js +3 -1
- package/src/server/cliRuntime/descriptorValidation.js +52 -0
- package/src/server/cliRuntime/mutations/fileMutations.js +18 -14
- package/src/server/cliRuntime/mutations/installMigrationMutation.js +10 -5
- package/src/server/cliRuntime/mutations/textMutations.js +17 -5
- package/src/server/cliRuntime/packageInstallFlow.js +42 -19
- package/src/server/cliRuntime/viteProxy.js +25 -7
- package/src/server/commandHandlers/health.js +88 -15
- package/src/server/commandHandlers/mobile.js +1316 -0
- package/src/server/commandHandlers/mobileCommandCatalog.js +196 -0
- package/src/server/commandHandlers/mobileShellSupport.js +929 -0
- package/src/server/commandHandlers/packageCommands/add.js +415 -2
- package/src/server/commandHandlers/packageCommands/migrations.js +2 -1
- package/src/server/core/argParser.js +6 -0
- package/src/server/core/commandCatalog.js +31 -3
- package/src/server/core/createCommandHandlers.js +3 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import {
|
|
5
|
+
createColorFormatter,
|
|
6
|
+
writeWrappedLines
|
|
7
|
+
} from "../shared/outputFormatting.js";
|
|
8
|
+
import {
|
|
9
|
+
buildMobileCommandOptionMeta,
|
|
10
|
+
listMobileCommandDefinitions,
|
|
11
|
+
resolveMobileCommandDefinition
|
|
12
|
+
} from "./mobileCommandCatalog.js";
|
|
13
|
+
import {
|
|
14
|
+
assertAndroidSdkConfigured,
|
|
15
|
+
assertCapacitorShellInstalled,
|
|
16
|
+
collectAndroidSdkComponentIssues,
|
|
17
|
+
collectAndroidNativeShellIdentityIssues,
|
|
18
|
+
ensureAndroidManifestDeepLinks,
|
|
19
|
+
ensureAndroidNativeShellIdentity,
|
|
20
|
+
renderManagedMobileFile,
|
|
21
|
+
resolveAndroidSdkDetails,
|
|
22
|
+
resolveInstalledMobileConfig
|
|
23
|
+
} from "./mobileShellSupport.js";
|
|
24
|
+
const CAPACITOR_RUNTIME_PACKAGE_ID = "@jskit-ai/mobile-capacitor";
|
|
25
|
+
const MOBILE_NOTES_RELATIVE_PATH = path.join(".jskit", "mobile-capacitor.md");
|
|
26
|
+
|
|
27
|
+
async function collectManagedMobileFileDriftIssues({
|
|
28
|
+
ctx,
|
|
29
|
+
appRoot,
|
|
30
|
+
issues = []
|
|
31
|
+
} = {}) {
|
|
32
|
+
const {
|
|
33
|
+
fileExists,
|
|
34
|
+
path: pathModule,
|
|
35
|
+
normalizeRelativePath
|
|
36
|
+
} = ctx;
|
|
37
|
+
const managedRelativePaths = [
|
|
38
|
+
"capacitor.config.json",
|
|
39
|
+
MOBILE_NOTES_RELATIVE_PATH
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const relativePath of managedRelativePaths) {
|
|
43
|
+
const absolutePath = pathModule.join(appRoot, relativePath);
|
|
44
|
+
if (!(await fileExists(absolutePath))) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const currentContent = await readFile(absolutePath, "utf8");
|
|
49
|
+
let expectedContent = "";
|
|
50
|
+
try {
|
|
51
|
+
expectedContent = await renderManagedMobileFile({
|
|
52
|
+
appRoot,
|
|
53
|
+
relativeTargetPath: relativePath
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
issues.push(
|
|
57
|
+
`Could not validate ${normalizeRelativePath(appRoot, absolutePath)} against the installed mobile package: ${String(error?.message || error || "unknown error")}`
|
|
58
|
+
);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (currentContent !== expectedContent) {
|
|
62
|
+
issues.push(
|
|
63
|
+
`${normalizeRelativePath(appRoot, absolutePath)} is stale and no longer matches config.mobile. Re-run jskit mobile sync android to refresh managed mobile-shell files.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function collectMissingInstalledDependencyNames(ctx, appRoot = "", packageJson = {}) {
|
|
70
|
+
const {
|
|
71
|
+
fileExists,
|
|
72
|
+
path: pathModule
|
|
73
|
+
} = ctx;
|
|
74
|
+
const sections = [
|
|
75
|
+
packageJson?.dependencies,
|
|
76
|
+
packageJson?.devDependencies,
|
|
77
|
+
packageJson?.optionalDependencies
|
|
78
|
+
];
|
|
79
|
+
const missing = [];
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
|
|
82
|
+
for (const section of sections) {
|
|
83
|
+
if (!section || typeof section !== "object" || Array.isArray(section)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const packageName of Object.keys(section).sort((left, right) => left.localeCompare(right))) {
|
|
88
|
+
const normalizedPackageName = String(packageName || "").trim();
|
|
89
|
+
if (!normalizedPackageName || seen.has(normalizedPackageName)) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
seen.add(normalizedPackageName);
|
|
93
|
+
|
|
94
|
+
const packageJsonPath = pathModule.join(
|
|
95
|
+
appRoot,
|
|
96
|
+
"node_modules",
|
|
97
|
+
...normalizedPackageName.split("/"),
|
|
98
|
+
"package.json"
|
|
99
|
+
);
|
|
100
|
+
if (!(await fileExists(packageJsonPath))) {
|
|
101
|
+
missing.push(normalizedPackageName);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return missing;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderMobileHelp(stream, definition = null) {
|
|
110
|
+
const color = createColorFormatter(stream);
|
|
111
|
+
const lines = [];
|
|
112
|
+
|
|
113
|
+
if (!definition) {
|
|
114
|
+
lines.push(`Command: ${color.emphasis("mobile")}`);
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push(color.heading("1) Minimal use"));
|
|
117
|
+
lines.push(" jskit mobile <subcommand>");
|
|
118
|
+
lines.push("");
|
|
119
|
+
lines.push(color.heading("2) Subcommands"));
|
|
120
|
+
for (const entry of listMobileCommandDefinitions()) {
|
|
121
|
+
lines.push(` - ${color.item(entry.name)}: ${entry.summary}`);
|
|
122
|
+
}
|
|
123
|
+
lines.push("");
|
|
124
|
+
lines.push(color.heading("3) Notes"));
|
|
125
|
+
lines.push(" - Mobile helpers are for the Stage 1 Android Capacitor shell flow.");
|
|
126
|
+
lines.push(" - Use jskit mobile <subcommand> help for subcommand-specific usage.");
|
|
127
|
+
writeWrappedLines({
|
|
128
|
+
stdout: stream,
|
|
129
|
+
lines
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
lines.push(`Mobile subcommand: ${color.emphasis(definition.name)}`);
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push(color.heading("1) Summary"));
|
|
137
|
+
lines.push(` ${definition.summary}`);
|
|
138
|
+
lines.push("");
|
|
139
|
+
lines.push(color.heading("2) Use"));
|
|
140
|
+
lines.push(` ${definition.usage}`);
|
|
141
|
+
|
|
142
|
+
if (definition.options.length > 0) {
|
|
143
|
+
lines.push("");
|
|
144
|
+
lines.push(color.heading("3) Options"));
|
|
145
|
+
for (const optionRow of definition.options) {
|
|
146
|
+
lines.push(` - ${optionRow.label}: ${optionRow.description}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (definition.defaults.length > 0) {
|
|
151
|
+
lines.push("");
|
|
152
|
+
lines.push(color.heading(definition.options.length > 0 ? "4) Defaults" : "3) Defaults"));
|
|
153
|
+
for (const defaultLine of definition.defaults) {
|
|
154
|
+
lines.push(` - ${defaultLine}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
writeWrappedLines({
|
|
159
|
+
stdout: stream,
|
|
160
|
+
lines
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isValidHttpOrHttpsUrl(value = "") {
|
|
165
|
+
const normalizedValue = String(value || "").trim();
|
|
166
|
+
if (!normalizedValue) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const parsed = new URL(normalizedValue);
|
|
172
|
+
const protocol = String(parsed.protocol || "").toLowerCase();
|
|
173
|
+
return protocol === "http:" || protocol === "https:";
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeInlineOptions(options = {}) {
|
|
180
|
+
return options?.inlineOptions && typeof options.inlineOptions === "object" ? options.inlineOptions : {};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parsePortNumber(rawValue, {
|
|
184
|
+
createCliError,
|
|
185
|
+
optionLabel = "--port"
|
|
186
|
+
} = {}) {
|
|
187
|
+
const normalizedValue = String(rawValue || "").trim();
|
|
188
|
+
if (!normalizedValue) {
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const numericValue = Number(normalizedValue);
|
|
193
|
+
if (!Number.isInteger(numericValue) || numericValue < 1 || numericValue > 65535) {
|
|
194
|
+
throw createCliError(`${optionLabel} must be an integer between 1 and 65535.`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return numericValue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseAdbDeviceList(output = "") {
|
|
201
|
+
return String(output || "")
|
|
202
|
+
.split(/\r?\n/u)
|
|
203
|
+
.map((line) => String(line || "").trim())
|
|
204
|
+
.filter((line) => line && line !== "List of devices attached")
|
|
205
|
+
.map((line) => {
|
|
206
|
+
const match = /^(\S+)\s+(\S+)(?:\s+(.*))?$/u.exec(line);
|
|
207
|
+
if (!match) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
serial: String(match[1] || "").trim(),
|
|
212
|
+
state: String(match[2] || "").trim(),
|
|
213
|
+
details: String(match[3] || "").trim()
|
|
214
|
+
});
|
|
215
|
+
})
|
|
216
|
+
.filter(Boolean);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function resolveAdbReversePort({
|
|
220
|
+
mobileConfig = null,
|
|
221
|
+
explicitPort = "",
|
|
222
|
+
createCliError
|
|
223
|
+
} = {}) {
|
|
224
|
+
const parsedExplicitPort = parsePortNumber(explicitPort, {
|
|
225
|
+
createCliError,
|
|
226
|
+
optionLabel: "--port"
|
|
227
|
+
});
|
|
228
|
+
if (parsedExplicitPort > 0) {
|
|
229
|
+
return parsedExplicitPort;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const apiBaseUrl = String(mobileConfig?.apiBaseUrl || "").trim();
|
|
233
|
+
if (!apiBaseUrl) {
|
|
234
|
+
throw createCliError("config.mobile.apiBaseUrl is required to infer the adb reverse port. Pass --port to override it.");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let parsedUrl = null;
|
|
238
|
+
try {
|
|
239
|
+
parsedUrl = new URL(apiBaseUrl);
|
|
240
|
+
} catch {
|
|
241
|
+
throw createCliError("config.mobile.apiBaseUrl must be a valid absolute URL to infer the adb reverse port.");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const hostname = String(parsedUrl.hostname || "").trim().toLowerCase();
|
|
245
|
+
const isLoopbackHost =
|
|
246
|
+
hostname === "127.0.0.1" ||
|
|
247
|
+
hostname === "localhost" ||
|
|
248
|
+
hostname === "::1" ||
|
|
249
|
+
hostname === "[::1]";
|
|
250
|
+
if (!isLoopbackHost) {
|
|
251
|
+
throw createCliError(
|
|
252
|
+
`config.mobile.apiBaseUrl points at "${apiBaseUrl}", which is not a loopback host. Pass --port explicitly if you still need adb reverse.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const port = Number(parsedUrl.port || "");
|
|
257
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
258
|
+
throw createCliError(
|
|
259
|
+
`config.mobile.apiBaseUrl "${apiBaseUrl}" must include an explicit port so jskit mobile tunnel android can infer adb reverse.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return port;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function runCapturedBinary(binaryName, args = [], {
|
|
267
|
+
cwd = process.cwd(),
|
|
268
|
+
env = {},
|
|
269
|
+
createCliError,
|
|
270
|
+
notFoundMessage = ""
|
|
271
|
+
} = {}) {
|
|
272
|
+
const spawnedEnv = {
|
|
273
|
+
...process.env,
|
|
274
|
+
...env
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
return await new Promise((resolve, reject) => {
|
|
278
|
+
const child = spawn(binaryName, Array.isArray(args) ? args : [], {
|
|
279
|
+
cwd,
|
|
280
|
+
env: spawnedEnv,
|
|
281
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
let stdout = "";
|
|
285
|
+
let stderr = "";
|
|
286
|
+
|
|
287
|
+
child.stdout?.on("data", (chunk) => {
|
|
288
|
+
stdout += String(chunk || "");
|
|
289
|
+
});
|
|
290
|
+
child.stderr?.on("data", (chunk) => {
|
|
291
|
+
stderr += String(chunk || "");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
child.on("error", (error) => {
|
|
295
|
+
if (error?.code === "ENOENT") {
|
|
296
|
+
reject(createCliError(notFoundMessage || `Could not find "${binaryName}" on PATH.`));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
reject(error);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
child.on("exit", (code) => {
|
|
303
|
+
if (code === 0) {
|
|
304
|
+
resolve(Object.freeze({
|
|
305
|
+
stdout,
|
|
306
|
+
stderr
|
|
307
|
+
}));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const renderedArgs = Array.isArray(args) ? args.join(" ") : "";
|
|
311
|
+
const errorMessage = stderr.trim() || stdout.trim() || `${binaryName} ${renderedArgs}`.trim();
|
|
312
|
+
reject(createCliError(`${binaryName}${renderedArgs ? ` ${renderedArgs}` : ""} failed: ${errorMessage}`));
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function listVisibleAndroidDevices({
|
|
318
|
+
ctx,
|
|
319
|
+
appRoot
|
|
320
|
+
} = {}) {
|
|
321
|
+
const result = await runCapturedBinary("adb", ["devices", "-l"], {
|
|
322
|
+
cwd: appRoot,
|
|
323
|
+
createCliError: ctx.createCliError,
|
|
324
|
+
notFoundMessage: 'Could not find "adb" on PATH. Install Android platform-tools first.'
|
|
325
|
+
});
|
|
326
|
+
return parseAdbDeviceList(result.stdout);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function resolveAndroidDeviceTarget({
|
|
330
|
+
ctx,
|
|
331
|
+
appRoot,
|
|
332
|
+
explicitTarget = "",
|
|
333
|
+
commandLabel = "this mobile command"
|
|
334
|
+
} = {}) {
|
|
335
|
+
const devices = await listVisibleAndroidDevices({
|
|
336
|
+
ctx,
|
|
337
|
+
appRoot
|
|
338
|
+
});
|
|
339
|
+
if (devices.length < 1) {
|
|
340
|
+
throw ctx.createCliError(`No Android devices are visible to adb. Run jskit mobile devices android before ${commandLabel}.`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const normalizedExplicitTarget = String(explicitTarget || "").trim();
|
|
344
|
+
const selectedDevice = normalizedExplicitTarget
|
|
345
|
+
? devices.find((device) => device.serial === normalizedExplicitTarget) || null
|
|
346
|
+
: devices[0];
|
|
347
|
+
|
|
348
|
+
if (!selectedDevice) {
|
|
349
|
+
throw ctx.createCliError(`Android device "${normalizedExplicitTarget}" is not visible to adb. Run jskit mobile devices android first.`);
|
|
350
|
+
}
|
|
351
|
+
if (selectedDevice.state !== "device") {
|
|
352
|
+
throw ctx.createCliError(`Android device "${selectedDevice.serial}" is currently "${selectedDevice.state}", not ready for ${commandLabel}.`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return selectedDevice;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function resolveInstalledMobileConfigForCommand({
|
|
359
|
+
appRoot,
|
|
360
|
+
createCliError
|
|
361
|
+
} = {}) {
|
|
362
|
+
try {
|
|
363
|
+
return await resolveInstalledMobileConfig(appRoot);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
const message = String(error?.message || error || "unknown error");
|
|
366
|
+
throw createCliError(`config.mobile is invalid: ${message}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function runLocalBinary(binaryName, args = [], {
|
|
371
|
+
appRoot,
|
|
372
|
+
cwd = appRoot,
|
|
373
|
+
env = {},
|
|
374
|
+
stderr,
|
|
375
|
+
stdout,
|
|
376
|
+
pathModule,
|
|
377
|
+
createCliError,
|
|
378
|
+
dryRun = false
|
|
379
|
+
} = {}) {
|
|
380
|
+
if (dryRun === true) {
|
|
381
|
+
const renderedArgs = Array.isArray(args) ? args.join(" ") : "";
|
|
382
|
+
stdout?.write(`[dry-run] ${binaryName}${renderedArgs ? ` ${renderedArgs}` : ""}\n`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const localBinDirectory = pathModule.join(appRoot, "node_modules", ".bin");
|
|
387
|
+
const inheritedPath = String(process.env.PATH || "");
|
|
388
|
+
const spawnedEnv = {
|
|
389
|
+
...process.env,
|
|
390
|
+
...env,
|
|
391
|
+
PATH: `${localBinDirectory}${pathModule.delimiter}${inheritedPath}`
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
await new Promise((resolve, reject) => {
|
|
395
|
+
const child = spawn(binaryName, Array.isArray(args) ? args : [], {
|
|
396
|
+
cwd,
|
|
397
|
+
env: spawnedEnv,
|
|
398
|
+
stdio: "inherit"
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
child.on("error", (error) => {
|
|
402
|
+
if (error?.code === "ENOENT") {
|
|
403
|
+
reject(
|
|
404
|
+
createCliError(
|
|
405
|
+
`Could not find local "${binaryName}" in node_modules/.bin. Re-run jskit mobile add capacitor after npm install succeeds.`
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
reject(error);
|
|
411
|
+
});
|
|
412
|
+
child.on("exit", (code) => {
|
|
413
|
+
if (code === 0) {
|
|
414
|
+
resolve();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
reject(createCliError(`${binaryName} ${args.join(" ")} failed with exit code ${code}.`));
|
|
418
|
+
});
|
|
419
|
+
}).catch((error) => {
|
|
420
|
+
stderr.write(`${binaryName} failed: ${error.message}\n`);
|
|
421
|
+
throw error;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function runMobileAppInstall({
|
|
426
|
+
ctx,
|
|
427
|
+
appRoot,
|
|
428
|
+
stdout,
|
|
429
|
+
stderr,
|
|
430
|
+
dryRun = false,
|
|
431
|
+
devlinks = false
|
|
432
|
+
} = {}) {
|
|
433
|
+
const {
|
|
434
|
+
path: pathModule,
|
|
435
|
+
loadAppPackageJson
|
|
436
|
+
} = ctx;
|
|
437
|
+
const { packageJson } = await loadAppPackageJson(appRoot);
|
|
438
|
+
const packageScripts = packageJson?.scripts && typeof packageJson.scripts === "object" ? packageJson.scripts : {};
|
|
439
|
+
|
|
440
|
+
await runLocalBinary("npm", ["install"], {
|
|
441
|
+
appRoot,
|
|
442
|
+
stderr,
|
|
443
|
+
stdout,
|
|
444
|
+
pathModule,
|
|
445
|
+
createCliError: ctx.createCliError,
|
|
446
|
+
dryRun
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
if (devlinks === true && Object.prototype.hasOwnProperty.call(packageScripts, "devlinks")) {
|
|
450
|
+
await runLocalBinary("npm", ["run", "--if-present", "devlinks"], {
|
|
451
|
+
appRoot,
|
|
452
|
+
stderr,
|
|
453
|
+
stdout,
|
|
454
|
+
pathModule,
|
|
455
|
+
createCliError: ctx.createCliError,
|
|
456
|
+
dryRun
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function refreshManagedMobileFiles({
|
|
462
|
+
ctx,
|
|
463
|
+
commandAdd,
|
|
464
|
+
appRoot,
|
|
465
|
+
options = {},
|
|
466
|
+
stdout,
|
|
467
|
+
stderr
|
|
468
|
+
} = {}) {
|
|
469
|
+
const {
|
|
470
|
+
path: pathModule
|
|
471
|
+
} = ctx;
|
|
472
|
+
const packageJsonPath = pathModule.join(appRoot, "package.json");
|
|
473
|
+
const packageJsonBefore = await readFile(packageJsonPath, "utf8");
|
|
474
|
+
let capturedStdout = "";
|
|
475
|
+
await commandAdd({
|
|
476
|
+
positional: ["package", CAPACITOR_RUNTIME_PACKAGE_ID],
|
|
477
|
+
options: {
|
|
478
|
+
...options,
|
|
479
|
+
forceReapplyTarget: true,
|
|
480
|
+
runNpmInstall: false,
|
|
481
|
+
inlineOptions: {}
|
|
482
|
+
},
|
|
483
|
+
cwd: appRoot,
|
|
484
|
+
io: {
|
|
485
|
+
stdout: {
|
|
486
|
+
write(chunk) {
|
|
487
|
+
capturedStdout += String(chunk || "");
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
stderr
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
const packageJsonAfter = await readFile(packageJsonPath, "utf8");
|
|
494
|
+
const parsedPackageJsonAfter = JSON.parse(packageJsonAfter);
|
|
495
|
+
const missingInstalledDependencies = await collectMissingInstalledDependencyNames(ctx, appRoot, parsedPackageJsonAfter);
|
|
496
|
+
|
|
497
|
+
if (!/Touched files \(0\):/u.test(capturedStdout)) {
|
|
498
|
+
stdout.write(capturedStdout);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (
|
|
502
|
+
options?.dryRun !== true &&
|
|
503
|
+
(packageJsonAfter !== packageJsonBefore || missingInstalledDependencies.length > 0)
|
|
504
|
+
) {
|
|
505
|
+
await runMobileAppInstall({
|
|
506
|
+
ctx,
|
|
507
|
+
appRoot,
|
|
508
|
+
stdout,
|
|
509
|
+
stderr,
|
|
510
|
+
dryRun: false,
|
|
511
|
+
devlinks: options?.devlinks === true
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function runMobileAddCapacitorCommand({
|
|
517
|
+
commandAdd,
|
|
518
|
+
appRoot,
|
|
519
|
+
options = {},
|
|
520
|
+
stdout,
|
|
521
|
+
stderr
|
|
522
|
+
}) {
|
|
523
|
+
return await commandAdd({
|
|
524
|
+
positional: ["package", CAPACITOR_RUNTIME_PACKAGE_ID],
|
|
525
|
+
options: {
|
|
526
|
+
...options,
|
|
527
|
+
runNpmInstall: true,
|
|
528
|
+
inlineOptions: {}
|
|
529
|
+
},
|
|
530
|
+
cwd: appRoot,
|
|
531
|
+
io: {
|
|
532
|
+
stdout,
|
|
533
|
+
stderr
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function runMobileSyncAndroidCommand({
|
|
539
|
+
ctx,
|
|
540
|
+
commandAdd,
|
|
541
|
+
appRoot,
|
|
542
|
+
options = {},
|
|
543
|
+
stdout,
|
|
544
|
+
stderr
|
|
545
|
+
}) {
|
|
546
|
+
const {
|
|
547
|
+
path: pathModule
|
|
548
|
+
} = ctx;
|
|
549
|
+
|
|
550
|
+
await refreshManagedMobileFiles({
|
|
551
|
+
ctx,
|
|
552
|
+
commandAdd,
|
|
553
|
+
appRoot,
|
|
554
|
+
options,
|
|
555
|
+
stdout,
|
|
556
|
+
stderr
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await assertCapacitorShellInstalled({
|
|
560
|
+
ctx,
|
|
561
|
+
appRoot
|
|
562
|
+
});
|
|
563
|
+
await ensureAndroidNativeShellIdentity({
|
|
564
|
+
ctx,
|
|
565
|
+
appRoot,
|
|
566
|
+
dryRun: options?.dryRun === true,
|
|
567
|
+
stdout
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
await runLocalBinary("npm", ["run", "build"], {
|
|
571
|
+
appRoot,
|
|
572
|
+
stderr,
|
|
573
|
+
stdout,
|
|
574
|
+
pathModule,
|
|
575
|
+
createCliError: ctx.createCliError,
|
|
576
|
+
dryRun: options?.dryRun === true
|
|
577
|
+
});
|
|
578
|
+
await runLocalBinary("cap", ["sync", "android"], {
|
|
579
|
+
appRoot,
|
|
580
|
+
stderr,
|
|
581
|
+
stdout,
|
|
582
|
+
pathModule,
|
|
583
|
+
createCliError: ctx.createCliError,
|
|
584
|
+
dryRun: options?.dryRun === true
|
|
585
|
+
});
|
|
586
|
+
await ensureAndroidManifestDeepLinks({
|
|
587
|
+
ctx,
|
|
588
|
+
appRoot,
|
|
589
|
+
dryRun: options?.dryRun === true,
|
|
590
|
+
stdout
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
if (options?.dryRun === true) {
|
|
594
|
+
return 0;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
stdout.write("[mobile] Built dist/ and synced the Android shell.\n");
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function runMobileRunAndroidCommand({
|
|
602
|
+
ctx,
|
|
603
|
+
commandAdd,
|
|
604
|
+
appRoot,
|
|
605
|
+
options = {},
|
|
606
|
+
stdout,
|
|
607
|
+
stderr
|
|
608
|
+
}) {
|
|
609
|
+
const {
|
|
610
|
+
path: pathModule
|
|
611
|
+
} = ctx;
|
|
612
|
+
const inlineOptions = normalizeInlineOptions(options);
|
|
613
|
+
const target = String(inlineOptions.target || "").trim();
|
|
614
|
+
const mobileConfig = await resolveInstalledMobileConfigForCommand({
|
|
615
|
+
appRoot,
|
|
616
|
+
createCliError: ctx.createCliError
|
|
617
|
+
});
|
|
618
|
+
if (options?.dryRun !== true) {
|
|
619
|
+
await assertAndroidSdkConfigured({
|
|
620
|
+
ctx,
|
|
621
|
+
appRoot
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (mobileConfig.assetMode === "bundled") {
|
|
626
|
+
await runMobileSyncAndroidCommand({
|
|
627
|
+
ctx,
|
|
628
|
+
commandAdd,
|
|
629
|
+
appRoot,
|
|
630
|
+
options,
|
|
631
|
+
stdout,
|
|
632
|
+
stderr
|
|
633
|
+
});
|
|
634
|
+
} else {
|
|
635
|
+
await refreshManagedMobileFiles({
|
|
636
|
+
ctx,
|
|
637
|
+
commandAdd,
|
|
638
|
+
appRoot,
|
|
639
|
+
options,
|
|
640
|
+
stdout,
|
|
641
|
+
stderr
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
await assertCapacitorShellInstalled({
|
|
645
|
+
ctx,
|
|
646
|
+
appRoot
|
|
647
|
+
});
|
|
648
|
+
await ensureAndroidNativeShellIdentity({
|
|
649
|
+
ctx,
|
|
650
|
+
appRoot,
|
|
651
|
+
dryRun: options?.dryRun === true,
|
|
652
|
+
stdout
|
|
653
|
+
});
|
|
654
|
+
await runLocalBinary("cap", ["sync", "android"], {
|
|
655
|
+
appRoot,
|
|
656
|
+
stderr,
|
|
657
|
+
stdout,
|
|
658
|
+
pathModule,
|
|
659
|
+
createCliError: ctx.createCliError,
|
|
660
|
+
dryRun: options?.dryRun === true
|
|
661
|
+
});
|
|
662
|
+
await ensureAndroidManifestDeepLinks({
|
|
663
|
+
ctx,
|
|
664
|
+
appRoot,
|
|
665
|
+
dryRun: options?.dryRun === true,
|
|
666
|
+
stdout
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
if (options?.dryRun !== true) {
|
|
670
|
+
stdout.write("[mobile] Synced the Android shell against the configured dev server.\n");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
await runCapRunAndroidCommand({
|
|
675
|
+
ctx,
|
|
676
|
+
appRoot,
|
|
677
|
+
pathModule,
|
|
678
|
+
target,
|
|
679
|
+
stdout,
|
|
680
|
+
stderr,
|
|
681
|
+
dryRun: options?.dryRun === true
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
if (options?.dryRun === true) {
|
|
685
|
+
return 0;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
stdout.write("[mobile] Ran the Android shell via Capacitor.\n");
|
|
689
|
+
return 0;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function runCapRunAndroidCommand({
|
|
693
|
+
ctx,
|
|
694
|
+
appRoot,
|
|
695
|
+
pathModule,
|
|
696
|
+
target = "",
|
|
697
|
+
stdout,
|
|
698
|
+
stderr,
|
|
699
|
+
dryRun = false
|
|
700
|
+
} = {}) {
|
|
701
|
+
const capRunArgs = ["run", "android"];
|
|
702
|
+
if (target) {
|
|
703
|
+
capRunArgs.push("--target", target);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
await runLocalBinary("cap", capRunArgs, {
|
|
707
|
+
appRoot,
|
|
708
|
+
stderr,
|
|
709
|
+
stdout,
|
|
710
|
+
pathModule,
|
|
711
|
+
createCliError: ctx.createCliError,
|
|
712
|
+
dryRun
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function runMobileBuildAndroidCommand({
|
|
717
|
+
ctx,
|
|
718
|
+
commandAdd,
|
|
719
|
+
appRoot,
|
|
720
|
+
options = {},
|
|
721
|
+
stdout,
|
|
722
|
+
stderr
|
|
723
|
+
}) {
|
|
724
|
+
const {
|
|
725
|
+
path: pathModule,
|
|
726
|
+
createCliError
|
|
727
|
+
} = ctx;
|
|
728
|
+
const mobileConfig = await resolveInstalledMobileConfigForCommand({
|
|
729
|
+
appRoot,
|
|
730
|
+
createCliError
|
|
731
|
+
});
|
|
732
|
+
if (options?.dryRun !== true) {
|
|
733
|
+
await assertAndroidSdkConfigured({
|
|
734
|
+
ctx,
|
|
735
|
+
appRoot
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (mobileConfig.assetMode !== "bundled") {
|
|
740
|
+
throw createCliError(
|
|
741
|
+
'jskit mobile build android requires config.mobile.assetMode="bundled" so the release shell does not depend on a live dev server.'
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
await runMobileSyncAndroidCommand({
|
|
746
|
+
ctx,
|
|
747
|
+
commandAdd,
|
|
748
|
+
appRoot,
|
|
749
|
+
options,
|
|
750
|
+
stdout,
|
|
751
|
+
stderr
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
const gradleCommand = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
|
|
755
|
+
await runLocalBinary(gradleCommand, ["bundleRelease"], {
|
|
756
|
+
appRoot,
|
|
757
|
+
cwd: path.join(appRoot, "android"),
|
|
758
|
+
stderr,
|
|
759
|
+
stdout,
|
|
760
|
+
pathModule,
|
|
761
|
+
createCliError,
|
|
762
|
+
dryRun: options?.dryRun === true
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
if (options?.dryRun === true) {
|
|
766
|
+
return 0;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
stdout.write("[mobile] Built the Android release AAB with Gradle.\n");
|
|
770
|
+
return 0;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function runMobileDoctorCommand({
|
|
774
|
+
ctx,
|
|
775
|
+
appRoot,
|
|
776
|
+
stdout
|
|
777
|
+
}) {
|
|
778
|
+
const {
|
|
779
|
+
fileExists,
|
|
780
|
+
createCliError,
|
|
781
|
+
path: pathModule,
|
|
782
|
+
normalizeRelativePath
|
|
783
|
+
} = ctx;
|
|
784
|
+
const issues = [];
|
|
785
|
+
let mobileConfig = null;
|
|
786
|
+
try {
|
|
787
|
+
mobileConfig = await resolveInstalledMobileConfigForCommand({
|
|
788
|
+
appRoot,
|
|
789
|
+
createCliError
|
|
790
|
+
});
|
|
791
|
+
} catch (error) {
|
|
792
|
+
issues.push(String(error?.message || error || "config.mobile is invalid."));
|
|
793
|
+
}
|
|
794
|
+
const sdkDetails = await resolveAndroidSdkDetails({
|
|
795
|
+
appRoot
|
|
796
|
+
});
|
|
797
|
+
const capacitorConfigPath = pathModule.join(appRoot, "capacitor.config.json");
|
|
798
|
+
const androidDirectoryPath = pathModule.join(appRoot, "android");
|
|
799
|
+
const manifestPath = pathModule.join(appRoot, "android", "app", "src", "main", "AndroidManifest.xml");
|
|
800
|
+
|
|
801
|
+
if (mobileConfig) {
|
|
802
|
+
if (mobileConfig.enabled !== true) {
|
|
803
|
+
issues.push("config.mobile.enabled must be true.");
|
|
804
|
+
}
|
|
805
|
+
if (mobileConfig.strategy !== "capacitor") {
|
|
806
|
+
issues.push('config.mobile.strategy must be "capacitor".');
|
|
807
|
+
}
|
|
808
|
+
if (!mobileConfig.apiBaseUrl) {
|
|
809
|
+
issues.push("config.mobile.apiBaseUrl must be set to the real JSKIT server origin.");
|
|
810
|
+
} else if (mobileConfig.apiBaseUrl === "https://api.example.com") {
|
|
811
|
+
issues.push("config.mobile.apiBaseUrl is still using the example placeholder.");
|
|
812
|
+
}
|
|
813
|
+
if (mobileConfig.assetMode === "dev_server") {
|
|
814
|
+
if (!mobileConfig.devServerUrl) {
|
|
815
|
+
issues.push('config.mobile.devServerUrl must be set when config.mobile.assetMode="dev_server".');
|
|
816
|
+
} else if (!isValidHttpOrHttpsUrl(mobileConfig.devServerUrl)) {
|
|
817
|
+
issues.push("config.mobile.devServerUrl must be a valid absolute http/https URL.");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (String(mobileConfig.appId || "").startsWith("com.example.")) {
|
|
821
|
+
issues.push("config.mobile.appId is still using the example placeholder namespace.");
|
|
822
|
+
}
|
|
823
|
+
if (String(mobileConfig.android.packageName || "").startsWith("com.example.")) {
|
|
824
|
+
issues.push("config.mobile.android.packageName is still using the example placeholder namespace.");
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (!(await fileExists(capacitorConfigPath))) {
|
|
828
|
+
issues.push(`Missing ${normalizeRelativePath(appRoot, capacitorConfigPath)}.`);
|
|
829
|
+
}
|
|
830
|
+
if (!(await fileExists(androidDirectoryPath))) {
|
|
831
|
+
issues.push(`Missing ${normalizeRelativePath(appRoot, androidDirectoryPath)}.`);
|
|
832
|
+
}
|
|
833
|
+
if (!(await fileExists(manifestPath))) {
|
|
834
|
+
issues.push(`Missing ${normalizeRelativePath(appRoot, manifestPath)}.`);
|
|
835
|
+
} else if (mobileConfig) {
|
|
836
|
+
const manifestSource = await readFile(manifestPath, "utf8");
|
|
837
|
+
const customScheme = String(mobileConfig?.auth?.customScheme || "").trim().toLowerCase();
|
|
838
|
+
if (customScheme && !manifestSource.includes(`android:scheme="${customScheme}"`)) {
|
|
839
|
+
issues.push(
|
|
840
|
+
`${normalizeRelativePath(appRoot, manifestPath)} is missing the managed deep-link filter for scheme "${customScheme}".`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (!sdkDetails.sdkRoot) {
|
|
845
|
+
issues.push("Android SDK location is not configured. Set ANDROID_HOME or ANDROID_SDK_ROOT, or add android/local.properties.");
|
|
846
|
+
} else if (!(await fileExists(sdkDetails.sdkRoot))) {
|
|
847
|
+
issues.push(`Configured Android SDK path does not exist: ${sdkDetails.sdkRoot} (${sdkDetails.source}).`);
|
|
848
|
+
} else {
|
|
849
|
+
issues.push(...await collectAndroidSdkComponentIssues({
|
|
850
|
+
appRoot,
|
|
851
|
+
sdkRoot: sdkDetails.sdkRoot
|
|
852
|
+
}));
|
|
853
|
+
}
|
|
854
|
+
if (mobileConfig) {
|
|
855
|
+
await collectManagedMobileFileDriftIssues({
|
|
856
|
+
ctx,
|
|
857
|
+
appRoot,
|
|
858
|
+
issues
|
|
859
|
+
});
|
|
860
|
+
issues.push(...await collectAndroidNativeShellIdentityIssues({
|
|
861
|
+
ctx,
|
|
862
|
+
appRoot
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (issues.length > 0) {
|
|
867
|
+
stdout.write("Mobile doctor found issues:\n");
|
|
868
|
+
for (const issue of issues) {
|
|
869
|
+
stdout.write(`- ${issue}\n`);
|
|
870
|
+
}
|
|
871
|
+
return 1;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
stdout.write("Mobile doctor: Android Capacitor shell looks healthy.\n");
|
|
875
|
+
return 0;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function runMobileDevicesAndroidCommand({
|
|
879
|
+
ctx,
|
|
880
|
+
appRoot,
|
|
881
|
+
stdout
|
|
882
|
+
}) {
|
|
883
|
+
const devices = await listVisibleAndroidDevices({
|
|
884
|
+
ctx,
|
|
885
|
+
appRoot
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
if (devices.length < 1) {
|
|
889
|
+
stdout.write("No Android devices visible to adb.\n");
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
stdout.write("Android devices:\n");
|
|
894
|
+
for (const device of devices) {
|
|
895
|
+
const detailSuffix = device.details ? ` ${device.details}` : "";
|
|
896
|
+
stdout.write(`- ${device.serial} ${device.state}${detailSuffix}\n`);
|
|
897
|
+
}
|
|
898
|
+
return 0;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
async function runMobileTunnelAndroidCommand({
|
|
902
|
+
ctx,
|
|
903
|
+
appRoot,
|
|
904
|
+
options = {},
|
|
905
|
+
stdout
|
|
906
|
+
}) {
|
|
907
|
+
const inlineOptions = normalizeInlineOptions(options);
|
|
908
|
+
const target = String(inlineOptions.target || "").trim();
|
|
909
|
+
if (!target) {
|
|
910
|
+
throw ctx.createCliError("jskit mobile tunnel android requires --target <device-id>.");
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const mobileConfig = await resolveInstalledMobileConfigForCommand({
|
|
914
|
+
appRoot,
|
|
915
|
+
createCliError: ctx.createCliError
|
|
916
|
+
});
|
|
917
|
+
const port = resolveAdbReversePort({
|
|
918
|
+
mobileConfig,
|
|
919
|
+
explicitPort: inlineOptions.port,
|
|
920
|
+
createCliError: ctx.createCliError
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
const matchingDevice = await resolveAndroidDeviceTarget({
|
|
924
|
+
ctx,
|
|
925
|
+
appRoot,
|
|
926
|
+
explicitTarget: target,
|
|
927
|
+
commandLabel: "adb reverse"
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
await runCapturedBinary("adb", ["-s", matchingDevice.serial, "reverse", `tcp:${port}`, `tcp:${port}`], {
|
|
931
|
+
cwd: appRoot,
|
|
932
|
+
createCliError: ctx.createCliError,
|
|
933
|
+
notFoundMessage: 'Could not find "adb" on PATH. Install Android platform-tools first.'
|
|
934
|
+
});
|
|
935
|
+
const reverseListResult = await runCapturedBinary("adb", ["-s", matchingDevice.serial, "reverse", "--list"], {
|
|
936
|
+
cwd: appRoot,
|
|
937
|
+
createCliError: ctx.createCliError,
|
|
938
|
+
notFoundMessage: 'Could not find "adb" on PATH. Install Android platform-tools first.'
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
stdout.write(`Android reverse tunnel ready for ${matchingDevice.serial}: tcp:${port} -> tcp:${port}\n`);
|
|
942
|
+
const reverseLines = String(reverseListResult.stdout || "")
|
|
943
|
+
.split(/\r?\n/u)
|
|
944
|
+
.map((line) => String(line || "").trim())
|
|
945
|
+
.filter(Boolean);
|
|
946
|
+
if (reverseLines.length > 0) {
|
|
947
|
+
stdout.write("adb reverse --list:\n");
|
|
948
|
+
for (const line of reverseLines) {
|
|
949
|
+
stdout.write(`- ${line}\n`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return 0;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function runMobileRestartAndroidCommand({
|
|
956
|
+
ctx,
|
|
957
|
+
appRoot,
|
|
958
|
+
options = {},
|
|
959
|
+
stdout
|
|
960
|
+
}) {
|
|
961
|
+
const inlineOptions = normalizeInlineOptions(options);
|
|
962
|
+
const target = String(inlineOptions.target || "").trim();
|
|
963
|
+
if (!target) {
|
|
964
|
+
throw ctx.createCliError("jskit mobile restart android requires --target <device-id>.");
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const mobileConfig = await resolveInstalledMobileConfigForCommand({
|
|
968
|
+
appRoot,
|
|
969
|
+
createCliError: ctx.createCliError
|
|
970
|
+
});
|
|
971
|
+
const packageName =
|
|
972
|
+
String(mobileConfig?.android?.packageName || "").trim() ||
|
|
973
|
+
String(mobileConfig?.appId || "").trim();
|
|
974
|
+
if (!packageName) {
|
|
975
|
+
throw ctx.createCliError("config.mobile.android.packageName or config.mobile.appId is required to restart the Android shell.");
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const matchingDevice = await resolveAndroidDeviceTarget({
|
|
979
|
+
ctx,
|
|
980
|
+
appRoot,
|
|
981
|
+
explicitTarget: target,
|
|
982
|
+
commandLabel: "restart"
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
await runCapturedBinary("adb", ["-s", matchingDevice.serial, "shell", "pm", "clear", packageName], {
|
|
986
|
+
cwd: appRoot,
|
|
987
|
+
createCliError: ctx.createCliError,
|
|
988
|
+
notFoundMessage: 'Could not find "adb" on PATH. Install Android platform-tools first.'
|
|
989
|
+
});
|
|
990
|
+
await runCapturedBinary("adb", ["-s", matchingDevice.serial, "shell", "am", "force-stop", packageName], {
|
|
991
|
+
cwd: appRoot,
|
|
992
|
+
createCliError: ctx.createCliError,
|
|
993
|
+
notFoundMessage: 'Could not find "adb" on PATH. Install Android platform-tools first.'
|
|
994
|
+
});
|
|
995
|
+
await runCapturedBinary("adb", ["-s", matchingDevice.serial, "shell", "am", "start", "-W", "-n", `${packageName}/.MainActivity`], {
|
|
996
|
+
cwd: appRoot,
|
|
997
|
+
createCliError: ctx.createCliError,
|
|
998
|
+
notFoundMessage: 'Could not find "adb" on PATH. Install Android platform-tools first.'
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
stdout.write(`Android app restarted on ${matchingDevice.serial}: ${packageName}\n`);
|
|
1002
|
+
return 0;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
async function runMobileDevAndroidCommand({
|
|
1006
|
+
ctx,
|
|
1007
|
+
commandAdd,
|
|
1008
|
+
appRoot,
|
|
1009
|
+
options = {},
|
|
1010
|
+
stdout,
|
|
1011
|
+
stderr
|
|
1012
|
+
}) {
|
|
1013
|
+
const inlineOptions = normalizeInlineOptions(options);
|
|
1014
|
+
const selectedDevice = await resolveAndroidDeviceTarget({
|
|
1015
|
+
ctx,
|
|
1016
|
+
appRoot,
|
|
1017
|
+
explicitTarget: inlineOptions.target,
|
|
1018
|
+
commandLabel: "the local Android dev flow"
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
stdout.write(`[mobile] Using Android device: ${selectedDevice.serial}\n`);
|
|
1022
|
+
stdout.write("[mobile] Building and syncing the Android shell:\n");
|
|
1023
|
+
stdout.write("[mobile] npx jskit mobile sync android\n");
|
|
1024
|
+
await runMobileSyncAndroidCommand({
|
|
1025
|
+
ctx,
|
|
1026
|
+
commandAdd,
|
|
1027
|
+
appRoot,
|
|
1028
|
+
options,
|
|
1029
|
+
stdout,
|
|
1030
|
+
stderr
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
stdout.write(`[mobile] Installing and launching the app on ${selectedDevice.serial}:\n`);
|
|
1034
|
+
stdout.write(`[mobile] npx jskit mobile run android --target ${selectedDevice.serial}\n`);
|
|
1035
|
+
await runCapRunAndroidCommand({
|
|
1036
|
+
ctx,
|
|
1037
|
+
appRoot,
|
|
1038
|
+
pathModule: ctx.path,
|
|
1039
|
+
target: selectedDevice.serial,
|
|
1040
|
+
stdout,
|
|
1041
|
+
stderr,
|
|
1042
|
+
dryRun: false
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
stdout.write(`[mobile] Creating the adb reverse tunnel on ${selectedDevice.serial}:\n`);
|
|
1046
|
+
stdout.write(`[mobile] npx jskit mobile tunnel android --target ${selectedDevice.serial}\n`);
|
|
1047
|
+
await runMobileTunnelAndroidCommand({
|
|
1048
|
+
ctx,
|
|
1049
|
+
appRoot,
|
|
1050
|
+
options: {
|
|
1051
|
+
inlineOptions: {
|
|
1052
|
+
target: selectedDevice.serial
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
stdout,
|
|
1056
|
+
stderr
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
return 0;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function createMobileCommands(ctx = {}, { commandAdd } = {}) {
|
|
1063
|
+
const {
|
|
1064
|
+
createCliError,
|
|
1065
|
+
resolveAppRootFromCwd
|
|
1066
|
+
} = ctx;
|
|
1067
|
+
|
|
1068
|
+
if (typeof commandAdd !== "function") {
|
|
1069
|
+
throw new TypeError("createMobileCommands requires commandAdd().");
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
async function commandMobile({ positional = [], options = {}, cwd = "", stdout, stderr }) {
|
|
1073
|
+
const firstToken = String(positional[0] || "").trim();
|
|
1074
|
+
const secondToken = String(positional[1] || "").trim();
|
|
1075
|
+
const remainingPositionals = positional.slice(2);
|
|
1076
|
+
|
|
1077
|
+
if (!firstToken) {
|
|
1078
|
+
renderMobileHelp(stdout);
|
|
1079
|
+
return 0;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (firstToken === "help") {
|
|
1083
|
+
renderMobileHelp(stdout, resolveMobileCommandDefinition(secondToken));
|
|
1084
|
+
return 0;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const definition = resolveMobileCommandDefinition(firstToken);
|
|
1088
|
+
if (!definition) {
|
|
1089
|
+
throw createCliError(`Unknown mobile subcommand: ${firstToken}.`, {
|
|
1090
|
+
renderUsage: () => renderMobileHelp(stderr)
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (secondToken === "help") {
|
|
1095
|
+
renderMobileHelp(stdout, definition);
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const optionMeta = buildMobileCommandOptionMeta(definition.name);
|
|
1100
|
+
const supportedOptionNames = new Set(Object.keys(optionMeta));
|
|
1101
|
+
const inlineOptionNames = Object.keys(options?.inlineOptions && typeof options.inlineOptions === "object" ? options.inlineOptions : {});
|
|
1102
|
+
const unknownInlineOptionNames = inlineOptionNames.filter((optionName) => !supportedOptionNames.has(optionName));
|
|
1103
|
+
if (unknownInlineOptionNames.length > 0) {
|
|
1104
|
+
throw createCliError(
|
|
1105
|
+
`Unknown option${unknownInlineOptionNames.length === 1 ? "" : "s"} for jskit mobile ${definition.name}: ${unknownInlineOptionNames.map((optionName) => `--${optionName}`).join(", ")}.`,
|
|
1106
|
+
{
|
|
1107
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1108
|
+
}
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
if (options?.dryRun === true && !supportedOptionNames.has("dry-run")) {
|
|
1112
|
+
throw createCliError(`Unknown option for jskit mobile ${definition.name}: --dry-run.`, {
|
|
1113
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const appRoot = await resolveAppRootFromCwd(cwd);
|
|
1118
|
+
|
|
1119
|
+
if (definition.name === "devices") {
|
|
1120
|
+
if (secondToken !== "android") {
|
|
1121
|
+
throw createCliError(`jskit mobile devices currently supports only "android".`, {
|
|
1122
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
if (remainingPositionals.length > 0) {
|
|
1126
|
+
throw createCliError(`Unexpected positional arguments for jskit mobile devices: ${remainingPositionals.join(" ")}`, {
|
|
1127
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return runMobileDevicesAndroidCommand({
|
|
1132
|
+
ctx,
|
|
1133
|
+
appRoot,
|
|
1134
|
+
stdout,
|
|
1135
|
+
stderr
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (definition.name === "dev") {
|
|
1140
|
+
if (secondToken !== "android") {
|
|
1141
|
+
throw createCliError(`jskit mobile dev currently supports only "android".`, {
|
|
1142
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
if (remainingPositionals.length > 0) {
|
|
1146
|
+
throw createCliError(`Unexpected positional arguments for jskit mobile dev: ${remainingPositionals.join(" ")}`, {
|
|
1147
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return runMobileDevAndroidCommand({
|
|
1152
|
+
ctx,
|
|
1153
|
+
commandAdd,
|
|
1154
|
+
appRoot,
|
|
1155
|
+
options,
|
|
1156
|
+
stdout,
|
|
1157
|
+
stderr
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (definition.name === "tunnel") {
|
|
1162
|
+
if (secondToken !== "android") {
|
|
1163
|
+
throw createCliError(`jskit mobile tunnel currently supports only "android".`, {
|
|
1164
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
if (remainingPositionals.length > 0) {
|
|
1168
|
+
throw createCliError(`Unexpected positional arguments for jskit mobile tunnel: ${remainingPositionals.join(" ")}`, {
|
|
1169
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return runMobileTunnelAndroidCommand({
|
|
1174
|
+
ctx,
|
|
1175
|
+
appRoot,
|
|
1176
|
+
options,
|
|
1177
|
+
stdout,
|
|
1178
|
+
stderr
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (definition.name === "restart") {
|
|
1183
|
+
if (secondToken !== "android") {
|
|
1184
|
+
throw createCliError(`jskit mobile restart currently supports only "android".`, {
|
|
1185
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
if (remainingPositionals.length > 0) {
|
|
1189
|
+
throw createCliError(`Unexpected positional arguments for jskit mobile restart: ${remainingPositionals.join(" ")}`, {
|
|
1190
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return runMobileRestartAndroidCommand({
|
|
1195
|
+
ctx,
|
|
1196
|
+
appRoot,
|
|
1197
|
+
options,
|
|
1198
|
+
stdout,
|
|
1199
|
+
stderr
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (definition.name === "add") {
|
|
1204
|
+
if (secondToken !== "capacitor") {
|
|
1205
|
+
throw createCliError(`jskit mobile add currently supports only "capacitor".`, {
|
|
1206
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
if (remainingPositionals.length > 0) {
|
|
1210
|
+
throw createCliError(`Unexpected positional arguments for jskit mobile add: ${remainingPositionals.join(" ")}`, {
|
|
1211
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return runMobileAddCapacitorCommand({
|
|
1216
|
+
ctx,
|
|
1217
|
+
commandAdd,
|
|
1218
|
+
appRoot,
|
|
1219
|
+
options,
|
|
1220
|
+
stdout,
|
|
1221
|
+
stderr
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (definition.name === "sync") {
|
|
1226
|
+
if (secondToken !== "android") {
|
|
1227
|
+
throw createCliError(`jskit mobile sync currently supports only "android".`, {
|
|
1228
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
if (remainingPositionals.length > 0) {
|
|
1232
|
+
throw createCliError(`Unexpected positional arguments for jskit mobile sync: ${remainingPositionals.join(" ")}`, {
|
|
1233
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
return runMobileSyncAndroidCommand({
|
|
1238
|
+
ctx,
|
|
1239
|
+
commandAdd,
|
|
1240
|
+
appRoot,
|
|
1241
|
+
options,
|
|
1242
|
+
stdout,
|
|
1243
|
+
stderr
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (definition.name === "run") {
|
|
1248
|
+
if (secondToken !== "android") {
|
|
1249
|
+
throw createCliError(`jskit mobile run currently supports only "android".`, {
|
|
1250
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
if (remainingPositionals.length > 0) {
|
|
1254
|
+
throw createCliError(`Unexpected positional arguments for jskit mobile run: ${remainingPositionals.join(" ")}`, {
|
|
1255
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
return runMobileRunAndroidCommand({
|
|
1260
|
+
ctx,
|
|
1261
|
+
commandAdd,
|
|
1262
|
+
appRoot,
|
|
1263
|
+
options,
|
|
1264
|
+
stdout,
|
|
1265
|
+
stderr
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (definition.name === "build") {
|
|
1270
|
+
if (secondToken !== "android") {
|
|
1271
|
+
throw createCliError(`jskit mobile build currently supports only "android".`, {
|
|
1272
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
if (remainingPositionals.length > 0) {
|
|
1276
|
+
throw createCliError(`Unexpected positional arguments for jskit mobile build: ${remainingPositionals.join(" ")}`, {
|
|
1277
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return runMobileBuildAndroidCommand({
|
|
1282
|
+
ctx,
|
|
1283
|
+
commandAdd,
|
|
1284
|
+
appRoot,
|
|
1285
|
+
options,
|
|
1286
|
+
stdout,
|
|
1287
|
+
stderr
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (definition.name === "doctor") {
|
|
1292
|
+
if (secondToken) {
|
|
1293
|
+
throw createCliError(`Unexpected positional arguments for jskit mobile doctor: ${[secondToken, ...remainingPositionals].join(" ")}`, {
|
|
1294
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return runMobileDoctorCommand({
|
|
1299
|
+
ctx,
|
|
1300
|
+
appRoot,
|
|
1301
|
+
stdout,
|
|
1302
|
+
stderr
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
throw createCliError(`Unhandled mobile subcommand: ${definition.name}.`, {
|
|
1307
|
+
renderUsage: () => renderMobileHelp(stderr, definition)
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return {
|
|
1312
|
+
commandMobile
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
export { createMobileCommands };
|