@applift/core 0.6.0 → 0.7.1

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.
@@ -0,0 +1,806 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ //#region src/framework.ts
4
+ /**
5
+ * 框架适配器:让 applift 框架无关(RN / Flutter / 原生安卓 / 原生 iOS / 原生鸿蒙)。
6
+ * 探测规则 + 产物 pattern + 缺省构建命令;插件可用 'channel:register' 旁路注册自定义 FrameworkAdapter。
7
+ */
8
+ /** 一层子目录列表(探测用,跳过隐藏目录与 node_modules/Pods) */
9
+ function subdirs(cwd, depth) {
10
+ if (depth <= 0) return [];
11
+ const out = [];
12
+ let names;
13
+ try {
14
+ names = fs.readdirSync(cwd, { withFileTypes: true });
15
+ } catch {
16
+ return [];
17
+ }
18
+ for (const name of names) {
19
+ if (!name.isDirectory() || name.name.startsWith(".") || name.name === "node_modules" || name.name === "Pods") continue;
20
+ const p = path.join(cwd, name.name);
21
+ out.push(p, ...subdirs(p, depth - 1));
22
+ }
23
+ return out;
24
+ }
25
+ /** package.json 读取(容错) */
26
+ function readJson(filePath) {
27
+ try {
28
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function deps(pkg) {
34
+ if (!pkg) return {};
35
+ return {
36
+ ...pkg.dependencies,
37
+ ...pkg.devDependencies
38
+ };
39
+ }
40
+ /**
41
+ * 声明式框架解析:插件注册的 adapter 是唯一来源(不再回退内置探测表)。
42
+ * - 未注册 → null(CLI 层负责提示声明 framework 插件)
43
+ * - 注册多个 → 报错(声明即意图,多个意图是配置错误)
44
+ * - 注册一个 → detect 降级为校验(声明与工程不符时显式报错,而非静默跳过)
45
+ */
46
+ function resolveFramework(cwd, registered) {
47
+ if (registered.length === 0) return null;
48
+ if (registered.length > 1) throw new Error(`多个 framework 插件注册:${registered.map((a) => a.id).join(", ")}——只能声明一个`);
49
+ const adapter = registered[0];
50
+ if (!adapter.detect(cwd)) throw new Error(`声明的 framework「${adapter.label}」与当前工程不匹配,请检查 plugins 声明`);
51
+ return adapter;
52
+ }
53
+ /** 按 pattern 扫描产物(支持 ** 与 * 简单通配,返回绝对路径,多产物按 mtime 新者在前) */
54
+ function findArtifacts(cwd, patterns) {
55
+ const byPath = /* @__PURE__ */ new Map();
56
+ for (const pattern of patterns) for (const p of globWalk(path.resolve(cwd), pattern.split("/"))) {
57
+ if (byPath.has(p)) continue;
58
+ try {
59
+ byPath.set(p, fs.statSync(p).mtimeMs);
60
+ } catch {}
61
+ }
62
+ return [...byPath].sort((a, b) => b[1] - a[1]).map(([p]) => p);
63
+ }
64
+ /** 文本读取(容错:不存在/不可读返回 null,绝不抛;detectBundleIds 各源共用) */
65
+ function readText$1(filePath) {
66
+ try {
67
+ return fs.readFileSync(filePath, "utf8");
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ /** 从 JSON 对象按路径取非空字符串(任一层缺失/非字符串返回 undefined) */
73
+ function pickString(obj, ...keys) {
74
+ let cur = obj;
75
+ for (const k of keys) {
76
+ if (cur === null || typeof cur !== "object") return void 0;
77
+ cur = cur[k];
78
+ }
79
+ return typeof cur === "string" && cur.trim() ? cur.trim() : void 0;
80
+ }
81
+ /** 原生安卓 applicationId(groovy/kts 两种赋值语法;app/ 与 android/app/ 两处布局;不中返回 undefined) */
82
+ function gradleApplicationId(cwd) {
83
+ const candidates = [
84
+ path.join(cwd, "app", "build.gradle"),
85
+ path.join(cwd, "app", "build.gradle.kts"),
86
+ path.join(cwd, "android", "app", "build.gradle"),
87
+ path.join(cwd, "android", "app", "build.gradle.kts")
88
+ ];
89
+ for (const file of candidates) {
90
+ const text = readText$1(file);
91
+ if (!text) continue;
92
+ const m = /(?:^|\s)applicationId\s*[=:]?\s*["']([^"'\s]+)["']/.exec(text);
93
+ if (m?.[1]) return m[1];
94
+ }
95
+ }
96
+ /** 鸿蒙 AppScope/app.json5 的 bundleName(json5 允许注释/尾逗号,正则取值而非 JSON 解析) */
97
+ function appJson5BundleName(file) {
98
+ const text = readText$1(file);
99
+ return (text && /"bundleName"\s*:\s*"([^"]+)"/.exec(text))?.[1];
100
+ }
101
+ /** 两层子目录内找首个 *.xcodeproj:pbxproj 第一个非变量(含 $ 的 $(...) 与 ${...})的 PRODUCT_BUNDLE_IDENTIFIER + 工程名(Pods 已被 subdirs 排除) */
102
+ function findPbxproj(cwd) {
103
+ for (const dir of [cwd, ...subdirs(cwd, 2)]) {
104
+ let entries;
105
+ try {
106
+ entries = fs.readdirSync(dir, { withFileTypes: true });
107
+ } catch {
108
+ continue;
109
+ }
110
+ const projects = entries.filter((e) => e.isDirectory() && e.name.endsWith(".xcodeproj")).map((e) => e.name).sort();
111
+ for (const name of projects) {
112
+ const pbx = readText$1(path.join(dir, name, "project.pbxproj"));
113
+ if (!pbx) continue;
114
+ for (const m of pbx.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;]+);/g)) {
115
+ const value = m[1].trim().replace(/^["']|["']$/g, "");
116
+ if (value && !value.includes("$")) return {
117
+ bundleId: value,
118
+ scheme: name.slice(0, -10)
119
+ };
120
+ }
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ /**
126
+ * 自动推导三端应用标识(项目配置缺省时的兜底,配置可全空):
127
+ * android:RN app.json android.applicationId > 原生 gradle applicationId;
128
+ * harmony:AppScope/app.json5(根或 harmony/ 下)> RN app.json harmony.bundleName;
129
+ * ios:RN app.json ios.bundleIdentifier > 两层内 xcodeproj 的 PRODUCT_BUNDLE_IDENTIFIER(scheme=工程名)。
130
+ * 容错:文件不存在/正则不中只置 undefined,绝不抛。
131
+ */
132
+ function detectBundleIds(cwd) {
133
+ const appJson = readJson(path.join(cwd, "app.json"));
134
+ const pbx = findPbxproj(cwd);
135
+ const expo = appJson?.expo ?? null;
136
+ const expoAndroid = expo ? pickString(expo, "android", "package") : void 0;
137
+ const expoIos = expo ? pickString(expo, "ios", "bundleIdentifier") : void 0;
138
+ const expoName = expo && typeof expo.name === "string" ? expo.name : void 0;
139
+ return {
140
+ androidPkgName: expoAndroid ?? pickString(appJson, "android", "applicationId") ?? gradleApplicationId(cwd),
141
+ harmonyBundleName: appJson5BundleName(path.join(cwd, "AppScope", "app.json5")) ?? appJson5BundleName(path.join(cwd, "harmony", "AppScope", "app.json5")) ?? pickString(appJson, "harmony", "bundleName"),
142
+ iosBundleId: expoIos ?? pickString(appJson, "ios", "bundleIdentifier") ?? pbx?.bundleId,
143
+ iosScheme: pbx?.scheme ?? expoName
144
+ };
145
+ }
146
+ /** 极简 glob 目录游走(** 跨层,* 单段),避免引入 minimatch 依赖 */
147
+ function globWalk(dir, segments) {
148
+ const [head, ...rest] = segments;
149
+ if (!head) return [dir];
150
+ if (!fs.existsSync(dir)) return [];
151
+ if (head === "**") {
152
+ const out = [];
153
+ const walkAll = (d) => {
154
+ out.push(...globWalk(d, rest));
155
+ for (const name of fs.readdirSync(d, { withFileTypes: true })) {
156
+ if (!name.isDirectory() || name.name.startsWith(".") || name.name === "node_modules") continue;
157
+ walkAll(path.join(d, name.name));
158
+ }
159
+ };
160
+ walkAll(dir);
161
+ return [...new Set(out)];
162
+ }
163
+ if (head.includes("*")) {
164
+ const re = new RegExp(`^${head.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")}$`);
165
+ const out = [];
166
+ for (const name of fs.readdirSync(dir)) if (re.test(name)) out.push(...globWalk(path.join(dir, name), rest));
167
+ return out;
168
+ }
169
+ return globWalk(path.join(dir, head), rest);
170
+ }
171
+ //#endregion
172
+ //#region src/plugins/frameworks.ts
173
+ /**
174
+ * framework 插件工厂:内置七个框架适配器的声明式形态。
175
+ *
176
+ * applift.config.ts 的 plugins 数组声明其一(如 rnFramework({...})),
177
+ * 经 'framework:register' 钩子注册 adapter;声明即唯一来源(见 resolveFramework)。
178
+ * 项目特化(如自定义构建命令)通过工厂参数覆盖,不进 applift core。
179
+ */
180
+ function frameworkPlugin(name, adapter) {
181
+ return {
182
+ name,
183
+ "framework:register"(adapters) {
184
+ adapters.push(adapter);
185
+ }
186
+ };
187
+ }
188
+ /** 生态通用 RN adapter:产物 pattern 按 RN 社区惯例;构建命令无统一标准(项目覆盖) */
189
+ const rnAdapter = {
190
+ id: "rn",
191
+ label: "React Native",
192
+ detect(cwd) {
193
+ return "react-native" in deps(readJson(path.join(cwd, "package.json")));
194
+ },
195
+ /** Expo 项目(依赖 expo 包):同走 RN 产物路径(prebuild 生成原生工程),label 区分展示 */
196
+ expoAwareLabel(cwd) {
197
+ return "expo" in deps(readJson(path.join(cwd, "package.json"))) ? "Expo (React Native)" : "React Native";
198
+ },
199
+ artifactPatterns(platform) {
200
+ if (platform === "android") return ["android/app/build/outputs/apk/release/*-release.apk"];
201
+ if (platform === "ios") return ["build/ios/ipa/*.ipa"];
202
+ return [
203
+ "harmony/entry/build/default/outputs/**/entry-*-signed.hap",
204
+ "harmony/entry/build/default/outputs/**/entry-*.app",
205
+ "harmony/build/outputs/**/*.app",
206
+ "build/outputs/**/*.app"
207
+ ];
208
+ }
209
+ };
210
+ function rnFramework() {
211
+ return frameworkPlugin("framework-react-native", rnAdapter);
212
+ }
213
+ const flutterAdapter = {
214
+ id: "flutter",
215
+ label: "Flutter",
216
+ detect(cwd) {
217
+ if (readJson(path.join(cwd, "pubspec.yaml"))) return true;
218
+ try {
219
+ const text = fs.readFileSync(path.join(cwd, "pubspec.yaml"), "utf8");
220
+ return /^sdk:\s*flutter$/m.test(text) || /^name:/m.test(text);
221
+ } catch {
222
+ return false;
223
+ }
224
+ },
225
+ artifactPatterns(platform) {
226
+ if (platform === "android") return ["build/app/outputs/flutter-apk/app-release.apk"];
227
+ if (platform === "ios") return ["build/ios/ipa/*.ipa"];
228
+ return [
229
+ "build/**/outputs/**/*-signed.hap",
230
+ "harmony/entry/build/**/outputs/**/*-signed.hap",
231
+ "ohos/entry/build/**/outputs/**/*-signed.hap",
232
+ "build/**/outputs/**/*.app"
233
+ ];
234
+ }
235
+ };
236
+ function flutterFramework() {
237
+ return frameworkPlugin("framework-flutter", flutterAdapter);
238
+ }
239
+ const nativeAndroidAdapter = {
240
+ id: "native-android",
241
+ label: "原生 Android",
242
+ detect(cwd) {
243
+ if (rnAdapter.detect(cwd)) return false;
244
+ return fs.existsSync(path.join(cwd, "settings.gradle")) || fs.existsSync(path.join(cwd, "settings.gradle.kts"));
245
+ },
246
+ artifactPatterns() {
247
+ return ["app/build/outputs/apk/release/*-release.apk"];
248
+ }
249
+ };
250
+ function nativeAndroidFramework() {
251
+ return frameworkPlugin("framework-native-android", nativeAndroidAdapter);
252
+ }
253
+ const nativeIosAdapter = {
254
+ id: "native-ios",
255
+ label: "原生 iOS",
256
+ detect(cwd) {
257
+ if (rnAdapter.detect(cwd)) return false;
258
+ for (const dir of [cwd, ...subdirs(cwd, 1)]) {
259
+ let entries;
260
+ try {
261
+ entries = fs.readdirSync(dir);
262
+ } catch {
263
+ continue;
264
+ }
265
+ if (entries.some((f) => String(f).endsWith(".xcodeproj") || String(f).endsWith(".xcworkspace"))) return true;
266
+ }
267
+ return false;
268
+ },
269
+ artifactPatterns() {
270
+ return ["build/**/*.ipa"];
271
+ }
272
+ };
273
+ function nativeIosFramework() {
274
+ return frameworkPlugin("framework-native-ios", nativeIosAdapter);
275
+ }
276
+ const nativeHarmonyAdapter = {
277
+ id: "native-harmony",
278
+ label: "原生鸿蒙 (ArkTS)",
279
+ detect(cwd) {
280
+ return fs.existsSync(path.join(cwd, "oh-package.json5")) || fs.existsSync(path.join(cwd, "harmony/oh-package.json5"));
281
+ },
282
+ artifactPatterns() {
283
+ return [
284
+ "entry/build/default/outputs/**/entry-*-signed.hap",
285
+ "entry/build/default/outputs/**/entry-*.app",
286
+ "harmony/entry/build/default/outputs/**/*-signed.hap",
287
+ "build/outputs/**/*.app",
288
+ "harmony/build/outputs/**/*.app",
289
+ "ohos/build/outputs/**/*.app"
290
+ ];
291
+ }
292
+ };
293
+ function nativeHarmonyFramework() {
294
+ return frameworkPlugin("framework-native-harmony", nativeHarmonyAdapter);
295
+ }
296
+ const electronAdapter = {
297
+ id: "electron",
298
+ label: "Electron",
299
+ detect(cwd) {
300
+ return "electron" in deps(readJson(path.join(cwd, "package.json")));
301
+ },
302
+ artifactPatterns(platform) {
303
+ if (platform === "windows") return [
304
+ "dist/*.exe",
305
+ "dist/**/*.exe",
306
+ "out/make/*.exe"
307
+ ];
308
+ if (platform === "macos") return [
309
+ "dist/*.dmg",
310
+ "dist/**/*.dmg",
311
+ "out/make/*.dmg"
312
+ ];
313
+ if (platform === "linux") return [
314
+ "dist/*.AppImage",
315
+ "dist/**/*.AppImage",
316
+ "out/make/*.AppImage"
317
+ ];
318
+ return [];
319
+ }
320
+ };
321
+ function electronFramework() {
322
+ return frameworkPlugin("framework-electron", electronAdapter);
323
+ }
324
+ const tauriAdapter = {
325
+ id: "tauri",
326
+ label: "Tauri",
327
+ detect(cwd) {
328
+ return "@tauri-apps/cli" in deps(readJson(path.join(cwd, "package.json"))) || fs.existsSync(path.join(cwd, "src-tauri"));
329
+ },
330
+ artifactPatterns(platform) {
331
+ if (platform === "windows") return ["src-tauri/target/release/bundle/nsis/*.exe", "src-tauri/target/release/bundle/msi/*.msi"];
332
+ if (platform === "macos") return ["src-tauri/target/release/bundle/dmg/*.dmg", "src-tauri/target/release/bundle/macos/*.app"];
333
+ if (platform === "linux") return ["src-tauri/target/release/bundle/deb/*.deb", "src-tauri/target/release/bundle/appimage/*.AppImage"];
334
+ return [];
335
+ }
336
+ };
337
+ const knownFrameworkAdapters = [
338
+ rnAdapter,
339
+ flutterAdapter,
340
+ electronAdapter,
341
+ tauriAdapter,
342
+ nativeAndroidAdapter,
343
+ nativeIosAdapter,
344
+ nativeHarmonyAdapter
345
+ ];
346
+ /** 按已知框架表探测建议(仅供 init/doctor 展示,不是发布依据) */
347
+ function suggestFramework(cwd) {
348
+ for (const adapter of knownFrameworkAdapters) try {
349
+ if (adapter.detect(cwd)) return adapter;
350
+ } catch {}
351
+ return null;
352
+ }
353
+ function tauriFramework() {
354
+ return frameworkPlugin("framework-tauri", tauriAdapter);
355
+ }
356
+ //#endregion
357
+ //#region src/channels/shared/redact.ts
358
+ /**
359
+ * 凭据脱敏(对拍 apkgo pkg/httptrace/redact.go 权威)。
360
+ * 判定依据是“名字”:JSON 键 / 表单字段 / query 参数 / header 名携带凭据语义即整值掩码为
361
+ * [REDACTED <len>](保留长度线索),空串保持空串——“签名为空”本身就是排查线索。
362
+ * 非敏感名下的值仍递归扫内容(signed URL / 内嵌 JSON / name=value 自由文本)。
363
+ * 故意分歧:掩码不做 percent-encode(日志可读性优先);JSON 再序列化保持原键序(Go map 序随机)。
364
+ * 挂接点:CLI / 错误消息层接入(core 先提供能力)。
365
+ */
366
+ /** 归一化后「包含即敏感」的词(对拍 redact.go sensitiveWords) */
367
+ const SENSITIVE_WORDS = [
368
+ "secret",
369
+ "token",
370
+ "password",
371
+ "passwd",
372
+ "authorization",
373
+ "credential",
374
+ "cookie",
375
+ "privatekey",
376
+ "apikey",
377
+ "assertion",
378
+ "jwt",
379
+ "signature"
380
+ ];
381
+ /** 归一化后全等即敏感的名字(对拍 redact.go sensitiveNames) */
382
+ const SENSITIVE_NAMES = {
383
+ sig: true,
384
+ sign: true,
385
+ pwd: true,
386
+ auth: true
387
+ };
388
+ /** 名字是否携带凭据语义(JSON 键 / 表单字段 / query 参数 / header 名通用;归一化 = 小写去非字母数字) */
389
+ function isSensitiveName(name) {
390
+ const n = name.toLowerCase().replace(/[^a-z0-9]/g, "");
391
+ if (n in SENSITIVE_NAMES) return true;
392
+ if (n === "tokentype" || n.startsWith("accesscontrol")) return false;
393
+ if (n.endsWith("sign") && n !== "design") return true;
394
+ return SENSITIVE_WORDS.some((w) => n.includes(w));
395
+ }
396
+ /** 整值掩码:保留长度;空串保持(“签名为空”是有效诊断信息)。3 处调用点需锁步一致 */
397
+ function mask(v) {
398
+ return v === "" ? "" : `[REDACTED ${v.length}]`;
399
+ }
400
+ /**
401
+ * 按名字脱敏单个值:敏感名 → 整值掩码;Authorization 保留 scheme("Bearer [REDACTED 812]",
402
+ * 对拍 redact.go redactHeaders);非敏感名 → 递归 redactText 扫值内凭据。
403
+ */
404
+ function redactValue(name, v) {
405
+ if (/^authorization$/i.test(name) && v.includes(" ")) {
406
+ const schemeEnd = v.indexOf(" ");
407
+ return `${v.slice(0, schemeEnd)} ${mask(v.slice(schemeEnd + 1))}`;
408
+ }
409
+ return isSensitiveName(name) ? mask(v) : redactText(v);
410
+ }
411
+ /** redactURL:剥离 userinfo + query 参数按名脱敏,路径/fragment 原样保留 */
412
+ function redactUrlString(v) {
413
+ let out = v.replace(/^(https?:\/\/)[^/?#]*@/, "$1");
414
+ const hashIdx = out.indexOf("#");
415
+ const hash = hashIdx >= 0 ? out.slice(hashIdx) : "";
416
+ if (hashIdx >= 0) out = out.slice(0, hashIdx);
417
+ const qIdx = out.indexOf("?");
418
+ if (qIdx < 0) return out + hash;
419
+ const query = new URLSearchParams(out.slice(qIdx + 1));
420
+ const pairs = [];
421
+ for (const [k, val] of query) pairs.push(`${k}=${isSensitiveName(k) ? mask(val) : val}`);
422
+ return `${out.slice(0, qIdx)}?${pairs.join("&")}${hash}`;
423
+ }
424
+ /** free 文本中的 name=value / "name":"value" 凭据对(对拍 redact.go scrubRe;掩码无长度) */
425
+ const SCRUB_RE = /([\w.-]*(?:secret|token|password|passwd|signature|credential|authorization|api_?key|assertion)[\w.-]*"?\s*[=:]\s*"?)([^"&\s,;<}]+)/gi;
426
+ /** JSON 树递归:敏感名下字符串掩码、数字/布尔置 [REDACTED],数组元素继承外层键名 */
427
+ function redactTree(name, v) {
428
+ if (v === null || typeof v !== "object") {
429
+ if (typeof v === "string") return redactValue(name, v);
430
+ return isSensitiveName(name) ? "[REDACTED]" : v;
431
+ }
432
+ if (Array.isArray(v)) return v.map((x) => redactTree(name, x));
433
+ const out = {};
434
+ for (const [k, val] of Object.entries(v)) out[k] = redactTree(k, val);
435
+ return out;
436
+ }
437
+ /**
438
+ * 文本脱敏(三层层叠,逐层尝试):
439
+ * 1) http(s) URL 带 query/userinfo → query 按名脱敏 + 剥 userinfo
440
+ * 2) { / [ 开头且整串为合法 JSON → 递归按键名脱敏
441
+ * 3) 其余 → name=value 正则扫(错误消息等无结构文本)
442
+ */
443
+ function redactText(v) {
444
+ if (/^https?:\/\//i.test(v) && (v.includes("?") || /^https?:\/\/[^/?#]*@/i.test(v))) return redactUrlString(v);
445
+ if (v.length > 1 && (v.startsWith("{") || v.startsWith("["))) try {
446
+ return JSON.stringify(redactTree("", JSON.parse(v)));
447
+ } catch {}
448
+ return v.replace(SCRUB_RE, "$1[REDACTED]");
449
+ }
450
+ //#endregion
451
+ //#region src/version/project.ts
452
+ /**
453
+ * 工程文件版本读写(可接管模式:版本事实源永远是工程文件,applift 只代读代写)。
454
+ * 只认标准模板形态(AGP/Xcode/DevEco/pubspec/package.json/tauri.conf);行级改写、
455
+ * 幂等(字段定位 + 写死目标值,不依赖旧值,跑 N 次 diff 不变——qy-driver sync-app-identity 同款模式)。
456
+ * version catalog 引用形态(libs.versions.xxx.get())读侧识别并标注 problem,不代写。
457
+ */
458
+ function readText(p) {
459
+ try {
460
+ return fs.readFileSync(p, "utf8");
461
+ } catch {
462
+ return null;
463
+ }
464
+ }
465
+ function firstExisting(...paths) {
466
+ return paths.find((p) => fs.existsSync(p));
467
+ }
468
+ function locateIosProject(dir) {
469
+ const find = (d) => {
470
+ try {
471
+ return fs.readdirSync(d, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.endsWith(".xcodeproj") && !e.name.startsWith(".") && e.name !== "Pods.xcodeproj");
472
+ } catch {
473
+ return;
474
+ }
475
+ };
476
+ let xcodeproj = find(dir);
477
+ let base = dir;
478
+ if (!xcodeproj) try {
479
+ const subs = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "Pods");
480
+ for (const s of subs) {
481
+ xcodeproj = find(path.join(dir, s.name));
482
+ if (xcodeproj) {
483
+ base = path.join(dir, s.name);
484
+ break;
485
+ }
486
+ }
487
+ } catch {}
488
+ const pbxproj = xcodeproj ? path.join(base, xcodeproj.name, "project.pbxproj") : void 0;
489
+ const projDir = xcodeproj?.name.replace(/\.xcodeproj$/, "");
490
+ const plist = projDir && fs.existsSync(path.join(base, projDir, "Info.plist")) ? path.join(base, projDir, "Info.plist") : void 0;
491
+ return {
492
+ pbxproj: pbxproj && fs.existsSync(pbxproj) ? pbxproj : void 0,
493
+ plist
494
+ };
495
+ }
496
+ function locateVersionFiles(cwd, framework) {
497
+ const loc = {};
498
+ const id = framework?.id ?? "";
499
+ const tryAll = framework === null;
500
+ if (tryAll || id === "rn" || id === "native-android" || id === "flutter") loc.android = firstExisting(path.join(cwd, "android", "app", "build.gradle.kts"), path.join(cwd, "android", "app", "build.gradle"), path.join(cwd, "app", "build.gradle.kts"), path.join(cwd, "app", "build.gradle"));
501
+ if (tryAll || id === "rn" || id === "native-ios" || id === "flutter") {
502
+ const sub = locateIosProject(path.join(cwd, "ios"));
503
+ const root = sub.pbxproj ? sub : locateIosProject(cwd);
504
+ loc.ios = root.pbxproj;
505
+ loc.plist = root.plist;
506
+ }
507
+ if (tryAll || id === "rn" || id === "native-harmony" || id === "flutter") loc.harmony = firstExisting(path.join(cwd, "harmony", "AppScope", "app.json5"), path.join(cwd, "AppScope", "app.json5"));
508
+ if (tryAll || id === "flutter") loc.pubspec = fs.existsSync(path.join(cwd, "pubspec.yaml")) ? path.join(cwd, "pubspec.yaml") : void 0;
509
+ if (tryAll || id === "rn" || id === "electron") loc.packageJson = fs.existsSync(path.join(cwd, "package.json")) ? path.join(cwd, "package.json") : void 0;
510
+ if (tryAll || id === "tauri") {
511
+ loc.tauri = firstExisting(path.join(cwd, "src-tauri", "tauri.conf.json"), path.join(cwd, "src-tauri", "tauri.conf.json5"));
512
+ loc.packageJson = fs.existsSync(path.join(cwd, "package.json")) ? path.join(cwd, "package.json") : void 0;
513
+ }
514
+ if (id === "rn") {
515
+ const appJson = readText(path.join(cwd, "app.json"));
516
+ if (appJson && /"expo"\s*:/.test(appJson)) loc.expoAppJson = path.join(cwd, "app.json");
517
+ }
518
+ return loc;
519
+ }
520
+ function readGradle(file) {
521
+ const text = readText(file);
522
+ if (!text) return {};
523
+ const out = {};
524
+ const codeM = /(versionCode(?!Suffix)\s*=?\s*)(\d+)/.exec(text);
525
+ const nameM = /(versionName(?!Suffix)\s*=?\s*)("[^"]*"|'[^']*'|[\w.+-]+)/.exec(text);
526
+ if (codeM) out.versionCode = codeM[2];
527
+ if (nameM) {
528
+ const nameVal = nameM[2].replace(/^["']|["']$/g, "");
529
+ if (nameVal.startsWith("libs.versions.")) out.problem = "versionName 走 version catalog 引用(libs.versions.*):非标准形态,请把版本字段搬回 app/build.gradle(.kts) 字面量";
530
+ else out.versionName = nameVal;
531
+ }
532
+ if (/(versionCode|versionName)\s*=?\s*libs\.versions\./.test(text) && !out.problem) out.problem = "版本字段走 version catalog 引用(libs.versions.*):非标准形态,请把版本字段搬回 app/build.gradle(.kts) 字面量";
533
+ return out;
534
+ }
535
+ function readPbxproj(file) {
536
+ const text = readText(file);
537
+ if (!text) return {};
538
+ const names = [...text.matchAll(/MARKETING_VERSION\s*=\s*([^;]+);/g)].map((m) => m[1].trim());
539
+ return {
540
+ versionName: names[0],
541
+ versionCode: /CURRENT_PROJECT_VERSION\s*=\s*([^;]+);/.exec(text)?.[1]?.trim(),
542
+ problem: new Set(names).size > 1 ? `多 target MARKETING_VERSION 不一致(${[...new Set(names)].join(" / ")}):读第一个,bump 时将全部同步` : void 0
543
+ };
544
+ }
545
+ function readPlist(file) {
546
+ const text = readText(file);
547
+ if (!text) return {};
548
+ const name = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(text)?.[1];
549
+ const code = /<key>CFBundleVersion<\/key>\s*<string>([^<]+)<\/string>/.exec(text)?.[1];
550
+ if (name && !name.startsWith("$")) return {
551
+ versionName: name,
552
+ versionCode: code
553
+ };
554
+ return {};
555
+ }
556
+ function readAppJson5(file) {
557
+ const text = readText(file);
558
+ if (!text) return {};
559
+ return {
560
+ versionName: /"versionName"\s*:\s*"([^"]+)"/.exec(text)?.[1],
561
+ versionCode: /"versionCode"\s*:\s*(\d+)/.exec(text)?.[1]
562
+ };
563
+ }
564
+ function readPubspec(file) {
565
+ const text = readText(file);
566
+ if (!text) return {};
567
+ const m = /^version:\s*(\S+)(?:\s*#.*)?$/m.exec(text);
568
+ if (!m) return {};
569
+ const [name, code] = m[1].split("+");
570
+ return {
571
+ versionName: name,
572
+ versionCode: code
573
+ };
574
+ }
575
+ function readJsonVersionKey(file) {
576
+ const text = readText(file);
577
+ if (!text) return {};
578
+ let best;
579
+ for (const m of text.matchAll(/^(\s*)"version"\s*:\s*"([^"]+)"\s*,?\s*$/gm)) {
580
+ const indent = m[1].length;
581
+ if (!best || indent < best.indent) best = {
582
+ indent,
583
+ value: m[2]
584
+ };
585
+ }
586
+ if (best) return { versionName: best.value };
587
+ try {
588
+ const obj = JSON.parse(text);
589
+ if (typeof obj.version === "string") return { versionName: obj.version };
590
+ } catch {}
591
+ const flat = /"version"\s*:\s*"([^"]+)"/.exec(text)?.[1];
592
+ return flat ? { versionName: flat } : {};
593
+ }
594
+ function readExpoAppJson(file) {
595
+ const text = readText(file);
596
+ if (!text) return {};
597
+ return {
598
+ versionName: /"version"\s*:\s*"([^"]+)"/.exec(text)?.[1],
599
+ versionCode: /"versionCode"\s*:\s*(\d+)/.exec(text)?.[1] ?? /"buildNumber"\s*:\s*"([^"]+)"/.exec(text)?.[1]
600
+ };
601
+ }
602
+ /** 各端版本实况(按框架布局;未识别框架时尝试全部标准位置) */
603
+ function readProjectVersions(cwd, framework) {
604
+ const loc = locateVersionFiles(cwd, framework);
605
+ const out = [];
606
+ if (loc.android) out.push({
607
+ platform: "android",
608
+ file: loc.android,
609
+ kind: "gradle",
610
+ ...readGradle(loc.android)
611
+ });
612
+ if (loc.ios) out.push({
613
+ platform: "ios",
614
+ file: loc.ios,
615
+ kind: "pbxproj",
616
+ ...readPbxproj(loc.ios)
617
+ });
618
+ else if (loc.plist) {
619
+ const plist = readPlist(loc.plist);
620
+ if (plist.versionName) out.push({
621
+ platform: "ios",
622
+ file: loc.plist,
623
+ kind: "pbxproj",
624
+ ...plist,
625
+ problem: "未找到 pbxproj,读 Info.plist 字面量"
626
+ });
627
+ }
628
+ if (loc.harmony) out.push({
629
+ platform: "harmony",
630
+ file: loc.harmony,
631
+ kind: "app-json5",
632
+ ...readAppJson5(loc.harmony)
633
+ });
634
+ if (loc.pubspec) out.push({
635
+ platform: "flutter",
636
+ file: loc.pubspec,
637
+ kind: "pubspec",
638
+ ...readPubspec(loc.pubspec)
639
+ });
640
+ if (loc.packageJson) out.push({
641
+ platform: "js",
642
+ file: loc.packageJson,
643
+ kind: "package-json",
644
+ ...readJsonVersionKey(loc.packageJson)
645
+ });
646
+ if (loc.tauri) out.push({
647
+ platform: "tauri",
648
+ file: loc.tauri,
649
+ kind: "tauri",
650
+ ...readJsonVersionKey(loc.tauri)
651
+ });
652
+ if (loc.expoAppJson) out.push({
653
+ platform: "expo",
654
+ file: loc.expoAppJson,
655
+ kind: "expo-app-json",
656
+ ...readExpoAppJson(loc.expoAppJson)
657
+ });
658
+ return out;
659
+ }
660
+ /** 指定捕获组替换为 value(其余组原样保留;约定各正则组外无字面量) */
661
+ function replaceGroup(re, text, group, value) {
662
+ let count = 0;
663
+ return {
664
+ text: text.replace(re, (_m, ...groups) => {
665
+ count++;
666
+ const g = groups.slice(0, -2);
667
+ g[group - 1] = value;
668
+ return g.join("");
669
+ }),
670
+ count
671
+ };
672
+ }
673
+ function writeIfChanged(file, next) {
674
+ if (readText(file) === next) return false;
675
+ fs.writeFileSync(file, next, "utf8");
676
+ return true;
677
+ }
678
+ function writeGradle(file, w) {
679
+ const text = readText(file);
680
+ if (!text) throw new Error(`无法读取 ${file}`);
681
+ if (/version(Code|Name)\s*=?\s*libs\.versions\./.test(text)) throw new Error(`${file} 的版本字段是 version catalog 引用形态:请先改造为字面量(applift 只写标准形态)`);
682
+ let next = text;
683
+ const code = replaceGroup(/(versionCode(?!Suffix)\s*=?\s*)(\d+)/g, next, 2, String(w.versionCode));
684
+ next = code.text;
685
+ const name = replaceGroup(/(versionName(?!Suffix)\s*=?\s*)("[^"]*"|'[^']*'|[\w.+-]+)/g, next, 2, `"${w.versionName}"`);
686
+ next = name.text;
687
+ if (code.count === 0 && name.count === 0) throw new Error(`${file} 未找到 versionCode/versionName 字面量`);
688
+ return writeIfChanged(file, next);
689
+ }
690
+ function writePbxproj(file, w) {
691
+ const text = readText(file);
692
+ if (!text) throw new Error(`无法读取 ${file}`);
693
+ let next = text;
694
+ const name = replaceGroup(/(MARKETING_VERSION\s*=\s*)([^;]+)(;)/g, next, 2, w.versionName);
695
+ next = name.text;
696
+ const code = replaceGroup(/(CURRENT_PROJECT_VERSION\s*=\s*)([^;]+)(;)/g, next, 2, String(w.versionCode));
697
+ next = code.text;
698
+ if (name.count === 0 && code.count === 0) throw new Error(`${file} 未找到 MARKETING_VERSION/CURRENT_PROJECT_VERSION`);
699
+ return writeIfChanged(file, next);
700
+ }
701
+ function writeAppJson5(file, w) {
702
+ const text = readText(file);
703
+ if (!text) throw new Error(`无法读取 ${file}`);
704
+ let next = text;
705
+ const name = replaceGroup(/("versionName"\s*:\s*")([^"]+)(")/g, next, 2, w.versionName);
706
+ next = name.text;
707
+ const code = replaceGroup(/("versionCode"\s*:\s*)(\d+)/g, next, 2, String(w.versionCode));
708
+ next = code.text;
709
+ if (name.count === 0 && code.count === 0) throw new Error(`${file} 未找到 versionName/versionCode`);
710
+ return writeIfChanged(file, next);
711
+ }
712
+ function writePubspec(file, w) {
713
+ const text = readText(file);
714
+ if (!text) throw new Error(`无法读取 ${file}`);
715
+ if (!/^version:\s*\S+/m.test(text)) throw new Error(`${file} 未找到 version: 行`);
716
+ return writeIfChanged(file, text.replace(/^version:[^\n#]*?(\s*)(#.*)?$/m, (_m, gap, comment) => `version: ${w.versionName}+${w.versionCode}${comment ? gap ?? " " : ""}${comment ?? ""}`));
717
+ }
718
+ /** JSON 顶层 version 键改写(两遍:先定位最小缩进行,再只改该行——单遍回调在嵌套键
719
+ * 先出现时会把它一并改写,篡改依赖声明)。单行 JSON 回退首个匹配。 */
720
+ function writeJsonVersionKey(file, versionName) {
721
+ const text = readText(file);
722
+ if (!text) throw new Error(`无法读取 ${file}`);
723
+ const lines = text.split("\n");
724
+ const re = /^(\s*)"version"\s*:\s*"([^"]+)"(\s*,?\s*)$/;
725
+ let target = -1;
726
+ let bestIndent = Number.POSITIVE_INFINITY;
727
+ for (let i = 0; i < lines.length; i++) {
728
+ const m = re.exec(lines[i]);
729
+ if (!m) continue;
730
+ const ind = m[1].length;
731
+ if (ind < bestIndent) {
732
+ bestIndent = ind;
733
+ target = i;
734
+ }
735
+ }
736
+ let next;
737
+ if (target >= 0) {
738
+ const m = re.exec(lines[target]);
739
+ lines[target] = `${m[1]}"version": "${versionName}"${m[3]}`;
740
+ next = lines.join("\n");
741
+ } else {
742
+ let topVersion;
743
+ try {
744
+ const obj = JSON.parse(text);
745
+ if (typeof obj.version === "string") topVersion = obj.version;
746
+ } catch {}
747
+ const esc = (topVersion ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
748
+ const re1 = topVersion ? new RegExp(`"version"\\s*:\\s*"${esc}"`) : /"version"\s*:\s*"([^"]+)"/;
749
+ if (!re1.test(text)) throw new Error(`${file} 未找到 "version" 键`);
750
+ next = text.replace(re1, `"version": "${versionName}"`);
751
+ }
752
+ return writeIfChanged(file, next);
753
+ }
754
+ function writeExpoAppJson(file, w) {
755
+ const text = readText(file);
756
+ if (!text) throw new Error(`无法读取 ${file}`);
757
+ const v = replaceGroup(/("version"\s*:\s*")([^"]+)(")/g, text, 2, w.versionName);
758
+ if (v.count === 0) throw new Error(`${file} 未找到 expo "version" 键`);
759
+ let next = v.text;
760
+ next = replaceGroup(/("versionCode"\s*:\s*)(\d+)/g, next, 2, String(w.versionCode)).text;
761
+ next = replaceGroup(/("buildNumber"\s*:\s*")([^"]+)(")/g, next, 2, String(w.versionCode)).text;
762
+ return writeIfChanged(file, next);
763
+ }
764
+ /**
765
+ * 写回各端版本(多端 versionName 同步、versionCode 各端独立)。
766
+ * 返回实际改动的文件列表(commit 范围)。problem 端跳过并在返回中列出(不阻断其他端)。
767
+ */
768
+ function writeProjectVersions(cwd, framework, writes) {
769
+ const loc = locateVersionFiles(cwd, framework);
770
+ const changed = [];
771
+ const skipped = [];
772
+ const plat = new Map(writes.map((w) => [w.platform, w]));
773
+ const apply = (file, fn, key) => {
774
+ if (!file) {
775
+ const w = plat.get(key);
776
+ if (w) skipped.push({
777
+ platform: w.platform,
778
+ reason: `未找到版本文件(${key})`
779
+ });
780
+ return;
781
+ }
782
+ try {
783
+ if (fn(file)) changed.push(file);
784
+ } catch (e) {
785
+ const code = e.code;
786
+ if (code === "EACCES" || code === "EPERM") throw e;
787
+ skipped.push({
788
+ platform: key,
789
+ reason: e.message
790
+ });
791
+ }
792
+ };
793
+ if (plat.has("android")) apply(loc.android, (f) => writeGradle(f, plat.get("android")), "android");
794
+ if (plat.has("ios")) apply(loc.ios, (f) => writePbxproj(f, plat.get("ios")), "ios");
795
+ if (plat.has("harmony")) apply(loc.harmony, (f) => writeAppJson5(f, plat.get("harmony")), "harmony");
796
+ if (plat.has("flutter")) apply(loc.pubspec, (f) => writePubspec(f, plat.get("flutter")), "flutter");
797
+ if (plat.has("js")) apply(loc.packageJson, (f) => writeJsonVersionKey(f, plat.get("js").versionName), "js");
798
+ if (plat.has("tauri")) apply(loc.tauri, (f) => writeJsonVersionKey(f, plat.get("tauri").versionName), "tauri");
799
+ if (plat.has("expo")) apply(loc.expoAppJson, (f) => writeExpoAppJson(f, plat.get("expo")), "expo");
800
+ return {
801
+ changed: [...new Set(changed)],
802
+ skipped
803
+ };
804
+ }
805
+ //#endregion
806
+ export { readJson as C, findArtifacts as S, subdirs as T, suggestFramework as _, electronFramework as a, deps as b, knownFrameworkAdapters as c, nativeHarmonyAdapter as d, nativeHarmonyFramework as f, rnFramework as g, rnAdapter as h, electronAdapter as i, nativeAndroidAdapter as l, nativeIosFramework as m, writeProjectVersions as n, flutterAdapter as o, nativeIosAdapter as p, redactText as r, flutterFramework as s, readProjectVersions as t, nativeAndroidFramework as u, tauriAdapter as v, resolveFramework as w, detectBundleIds as x, tauriFramework as y };