@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,929 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { access, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import {
|
|
4
|
+
loadAppConfigFromAppRoot,
|
|
5
|
+
resolveMobileConfig
|
|
6
|
+
} from "@jskit-ai/kernel/server/support";
|
|
7
|
+
import { ensureArray, ensureObject } from "../shared/collectionUtils.js";
|
|
8
|
+
import { loadPackageRegistry } from "../cliRuntime/packageRegistries.js";
|
|
9
|
+
import { resolvePackageTemplateRoot } from "../cliRuntime/packageTemplateResolution.js";
|
|
10
|
+
import {
|
|
11
|
+
interpolateFileMutationRecord,
|
|
12
|
+
renderTemplateFile,
|
|
13
|
+
resolveTemplateContextReplacementsForMutation
|
|
14
|
+
} from "../cliRuntime/mutations/templateContext.js";
|
|
15
|
+
|
|
16
|
+
const CAPACITOR_CONFIG_FILE = "capacitor.config.json";
|
|
17
|
+
const CAPACITOR_RUNTIME_PACKAGE_ID = "@jskit-ai/mobile-capacitor";
|
|
18
|
+
const PUBLIC_CONFIG_RELATIVE_PATH = path.join("config", "public.js");
|
|
19
|
+
const ANDROID_DIRECTORY_NAME = "android";
|
|
20
|
+
const ANDROID_MANIFEST_RELATIVE_PATH = path.join(
|
|
21
|
+
ANDROID_DIRECTORY_NAME,
|
|
22
|
+
"app",
|
|
23
|
+
"src",
|
|
24
|
+
"main",
|
|
25
|
+
"AndroidManifest.xml"
|
|
26
|
+
);
|
|
27
|
+
const ANDROID_VARIABLES_RELATIVE_PATH = path.join(
|
|
28
|
+
ANDROID_DIRECTORY_NAME,
|
|
29
|
+
"variables.gradle"
|
|
30
|
+
);
|
|
31
|
+
const ANDROID_APP_BUILD_GRADLE_RELATIVE_PATH = path.join(
|
|
32
|
+
ANDROID_DIRECTORY_NAME,
|
|
33
|
+
"app",
|
|
34
|
+
"build.gradle"
|
|
35
|
+
);
|
|
36
|
+
const ANDROID_STRINGS_RELATIVE_PATH = path.join(
|
|
37
|
+
ANDROID_DIRECTORY_NAME,
|
|
38
|
+
"app",
|
|
39
|
+
"src",
|
|
40
|
+
"main",
|
|
41
|
+
"res",
|
|
42
|
+
"values",
|
|
43
|
+
"strings.xml"
|
|
44
|
+
);
|
|
45
|
+
const ANDROID_MAIN_JAVA_ROOT_RELATIVE_PATH = path.join(
|
|
46
|
+
ANDROID_DIRECTORY_NAME,
|
|
47
|
+
"app",
|
|
48
|
+
"src",
|
|
49
|
+
"main",
|
|
50
|
+
"java"
|
|
51
|
+
);
|
|
52
|
+
const ANDROID_MAIN_KOTLIN_ROOT_RELATIVE_PATH = path.join(
|
|
53
|
+
ANDROID_DIRECTORY_NAME,
|
|
54
|
+
"app",
|
|
55
|
+
"src",
|
|
56
|
+
"main",
|
|
57
|
+
"kotlin"
|
|
58
|
+
);
|
|
59
|
+
const MANAGED_DEEP_LINK_START_MARKER = "<!-- jskit-mobile-capacitor:deep-links:start -->";
|
|
60
|
+
const MANAGED_DEEP_LINK_END_MARKER = "<!-- jskit-mobile-capacitor:deep-links:end -->";
|
|
61
|
+
const MANAGED_MOBILE_CONFIG_START_MARKER = "// jskit-mobile-capacitor:config:start";
|
|
62
|
+
const MANAGED_MOBILE_CONFIG_END_MARKER = "// jskit-mobile-capacitor:config:end";
|
|
63
|
+
|
|
64
|
+
function normalizeRelativePosixPath(pathValue = "") {
|
|
65
|
+
return String(pathValue || "")
|
|
66
|
+
.trim()
|
|
67
|
+
.replace(/\\/gu, "/")
|
|
68
|
+
.replace(/^\/+|\/+$/gu, "")
|
|
69
|
+
.replace(/\/{2,}/gu, "/");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function escapeRegExp(value = "") {
|
|
73
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function pathExists(targetPath = "") {
|
|
77
|
+
try {
|
|
78
|
+
await access(targetPath);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function humanizeAppName(value = "") {
|
|
86
|
+
const normalized = String(value || "").trim();
|
|
87
|
+
if (!normalized) {
|
|
88
|
+
return "Example App";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const words = normalized
|
|
92
|
+
.replace(/^@/u, "")
|
|
93
|
+
.replace(/[/._-]+/gu, " ")
|
|
94
|
+
.split(/\s+/u)
|
|
95
|
+
.map((entry) => entry.trim())
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
if (words.length < 1) {
|
|
98
|
+
return "Example App";
|
|
99
|
+
}
|
|
100
|
+
return words
|
|
101
|
+
.map((entry) => entry.charAt(0).toUpperCase() + entry.slice(1))
|
|
102
|
+
.join(" ");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function slugifyForIdentifier(value = "") {
|
|
106
|
+
const normalizedValue = String(value || "").trim().toLowerCase();
|
|
107
|
+
if (!normalizedValue) {
|
|
108
|
+
return "exampleapp";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const packageLeaf = normalizedValue.split("/").filter(Boolean).pop() || normalizedValue;
|
|
112
|
+
const rawParts = packageLeaf
|
|
113
|
+
.replace(/[^a-z0-9]+/gu, " ")
|
|
114
|
+
.split(/\s+/u)
|
|
115
|
+
.map((entry) => entry.trim())
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
const genericSuffixParts = new Set(["app", "mobile", "web", "site", "client"]);
|
|
118
|
+
const filteredParts = rawParts.filter((entry) => !genericSuffixParts.has(entry));
|
|
119
|
+
const candidateParts = filteredParts.length > 0 ? filteredParts : rawParts;
|
|
120
|
+
|
|
121
|
+
return candidateParts
|
|
122
|
+
.join("")
|
|
123
|
+
.replace(/^([0-9]+)/u, "")
|
|
124
|
+
|| "exampleapp";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildManagedMobileConfigStub({ packageJson = {} } = {}) {
|
|
128
|
+
const packageName = String(packageJson?.name || "").trim();
|
|
129
|
+
const packageVersion = String(packageJson?.version || "").trim() || "0.1.0";
|
|
130
|
+
const scheme = slugifyForIdentifier(packageName);
|
|
131
|
+
const appId = `ai.jskit.${scheme}`;
|
|
132
|
+
const appName = humanizeAppName(packageName);
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
`${MANAGED_MOBILE_CONFIG_START_MARKER}`,
|
|
136
|
+
"config.mobile = {",
|
|
137
|
+
" enabled: true,",
|
|
138
|
+
' strategy: "capacitor",',
|
|
139
|
+
` appId: ${JSON.stringify(appId)},`,
|
|
140
|
+
` appName: ${JSON.stringify(appName)},`,
|
|
141
|
+
' assetMode: "bundled",',
|
|
142
|
+
' devServerUrl: "",',
|
|
143
|
+
' apiBaseUrl: "http://127.0.0.1:3000",',
|
|
144
|
+
" auth: {",
|
|
145
|
+
' callbackPath: "/auth/login",',
|
|
146
|
+
` customScheme: ${JSON.stringify(scheme)},`,
|
|
147
|
+
" appLinkDomains: []",
|
|
148
|
+
" },",
|
|
149
|
+
" android: {",
|
|
150
|
+
` packageName: ${JSON.stringify(appId)},`,
|
|
151
|
+
" minSdk: 26,",
|
|
152
|
+
" targetSdk: 35,",
|
|
153
|
+
" versionCode: 1,",
|
|
154
|
+
` versionName: ${JSON.stringify(packageVersion)}`,
|
|
155
|
+
" }",
|
|
156
|
+
"};",
|
|
157
|
+
`${MANAGED_MOBILE_CONFIG_END_MARKER}`
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseAndroidSdkDirFromLocalProperties(source = "") {
|
|
162
|
+
const lines = String(source || "").split(/\r?\n/u);
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
const match = /^\s*sdk\.dir\s*=\s*(.+?)\s*$/u.exec(line);
|
|
165
|
+
if (!match) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
return match[1].replace(/\\:/gu, ":").replace(/\\\\/gu, "\\").trim();
|
|
169
|
+
}
|
|
170
|
+
return "";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildAndroidNativeConfig(mobileConfig = {}) {
|
|
174
|
+
const packageName = String(mobileConfig?.android?.packageName || "").trim();
|
|
175
|
+
const appName = String(mobileConfig?.appName || "").trim();
|
|
176
|
+
const customScheme = String(mobileConfig?.auth?.customScheme || "").trim().toLowerCase();
|
|
177
|
+
const minSdk = String(mobileConfig?.android?.minSdk || "").trim();
|
|
178
|
+
const targetSdk = String(mobileConfig?.android?.targetSdk || "").trim();
|
|
179
|
+
const versionCode = String(mobileConfig?.android?.versionCode || "").trim();
|
|
180
|
+
const versionName = String(mobileConfig?.android?.versionName || "").trim();
|
|
181
|
+
|
|
182
|
+
if (!packageName) {
|
|
183
|
+
throw new Error("config.mobile.android.packageName is required before refreshing the Android shell.");
|
|
184
|
+
}
|
|
185
|
+
if (!appName) {
|
|
186
|
+
throw new Error("config.mobile.appName is required before refreshing the Android shell.");
|
|
187
|
+
}
|
|
188
|
+
if (!customScheme) {
|
|
189
|
+
throw new Error("config.mobile.auth.customScheme is required before refreshing the Android shell.");
|
|
190
|
+
}
|
|
191
|
+
if (!minSdk || !targetSdk || !versionCode || !versionName) {
|
|
192
|
+
throw new Error("config.mobile.android min/target SDK and version fields are required before refreshing the Android shell.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
packageName,
|
|
197
|
+
appName,
|
|
198
|
+
customScheme,
|
|
199
|
+
minSdk,
|
|
200
|
+
compileSdk: targetSdk,
|
|
201
|
+
targetSdk,
|
|
202
|
+
versionCode,
|
|
203
|
+
versionName
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function replaceRequiredPattern(source = "", pattern, replacement, label = "pattern") {
|
|
208
|
+
const normalizedSource = String(source || "");
|
|
209
|
+
if (!pattern.test(normalizedSource)) {
|
|
210
|
+
throw new Error(`Could not locate ${label} while refreshing the Android shell.`);
|
|
211
|
+
}
|
|
212
|
+
return normalizedSource.replace(pattern, replacement);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function escapeXmlText(value = "") {
|
|
216
|
+
return String(value || "")
|
|
217
|
+
.replace(/&/gu, "&")
|
|
218
|
+
.replace(/</gu, "<")
|
|
219
|
+
.replace(/>/gu, ">")
|
|
220
|
+
.replace(/"/gu, """)
|
|
221
|
+
.replace(/'/gu, "'");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function replaceXmlStringValue(source = "", stringName = "", value = "") {
|
|
225
|
+
const escapedValue = escapeXmlText(value);
|
|
226
|
+
return replaceRequiredPattern(
|
|
227
|
+
source,
|
|
228
|
+
new RegExp(`(<string\\s+name="${escapeRegExp(stringName)}">)([\\s\\S]*?)(</string>)`, "u"),
|
|
229
|
+
`$1${escapedValue}$3`,
|
|
230
|
+
`strings.xml value ${stringName}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function renderAndroidVariablesGradleSource(source = "", nativeConfig = {}) {
|
|
235
|
+
let nextSource = String(source || "");
|
|
236
|
+
nextSource = replaceRequiredPattern(
|
|
237
|
+
nextSource,
|
|
238
|
+
/(minSdkVersion\s*=\s*)(\d+)/u,
|
|
239
|
+
`$1${nativeConfig.minSdk}`,
|
|
240
|
+
"variables.gradle minSdkVersion"
|
|
241
|
+
);
|
|
242
|
+
nextSource = replaceRequiredPattern(
|
|
243
|
+
nextSource,
|
|
244
|
+
/(compileSdkVersion\s*=\s*)(\d+)/u,
|
|
245
|
+
`$1${nativeConfig.compileSdk}`,
|
|
246
|
+
"variables.gradle compileSdkVersion"
|
|
247
|
+
);
|
|
248
|
+
nextSource = replaceRequiredPattern(
|
|
249
|
+
nextSource,
|
|
250
|
+
/(targetSdkVersion\s*=\s*)(\d+)/u,
|
|
251
|
+
`$1${nativeConfig.targetSdk}`,
|
|
252
|
+
"variables.gradle targetSdkVersion"
|
|
253
|
+
);
|
|
254
|
+
return nextSource;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderAndroidAppBuildGradleSource(source = "", nativeConfig = {}) {
|
|
258
|
+
let nextSource = String(source || "");
|
|
259
|
+
nextSource = replaceRequiredPattern(
|
|
260
|
+
nextSource,
|
|
261
|
+
/(namespace\s+)(["'])([^"']+)(["'])/u,
|
|
262
|
+
`$1"${nativeConfig.packageName}"`,
|
|
263
|
+
"app/build.gradle namespace"
|
|
264
|
+
);
|
|
265
|
+
nextSource = replaceRequiredPattern(
|
|
266
|
+
nextSource,
|
|
267
|
+
/(applicationId\s+)(["'])([^"']+)(["'])/u,
|
|
268
|
+
`$1"${nativeConfig.packageName}"`,
|
|
269
|
+
"app/build.gradle applicationId"
|
|
270
|
+
);
|
|
271
|
+
nextSource = replaceRequiredPattern(
|
|
272
|
+
nextSource,
|
|
273
|
+
/(versionCode\s+)(\d+)/u,
|
|
274
|
+
`$1${nativeConfig.versionCode}`,
|
|
275
|
+
"app/build.gradle versionCode"
|
|
276
|
+
);
|
|
277
|
+
nextSource = replaceRequiredPattern(
|
|
278
|
+
nextSource,
|
|
279
|
+
/(versionName\s+)(["'])([^"']+)(["'])/u,
|
|
280
|
+
`$1"${nativeConfig.versionName}"`,
|
|
281
|
+
"app/build.gradle versionName"
|
|
282
|
+
);
|
|
283
|
+
return nextSource;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function renderAndroidStringsSource(source = "", nativeConfig = {}) {
|
|
287
|
+
let nextSource = String(source || "");
|
|
288
|
+
nextSource = replaceXmlStringValue(nextSource, "app_name", nativeConfig.appName);
|
|
289
|
+
nextSource = replaceXmlStringValue(nextSource, "title_activity_main", nativeConfig.appName);
|
|
290
|
+
nextSource = replaceXmlStringValue(nextSource, "package_name", nativeConfig.packageName);
|
|
291
|
+
nextSource = replaceXmlStringValue(nextSource, "custom_url_scheme", nativeConfig.customScheme);
|
|
292
|
+
return nextSource;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderAndroidMainActivitySource(source = "", packageName = "", extension = ".java") {
|
|
296
|
+
const normalizedExtension = String(extension || "").trim().toLowerCase();
|
|
297
|
+
if (normalizedExtension === ".kt") {
|
|
298
|
+
return replaceRequiredPattern(
|
|
299
|
+
source,
|
|
300
|
+
/^[ \t]*package[ \t]+[A-Za-z0-9_.]+[ \t]*$/mu,
|
|
301
|
+
`package ${packageName}`,
|
|
302
|
+
"MainActivity package declaration"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return replaceRequiredPattern(
|
|
307
|
+
source,
|
|
308
|
+
/^[ \t]*package[ \t]+[A-Za-z0-9_.]+[ \t]*;[ \t]*$/mu,
|
|
309
|
+
`package ${packageName};`,
|
|
310
|
+
"MainActivity package declaration"
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function listFilesRecursively(rootDirectoryPath = "") {
|
|
315
|
+
const collected = [];
|
|
316
|
+
if (!(await pathExists(rootDirectoryPath))) {
|
|
317
|
+
return collected;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const entries = await readdir(rootDirectoryPath, { withFileTypes: true });
|
|
321
|
+
for (const entry of entries) {
|
|
322
|
+
const absolutePath = path.join(rootDirectoryPath, entry.name);
|
|
323
|
+
if (entry.isDirectory()) {
|
|
324
|
+
collected.push(...await listFilesRecursively(absolutePath));
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (entry.isFile()) {
|
|
328
|
+
collected.push(absolutePath);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return collected;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function resolveAndroidMainActivityEntry(appRoot = "") {
|
|
336
|
+
const candidateRoots = [
|
|
337
|
+
path.join(appRoot, ANDROID_MAIN_JAVA_ROOT_RELATIVE_PATH),
|
|
338
|
+
path.join(appRoot, ANDROID_MAIN_KOTLIN_ROOT_RELATIVE_PATH)
|
|
339
|
+
];
|
|
340
|
+
const candidates = [];
|
|
341
|
+
|
|
342
|
+
for (const rootDirectoryPath of candidateRoots) {
|
|
343
|
+
const files = await listFilesRecursively(rootDirectoryPath);
|
|
344
|
+
for (const absolutePath of files) {
|
|
345
|
+
if (path.basename(absolutePath) !== "MainActivity.java" && path.basename(absolutePath) !== "MainActivity.kt") {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
candidates.push({
|
|
349
|
+
absolutePath,
|
|
350
|
+
sourceRoot: rootDirectoryPath,
|
|
351
|
+
extension: path.extname(absolutePath)
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (candidates.length < 1) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
if (candidates.length > 1) {
|
|
360
|
+
throw new Error("Found multiple MainActivity source files in the Android shell.");
|
|
361
|
+
}
|
|
362
|
+
return candidates[0];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function resolveInstalledMobileConfig(appRoot = "") {
|
|
366
|
+
const mergedAppConfig = await loadAppConfigFromAppRoot({
|
|
367
|
+
appRoot
|
|
368
|
+
});
|
|
369
|
+
return resolveMobileConfig({
|
|
370
|
+
mobile: mergedAppConfig.mobile
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function resolveAndroidSdkDetails({ appRoot = "" } = {}) {
|
|
375
|
+
const envSdkRoot = String(process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || "").trim();
|
|
376
|
+
if (envSdkRoot) {
|
|
377
|
+
return Object.freeze({
|
|
378
|
+
source: process.env.ANDROID_HOME ? "ANDROID_HOME" : "ANDROID_SDK_ROOT",
|
|
379
|
+
sdkRoot: envSdkRoot
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const localPropertiesPath = path.join(appRoot, ANDROID_DIRECTORY_NAME, "local.properties");
|
|
384
|
+
if (!(await pathExists(localPropertiesPath))) {
|
|
385
|
+
return Object.freeze({
|
|
386
|
+
source: "",
|
|
387
|
+
sdkRoot: ""
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const localPropertiesSource = await readFile(localPropertiesPath, "utf8");
|
|
392
|
+
const sdkRoot = parseAndroidSdkDirFromLocalProperties(localPropertiesSource);
|
|
393
|
+
return Object.freeze({
|
|
394
|
+
source: sdkRoot ? "android/local.properties" : "",
|
|
395
|
+
sdkRoot
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function collectAndroidSdkComponentIssues({ appRoot = "", sdkRoot = "" } = {}) {
|
|
400
|
+
const issues = [];
|
|
401
|
+
const normalizedSdkRoot = String(sdkRoot || "").trim();
|
|
402
|
+
if (!normalizedSdkRoot) {
|
|
403
|
+
return issues;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const variablesGradlePath = path.join(appRoot, ANDROID_VARIABLES_RELATIVE_PATH);
|
|
407
|
+
if (!(await pathExists(variablesGradlePath))) {
|
|
408
|
+
return issues;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const variablesGradleSource = await readFile(variablesGradlePath, "utf8");
|
|
412
|
+
const compileSdkMatch = /compileSdkVersion\s*=\s*(\d+)/u.exec(variablesGradleSource);
|
|
413
|
+
const compileSdkVersion = String(compileSdkMatch?.[1] || "").trim();
|
|
414
|
+
if (compileSdkVersion) {
|
|
415
|
+
const platformDirectoryPath = path.join(
|
|
416
|
+
normalizedSdkRoot,
|
|
417
|
+
"platforms",
|
|
418
|
+
`android-${compileSdkVersion}`
|
|
419
|
+
);
|
|
420
|
+
if (!(await pathExists(platformDirectoryPath))) {
|
|
421
|
+
issues.push(
|
|
422
|
+
`Android SDK platform android-${compileSdkVersion} is missing under ${normalizedSdkRoot}.`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const buildToolsRoot = path.join(normalizedSdkRoot, "build-tools");
|
|
428
|
+
if (!(await pathExists(buildToolsRoot))) {
|
|
429
|
+
issues.push(`Android SDK build-tools directory is missing under ${normalizedSdkRoot}.`);
|
|
430
|
+
return issues;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const buildToolsEntries = await readdir(buildToolsRoot, { withFileTypes: true });
|
|
434
|
+
const hasBuildToolsVersion = buildToolsEntries.some((entry) => entry.isDirectory());
|
|
435
|
+
if (!hasBuildToolsVersion) {
|
|
436
|
+
issues.push(`Android SDK build-tools has no installed versions under ${buildToolsRoot}.`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const licensesRoot = path.join(normalizedSdkRoot, "licenses");
|
|
440
|
+
if (!(await pathExists(licensesRoot))) {
|
|
441
|
+
issues.push(`Android SDK licenses directory is missing under ${normalizedSdkRoot}. Run sdkmanager --licenses after installing the required components.`);
|
|
442
|
+
return issues;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const licenseEntries = await readdir(licensesRoot, { withFileTypes: true });
|
|
446
|
+
const hasLicenseFiles = licenseEntries.some((entry) => entry.isFile());
|
|
447
|
+
if (!hasLicenseFiles) {
|
|
448
|
+
issues.push(`Android SDK licenses are not accepted under ${licensesRoot}. Run sdkmanager --licenses before building the Android shell.`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return issues;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function assertAndroidSdkConfigured({ ctx, appRoot } = {}) {
|
|
455
|
+
const { createCliError } = ctx;
|
|
456
|
+
const sdkDetails = await resolveAndroidSdkDetails({
|
|
457
|
+
appRoot
|
|
458
|
+
});
|
|
459
|
+
if (!sdkDetails.sdkRoot) {
|
|
460
|
+
throw createCliError(
|
|
461
|
+
`Android SDK location is not configured. Set ANDROID_HOME or ANDROID_SDK_ROOT, or create android/local.properties with sdk.dir=... before running the Android shell.`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (!(await pathExists(sdkDetails.sdkRoot))) {
|
|
465
|
+
throw createCliError(
|
|
466
|
+
`Configured Android SDK path does not exist: ${sdkDetails.sdkRoot} (${sdkDetails.source}).`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const componentIssues = await collectAndroidSdkComponentIssues({
|
|
470
|
+
appRoot,
|
|
471
|
+
sdkRoot: sdkDetails.sdkRoot
|
|
472
|
+
});
|
|
473
|
+
if (componentIssues.length > 0) {
|
|
474
|
+
throw createCliError(componentIssues.join(" "));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return sdkDetails;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function ensureMobileConfigStub({
|
|
481
|
+
ctx,
|
|
482
|
+
appRoot,
|
|
483
|
+
packageJson = {},
|
|
484
|
+
dryRun = false,
|
|
485
|
+
stdout
|
|
486
|
+
} = {}) {
|
|
487
|
+
const {
|
|
488
|
+
normalizeRelativePath
|
|
489
|
+
} = ctx;
|
|
490
|
+
const publicConfigPath = path.join(appRoot, PUBLIC_CONFIG_RELATIVE_PATH);
|
|
491
|
+
const currentSource = await readFile(publicConfigPath, "utf8");
|
|
492
|
+
if (/\bconfig\.mobile\b|\bmobile\s*:/u.test(currentSource)) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const stubSource = buildManagedMobileConfigStub({
|
|
497
|
+
packageJson
|
|
498
|
+
});
|
|
499
|
+
const nextSource = `${String(currentSource || "").replace(/\s*$/u, "")}\n\n${stubSource}\n`;
|
|
500
|
+
|
|
501
|
+
if (dryRun === true) {
|
|
502
|
+
stdout?.write(`[dry-run] append managed mobile config to ${normalizeRelativePath(appRoot, publicConfigPath)}\n`);
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
await writeFile(publicConfigPath, nextSource, "utf8");
|
|
507
|
+
stdout?.write(`[mobile] Added managed mobile config stub to ${normalizeRelativePath(appRoot, publicConfigPath)}.\n`);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function buildManagedDeepLinkIntentFilterBlock(mobileConfig = {}) {
|
|
512
|
+
const customScheme = String(mobileConfig?.auth?.customScheme || "").trim().toLowerCase();
|
|
513
|
+
if (!customScheme) {
|
|
514
|
+
throw new Error("config.mobile.auth.customScheme is required before wiring Android deep links.");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return [
|
|
518
|
+
` ${MANAGED_DEEP_LINK_START_MARKER}`,
|
|
519
|
+
' <intent-filter>',
|
|
520
|
+
' <action android:name="android.intent.action.VIEW" />',
|
|
521
|
+
' <category android:name="android.intent.category.DEFAULT" />',
|
|
522
|
+
' <category android:name="android.intent.category.BROWSABLE" />',
|
|
523
|
+
` <data android:scheme="${customScheme}" />`,
|
|
524
|
+
" </intent-filter>",
|
|
525
|
+
` ${MANAGED_DEEP_LINK_END_MARKER}`
|
|
526
|
+
].join("\n");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function shouldAllowAndroidCleartextTraffic(mobileConfig = {}) {
|
|
530
|
+
const assetMode = String(mobileConfig?.assetMode || "").trim().toLowerCase();
|
|
531
|
+
const devServerUrl = String(mobileConfig?.devServerUrl || "").trim();
|
|
532
|
+
const apiBaseUrl = String(mobileConfig?.apiBaseUrl || "").trim();
|
|
533
|
+
|
|
534
|
+
if (assetMode === "dev_server" && devServerUrl) {
|
|
535
|
+
try {
|
|
536
|
+
if (String(new URL(devServerUrl).protocol || "").toLowerCase() === "http:") {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
} catch {}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (apiBaseUrl) {
|
|
543
|
+
try {
|
|
544
|
+
if (String(new URL(apiBaseUrl).protocol || "").toLowerCase() === "http:") {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
} catch {}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function renderAndroidManifestApplicationTrafficPolicy(manifestSource = "", mobileConfig = {}) {
|
|
554
|
+
const normalizedSource = String(manifestSource || "");
|
|
555
|
+
if (!normalizedSource) {
|
|
556
|
+
throw new Error("AndroidManifest.xml is empty.");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const applicationPattern = /(<application\b)([\s\S]*?)(>)/u;
|
|
560
|
+
const match = applicationPattern.exec(normalizedSource);
|
|
561
|
+
if (!match) {
|
|
562
|
+
throw new Error("Could not locate <application> in AndroidManifest.xml.");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const allowCleartext = shouldAllowAndroidCleartextTraffic(mobileConfig);
|
|
566
|
+
let applicationAttributes = String(match[2] || "");
|
|
567
|
+
applicationAttributes = applicationAttributes.replace(/\s+android:usesCleartextTraffic="(?:true|false)"/gu, "");
|
|
568
|
+
if (allowCleartext) {
|
|
569
|
+
applicationAttributes = `${applicationAttributes} android:usesCleartextTraffic="true"`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return normalizedSource.replace(applicationPattern, `$1${applicationAttributes}$3`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function renderManagedAndroidManifest(manifestSource = "", mobileConfig = {}) {
|
|
576
|
+
const manifestWithTrafficPolicy = renderAndroidManifestApplicationTrafficPolicy(manifestSource, mobileConfig);
|
|
577
|
+
const managedBlock = buildManagedDeepLinkIntentFilterBlock(mobileConfig);
|
|
578
|
+
return injectManagedDeepLinkBlock(manifestWithTrafficPolicy, managedBlock);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function injectManagedDeepLinkBlock(manifestSource = "", managedBlock = "") {
|
|
582
|
+
const normalizedSource = String(manifestSource || "");
|
|
583
|
+
const normalizedManagedBlock = String(managedBlock || "").trim();
|
|
584
|
+
if (!normalizedSource) {
|
|
585
|
+
throw new Error("AndroidManifest.xml is empty.");
|
|
586
|
+
}
|
|
587
|
+
if (!normalizedManagedBlock) {
|
|
588
|
+
throw new Error("Managed deep-link block is empty.");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const managedBlockPattern = new RegExp(
|
|
592
|
+
`\\n?\\s*${escapeRegExp(MANAGED_DEEP_LINK_START_MARKER)}[\\s\\S]*?${escapeRegExp(MANAGED_DEEP_LINK_END_MARKER)}\\n?`,
|
|
593
|
+
"u"
|
|
594
|
+
);
|
|
595
|
+
const sourceWithoutManagedBlock = normalizedSource.replace(managedBlockPattern, "\n");
|
|
596
|
+
const mainActivityPattern = /(<activity\b[^>]*android:name="\.MainActivity"[\s\S]*?>)([\s\S]*?)(\n\s*<\/activity>)/u;
|
|
597
|
+
const match = mainActivityPattern.exec(sourceWithoutManagedBlock);
|
|
598
|
+
if (!match) {
|
|
599
|
+
throw new Error("Could not locate MainActivity in AndroidManifest.xml.");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const [, activityOpen, activityBody, activityClose] = match;
|
|
603
|
+
const normalizedBody = String(activityBody || "").replace(/\s+$/u, "");
|
|
604
|
+
const nextActivityBody = normalizedBody
|
|
605
|
+
? `${normalizedBody}\n\n${normalizedManagedBlock}`
|
|
606
|
+
: `\n${normalizedManagedBlock}`;
|
|
607
|
+
|
|
608
|
+
return sourceWithoutManagedBlock.replace(
|
|
609
|
+
mainActivityPattern,
|
|
610
|
+
`${activityOpen}${nextActivityBody}${activityClose}`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function assertCapacitorShellInstalled({ ctx, appRoot }) {
|
|
615
|
+
const missingPaths = await collectCapacitorShellInstallIssues({
|
|
616
|
+
ctx,
|
|
617
|
+
appRoot
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
if (missingPaths.length > 0) {
|
|
621
|
+
throw ctx.createCliError(
|
|
622
|
+
`Capacitor Android shell is not installed for this app. Missing: ${missingPaths.join(", ")}. Run jskit mobile add capacitor first.`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function collectCapacitorShellInstallIssues({ ctx, appRoot } = {}) {
|
|
628
|
+
const {
|
|
629
|
+
fileExists,
|
|
630
|
+
path: pathModule,
|
|
631
|
+
normalizeRelativePath
|
|
632
|
+
} = ctx;
|
|
633
|
+
const missingPaths = [];
|
|
634
|
+
|
|
635
|
+
const capacitorConfigPath = pathModule.join(appRoot, CAPACITOR_CONFIG_FILE);
|
|
636
|
+
if (!(await fileExists(capacitorConfigPath))) {
|
|
637
|
+
missingPaths.push(normalizeRelativePath(appRoot, capacitorConfigPath));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const androidDirectoryPath = pathModule.join(appRoot, ANDROID_DIRECTORY_NAME);
|
|
641
|
+
if (!(await fileExists(androidDirectoryPath))) {
|
|
642
|
+
missingPaths.push(normalizeRelativePath(appRoot, androidDirectoryPath));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const requiredAndroidPaths = [
|
|
646
|
+
ANDROID_MANIFEST_RELATIVE_PATH,
|
|
647
|
+
ANDROID_APP_BUILD_GRADLE_RELATIVE_PATH,
|
|
648
|
+
ANDROID_VARIABLES_RELATIVE_PATH,
|
|
649
|
+
ANDROID_STRINGS_RELATIVE_PATH
|
|
650
|
+
];
|
|
651
|
+
for (const relativePath of requiredAndroidPaths) {
|
|
652
|
+
const absolutePath = pathModule.join(appRoot, relativePath);
|
|
653
|
+
if (!(await fileExists(absolutePath))) {
|
|
654
|
+
missingPaths.push(normalizeRelativePath(appRoot, absolutePath));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const mainActivityEntry = await resolveAndroidMainActivityEntry(appRoot);
|
|
659
|
+
if (!mainActivityEntry) {
|
|
660
|
+
missingPaths.push("Android MainActivity source file");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return missingPaths;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function ensureAndroidManifestDeepLinks({
|
|
667
|
+
ctx,
|
|
668
|
+
appRoot,
|
|
669
|
+
dryRun = false,
|
|
670
|
+
stdout
|
|
671
|
+
} = {}) {
|
|
672
|
+
const {
|
|
673
|
+
fileExists,
|
|
674
|
+
path: pathModule,
|
|
675
|
+
normalizeRelativePath,
|
|
676
|
+
createCliError
|
|
677
|
+
} = ctx;
|
|
678
|
+
const manifestPath = pathModule.join(appRoot, ANDROID_MANIFEST_RELATIVE_PATH);
|
|
679
|
+
if (!(await fileExists(manifestPath))) {
|
|
680
|
+
throw createCliError(
|
|
681
|
+
`Capacitor Android shell is missing ${normalizeRelativePath(appRoot, manifestPath)}. Run jskit mobile add capacitor first.`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const mobileConfig = await resolveInstalledMobileConfig(appRoot);
|
|
686
|
+
const currentManifestSource = await readFile(manifestPath, "utf8");
|
|
687
|
+
const nextManifestSource = renderManagedAndroidManifest(currentManifestSource, mobileConfig);
|
|
688
|
+
|
|
689
|
+
if (nextManifestSource === currentManifestSource) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (dryRun === true) {
|
|
694
|
+
stdout?.write(`[dry-run] refresh ${normalizeRelativePath(appRoot, manifestPath)}\n`);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
await writeFile(manifestPath, nextManifestSource, "utf8");
|
|
699
|
+
stdout?.write(`[mobile] Refreshed ${normalizeRelativePath(appRoot, manifestPath)}.\n`);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function collectAndroidNativeShellIdentityIssues({ ctx, appRoot } = {}) {
|
|
704
|
+
const {
|
|
705
|
+
fileExists,
|
|
706
|
+
path: pathModule,
|
|
707
|
+
normalizeRelativePath
|
|
708
|
+
} = ctx;
|
|
709
|
+
const issues = [];
|
|
710
|
+
const mobileConfig = await resolveInstalledMobileConfig(appRoot);
|
|
711
|
+
const nativeConfig = buildAndroidNativeConfig(mobileConfig);
|
|
712
|
+
const buildGradlePath = pathModule.join(appRoot, ANDROID_APP_BUILD_GRADLE_RELATIVE_PATH);
|
|
713
|
+
const variablesGradlePath = pathModule.join(appRoot, ANDROID_VARIABLES_RELATIVE_PATH);
|
|
714
|
+
const stringsPath = pathModule.join(appRoot, ANDROID_STRINGS_RELATIVE_PATH);
|
|
715
|
+
|
|
716
|
+
const compareRenderedFile = async (absolutePath, renderer) => {
|
|
717
|
+
if (!(await fileExists(absolutePath))) {
|
|
718
|
+
issues.push(`Missing ${normalizeRelativePath(appRoot, absolutePath)}.`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const currentSource = await readFile(absolutePath, "utf8");
|
|
722
|
+
const expectedSource = renderer(currentSource, nativeConfig);
|
|
723
|
+
if (currentSource !== expectedSource) {
|
|
724
|
+
issues.push(
|
|
725
|
+
`${normalizeRelativePath(appRoot, absolutePath)} is stale and no longer matches config.mobile. Re-run jskit mobile sync android to refresh the Android shell.`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
await compareRenderedFile(buildGradlePath, renderAndroidAppBuildGradleSource);
|
|
731
|
+
await compareRenderedFile(variablesGradlePath, renderAndroidVariablesGradleSource);
|
|
732
|
+
await compareRenderedFile(stringsPath, renderAndroidStringsSource);
|
|
733
|
+
await compareRenderedFile(
|
|
734
|
+
pathModule.join(appRoot, ANDROID_MANIFEST_RELATIVE_PATH),
|
|
735
|
+
(currentSource) => renderManagedAndroidManifest(currentSource, mobileConfig)
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
const mainActivityEntry = await resolveAndroidMainActivityEntry(appRoot);
|
|
739
|
+
if (!mainActivityEntry) {
|
|
740
|
+
issues.push("Missing Android MainActivity source file.");
|
|
741
|
+
return issues;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const expectedMainActivityPath = path.join(
|
|
745
|
+
mainActivityEntry.sourceRoot,
|
|
746
|
+
...nativeConfig.packageName.split("."),
|
|
747
|
+
`MainActivity${mainActivityEntry.extension}`
|
|
748
|
+
);
|
|
749
|
+
const currentMainActivitySource = await readFile(mainActivityEntry.absolutePath, "utf8");
|
|
750
|
+
const expectedMainActivitySource = renderAndroidMainActivitySource(
|
|
751
|
+
currentMainActivitySource,
|
|
752
|
+
nativeConfig.packageName,
|
|
753
|
+
mainActivityEntry.extension
|
|
754
|
+
);
|
|
755
|
+
if (
|
|
756
|
+
mainActivityEntry.absolutePath !== expectedMainActivityPath ||
|
|
757
|
+
currentMainActivitySource !== expectedMainActivitySource
|
|
758
|
+
) {
|
|
759
|
+
issues.push(
|
|
760
|
+
`${normalizeRelativePath(appRoot, mainActivityEntry.absolutePath)} is stale and no longer matches config.mobile. Re-run jskit mobile sync android to refresh the Android shell.`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return issues;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function ensureAndroidNativeShellIdentity({
|
|
768
|
+
ctx,
|
|
769
|
+
appRoot,
|
|
770
|
+
dryRun = false,
|
|
771
|
+
stdout
|
|
772
|
+
} = {}) {
|
|
773
|
+
const {
|
|
774
|
+
fileExists,
|
|
775
|
+
path: pathModule,
|
|
776
|
+
normalizeRelativePath,
|
|
777
|
+
createCliError
|
|
778
|
+
} = ctx;
|
|
779
|
+
const mobileConfig = await resolveInstalledMobileConfig(appRoot);
|
|
780
|
+
const nativeConfig = buildAndroidNativeConfig(mobileConfig);
|
|
781
|
+
let touched = false;
|
|
782
|
+
const refreshRenderedFile = async (relativePath, renderer) => {
|
|
783
|
+
const absolutePath = pathModule.join(appRoot, relativePath);
|
|
784
|
+
if (!(await fileExists(absolutePath))) {
|
|
785
|
+
throw createCliError(
|
|
786
|
+
`Capacitor Android shell is missing ${normalizeRelativePath(appRoot, absolutePath)}. Run jskit mobile add capacitor first.`
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
const currentSource = await readFile(absolutePath, "utf8");
|
|
790
|
+
const nextSource = renderer(currentSource, nativeConfig);
|
|
791
|
+
if (nextSource === currentSource) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
touched = true;
|
|
795
|
+
if (dryRun === true) {
|
|
796
|
+
stdout?.write(`[dry-run] refresh ${normalizeRelativePath(appRoot, absolutePath)}\n`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
await writeFile(absolutePath, nextSource, "utf8");
|
|
800
|
+
stdout?.write(`[mobile] Refreshed ${normalizeRelativePath(appRoot, absolutePath)}.\n`);
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
await refreshRenderedFile(ANDROID_APP_BUILD_GRADLE_RELATIVE_PATH, renderAndroidAppBuildGradleSource);
|
|
804
|
+
await refreshRenderedFile(ANDROID_VARIABLES_RELATIVE_PATH, renderAndroidVariablesGradleSource);
|
|
805
|
+
await refreshRenderedFile(ANDROID_STRINGS_RELATIVE_PATH, renderAndroidStringsSource);
|
|
806
|
+
await refreshRenderedFile(ANDROID_MANIFEST_RELATIVE_PATH, (currentSource) => renderManagedAndroidManifest(currentSource, mobileConfig));
|
|
807
|
+
|
|
808
|
+
const mainActivityEntry = await resolveAndroidMainActivityEntry(appRoot);
|
|
809
|
+
if (!mainActivityEntry) {
|
|
810
|
+
throw createCliError("Capacitor Android shell is missing MainActivity.java or MainActivity.kt. Run jskit mobile add capacitor first.");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const currentMainActivitySource = await readFile(mainActivityEntry.absolutePath, "utf8");
|
|
814
|
+
const nextMainActivitySource = renderAndroidMainActivitySource(
|
|
815
|
+
currentMainActivitySource,
|
|
816
|
+
nativeConfig.packageName,
|
|
817
|
+
mainActivityEntry.extension
|
|
818
|
+
);
|
|
819
|
+
const nextMainActivityPath = path.join(
|
|
820
|
+
mainActivityEntry.sourceRoot,
|
|
821
|
+
...nativeConfig.packageName.split("."),
|
|
822
|
+
`MainActivity${mainActivityEntry.extension}`
|
|
823
|
+
);
|
|
824
|
+
if (
|
|
825
|
+
nextMainActivitySource !== currentMainActivitySource ||
|
|
826
|
+
nextMainActivityPath !== mainActivityEntry.absolutePath
|
|
827
|
+
) {
|
|
828
|
+
touched = true;
|
|
829
|
+
if (dryRun === true) {
|
|
830
|
+
const currentRelativePath = normalizeRelativePath(appRoot, mainActivityEntry.absolutePath);
|
|
831
|
+
const nextRelativePath = normalizeRelativePath(appRoot, nextMainActivityPath);
|
|
832
|
+
if (currentRelativePath === nextRelativePath) {
|
|
833
|
+
stdout?.write(`[dry-run] refresh ${currentRelativePath}\n`);
|
|
834
|
+
} else {
|
|
835
|
+
stdout?.write(`[dry-run] move ${currentRelativePath} -> ${nextRelativePath}\n`);
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
838
|
+
await mkdir(path.dirname(nextMainActivityPath), { recursive: true });
|
|
839
|
+
await writeFile(nextMainActivityPath, nextMainActivitySource, "utf8");
|
|
840
|
+
if (nextMainActivityPath !== mainActivityEntry.absolutePath) {
|
|
841
|
+
await unlink(mainActivityEntry.absolutePath);
|
|
842
|
+
}
|
|
843
|
+
const currentRelativePath = normalizeRelativePath(appRoot, mainActivityEntry.absolutePath);
|
|
844
|
+
const nextRelativePath = normalizeRelativePath(appRoot, nextMainActivityPath);
|
|
845
|
+
if (currentRelativePath === nextRelativePath) {
|
|
846
|
+
stdout?.write(`[mobile] Refreshed ${nextRelativePath}.\n`);
|
|
847
|
+
} else {
|
|
848
|
+
stdout?.write(`[mobile] Moved ${currentRelativePath} -> ${nextRelativePath}.\n`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return touched;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function renderManagedMobileFile({
|
|
857
|
+
appRoot,
|
|
858
|
+
relativeTargetPath,
|
|
859
|
+
packageId = CAPACITOR_RUNTIME_PACKAGE_ID
|
|
860
|
+
} = {}) {
|
|
861
|
+
const normalizedTargetPath = normalizeRelativePosixPath(relativeTargetPath);
|
|
862
|
+
if (!normalizedTargetPath) {
|
|
863
|
+
throw new Error("relativeTargetPath is required to render a managed mobile file.");
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const packageRegistry = await loadPackageRegistry();
|
|
867
|
+
const packageEntry = packageRegistry.get(packageId);
|
|
868
|
+
if (!packageEntry) {
|
|
869
|
+
throw new Error(`Could not resolve package ${packageId} from the JSKIT package registry.`);
|
|
870
|
+
}
|
|
871
|
+
const templateRoot = await resolvePackageTemplateRoot({
|
|
872
|
+
packageEntry,
|
|
873
|
+
appRoot
|
|
874
|
+
});
|
|
875
|
+
const packageEntryForMutations =
|
|
876
|
+
templateRoot === packageEntry.rootDir
|
|
877
|
+
? packageEntry
|
|
878
|
+
: {
|
|
879
|
+
...packageEntry,
|
|
880
|
+
rootDir: templateRoot
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const mutation = ensureArray(ensureObject(packageEntryForMutations.descriptor).mutations?.files)
|
|
884
|
+
.map((entry) => interpolateFileMutationRecord(ensureObject(entry), {}, packageEntryForMutations.packageId))
|
|
885
|
+
.find((entry) => normalizeRelativePosixPath(entry.to) === normalizedTargetPath);
|
|
886
|
+
if (!mutation) {
|
|
887
|
+
throw new Error(`Package ${packageId} does not manage ${normalizedTargetPath}.`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const sourcePath = path.join(packageEntryForMutations.rootDir, mutation.from);
|
|
891
|
+
const targetPath = path.join(appRoot, mutation.to);
|
|
892
|
+
const templateContextReplacements = await resolveTemplateContextReplacementsForMutation({
|
|
893
|
+
packageEntry: packageEntryForMutations,
|
|
894
|
+
mutation,
|
|
895
|
+
options: {},
|
|
896
|
+
appRoot,
|
|
897
|
+
sourcePath,
|
|
898
|
+
targetPaths: [targetPath],
|
|
899
|
+
mutationContext: "files mutation"
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
return renderTemplateFile(
|
|
903
|
+
sourcePath,
|
|
904
|
+
{},
|
|
905
|
+
packageEntryForMutations.packageId,
|
|
906
|
+
`${mutation.id || mutation.to || mutation.from}.source`,
|
|
907
|
+
templateContextReplacements
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export {
|
|
912
|
+
CAPACITOR_CONFIG_FILE,
|
|
913
|
+
ANDROID_DIRECTORY_NAME,
|
|
914
|
+
ANDROID_MANIFEST_RELATIVE_PATH,
|
|
915
|
+
buildManagedMobileConfigStub,
|
|
916
|
+
resolveInstalledMobileConfig,
|
|
917
|
+
resolveAndroidSdkDetails,
|
|
918
|
+
collectAndroidSdkComponentIssues,
|
|
919
|
+
assertAndroidSdkConfigured,
|
|
920
|
+
collectCapacitorShellInstallIssues,
|
|
921
|
+
ensureMobileConfigStub,
|
|
922
|
+
buildManagedDeepLinkIntentFilterBlock,
|
|
923
|
+
injectManagedDeepLinkBlock,
|
|
924
|
+
assertCapacitorShellInstalled,
|
|
925
|
+
ensureAndroidManifestDeepLinks,
|
|
926
|
+
collectAndroidNativeShellIdentityIssues,
|
|
927
|
+
ensureAndroidNativeShellIdentity,
|
|
928
|
+
renderManagedMobileFile
|
|
929
|
+
};
|