@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.
- package/dist/{plugin-B2GGlI_2.d.mts → frameworks-D83HVg8t.d.mts} +100 -29
- package/dist/index.d.mts +26 -6
- package/dist/index.mjs +347 -971
- package/dist/plugins/index.d.mts +73 -2
- package/dist/plugins/index.mjs +437 -2
- package/dist/project-4_orxby_.mjs +806 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { C as readJson, S as findArtifacts, T as subdirs, _ as suggestFramework, a as electronFramework, b as deps, c as knownFrameworkAdapters, d as nativeHarmonyAdapter, f as nativeHarmonyFramework, g as rnFramework, h as rnAdapter, i as electronAdapter, l as nativeAndroidAdapter, m as nativeIosFramework, n as writeProjectVersions, o as flutterAdapter, p as nativeIosAdapter, r as redactText, s as flutterFramework, t as readProjectVersions, u as nativeAndroidFramework, v as tauriAdapter, w as resolveFramework, x as detectBundleIds, y as tauriFramework } from "./project-4_orxby_.mjs";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
@@ -49,15 +50,24 @@ const StoreConfigSchema = z.record(z.string(), z.string());
|
|
|
49
50
|
* 值走 ③ 同款 appId 简写归一
|
|
50
51
|
*/
|
|
51
52
|
const storesNormalize = (stores) => {
|
|
52
|
-
if (Array.isArray(stores)) return Object.fromEntries(stores.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return [id, typeof cfg === "string" ? { appId: cfg } : cfg];
|
|
57
|
-
})
|
|
53
|
+
if (Array.isArray(stores)) return Object.fromEntries(stores.flatMap((el) => {
|
|
54
|
+
if (typeof el === "string") return [[el, {}]];
|
|
55
|
+
const [id, cfg] = Object.entries(el)[0] ?? [];
|
|
56
|
+
if (id === void 0 || cfg === void 0) return [];
|
|
57
|
+
return [[id, typeof cfg === "string" ? { appId: cfg } : cfg]];
|
|
58
|
+
}));
|
|
58
59
|
return Object.fromEntries(Object.entries(stores).map(([id, cfg]) => [id, typeof cfg === "string" ? { appId: cfg } : cfg]));
|
|
59
60
|
};
|
|
60
|
-
const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z.string()])), z.array(z.union([z.string(), StoreConfigSchema]))]).
|
|
61
|
+
const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z.string()])), z.array(z.union([z.string(), StoreConfigSchema]))]).superRefine((val, ctx) => {
|
|
62
|
+
if (!Array.isArray(val)) return;
|
|
63
|
+
for (const el of val) {
|
|
64
|
+
if (typeof el === "string") continue;
|
|
65
|
+
if (Object.keys(el).length !== 1) ctx.addIssue({
|
|
66
|
+
code: "custom",
|
|
67
|
+
message: `stores 数组内的对象元素必须形如 { 渠道id: appId简写 }(单键),收到: ${JSON.stringify(el)}`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}).transform(storesNormalize).default({});
|
|
61
71
|
/** 项目级配置(applift.config.ts 的形状);beta 段见上方 BetaConfigSchema */
|
|
62
72
|
/**
|
|
63
73
|
* 内测分发配置(applift beta 无平台参数时并行发布的渠道开关):
|
|
@@ -66,8 +76,6 @@ const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z
|
|
|
66
76
|
* - testflight: App Store Connect 上传(ipa 进 TestFlight)
|
|
67
77
|
*/
|
|
68
78
|
const BetaConfigSchema = z.object({
|
|
69
|
-
/** 自动 version:true(走推断)| major/minor/patch/prerelease | <semver>;CLI --bump/--no-bump 覆盖 */
|
|
70
|
-
bump: z.union([z.boolean(), z.string()]).optional(),
|
|
71
79
|
pgyer: z.object({
|
|
72
80
|
platforms: z.array(z.enum([
|
|
73
81
|
"android",
|
|
@@ -94,8 +102,6 @@ const VersionCodeConfigSchema = z.union([z.literal("increment"), z.object({
|
|
|
94
102
|
platforms: z.array(z.string()).optional()
|
|
95
103
|
})]);
|
|
96
104
|
const VersionConfigSchema = z.object({ versionCode: VersionCodeConfigSchema.default("increment") });
|
|
97
|
-
/** 正式发版段的自动 version 开关(语义同 beta.bump) */
|
|
98
|
-
const ReleaseConfigSchema = z.object({ bump: z.union([z.boolean(), z.string()]).optional() });
|
|
99
105
|
const ProjectConfigSchema = z.object({
|
|
100
106
|
/** 项目显示名 */
|
|
101
107
|
name: z.string().optional(),
|
|
@@ -119,13 +125,6 @@ const ProjectConfigSchema = z.object({
|
|
|
119
125
|
/** App Store Connect 按 bundleId 定位 App(asc 渠道 submit 用) */
|
|
120
126
|
bundleId: z.string().optional()
|
|
121
127
|
}).optional(),
|
|
122
|
-
/** 构建命令(缺省用项目自带 npm scripts,见 pipeline 约定) */
|
|
123
|
-
build: z.object({
|
|
124
|
-
android: z.string().optional(),
|
|
125
|
-
harmony: z.string().optional(),
|
|
126
|
-
harmonyApp: z.string().optional(),
|
|
127
|
-
ios: z.string().optional()
|
|
128
|
-
}).optional(),
|
|
129
128
|
/** 产物目录覆盖(非标准工程):每平台 glob 数组,优先于框架内置 pattern(桌面平台 + 插件自报平台经 catchall 收) */
|
|
130
129
|
artifacts: z.object({
|
|
131
130
|
android: z.array(z.string()).optional(),
|
|
@@ -137,8 +136,6 @@ const ProjectConfigSchema = z.object({
|
|
|
137
136
|
}).catchall(z.array(z.string()).optional()).optional(),
|
|
138
137
|
/** 版本管理(versionCode 策略等) */
|
|
139
138
|
version: VersionConfigSchema.optional(),
|
|
140
|
-
/** 正式发版自动 version 开关 */
|
|
141
|
-
release: ReleaseConfigSchema.optional(),
|
|
142
139
|
/** 内测分发(beta 无平台参数时的多渠道并行配置) */
|
|
143
140
|
beta: BetaConfigSchema.optional()
|
|
144
141
|
});
|
|
@@ -253,10 +250,19 @@ function appliftAlias() {
|
|
|
253
250
|
const distEntry = fileURLToPath(import.meta.resolve("@applift/core"));
|
|
254
251
|
const coreEntry = fs.existsSync(distEntry) ? distEntry : path.join(path.dirname(path.dirname(distEntry)), "src", "index.ts");
|
|
255
252
|
if (!fs.existsSync(coreEntry)) return {};
|
|
256
|
-
|
|
253
|
+
const alias = {
|
|
257
254
|
"@applift/core": coreEntry,
|
|
258
255
|
"@applift/cli": coreEntry
|
|
259
256
|
};
|
|
257
|
+
try {
|
|
258
|
+
const distPlugins = fileURLToPath(import.meta.resolve("@applift/core/plugins"));
|
|
259
|
+
const pluginsEntry = fs.existsSync(distPlugins) ? distPlugins : path.join(path.dirname(coreEntry), "plugins", "index.ts");
|
|
260
|
+
if (fs.existsSync(pluginsEntry)) {
|
|
261
|
+
alias["@applift/core/plugins"] = pluginsEntry;
|
|
262
|
+
alias["@applift/cli/plugins"] = pluginsEntry;
|
|
263
|
+
}
|
|
264
|
+
} catch {}
|
|
265
|
+
return alias;
|
|
260
266
|
} catch {
|
|
261
267
|
return {};
|
|
262
268
|
}
|
|
@@ -279,14 +285,34 @@ async function loadProjectConfig(cwd) {
|
|
|
279
285
|
packageJson: false,
|
|
280
286
|
defaults: ProjectConfigSchema.parse({})
|
|
281
287
|
});
|
|
282
|
-
const
|
|
288
|
+
const raw = result.config ?? {};
|
|
289
|
+
const parsed = ProjectConfigSchema.safeParse(raw);
|
|
283
290
|
if (!parsed.success) {
|
|
284
291
|
const details = parsed.error.issues.map((i) => `${i.path.join(".") || "(根)"}: ${i.message}`).join(";");
|
|
285
292
|
throw new Error(`applift.config.ts 字段校验失败 → ${details}`);
|
|
286
293
|
}
|
|
294
|
+
const config = parsed.data;
|
|
295
|
+
const rawRec = raw;
|
|
296
|
+
const deprecated = [];
|
|
297
|
+
if ("build" in rawRec) deprecated.push({
|
|
298
|
+
key: "build",
|
|
299
|
+
hint: "构建已回归项目自身(applift 不接管构建),请删除该键"
|
|
300
|
+
});
|
|
301
|
+
if ("bump" in (rawRec.beta ?? {})) deprecated.push({
|
|
302
|
+
key: "beta.bump",
|
|
303
|
+
hint: "改用 applift version bump 显式升版本(applift 不再代构建)"
|
|
304
|
+
});
|
|
305
|
+
if ("release" in rawRec && "bump" in (rawRec.release ?? {})) deprecated.push({
|
|
306
|
+
key: "release.bump",
|
|
307
|
+
hint: "改用 applift version bump 显式升版本(applift 不再代构建)"
|
|
308
|
+
});
|
|
309
|
+
if (deprecated.length > 0) {
|
|
310
|
+
const lines = deprecated.map((d) => ` - ${d.key}: ${d.hint}`).join("\n");
|
|
311
|
+
process.stderr.write(`⚠ applift.config.ts 含已废弃配置键(将被忽略):\n${lines}\n`);
|
|
312
|
+
}
|
|
287
313
|
return {
|
|
288
|
-
config
|
|
289
|
-
raw
|
|
314
|
+
config,
|
|
315
|
+
raw,
|
|
290
316
|
cwd: result.cwd ?? cwd
|
|
291
317
|
};
|
|
292
318
|
}
|
|
@@ -323,13 +349,27 @@ function setProjectProfile(cwd, profile, filePath = PROJECTS_MAP_PATH) {
|
|
|
323
349
|
saveProjectProfileMap(map, filePath);
|
|
324
350
|
}
|
|
325
351
|
/**
|
|
326
|
-
* 解析项目生效 profile
|
|
327
|
-
* CLI 的 --profile 旗标在调用方以 `opts.profile ??
|
|
352
|
+
* 解析项目生效 profile 及其来源:projects.json 个人选择 > 项目配置 profile(团队默认)> 'default'。
|
|
353
|
+
* CLI 的 --profile 旗标在调用方以 `opts.profile ?? ...` 再盖一层(最高优先级,来源即"旗标")。
|
|
328
354
|
*/
|
|
329
|
-
function
|
|
355
|
+
function resolveProfileSource(cwd, projectCfgProfile, filePath = PROJECTS_MAP_PATH) {
|
|
330
356
|
const mapped = loadProjectProfileMap(filePath)[path.resolve(cwd)];
|
|
331
|
-
if (mapped) return
|
|
332
|
-
|
|
357
|
+
if (mapped) return {
|
|
358
|
+
profile: mapped,
|
|
359
|
+
source: "applift use(个人)"
|
|
360
|
+
};
|
|
361
|
+
if (projectCfgProfile) return {
|
|
362
|
+
profile: projectCfgProfile,
|
|
363
|
+
source: "项目配置 profile(团队默认)"
|
|
364
|
+
};
|
|
365
|
+
return {
|
|
366
|
+
profile: "default",
|
|
367
|
+
source: "默认 default"
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/** 解析项目生效 profile(只取名不问来源;来源用 resolveProfileSource) */
|
|
371
|
+
function resolveProfileFor(cwd, projectCfgProfile, filePath = PROJECTS_MAP_PATH) {
|
|
372
|
+
return resolveProfileSource(cwd, projectCfgProfile, filePath).profile;
|
|
333
373
|
}
|
|
334
374
|
//#endregion
|
|
335
375
|
//#region src/channels/shared/sign.ts
|
|
@@ -395,7 +435,12 @@ async function formPost(url, params, { timeout = 6e4 } = {}) {
|
|
|
395
435
|
* 能力检测不存在时回退整读(旧运行时兜底)。
|
|
396
436
|
*/
|
|
397
437
|
async function fileBlob(p) {
|
|
398
|
-
if (typeof fs.openAsBlob === "function")
|
|
438
|
+
if (typeof fs.openAsBlob === "function") try {
|
|
439
|
+
return await fs.openAsBlob(p);
|
|
440
|
+
} catch (e) {
|
|
441
|
+
const cause = e instanceof Error ? e.message : String(e);
|
|
442
|
+
throw new Error(`打开上传文件失败 ${p}: ${cause}`);
|
|
443
|
+
}
|
|
399
444
|
return new Blob([await fs.promises.readFile(p)]);
|
|
400
445
|
}
|
|
401
446
|
//#endregion
|
|
@@ -452,6 +497,20 @@ async function pgyerPost(host, apiPath, form) {
|
|
|
452
497
|
throw new Error(`蒲公英 ${apiPath} 返回非 JSON(HTTP ${res.status}): ${text.slice(0, 200)}`);
|
|
453
498
|
}
|
|
454
499
|
}
|
|
500
|
+
/** 按 HOSTS 序尝试的 POST(任一域名成功即返回;全挂抛最后一个错误)——状态查询等幂等接口用 */
|
|
501
|
+
async function pgyerPostFailover(ctx, apiPath, form) {
|
|
502
|
+
let lastErr;
|
|
503
|
+
for (const host of HOSTS) try {
|
|
504
|
+
return await pgyerPost(host, apiPath, form);
|
|
505
|
+
} catch (e) {
|
|
506
|
+
lastErr = e;
|
|
507
|
+
ctx.emit({
|
|
508
|
+
type: "warn",
|
|
509
|
+
message: `${host} 不可用,尝试备用域名`
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
throw lastErr instanceof Error ? lastErr : /* @__PURE__ */ new Error(`蒲公英 ${apiPath} 请求失败`);
|
|
513
|
+
}
|
|
455
514
|
var PgyerChannel = class {
|
|
456
515
|
id = "pgyer";
|
|
457
516
|
label = "蒲公英";
|
|
@@ -532,12 +591,29 @@ var PgyerChannel = class {
|
|
|
532
591
|
message: "等待蒲公英处理"
|
|
533
592
|
});
|
|
534
593
|
const deadline = Date.now() + 9e5;
|
|
594
|
+
let consecutiveErrors = 0;
|
|
535
595
|
while (Date.now() < deadline) {
|
|
536
596
|
await sleep(3e3);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
597
|
+
let info;
|
|
598
|
+
try {
|
|
599
|
+
info = await pgyerPostFailover(ctx, "/apiv2/app/buildInfo", {
|
|
600
|
+
_api_key: apiKey,
|
|
601
|
+
buildKey
|
|
602
|
+
});
|
|
603
|
+
consecutiveErrors = 0;
|
|
604
|
+
} catch (e) {
|
|
605
|
+
consecutiveErrors++;
|
|
606
|
+
const lastErr = e instanceof Error ? e.message : String(e);
|
|
607
|
+
ctx.emit({
|
|
608
|
+
type: "warn",
|
|
609
|
+
message: `buildInfo 状态查询失败(连续 ${consecutiveErrors}/10): ${lastErr}`
|
|
610
|
+
});
|
|
611
|
+
if (consecutiveErrors >= 10) return {
|
|
612
|
+
ok: false,
|
|
613
|
+
error: `蒲公英状态查询持续失败(包已上传,可到控制台确认): ${lastErr}`
|
|
614
|
+
};
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
541
617
|
if (info.code === 0 && info.data?.buildShortcutUrl) {
|
|
542
618
|
ctx.emit({
|
|
543
619
|
type: "phase",
|
|
@@ -588,9 +664,8 @@ function platformOf(artifactPath) {
|
|
|
588
664
|
}
|
|
589
665
|
//#endregion
|
|
590
666
|
//#region src/channels/shared/agc-token.ts
|
|
591
|
-
/** 华为 AGC token(安卓/鸿蒙同源)——自 v1 lib/agc.mjs 迁移 */
|
|
592
667
|
async function getAgcToken({ clientId, clientSecret }) {
|
|
593
|
-
const
|
|
668
|
+
const res = await fetch("https://connect-api.cloud.huawei.com/api/oauth2/v1/token", {
|
|
594
669
|
method: "POST",
|
|
595
670
|
headers: { "Content-Type": "application/json" },
|
|
596
671
|
body: JSON.stringify({
|
|
@@ -599,7 +674,14 @@ async function getAgcToken({ clientId, clientSecret }) {
|
|
|
599
674
|
client_secret: clientSecret
|
|
600
675
|
}),
|
|
601
676
|
signal: AbortSignal.timeout(3e4)
|
|
602
|
-
})
|
|
677
|
+
});
|
|
678
|
+
const text = await res.text();
|
|
679
|
+
let json;
|
|
680
|
+
try {
|
|
681
|
+
json = JSON.parse(text);
|
|
682
|
+
} catch {
|
|
683
|
+
throw new Error(`AGC token 接口返回非 JSON(HTTP ${res.status}): ${text.slice(0, 200)}`);
|
|
684
|
+
}
|
|
603
685
|
if (!json.access_token) {
|
|
604
686
|
const desc = json.error_description ?? json.ret?.msg ?? json.error ?? JSON.stringify(json).slice(0, 200);
|
|
605
687
|
throw new Error(`AGC 获取 token 失败: ${desc}(检查 client_id/client_secret)`);
|
|
@@ -784,6 +866,7 @@ var HuaweiAndroidChannel = class {
|
|
|
784
866
|
if (remarkLen < 10 || remarkLen > 300) throw new Error(`华为提审备注(remark)长度需在 10-300 字之间,当前 ${remarkLen} 字`);
|
|
785
867
|
}
|
|
786
868
|
if (ctx.updateDescription !== void 0 && [...ctx.updateDescription].length > 500) throw new Error(`华为更新说明(newFeatures)不能超过 500 字符,当前 ${[...ctx.updateDescription].length} 字符`);
|
|
869
|
+
if (ctx.releaseTime && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(ctx.releaseTime)) throw new Error(`华为 releaseTime 格式异常(需 RFC3339 带时区,如 2026-06-20T10:00:00+08:00): ${ctx.releaseTime}`);
|
|
787
870
|
const { headers } = await this.authHeaders(ctx);
|
|
788
871
|
if (ctx.updateDescription) try {
|
|
789
872
|
await hwJson(`${BASE$1}/publish/v2/app-language-info?appId=${encodeURIComponent(appId)}`, {
|
|
@@ -1481,7 +1564,7 @@ var OppoChannel = class {
|
|
|
1481
1564
|
ctx.emit({
|
|
1482
1565
|
type: "phase",
|
|
1483
1566
|
phase: "done",
|
|
1484
|
-
message:
|
|
1567
|
+
message: `已提交(versionCode=${versionCode})`
|
|
1485
1568
|
});
|
|
1486
1569
|
return {
|
|
1487
1570
|
ok: true,
|
|
@@ -2244,8 +2327,17 @@ var YybChannel = class {
|
|
|
2244
2327
|
}
|
|
2245
2328
|
async submit(ctx) {
|
|
2246
2329
|
const { userId, accessSecret, appId, pkgName } = this.creds(ctx);
|
|
2247
|
-
|
|
2248
|
-
if (!stagedInfo)
|
|
2330
|
+
let stagedInfo = this.staged.get(appId);
|
|
2331
|
+
if (!stagedInfo) {
|
|
2332
|
+
ctx.emit({
|
|
2333
|
+
type: "warn",
|
|
2334
|
+
message: "无本进程 stage 记录(--skip-stage 跨进程),重新上传当前包后提审"
|
|
2335
|
+
});
|
|
2336
|
+
const r = await this.stage(ctx);
|
|
2337
|
+
if (!r.ok) throw new Error(`应用宝重新上传失败: ${r.error ?? "未知错误"}`);
|
|
2338
|
+
stagedInfo = this.staged.get(appId);
|
|
2339
|
+
if (!stagedInfo) throw new Error("应用宝重新上传后仍缺少流水号(内部状态异常)");
|
|
2340
|
+
}
|
|
2249
2341
|
ctx.emit({
|
|
2250
2342
|
type: "phase",
|
|
2251
2343
|
phase: "submit"
|
|
@@ -2271,14 +2363,14 @@ var YybChannel = class {
|
|
|
2271
2363
|
businessParams["deploy_type"] = "2";
|
|
2272
2364
|
businessParams["deploy_time"] = String(deployTime);
|
|
2273
2365
|
} else businessParams["deploy_type"] = "1";
|
|
2274
|
-
|
|
2366
|
+
await yybApi("/update_app", {
|
|
2275
2367
|
userId,
|
|
2276
2368
|
accessSecret
|
|
2277
2369
|
}, businessParams);
|
|
2278
2370
|
ctx.emit({
|
|
2279
2371
|
type: "phase",
|
|
2280
2372
|
phase: "done",
|
|
2281
|
-
message:
|
|
2373
|
+
message: `已提交审核(流水号 ${stagedInfo.serialNumber})`
|
|
2282
2374
|
});
|
|
2283
2375
|
return {
|
|
2284
2376
|
ok: true,
|
|
@@ -2485,8 +2577,9 @@ var AgcHarmonyChannel = class {
|
|
|
2485
2577
|
ref: packageId || objectId
|
|
2486
2578
|
};
|
|
2487
2579
|
}
|
|
2488
|
-
/** submit = v1 releaseHarmonyApp 完整链路:上传+绑定+文案 → 等编译轮询 →
|
|
2489
|
-
|
|
2580
|
+
/** submit = v1 releaseHarmonyApp 完整链路:上传+绑定+文案 → 等编译轮询 → 提审(带重试)。
|
|
2581
|
+
* stagedRef = stage() 返回的 packageId:包已在草稿区,跳过重传直接等编译+提审(.app 常为数百 MB~GB,双倍上传不可接受) */
|
|
2582
|
+
async submit(ctx, stagedRef) {
|
|
2490
2583
|
if (ctx.target.releaseType === "6") return {
|
|
2491
2584
|
ok: false,
|
|
2492
2585
|
error: "AGC 测试发布(releaseType=6)的自动提审走 beta 配置 test['agc'].submit=true(上传绑定后自动提交测试版本审核)"
|
|
@@ -2498,7 +2591,13 @@ var AgcHarmonyChannel = class {
|
|
|
2498
2591
|
phase: "auth",
|
|
2499
2592
|
message: "token 获取成功"
|
|
2500
2593
|
});
|
|
2501
|
-
|
|
2594
|
+
let packageId = stagedRef;
|
|
2595
|
+
if (packageId) ctx.emit({
|
|
2596
|
+
type: "phase",
|
|
2597
|
+
phase: "upload",
|
|
2598
|
+
message: `复用 stage 已上传的软件包(pkgId: ${packageId},跳过重传)`
|
|
2599
|
+
});
|
|
2600
|
+
else ({packageId} = await this.uploadFormal(ctx, appId, headers));
|
|
2502
2601
|
if (packageId) await this.awaitCompile(ctx, appId, packageId, headers);
|
|
2503
2602
|
if (!ctx.updateDescription) ctx.emit({
|
|
2504
2603
|
type: "warn",
|
|
@@ -2609,7 +2708,8 @@ var AgcHarmonyChannel = class {
|
|
|
2609
2708
|
const upRes = await fetch(urlInfo.url, {
|
|
2610
2709
|
method,
|
|
2611
2710
|
headers: upHeaders,
|
|
2612
|
-
body: fs.
|
|
2711
|
+
body: Readable.toWeb(fs.createReadStream(ctx.artifactPath)),
|
|
2712
|
+
duplex: "half",
|
|
2613
2713
|
signal: AbortSignal.timeout(18e5)
|
|
2614
2714
|
});
|
|
2615
2715
|
if (![
|
|
@@ -2915,7 +3015,13 @@ async function ascGet(jwt, apiPath) {
|
|
|
2915
3015
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
2916
3016
|
signal: AbortSignal.timeout(3e4)
|
|
2917
3017
|
});
|
|
2918
|
-
const
|
|
3018
|
+
const text = await res.text();
|
|
3019
|
+
let json;
|
|
3020
|
+
try {
|
|
3021
|
+
json = JSON.parse(text);
|
|
3022
|
+
} catch {
|
|
3023
|
+
throw new Error(`ASC API ${apiPath} 返回非 JSON(HTTP ${res.status}): ${text.slice(0, 300)}`);
|
|
3024
|
+
}
|
|
2919
3025
|
if (!res.ok) throw new Error(`ASC API ${apiPath} 失败 HTTP ${res.status}: ${JSON.stringify(json.errors ?? json).slice(0, 300)}`);
|
|
2920
3026
|
return json;
|
|
2921
3027
|
}
|
|
@@ -3015,9 +3121,13 @@ var AscChannel = class {
|
|
|
3015
3121
|
issuerId
|
|
3016
3122
|
], {
|
|
3017
3123
|
cwd,
|
|
3018
|
-
encoding: "utf8"
|
|
3124
|
+
encoding: "utf8",
|
|
3125
|
+
timeout: 18e5
|
|
3019
3126
|
});
|
|
3020
|
-
if (r.error)
|
|
3127
|
+
if (r.error) {
|
|
3128
|
+
if ("code" in r.error && r.error.code === "ETIMEDOUT") throw new Error("xcrun altool 超时(30min)——检查网络/钥匙串/CI 无 TTY 弹窗");
|
|
3129
|
+
throw new Error(`无法执行 xcrun: ${r.error.message}(altool 需 macOS + Xcode)`);
|
|
3130
|
+
}
|
|
3021
3131
|
const output = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim().slice(-2e3);
|
|
3022
3132
|
if (r.status !== 0) throw new Error(`altool 上传失败(exit ${r.status}): ${output || "无输出"}`);
|
|
3023
3133
|
if (output) ctx.emit({
|
|
@@ -3470,6 +3580,14 @@ async function runLimited(items, limit, fn) {
|
|
|
3470
3580
|
});
|
|
3471
3581
|
await Promise.all(workers);
|
|
3472
3582
|
}
|
|
3583
|
+
/** 错误消息(带一层 cause 解包:fetch 网络层的真实原因如 NotReadableError 藏在 cause 里) */
|
|
3584
|
+
function errText(e) {
|
|
3585
|
+
if (e instanceof Error) {
|
|
3586
|
+
const cause = e.cause instanceof Error ? `(${e.cause.message})` : "";
|
|
3587
|
+
return `${e.message}${cause}`;
|
|
3588
|
+
}
|
|
3589
|
+
return String(e);
|
|
3590
|
+
}
|
|
3473
3591
|
/** stage 阶段:并发传包到各渠道草稿(不提审);'stage:before' 返回 false 跳过该渠道 */
|
|
3474
3592
|
async function runStage(input) {
|
|
3475
3593
|
const plugins = input.plugins;
|
|
@@ -3504,7 +3622,8 @@ async function runStage(input) {
|
|
|
3504
3622
|
channel: channel.id,
|
|
3505
3623
|
ok: r.ok,
|
|
3506
3624
|
ref: r.ref,
|
|
3507
|
-
error: r.error
|
|
3625
|
+
error: r.error,
|
|
3626
|
+
artifactPath: effectiveCtx.artifactPath
|
|
3508
3627
|
});
|
|
3509
3628
|
const plan = await safePlan(channel, effectiveCtx);
|
|
3510
3629
|
summary.push({
|
|
@@ -3527,7 +3646,7 @@ async function runStage(input) {
|
|
|
3527
3646
|
channel: channel.id,
|
|
3528
3647
|
label: channel.label,
|
|
3529
3648
|
ok: false,
|
|
3530
|
-
stageError: e
|
|
3649
|
+
stageError: errText(e),
|
|
3531
3650
|
plan
|
|
3532
3651
|
});
|
|
3533
3652
|
input.historySink?.({
|
|
@@ -3619,6 +3738,15 @@ async function runSubmit(input, options) {
|
|
|
3619
3738
|
ref: stageResult.ref,
|
|
3620
3739
|
artifactVersion: plans.find((p) => p.channel === channel.id)?.artifactVersion || void 0
|
|
3621
3740
|
});
|
|
3741
|
+
if (plugins) await plugins.broadcast("stage:after", {
|
|
3742
|
+
channel: channel.id,
|
|
3743
|
+
cwd: input.cwd
|
|
3744
|
+
}, {
|
|
3745
|
+
channel: channel.id,
|
|
3746
|
+
ok: true,
|
|
3747
|
+
ref: stageResult.ref,
|
|
3748
|
+
artifactPath: effectiveCtx.artifactPath
|
|
3749
|
+
});
|
|
3622
3750
|
}
|
|
3623
3751
|
const r = await channel.submit(effectiveCtx, stagedRef);
|
|
3624
3752
|
summary.push({
|
|
@@ -3640,7 +3768,7 @@ async function runSubmit(input, options) {
|
|
|
3640
3768
|
channel: channel.id,
|
|
3641
3769
|
label: channel.label,
|
|
3642
3770
|
ok: false,
|
|
3643
|
-
submitError: e
|
|
3771
|
+
submitError: errText(e),
|
|
3644
3772
|
plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
|
|
3645
3773
|
});
|
|
3646
3774
|
input.historySink?.({
|
|
@@ -3689,6 +3817,166 @@ function planFor(plans, base, id, artifactPath) {
|
|
|
3689
3817
|
return plans.find((p) => p.channel === id) ?? base.get(id) ?? fallbackPlan(id, artifactPath);
|
|
3690
3818
|
}
|
|
3691
3819
|
//#endregion
|
|
3820
|
+
//#region src/version/git.ts
|
|
3821
|
+
/**
|
|
3822
|
+
* git 只读/写小助手(bump 编排与推断共用)。
|
|
3823
|
+
* spawnSync + 超时;失败返回 ok:false 由调用方决定语义,不在此抛。
|
|
3824
|
+
*/
|
|
3825
|
+
function git(cwd, args) {
|
|
3826
|
+
const r = spawnSync("git", args, {
|
|
3827
|
+
cwd,
|
|
3828
|
+
encoding: "utf8",
|
|
3829
|
+
timeout: 6e4
|
|
3830
|
+
});
|
|
3831
|
+
return {
|
|
3832
|
+
ok: !r.error && r.status === 0,
|
|
3833
|
+
stdout: (r.stdout ?? "").trim(),
|
|
3834
|
+
stderr: (r.stderr ?? "").trim()
|
|
3835
|
+
};
|
|
3836
|
+
}
|
|
3837
|
+
function isGitRepo(cwd) {
|
|
3838
|
+
return git(cwd, ["rev-parse", "--is-inside-work-tree"]).stdout === "true";
|
|
3839
|
+
}
|
|
3840
|
+
/** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
|
|
3841
|
+
function looseSemver(tag) {
|
|
3842
|
+
const direct = semver.valid(tag);
|
|
3843
|
+
if (direct) return direct;
|
|
3844
|
+
const m = /(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/.exec(tag);
|
|
3845
|
+
return m ? semver.valid(m[1]) ?? void 0 : void 0;
|
|
3846
|
+
}
|
|
3847
|
+
/**
|
|
3848
|
+
* 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
|
|
3849
|
+
* 无 tag / 非 git 仓库返回 undefined。
|
|
3850
|
+
*/
|
|
3851
|
+
function latestSemverTag(cwd) {
|
|
3852
|
+
const r = git(cwd, ["tag", "-l"]);
|
|
3853
|
+
if (!r.ok) return void 0;
|
|
3854
|
+
let best;
|
|
3855
|
+
for (const raw of r.stdout.split("\n")) {
|
|
3856
|
+
const t = raw.trim();
|
|
3857
|
+
if (!t) continue;
|
|
3858
|
+
const v = looseSemver(t);
|
|
3859
|
+
if (!v) continue;
|
|
3860
|
+
if (!best || semver.gt(v, best.v)) best = {
|
|
3861
|
+
raw: t,
|
|
3862
|
+
v
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
return best?.raw;
|
|
3866
|
+
}
|
|
3867
|
+
/** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
|
|
3868
|
+
function commitsSince(cwd, ref) {
|
|
3869
|
+
const r = git(cwd, [
|
|
3870
|
+
"log",
|
|
3871
|
+
ref ? `${ref}..HEAD` : "HEAD",
|
|
3872
|
+
"--format=%B%x00",
|
|
3873
|
+
"--no-merges"
|
|
3874
|
+
]);
|
|
3875
|
+
if (!r.ok) return [];
|
|
3876
|
+
return r.stdout.split("\0").map((m) => m.trim()).filter(Boolean);
|
|
3877
|
+
}
|
|
3878
|
+
/** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
|
|
3879
|
+
* 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
|
|
3880
|
+
function statusShort(cwd) {
|
|
3881
|
+
const r = spawnSync("git", ["status", "--porcelain"], {
|
|
3882
|
+
cwd,
|
|
3883
|
+
encoding: "utf8",
|
|
3884
|
+
timeout: 3e4
|
|
3885
|
+
});
|
|
3886
|
+
if (r.error || r.status !== 0) return [];
|
|
3887
|
+
return (r.stdout ?? "").split("\n").map((l) => l.trimEnd()).filter(Boolean);
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* release 分发成功后补 tag(tag = 实际已发布的正式版本;beta 不打 tag,追溯走 history)。
|
|
3891
|
+
* - tag 已存在(多端发布,前一端已打)→ 跳过
|
|
3892
|
+
* - dryRun / 无版本 / 非 git 仓库 → 不动、不炸(tag 失败不阻断发布主流程)
|
|
3893
|
+
*/
|
|
3894
|
+
function tagReleaseVersion(cwd, version, opts = {}) {
|
|
3895
|
+
if (!version) return {
|
|
3896
|
+
tagged: false,
|
|
3897
|
+
reason: "无版本"
|
|
3898
|
+
};
|
|
3899
|
+
if (opts.dryRun) return {
|
|
3900
|
+
tagged: false,
|
|
3901
|
+
reason: "dry-run"
|
|
3902
|
+
};
|
|
3903
|
+
const tag = `v${version}`;
|
|
3904
|
+
if (spawnSync("git", [
|
|
3905
|
+
"rev-parse",
|
|
3906
|
+
"-q",
|
|
3907
|
+
"--verify",
|
|
3908
|
+
`refs/tags/${tag}`
|
|
3909
|
+
], {
|
|
3910
|
+
cwd,
|
|
3911
|
+
encoding: "utf8"
|
|
3912
|
+
}).status === 0) return {
|
|
3913
|
+
tagged: false,
|
|
3914
|
+
reason: `tag ${tag} 已存在(前一端已发布)`
|
|
3915
|
+
};
|
|
3916
|
+
const r = spawnSync("git", ["tag", tag], {
|
|
3917
|
+
cwd,
|
|
3918
|
+
encoding: "utf8"
|
|
3919
|
+
});
|
|
3920
|
+
if (r.status !== 0) return {
|
|
3921
|
+
tagged: false,
|
|
3922
|
+
reason: `tag ${tag} 创建失败:${r.stderr.trim()}`
|
|
3923
|
+
};
|
|
3924
|
+
return { tagged: true };
|
|
3925
|
+
}
|
|
3926
|
+
//#endregion
|
|
3927
|
+
//#region src/update.ts
|
|
3928
|
+
/**
|
|
3929
|
+
* 热更能力契约:applift 的第三维扩展(framework × channel 之外的 update 维度)。
|
|
3930
|
+
*
|
|
3931
|
+
* provider 与 framework 绑定(如 react-native-update 只服务 RN 工程),由插件经
|
|
3932
|
+
* 'update:register' 注册;beta/release 管线的投递路由(热更 vs 原生包)消费 judge 结果,
|
|
3933
|
+
* 只做推荐与证据展示,最终投递方式由用户在确认界面拍板(CI 非交互时 uncertain 即中止)。
|
|
3934
|
+
*/
|
|
3935
|
+
/**
|
|
3936
|
+
* 投递路由:按当前 framework 匹配 provider 并执行判定。
|
|
3937
|
+
* - 无 provider / 无匹配 → null(走原生包路径,原始行为不受影响)
|
|
3938
|
+
* - 多个 provider 匹配同一 framework → 配置错误(一个框架一个热更实现)
|
|
3939
|
+
* 结论是推荐值:最终投递方式由用户在确认界面拍板,CI 非交互时 uncertain 中止
|
|
3940
|
+
*/
|
|
3941
|
+
async function routeDelivery(providers, ctx, diff, platform) {
|
|
3942
|
+
const matched = providers.filter((p) => p.frameworks.includes(ctx.framework.id) && (!platform || !p.platforms || p.platforms.includes(platform)));
|
|
3943
|
+
if (matched.length === 0) return null;
|
|
3944
|
+
if (matched.length > 1) throw new Error(`多个热更 provider 匹配 framework「${ctx.framework.id}」:${matched.map((p) => p.id).join(", ")}——一个框架只应有一个热更实现`);
|
|
3945
|
+
const provider = matched[0];
|
|
3946
|
+
return {
|
|
3947
|
+
provider,
|
|
3948
|
+
judgement: await provider.judge(diff, ctx)
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* 收集上个发版锚点(最新 semver tag)到工作区的变更文件列表。
|
|
3953
|
+
* 含未提交改动与未跟踪文件(pushy bundle 打包的是工作区内容,判定口径必须一致)。
|
|
3954
|
+
* 无锚点 / 非 git 仓库 → null(调用方按 uncertain 处理,不猜)。
|
|
3955
|
+
*/
|
|
3956
|
+
async function collectReleaseDiff(cwd) {
|
|
3957
|
+
const anchor = latestSemverTag(cwd);
|
|
3958
|
+
if (!anchor) return null;
|
|
3959
|
+
const r = spawnSync("git", [
|
|
3960
|
+
"diff",
|
|
3961
|
+
"--name-only",
|
|
3962
|
+
anchor
|
|
3963
|
+
], {
|
|
3964
|
+
cwd,
|
|
3965
|
+
encoding: "utf8"
|
|
3966
|
+
});
|
|
3967
|
+
const untracked = spawnSync("git", [
|
|
3968
|
+
"ls-files",
|
|
3969
|
+
"--others",
|
|
3970
|
+
"--exclude-standard"
|
|
3971
|
+
], {
|
|
3972
|
+
cwd,
|
|
3973
|
+
encoding: "utf8"
|
|
3974
|
+
});
|
|
3975
|
+
if (r.status !== 0 || untracked.status !== 0) return null;
|
|
3976
|
+
const files = [...r.stdout.split("\n"), ...untracked.stdout.split("\n")].map((l) => l.trim());
|
|
3977
|
+
return [...new Set(files)].filter(Boolean);
|
|
3978
|
+
}
|
|
3979
|
+
//#endregion
|
|
3692
3980
|
//#region src/plugin.ts
|
|
3693
3981
|
var PluginHost = class {
|
|
3694
3982
|
plugins;
|
|
@@ -3735,403 +4023,6 @@ function resolvePlugins(userConfig) {
|
|
|
3735
4023
|
return (userConfig.plugins ?? []).map((p) => typeof p === "function" ? p() : p);
|
|
3736
4024
|
}
|
|
3737
4025
|
//#endregion
|
|
3738
|
-
//#region src/framework.ts
|
|
3739
|
-
/**
|
|
3740
|
-
* 框架适配器:让 applift 框架无关(RN / Flutter / 原生安卓 / 原生 iOS / 原生鸿蒙)。
|
|
3741
|
-
* 探测规则 + 产物 pattern + 缺省构建命令;插件可用 'channel:register' 旁路注册自定义 FrameworkAdapter。
|
|
3742
|
-
*/
|
|
3743
|
-
/** 一层子目录列表(探测用,跳过隐藏目录与 node_modules/Pods) */
|
|
3744
|
-
function subdirs(cwd, depth) {
|
|
3745
|
-
if (depth <= 0) return [];
|
|
3746
|
-
const out = [];
|
|
3747
|
-
let names;
|
|
3748
|
-
try {
|
|
3749
|
-
names = fs.readdirSync(cwd, { withFileTypes: true });
|
|
3750
|
-
} catch {
|
|
3751
|
-
return [];
|
|
3752
|
-
}
|
|
3753
|
-
for (const name of names) {
|
|
3754
|
-
if (!name.isDirectory() || name.name.startsWith(".") || name.name === "node_modules" || name.name === "Pods") continue;
|
|
3755
|
-
const p = path.join(cwd, name.name);
|
|
3756
|
-
out.push(p, ...subdirs(p, depth - 1));
|
|
3757
|
-
}
|
|
3758
|
-
return out;
|
|
3759
|
-
}
|
|
3760
|
-
/** package.json 读取(容错) */
|
|
3761
|
-
function readJson(filePath) {
|
|
3762
|
-
try {
|
|
3763
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
3764
|
-
} catch {
|
|
3765
|
-
return null;
|
|
3766
|
-
}
|
|
3767
|
-
}
|
|
3768
|
-
function deps(pkg) {
|
|
3769
|
-
if (!pkg) return {};
|
|
3770
|
-
return {
|
|
3771
|
-
...pkg.dependencies,
|
|
3772
|
-
...pkg.devDependencies
|
|
3773
|
-
};
|
|
3774
|
-
}
|
|
3775
|
-
const rnAdapter = {
|
|
3776
|
-
id: "rn",
|
|
3777
|
-
label: "React Native",
|
|
3778
|
-
detect(cwd) {
|
|
3779
|
-
return "react-native" in deps(readJson(path.join(cwd, "package.json")));
|
|
3780
|
-
},
|
|
3781
|
-
/** Expo 项目(依赖 expo 包):同走 RN 产物路径(prebuild 生成原生工程),label 区分展示 */
|
|
3782
|
-
expoAwareLabel(cwd) {
|
|
3783
|
-
return "expo" in deps(readJson(path.join(cwd, "package.json"))) ? "Expo (React Native)" : "React Native";
|
|
3784
|
-
},
|
|
3785
|
-
artifactPatterns(platform) {
|
|
3786
|
-
if (platform === "android") return ["android/app/build/outputs/apk/release/*-release.apk"];
|
|
3787
|
-
if (platform === "ios") return ["build/ios/ipa/*.ipa"];
|
|
3788
|
-
return [
|
|
3789
|
-
"harmony/entry/build/default/outputs/**/entry-*-signed.hap",
|
|
3790
|
-
"harmony/entry/build/default/outputs/**/entry-*.app",
|
|
3791
|
-
"harmony/build/outputs/**/*.app",
|
|
3792
|
-
"build/outputs/**/*.app"
|
|
3793
|
-
];
|
|
3794
|
-
},
|
|
3795
|
-
buildCommand(platform) {
|
|
3796
|
-
if (platform === "android") return [
|
|
3797
|
-
"npm",
|
|
3798
|
-
"run",
|
|
3799
|
-
"build:test",
|
|
3800
|
-
"--",
|
|
3801
|
-
"--platform",
|
|
3802
|
-
"android"
|
|
3803
|
-
];
|
|
3804
|
-
if (platform === "ios") return null;
|
|
3805
|
-
return [
|
|
3806
|
-
"npm",
|
|
3807
|
-
"run",
|
|
3808
|
-
"build:test",
|
|
3809
|
-
"--",
|
|
3810
|
-
"--platform",
|
|
3811
|
-
"harmony"
|
|
3812
|
-
];
|
|
3813
|
-
}
|
|
3814
|
-
};
|
|
3815
|
-
const flutterAdapter = {
|
|
3816
|
-
id: "flutter",
|
|
3817
|
-
label: "Flutter",
|
|
3818
|
-
detect(cwd) {
|
|
3819
|
-
if (readJson(path.join(cwd, "pubspec.yaml"))) return true;
|
|
3820
|
-
try {
|
|
3821
|
-
const text = fs.readFileSync(path.join(cwd, "pubspec.yaml"), "utf8");
|
|
3822
|
-
return /^sdk:\s*flutter$/m.test(text) || /^name:/m.test(text);
|
|
3823
|
-
} catch {
|
|
3824
|
-
return false;
|
|
3825
|
-
}
|
|
3826
|
-
},
|
|
3827
|
-
artifactPatterns(platform) {
|
|
3828
|
-
if (platform === "android") return ["build/app/outputs/flutter-apk/app-release.apk"];
|
|
3829
|
-
if (platform === "ios") return ["build/ios/ipa/*.ipa"];
|
|
3830
|
-
return [
|
|
3831
|
-
"build/**/outputs/**/*-signed.hap",
|
|
3832
|
-
"harmony/entry/build/**/outputs/**/*-signed.hap",
|
|
3833
|
-
"ohos/entry/build/**/outputs/**/*-signed.hap",
|
|
3834
|
-
"build/**/outputs/**/*.app"
|
|
3835
|
-
];
|
|
3836
|
-
},
|
|
3837
|
-
buildCommand(platform) {
|
|
3838
|
-
if (platform === "android") return [
|
|
3839
|
-
"flutter",
|
|
3840
|
-
"build",
|
|
3841
|
-
"apk",
|
|
3842
|
-
"--release"
|
|
3843
|
-
];
|
|
3844
|
-
if (platform === "ios") return [
|
|
3845
|
-
"flutter",
|
|
3846
|
-
"build",
|
|
3847
|
-
"ipa",
|
|
3848
|
-
"--release"
|
|
3849
|
-
];
|
|
3850
|
-
return [
|
|
3851
|
-
"flutter",
|
|
3852
|
-
"build",
|
|
3853
|
-
"hap",
|
|
3854
|
-
"--release"
|
|
3855
|
-
];
|
|
3856
|
-
}
|
|
3857
|
-
};
|
|
3858
|
-
const nativeAndroidAdapter = {
|
|
3859
|
-
id: "native-android",
|
|
3860
|
-
label: "原生 Android",
|
|
3861
|
-
detect(cwd) {
|
|
3862
|
-
if (rnAdapter.detect(cwd)) return false;
|
|
3863
|
-
return fs.existsSync(path.join(cwd, "settings.gradle")) || fs.existsSync(path.join(cwd, "settings.gradle.kts"));
|
|
3864
|
-
},
|
|
3865
|
-
artifactPatterns() {
|
|
3866
|
-
return ["app/build/outputs/apk/release/*-release.apk"];
|
|
3867
|
-
},
|
|
3868
|
-
buildCommand() {
|
|
3869
|
-
return ["./gradlew", "assembleRelease"];
|
|
3870
|
-
}
|
|
3871
|
-
};
|
|
3872
|
-
const nativeIosAdapter = {
|
|
3873
|
-
id: "native-ios",
|
|
3874
|
-
label: "原生 iOS",
|
|
3875
|
-
detect(cwd) {
|
|
3876
|
-
if (rnAdapter.detect(cwd)) return false;
|
|
3877
|
-
for (const dir of [cwd, ...subdirs(cwd, 1)]) {
|
|
3878
|
-
let entries;
|
|
3879
|
-
try {
|
|
3880
|
-
entries = fs.readdirSync(dir);
|
|
3881
|
-
} catch {
|
|
3882
|
-
continue;
|
|
3883
|
-
}
|
|
3884
|
-
if (entries.some((f) => String(f).endsWith(".xcodeproj") || String(f).endsWith(".xcworkspace"))) return true;
|
|
3885
|
-
}
|
|
3886
|
-
return false;
|
|
3887
|
-
},
|
|
3888
|
-
artifactPatterns() {
|
|
3889
|
-
return ["build/**/*.ipa"];
|
|
3890
|
-
},
|
|
3891
|
-
buildCommand() {
|
|
3892
|
-
return null;
|
|
3893
|
-
}
|
|
3894
|
-
};
|
|
3895
|
-
const nativeHarmonyAdapter = {
|
|
3896
|
-
id: "native-harmony",
|
|
3897
|
-
label: "原生鸿蒙 (ArkTS)",
|
|
3898
|
-
detect(cwd) {
|
|
3899
|
-
return fs.existsSync(path.join(cwd, "oh-package.json5")) || fs.existsSync(path.join(cwd, "harmony/oh-package.json5"));
|
|
3900
|
-
},
|
|
3901
|
-
artifactPatterns() {
|
|
3902
|
-
return [
|
|
3903
|
-
"entry/build/default/outputs/**/entry-*-signed.hap",
|
|
3904
|
-
"entry/build/default/outputs/**/entry-*.app",
|
|
3905
|
-
"harmony/entry/build/default/outputs/**/*-signed.hap",
|
|
3906
|
-
"build/outputs/**/*.app",
|
|
3907
|
-
"harmony/build/outputs/**/*.app",
|
|
3908
|
-
"ohos/build/outputs/**/*.app"
|
|
3909
|
-
];
|
|
3910
|
-
},
|
|
3911
|
-
buildCommand() {
|
|
3912
|
-
return [
|
|
3913
|
-
"hvigorw",
|
|
3914
|
-
"--mode",
|
|
3915
|
-
"module",
|
|
3916
|
-
"-p",
|
|
3917
|
-
"module=entry@default",
|
|
3918
|
-
"-p",
|
|
3919
|
-
"product=default",
|
|
3920
|
-
"-p",
|
|
3921
|
-
"buildMode=release",
|
|
3922
|
-
"assembleHap",
|
|
3923
|
-
"--no-daemon"
|
|
3924
|
-
];
|
|
3925
|
-
}
|
|
3926
|
-
};
|
|
3927
|
-
const electronAdapter = {
|
|
3928
|
-
id: "electron",
|
|
3929
|
-
label: "Electron",
|
|
3930
|
-
detect(cwd) {
|
|
3931
|
-
return "electron" in deps(readJson(path.join(cwd, "package.json")));
|
|
3932
|
-
},
|
|
3933
|
-
artifactPatterns(platform) {
|
|
3934
|
-
if (platform === "windows") return [
|
|
3935
|
-
"dist/*.exe",
|
|
3936
|
-
"dist/**/*.exe",
|
|
3937
|
-
"out/make/*.exe"
|
|
3938
|
-
];
|
|
3939
|
-
if (platform === "macos") return [
|
|
3940
|
-
"dist/*.dmg",
|
|
3941
|
-
"dist/**/*.dmg",
|
|
3942
|
-
"out/make/*.dmg"
|
|
3943
|
-
];
|
|
3944
|
-
if (platform === "linux") return [
|
|
3945
|
-
"dist/*.AppImage",
|
|
3946
|
-
"dist/**/*.AppImage",
|
|
3947
|
-
"out/make/*.AppImage"
|
|
3948
|
-
];
|
|
3949
|
-
return [];
|
|
3950
|
-
},
|
|
3951
|
-
buildCommand(platform) {
|
|
3952
|
-
if (platform === "windows") return [
|
|
3953
|
-
"npx",
|
|
3954
|
-
"electron-forge",
|
|
3955
|
-
"make",
|
|
3956
|
-
"--platform",
|
|
3957
|
-
"win32"
|
|
3958
|
-
];
|
|
3959
|
-
if (platform === "macos") return [
|
|
3960
|
-
"npx",
|
|
3961
|
-
"electron-forge",
|
|
3962
|
-
"make",
|
|
3963
|
-
"--platform",
|
|
3964
|
-
"darwin"
|
|
3965
|
-
];
|
|
3966
|
-
return [
|
|
3967
|
-
"npx",
|
|
3968
|
-
"electron-forge",
|
|
3969
|
-
"make",
|
|
3970
|
-
"--platform",
|
|
3971
|
-
"linux"
|
|
3972
|
-
];
|
|
3973
|
-
}
|
|
3974
|
-
};
|
|
3975
|
-
const tauriAdapter = {
|
|
3976
|
-
id: "tauri",
|
|
3977
|
-
label: "Tauri",
|
|
3978
|
-
detect(cwd) {
|
|
3979
|
-
return "@tauri-apps/cli" in deps(readJson(path.join(cwd, "package.json"))) || fs.existsSync(path.join(cwd, "src-tauri"));
|
|
3980
|
-
},
|
|
3981
|
-
artifactPatterns(platform) {
|
|
3982
|
-
if (platform === "windows") return ["src-tauri/target/release/bundle/nsis/*.exe", "src-tauri/target/release/bundle/msi/*.msi"];
|
|
3983
|
-
if (platform === "macos") return ["src-tauri/target/release/bundle/dmg/*.dmg", "src-tauri/target/release/bundle/macos/*.app"];
|
|
3984
|
-
if (platform === "linux") return ["src-tauri/target/release/bundle/deb/*.deb", "src-tauri/target/release/bundle/appimage/*.AppImage"];
|
|
3985
|
-
return [];
|
|
3986
|
-
},
|
|
3987
|
-
buildCommand(platform) {
|
|
3988
|
-
if (platform === "macos") return [
|
|
3989
|
-
"npx",
|
|
3990
|
-
"tauri",
|
|
3991
|
-
"build"
|
|
3992
|
-
];
|
|
3993
|
-
return [
|
|
3994
|
-
"npx",
|
|
3995
|
-
"tauri",
|
|
3996
|
-
"build"
|
|
3997
|
-
];
|
|
3998
|
-
}
|
|
3999
|
-
};
|
|
4000
|
-
const BUILTIN_ADAPTERS = [
|
|
4001
|
-
rnAdapter,
|
|
4002
|
-
flutterAdapter,
|
|
4003
|
-
electronAdapter,
|
|
4004
|
-
tauriAdapter,
|
|
4005
|
-
nativeAndroidAdapter,
|
|
4006
|
-
nativeIosAdapter,
|
|
4007
|
-
nativeHarmonyAdapter
|
|
4008
|
-
];
|
|
4009
|
-
/** 探测项目框架(可注入额外适配器,插件自定义框架的扩展点) */
|
|
4010
|
-
function detectFramework(cwd, extra = []) {
|
|
4011
|
-
for (const adapter of [...extra, ...BUILTIN_ADAPTERS]) try {
|
|
4012
|
-
if (adapter.detect(cwd)) return adapter;
|
|
4013
|
-
} catch {}
|
|
4014
|
-
return null;
|
|
4015
|
-
}
|
|
4016
|
-
/** 按 pattern 扫描产物(支持 ** 与 * 简单通配,返回绝对路径,多产物按 mtime 新者在前) */
|
|
4017
|
-
function findArtifacts(cwd, patterns) {
|
|
4018
|
-
const byPath = /* @__PURE__ */ new Map();
|
|
4019
|
-
for (const pattern of patterns) for (const p of globWalk(path.resolve(cwd), pattern.split("/"))) {
|
|
4020
|
-
if (byPath.has(p)) continue;
|
|
4021
|
-
try {
|
|
4022
|
-
byPath.set(p, fs.statSync(p).mtimeMs);
|
|
4023
|
-
} catch {}
|
|
4024
|
-
}
|
|
4025
|
-
return [...byPath].sort((a, b) => b[1] - a[1]).map(([p]) => p);
|
|
4026
|
-
}
|
|
4027
|
-
/** 文本读取(容错:不存在/不可读返回 null,绝不抛;detectBundleIds 各源共用) */
|
|
4028
|
-
function readText$1(filePath) {
|
|
4029
|
-
try {
|
|
4030
|
-
return fs.readFileSync(filePath, "utf8");
|
|
4031
|
-
} catch {
|
|
4032
|
-
return null;
|
|
4033
|
-
}
|
|
4034
|
-
}
|
|
4035
|
-
/** 从 JSON 对象按路径取非空字符串(任一层缺失/非字符串返回 undefined) */
|
|
4036
|
-
function pickString(obj, ...keys) {
|
|
4037
|
-
let cur = obj;
|
|
4038
|
-
for (const k of keys) {
|
|
4039
|
-
if (cur === null || typeof cur !== "object") return void 0;
|
|
4040
|
-
cur = cur[k];
|
|
4041
|
-
}
|
|
4042
|
-
return typeof cur === "string" && cur.trim() ? cur.trim() : void 0;
|
|
4043
|
-
}
|
|
4044
|
-
/** 原生安卓 applicationId(groovy/kts 两种赋值语法;app/ 与 android/app/ 两处布局;不中返回 undefined) */
|
|
4045
|
-
function gradleApplicationId(cwd) {
|
|
4046
|
-
const candidates = [
|
|
4047
|
-
path.join(cwd, "app", "build.gradle"),
|
|
4048
|
-
path.join(cwd, "app", "build.gradle.kts"),
|
|
4049
|
-
path.join(cwd, "android", "app", "build.gradle"),
|
|
4050
|
-
path.join(cwd, "android", "app", "build.gradle.kts")
|
|
4051
|
-
];
|
|
4052
|
-
for (const file of candidates) {
|
|
4053
|
-
const text = readText$1(file);
|
|
4054
|
-
if (!text) continue;
|
|
4055
|
-
const m = /(?:^|\s)applicationId\s*[=:]?\s*["']([^"'\s]+)["']/.exec(text);
|
|
4056
|
-
if (m?.[1]) return m[1];
|
|
4057
|
-
}
|
|
4058
|
-
}
|
|
4059
|
-
/** 鸿蒙 AppScope/app.json5 的 bundleName(json5 允许注释/尾逗号,正则取值而非 JSON 解析) */
|
|
4060
|
-
function appJson5BundleName(file) {
|
|
4061
|
-
const text = readText$1(file);
|
|
4062
|
-
return (text && /"bundleName"\s*:\s*"([^"]+)"/.exec(text))?.[1];
|
|
4063
|
-
}
|
|
4064
|
-
/** 两层子目录内找首个 *.xcodeproj:pbxproj 第一个非变量(含 $ 的 $(...) 与 ${...})的 PRODUCT_BUNDLE_IDENTIFIER + 工程名(Pods 已被 subdirs 排除) */
|
|
4065
|
-
function findPbxproj(cwd) {
|
|
4066
|
-
for (const dir of [cwd, ...subdirs(cwd, 2)]) {
|
|
4067
|
-
let entries;
|
|
4068
|
-
try {
|
|
4069
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
4070
|
-
} catch {
|
|
4071
|
-
continue;
|
|
4072
|
-
}
|
|
4073
|
-
const projects = entries.filter((e) => e.isDirectory() && e.name.endsWith(".xcodeproj")).map((e) => e.name).sort();
|
|
4074
|
-
for (const name of projects) {
|
|
4075
|
-
const pbx = readText$1(path.join(dir, name, "project.pbxproj"));
|
|
4076
|
-
if (!pbx) continue;
|
|
4077
|
-
for (const m of pbx.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;]+);/g)) {
|
|
4078
|
-
const value = m[1].trim().replace(/^["']|["']$/g, "");
|
|
4079
|
-
if (value && !value.includes("$")) return {
|
|
4080
|
-
bundleId: value,
|
|
4081
|
-
scheme: name.slice(0, -10)
|
|
4082
|
-
};
|
|
4083
|
-
}
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4086
|
-
return null;
|
|
4087
|
-
}
|
|
4088
|
-
/**
|
|
4089
|
-
* 自动推导三端应用标识(项目配置缺省时的兜底,配置可全空):
|
|
4090
|
-
* android:RN app.json android.applicationId > 原生 gradle applicationId;
|
|
4091
|
-
* harmony:AppScope/app.json5(根或 harmony/ 下)> RN app.json harmony.bundleName;
|
|
4092
|
-
* ios:RN app.json ios.bundleIdentifier > 两层内 xcodeproj 的 PRODUCT_BUNDLE_IDENTIFIER(scheme=工程名)。
|
|
4093
|
-
* 容错:文件不存在/正则不中只置 undefined,绝不抛。
|
|
4094
|
-
*/
|
|
4095
|
-
function detectBundleIds(cwd) {
|
|
4096
|
-
const appJson = readJson(path.join(cwd, "app.json"));
|
|
4097
|
-
const pbx = findPbxproj(cwd);
|
|
4098
|
-
const expo = appJson?.expo ?? null;
|
|
4099
|
-
const expoAndroid = expo ? pickString(expo, "android", "package") : void 0;
|
|
4100
|
-
const expoIos = expo ? pickString(expo, "ios", "bundleIdentifier") : void 0;
|
|
4101
|
-
const expoName = expo && typeof expo.name === "string" ? expo.name : void 0;
|
|
4102
|
-
return {
|
|
4103
|
-
androidPkgName: expoAndroid ?? pickString(appJson, "android", "applicationId") ?? gradleApplicationId(cwd),
|
|
4104
|
-
harmonyBundleName: appJson5BundleName(path.join(cwd, "AppScope", "app.json5")) ?? appJson5BundleName(path.join(cwd, "harmony", "AppScope", "app.json5")) ?? pickString(appJson, "harmony", "bundleName"),
|
|
4105
|
-
iosBundleId: expoIos ?? pickString(appJson, "ios", "bundleIdentifier") ?? pbx?.bundleId,
|
|
4106
|
-
iosScheme: pbx?.scheme ?? expoName
|
|
4107
|
-
};
|
|
4108
|
-
}
|
|
4109
|
-
/** 极简 glob 目录游走(** 跨层,* 单段),避免引入 minimatch 依赖 */
|
|
4110
|
-
function globWalk(dir, segments) {
|
|
4111
|
-
const [head, ...rest] = segments;
|
|
4112
|
-
if (!head) return [dir];
|
|
4113
|
-
if (!fs.existsSync(dir)) return [];
|
|
4114
|
-
if (head === "**") {
|
|
4115
|
-
const out = [];
|
|
4116
|
-
const walkAll = (d) => {
|
|
4117
|
-
out.push(...globWalk(d, rest));
|
|
4118
|
-
for (const name of fs.readdirSync(d, { withFileTypes: true })) {
|
|
4119
|
-
if (!name.isDirectory() || name.name.startsWith(".") || name.name === "node_modules") continue;
|
|
4120
|
-
walkAll(path.join(d, name.name));
|
|
4121
|
-
}
|
|
4122
|
-
};
|
|
4123
|
-
walkAll(dir);
|
|
4124
|
-
return [...new Set(out)];
|
|
4125
|
-
}
|
|
4126
|
-
if (head.includes("*")) {
|
|
4127
|
-
const re = new RegExp(`^${head.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")}$`);
|
|
4128
|
-
const out = [];
|
|
4129
|
-
for (const name of fs.readdirSync(dir)) if (re.test(name)) out.push(...globWalk(path.join(dir, name), rest));
|
|
4130
|
-
return out;
|
|
4131
|
-
}
|
|
4132
|
-
return globWalk(path.join(dir, head), rest);
|
|
4133
|
-
}
|
|
4134
|
-
//#endregion
|
|
4135
4026
|
//#region src/history.ts
|
|
4136
4027
|
/**
|
|
4137
4028
|
* 发版历史:~/.applift/history.jsonl,每行一条 JSON(HistoryEntry)。
|
|
@@ -4175,8 +4066,12 @@ function readEntries(query = {}, filePath = HISTORY_PATH) {
|
|
|
4175
4066
|
* 错误码人话化:消费各渠道自带 errorHints(平台内聚——表与渠道实现同文件)。
|
|
4176
4067
|
* 网络类故障(AbortSignal 超时/断连/DNS)渠道层不转译,统一在此兜底提示。
|
|
4177
4068
|
*/
|
|
4178
|
-
/** 网络故障特征 →
|
|
4179
|
-
const NETWORK_HINTS = [
|
|
4069
|
+
/** 网络故障特征 → 人话提示(命中即返回,按序优先:文件可读性 > 超时 > 网络故障) */
|
|
4070
|
+
const NETWORK_HINTS = [
|
|
4071
|
+
[/NotReadableError/i, "产物/图标文件在上传期间被修改或不可读(惰性 Blob 读盘中途失败):确认构建已完成、无并发进程改动产物,重跑"],
|
|
4072
|
+
[/aborted due to timeout|TimeoutError/i, "请求超时(服务端无响应或网络不稳):可重跑;大包上传建议在稳定网络环境执行"],
|
|
4073
|
+
[/\b(?:ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|EPIPE)\b|socket hang up|fetch failed/i, "网络故障(连接被重置/DNS 解析失败等):检查网络或代理后重跑"]
|
|
4074
|
+
];
|
|
4180
4075
|
/** 码匹配:纯数字码加数字边界(防 1003 误中 11003/10031);其余按包含匹配 */
|
|
4181
4076
|
function matchesCode(message, code) {
|
|
4182
4077
|
if (/^\d+$/.test(code)) return new RegExp(`(?<!\\d)${code}(?!\\d)`).test(message);
|
|
@@ -4189,525 +4084,6 @@ function humanizeError(hints, message) {
|
|
|
4189
4084
|
return message;
|
|
4190
4085
|
}
|
|
4191
4086
|
//#endregion
|
|
4192
|
-
//#region src/channels/shared/redact.ts
|
|
4193
|
-
/**
|
|
4194
|
-
* 凭据脱敏(对拍 apkgo pkg/httptrace/redact.go 权威)。
|
|
4195
|
-
* 判定依据是“名字”:JSON 键 / 表单字段 / query 参数 / header 名携带凭据语义即整值掩码为
|
|
4196
|
-
* [REDACTED <len>](保留长度线索),空串保持空串——“签名为空”本身就是排查线索。
|
|
4197
|
-
* 非敏感名下的值仍递归扫内容(signed URL / 内嵌 JSON / name=value 自由文本)。
|
|
4198
|
-
* 故意分歧:掩码不做 percent-encode(日志可读性优先);JSON 再序列化保持原键序(Go map 序随机)。
|
|
4199
|
-
* 挂接点:CLI / 错误消息层接入(core 先提供能力)。
|
|
4200
|
-
*/
|
|
4201
|
-
/** 归一化后「包含即敏感」的词(对拍 redact.go sensitiveWords) */
|
|
4202
|
-
const SENSITIVE_WORDS = [
|
|
4203
|
-
"secret",
|
|
4204
|
-
"token",
|
|
4205
|
-
"password",
|
|
4206
|
-
"passwd",
|
|
4207
|
-
"authorization",
|
|
4208
|
-
"credential",
|
|
4209
|
-
"cookie",
|
|
4210
|
-
"privatekey",
|
|
4211
|
-
"apikey",
|
|
4212
|
-
"assertion",
|
|
4213
|
-
"jwt",
|
|
4214
|
-
"signature"
|
|
4215
|
-
];
|
|
4216
|
-
/** 归一化后全等即敏感的名字(对拍 redact.go sensitiveNames) */
|
|
4217
|
-
const SENSITIVE_NAMES = {
|
|
4218
|
-
sig: true,
|
|
4219
|
-
sign: true,
|
|
4220
|
-
pwd: true,
|
|
4221
|
-
auth: true
|
|
4222
|
-
};
|
|
4223
|
-
/** 名字是否携带凭据语义(JSON 键 / 表单字段 / query 参数 / header 名通用;归一化 = 小写去非字母数字) */
|
|
4224
|
-
function isSensitiveName(name) {
|
|
4225
|
-
const n = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4226
|
-
if (n in SENSITIVE_NAMES) return true;
|
|
4227
|
-
if (n === "tokentype" || n.startsWith("accesscontrol")) return false;
|
|
4228
|
-
if (n.endsWith("sign") && n !== "design") return true;
|
|
4229
|
-
return SENSITIVE_WORDS.some((w) => n.includes(w));
|
|
4230
|
-
}
|
|
4231
|
-
/** 整值掩码:保留长度;空串保持(“签名为空”是有效诊断信息)。3 处调用点需锁步一致 */
|
|
4232
|
-
function mask(v) {
|
|
4233
|
-
return v === "" ? "" : `[REDACTED ${v.length}]`;
|
|
4234
|
-
}
|
|
4235
|
-
/**
|
|
4236
|
-
* 按名字脱敏单个值:敏感名 → 整值掩码;Authorization 保留 scheme("Bearer [REDACTED 812]",
|
|
4237
|
-
* 对拍 redact.go redactHeaders);非敏感名 → 递归 redactText 扫值内凭据。
|
|
4238
|
-
*/
|
|
4239
|
-
function redactValue(name, v) {
|
|
4240
|
-
if (/^authorization$/i.test(name) && v.includes(" ")) {
|
|
4241
|
-
const schemeEnd = v.indexOf(" ");
|
|
4242
|
-
return `${v.slice(0, schemeEnd)} ${mask(v.slice(schemeEnd + 1))}`;
|
|
4243
|
-
}
|
|
4244
|
-
return isSensitiveName(name) ? mask(v) : redactText(v);
|
|
4245
|
-
}
|
|
4246
|
-
/** redactURL:剥离 userinfo + query 参数按名脱敏,路径/fragment 原样保留 */
|
|
4247
|
-
function redactUrlString(v) {
|
|
4248
|
-
let out = v.replace(/^(https?:\/\/)[^/?#]*@/, "$1");
|
|
4249
|
-
const hashIdx = out.indexOf("#");
|
|
4250
|
-
const hash = hashIdx >= 0 ? out.slice(hashIdx) : "";
|
|
4251
|
-
if (hashIdx >= 0) out = out.slice(0, hashIdx);
|
|
4252
|
-
const qIdx = out.indexOf("?");
|
|
4253
|
-
if (qIdx < 0) return out + hash;
|
|
4254
|
-
const query = new URLSearchParams(out.slice(qIdx + 1));
|
|
4255
|
-
const pairs = [];
|
|
4256
|
-
for (const [k, val] of query) pairs.push(`${k}=${isSensitiveName(k) ? mask(val) : val}`);
|
|
4257
|
-
return `${out.slice(0, qIdx)}?${pairs.join("&")}${hash}`;
|
|
4258
|
-
}
|
|
4259
|
-
/** free 文本中的 name=value / "name":"value" 凭据对(对拍 redact.go scrubRe;掩码无长度) */
|
|
4260
|
-
const SCRUB_RE = /([\w.-]*(?:secret|token|password|passwd|signature|credential|authorization|api_?key|assertion)[\w.-]*"?\s*[=:]\s*"?)([^"&\s,;<}]+)/gi;
|
|
4261
|
-
/** JSON 树递归:敏感名下字符串掩码、数字/布尔置 [REDACTED],数组元素继承外层键名 */
|
|
4262
|
-
function redactTree(name, v) {
|
|
4263
|
-
if (v === null || typeof v !== "object") {
|
|
4264
|
-
if (typeof v === "string") return redactValue(name, v);
|
|
4265
|
-
return isSensitiveName(name) ? "[REDACTED]" : v;
|
|
4266
|
-
}
|
|
4267
|
-
if (Array.isArray(v)) return v.map((x) => redactTree(name, x));
|
|
4268
|
-
const out = {};
|
|
4269
|
-
for (const [k, val] of Object.entries(v)) out[k] = redactTree(k, val);
|
|
4270
|
-
return out;
|
|
4271
|
-
}
|
|
4272
|
-
/**
|
|
4273
|
-
* 文本脱敏(三层层叠,逐层尝试):
|
|
4274
|
-
* 1) http(s) URL 带 query/userinfo → query 按名脱敏 + 剥 userinfo
|
|
4275
|
-
* 2) { / [ 开头且整串为合法 JSON → 递归按键名脱敏
|
|
4276
|
-
* 3) 其余 → name=value 正则扫(错误消息等无结构文本)
|
|
4277
|
-
*/
|
|
4278
|
-
function redactText(v) {
|
|
4279
|
-
if (/^https?:\/\//i.test(v) && (v.includes("?") || /^https?:\/\/[^/?#]*@/i.test(v))) return redactUrlString(v);
|
|
4280
|
-
if (v.length > 1 && (v.startsWith("{") || v.startsWith("["))) try {
|
|
4281
|
-
return JSON.stringify(redactTree("", JSON.parse(v)));
|
|
4282
|
-
} catch {}
|
|
4283
|
-
return v.replace(SCRUB_RE, "$1[REDACTED]");
|
|
4284
|
-
}
|
|
4285
|
-
//#endregion
|
|
4286
|
-
//#region src/version/project.ts
|
|
4287
|
-
/**
|
|
4288
|
-
* 工程文件版本读写(可接管模式:版本事实源永远是工程文件,applift 只代读代写)。
|
|
4289
|
-
* 只认标准模板形态(AGP/Xcode/DevEco/pubspec/package.json/tauri.conf);行级改写、
|
|
4290
|
-
* 幂等(字段定位 + 写死目标值,不依赖旧值,跑 N 次 diff 不变——qy-driver sync-app-identity 同款模式)。
|
|
4291
|
-
* version catalog 引用形态(libs.versions.xxx.get())读侧识别并标注 problem,不代写。
|
|
4292
|
-
*/
|
|
4293
|
-
function readText(p) {
|
|
4294
|
-
try {
|
|
4295
|
-
return fs.readFileSync(p, "utf8");
|
|
4296
|
-
} catch {
|
|
4297
|
-
return null;
|
|
4298
|
-
}
|
|
4299
|
-
}
|
|
4300
|
-
function firstExisting(...paths) {
|
|
4301
|
-
return paths.find((p) => fs.existsSync(p));
|
|
4302
|
-
}
|
|
4303
|
-
function locateIosProject(dir) {
|
|
4304
|
-
const find = (d) => {
|
|
4305
|
-
try {
|
|
4306
|
-
return fs.readdirSync(d, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.endsWith(".xcodeproj") && !e.name.startsWith(".") && e.name !== "Pods.xcodeproj");
|
|
4307
|
-
} catch {
|
|
4308
|
-
return;
|
|
4309
|
-
}
|
|
4310
|
-
};
|
|
4311
|
-
let xcodeproj = find(dir);
|
|
4312
|
-
let base = dir;
|
|
4313
|
-
if (!xcodeproj) try {
|
|
4314
|
-
const subs = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "Pods");
|
|
4315
|
-
for (const s of subs) {
|
|
4316
|
-
xcodeproj = find(path.join(dir, s.name));
|
|
4317
|
-
if (xcodeproj) {
|
|
4318
|
-
base = path.join(dir, s.name);
|
|
4319
|
-
break;
|
|
4320
|
-
}
|
|
4321
|
-
}
|
|
4322
|
-
} catch {}
|
|
4323
|
-
const pbxproj = xcodeproj ? path.join(base, xcodeproj.name, "project.pbxproj") : void 0;
|
|
4324
|
-
const projDir = xcodeproj?.name.replace(/\.xcodeproj$/, "");
|
|
4325
|
-
const plist = projDir && fs.existsSync(path.join(base, projDir, "Info.plist")) ? path.join(base, projDir, "Info.plist") : void 0;
|
|
4326
|
-
return {
|
|
4327
|
-
pbxproj: pbxproj && fs.existsSync(pbxproj) ? pbxproj : void 0,
|
|
4328
|
-
plist
|
|
4329
|
-
};
|
|
4330
|
-
}
|
|
4331
|
-
function locateVersionFiles(cwd, framework) {
|
|
4332
|
-
const loc = {};
|
|
4333
|
-
const id = framework?.id ?? "";
|
|
4334
|
-
const tryAll = framework === null;
|
|
4335
|
-
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"));
|
|
4336
|
-
if (tryAll || id === "rn" || id === "native-ios" || id === "flutter") {
|
|
4337
|
-
const sub = locateIosProject(path.join(cwd, "ios"));
|
|
4338
|
-
const root = sub.pbxproj ? sub : locateIosProject(cwd);
|
|
4339
|
-
loc.ios = root.pbxproj;
|
|
4340
|
-
loc.plist = root.plist;
|
|
4341
|
-
}
|
|
4342
|
-
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"));
|
|
4343
|
-
if (tryAll || id === "flutter") loc.pubspec = fs.existsSync(path.join(cwd, "pubspec.yaml")) ? path.join(cwd, "pubspec.yaml") : void 0;
|
|
4344
|
-
if (tryAll || id === "rn" || id === "electron") loc.packageJson = fs.existsSync(path.join(cwd, "package.json")) ? path.join(cwd, "package.json") : void 0;
|
|
4345
|
-
if (tryAll || id === "tauri") {
|
|
4346
|
-
loc.tauri = firstExisting(path.join(cwd, "src-tauri", "tauri.conf.json"), path.join(cwd, "src-tauri", "tauri.conf.json5"));
|
|
4347
|
-
loc.packageJson = fs.existsSync(path.join(cwd, "package.json")) ? path.join(cwd, "package.json") : void 0;
|
|
4348
|
-
}
|
|
4349
|
-
if (id === "rn") {
|
|
4350
|
-
const appJson = readText(path.join(cwd, "app.json"));
|
|
4351
|
-
if (appJson && /"expo"\s*:/.test(appJson)) loc.expoAppJson = path.join(cwd, "app.json");
|
|
4352
|
-
}
|
|
4353
|
-
return loc;
|
|
4354
|
-
}
|
|
4355
|
-
function readGradle(file) {
|
|
4356
|
-
const text = readText(file);
|
|
4357
|
-
if (!text) return {};
|
|
4358
|
-
const out = {};
|
|
4359
|
-
const codeM = /(versionCode(?!Suffix)\s*=?\s*)(\d+)/.exec(text);
|
|
4360
|
-
const nameM = /(versionName(?!Suffix)\s*=?\s*)("[^"]*"|'[^']*'|[\w.+-]+)/.exec(text);
|
|
4361
|
-
if (codeM) out.versionCode = codeM[2];
|
|
4362
|
-
if (nameM) {
|
|
4363
|
-
const nameVal = nameM[2].replace(/^["']|["']$/g, "");
|
|
4364
|
-
if (nameVal.startsWith("libs.versions.")) out.problem = "versionName 走 version catalog 引用(libs.versions.*):非标准形态,请把版本字段搬回 app/build.gradle(.kts) 字面量";
|
|
4365
|
-
else out.versionName = nameVal;
|
|
4366
|
-
}
|
|
4367
|
-
if (/(versionCode|versionName)\s*=?\s*libs\.versions\./.test(text) && !out.problem) out.problem = "版本字段走 version catalog 引用(libs.versions.*):非标准形态,请把版本字段搬回 app/build.gradle(.kts) 字面量";
|
|
4368
|
-
return out;
|
|
4369
|
-
}
|
|
4370
|
-
function readPbxproj(file) {
|
|
4371
|
-
const text = readText(file);
|
|
4372
|
-
if (!text) return {};
|
|
4373
|
-
const names = [...text.matchAll(/MARKETING_VERSION\s*=\s*([^;]+);/g)].map((m) => m[1].trim());
|
|
4374
|
-
return {
|
|
4375
|
-
versionName: names[0],
|
|
4376
|
-
versionCode: /CURRENT_PROJECT_VERSION\s*=\s*([^;]+);/.exec(text)?.[1]?.trim(),
|
|
4377
|
-
problem: new Set(names).size > 1 ? `多 target MARKETING_VERSION 不一致(${[...new Set(names)].join(" / ")}):读第一个,bump 时将全部同步` : void 0
|
|
4378
|
-
};
|
|
4379
|
-
}
|
|
4380
|
-
function readPlist(file) {
|
|
4381
|
-
const text = readText(file);
|
|
4382
|
-
if (!text) return {};
|
|
4383
|
-
const name = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(text)?.[1];
|
|
4384
|
-
const code = /<key>CFBundleVersion<\/key>\s*<string>([^<]+)<\/string>/.exec(text)?.[1];
|
|
4385
|
-
if (name && !name.startsWith("$")) return {
|
|
4386
|
-
versionName: name,
|
|
4387
|
-
versionCode: code
|
|
4388
|
-
};
|
|
4389
|
-
return {};
|
|
4390
|
-
}
|
|
4391
|
-
function readAppJson5(file) {
|
|
4392
|
-
const text = readText(file);
|
|
4393
|
-
if (!text) return {};
|
|
4394
|
-
return {
|
|
4395
|
-
versionName: /"versionName"\s*:\s*"([^"]+)"/.exec(text)?.[1],
|
|
4396
|
-
versionCode: /"versionCode"\s*:\s*(\d+)/.exec(text)?.[1]
|
|
4397
|
-
};
|
|
4398
|
-
}
|
|
4399
|
-
function readPubspec(file) {
|
|
4400
|
-
const text = readText(file);
|
|
4401
|
-
if (!text) return {};
|
|
4402
|
-
const m = /^version:\s*(\S+)(?:\s*#.*)?$/m.exec(text);
|
|
4403
|
-
if (!m) return {};
|
|
4404
|
-
const [name, code] = m[1].split("+");
|
|
4405
|
-
return {
|
|
4406
|
-
versionName: name,
|
|
4407
|
-
versionCode: code
|
|
4408
|
-
};
|
|
4409
|
-
}
|
|
4410
|
-
function readJsonVersionKey(file) {
|
|
4411
|
-
const text = readText(file);
|
|
4412
|
-
if (!text) return {};
|
|
4413
|
-
let best;
|
|
4414
|
-
for (const m of text.matchAll(/^(\s*)"version"\s*:\s*"([^"]+)"\s*,?\s*$/gm)) {
|
|
4415
|
-
const indent = m[1].length;
|
|
4416
|
-
if (!best || indent < best.indent) best = {
|
|
4417
|
-
indent,
|
|
4418
|
-
value: m[2]
|
|
4419
|
-
};
|
|
4420
|
-
}
|
|
4421
|
-
if (best) return { versionName: best.value };
|
|
4422
|
-
try {
|
|
4423
|
-
const obj = JSON.parse(text);
|
|
4424
|
-
if (typeof obj.version === "string") return { versionName: obj.version };
|
|
4425
|
-
} catch {}
|
|
4426
|
-
const flat = /"version"\s*:\s*"([^"]+)"/.exec(text)?.[1];
|
|
4427
|
-
return flat ? { versionName: flat } : {};
|
|
4428
|
-
}
|
|
4429
|
-
function readExpoAppJson(file) {
|
|
4430
|
-
const text = readText(file);
|
|
4431
|
-
if (!text) return {};
|
|
4432
|
-
return {
|
|
4433
|
-
versionName: /"version"\s*:\s*"([^"]+)"/.exec(text)?.[1],
|
|
4434
|
-
versionCode: /"versionCode"\s*:\s*(\d+)/.exec(text)?.[1] ?? /"buildNumber"\s*:\s*"([^"]+)"/.exec(text)?.[1]
|
|
4435
|
-
};
|
|
4436
|
-
}
|
|
4437
|
-
/** 各端版本实况(按框架布局;未识别框架时尝试全部标准位置) */
|
|
4438
|
-
function readProjectVersions(cwd, framework) {
|
|
4439
|
-
const loc = locateVersionFiles(cwd, framework);
|
|
4440
|
-
const out = [];
|
|
4441
|
-
if (loc.android) out.push({
|
|
4442
|
-
platform: "android",
|
|
4443
|
-
file: loc.android,
|
|
4444
|
-
kind: "gradle",
|
|
4445
|
-
...readGradle(loc.android)
|
|
4446
|
-
});
|
|
4447
|
-
if (loc.ios) out.push({
|
|
4448
|
-
platform: "ios",
|
|
4449
|
-
file: loc.ios,
|
|
4450
|
-
kind: "pbxproj",
|
|
4451
|
-
...readPbxproj(loc.ios)
|
|
4452
|
-
});
|
|
4453
|
-
else if (loc.plist) {
|
|
4454
|
-
const plist = readPlist(loc.plist);
|
|
4455
|
-
if (plist.versionName) out.push({
|
|
4456
|
-
platform: "ios",
|
|
4457
|
-
file: loc.plist,
|
|
4458
|
-
kind: "pbxproj",
|
|
4459
|
-
...plist,
|
|
4460
|
-
problem: "未找到 pbxproj,读 Info.plist 字面量"
|
|
4461
|
-
});
|
|
4462
|
-
}
|
|
4463
|
-
if (loc.harmony) out.push({
|
|
4464
|
-
platform: "harmony",
|
|
4465
|
-
file: loc.harmony,
|
|
4466
|
-
kind: "app-json5",
|
|
4467
|
-
...readAppJson5(loc.harmony)
|
|
4468
|
-
});
|
|
4469
|
-
if (loc.pubspec) out.push({
|
|
4470
|
-
platform: "flutter",
|
|
4471
|
-
file: loc.pubspec,
|
|
4472
|
-
kind: "pubspec",
|
|
4473
|
-
...readPubspec(loc.pubspec)
|
|
4474
|
-
});
|
|
4475
|
-
if (loc.packageJson) out.push({
|
|
4476
|
-
platform: "js",
|
|
4477
|
-
file: loc.packageJson,
|
|
4478
|
-
kind: "package-json",
|
|
4479
|
-
...readJsonVersionKey(loc.packageJson)
|
|
4480
|
-
});
|
|
4481
|
-
if (loc.tauri) out.push({
|
|
4482
|
-
platform: "tauri",
|
|
4483
|
-
file: loc.tauri,
|
|
4484
|
-
kind: "tauri",
|
|
4485
|
-
...readJsonVersionKey(loc.tauri)
|
|
4486
|
-
});
|
|
4487
|
-
if (loc.expoAppJson) out.push({
|
|
4488
|
-
platform: "expo",
|
|
4489
|
-
file: loc.expoAppJson,
|
|
4490
|
-
kind: "expo-app-json",
|
|
4491
|
-
...readExpoAppJson(loc.expoAppJson)
|
|
4492
|
-
});
|
|
4493
|
-
return out;
|
|
4494
|
-
}
|
|
4495
|
-
/** 指定捕获组替换为 value(其余组原样保留;约定各正则组外无字面量) */
|
|
4496
|
-
function replaceGroup(re, text, group, value) {
|
|
4497
|
-
let count = 0;
|
|
4498
|
-
return {
|
|
4499
|
-
text: text.replace(re, (_m, ...groups) => {
|
|
4500
|
-
count++;
|
|
4501
|
-
const g = groups.slice(0, -2);
|
|
4502
|
-
g[group - 1] = value;
|
|
4503
|
-
return g.join("");
|
|
4504
|
-
}),
|
|
4505
|
-
count
|
|
4506
|
-
};
|
|
4507
|
-
}
|
|
4508
|
-
function writeIfChanged(file, next) {
|
|
4509
|
-
if (readText(file) === next) return false;
|
|
4510
|
-
fs.writeFileSync(file, next, "utf8");
|
|
4511
|
-
return true;
|
|
4512
|
-
}
|
|
4513
|
-
function writeGradle(file, w) {
|
|
4514
|
-
const text = readText(file);
|
|
4515
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4516
|
-
if (/version(Code|Name)\s*=?\s*libs\.versions\./.test(text)) throw new Error(`${file} 的版本字段是 version catalog 引用形态:请先改造为字面量(applift 只写标准形态)`);
|
|
4517
|
-
let next = text;
|
|
4518
|
-
const code = replaceGroup(/(versionCode(?!Suffix)\s*=?\s*)(\d+)/g, next, 2, String(w.versionCode));
|
|
4519
|
-
next = code.text;
|
|
4520
|
-
const name = replaceGroup(/(versionName(?!Suffix)\s*=?\s*)("[^"]*"|'[^']*'|[\w.+-]+)/g, next, 2, `"${w.versionName}"`);
|
|
4521
|
-
next = name.text;
|
|
4522
|
-
if (code.count === 0 && name.count === 0) throw new Error(`${file} 未找到 versionCode/versionName 字面量`);
|
|
4523
|
-
return writeIfChanged(file, next);
|
|
4524
|
-
}
|
|
4525
|
-
function writePbxproj(file, w) {
|
|
4526
|
-
const text = readText(file);
|
|
4527
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4528
|
-
let next = text;
|
|
4529
|
-
const name = replaceGroup(/(MARKETING_VERSION\s*=\s*)([^;]+)(;)/g, next, 2, w.versionName);
|
|
4530
|
-
next = name.text;
|
|
4531
|
-
const code = replaceGroup(/(CURRENT_PROJECT_VERSION\s*=\s*)([^;]+)(;)/g, next, 2, String(w.versionCode));
|
|
4532
|
-
next = code.text;
|
|
4533
|
-
if (name.count === 0 && code.count === 0) throw new Error(`${file} 未找到 MARKETING_VERSION/CURRENT_PROJECT_VERSION`);
|
|
4534
|
-
return writeIfChanged(file, next);
|
|
4535
|
-
}
|
|
4536
|
-
function writeAppJson5(file, w) {
|
|
4537
|
-
const text = readText(file);
|
|
4538
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4539
|
-
let next = text;
|
|
4540
|
-
const name = replaceGroup(/("versionName"\s*:\s*")([^"]+)(")/g, next, 2, w.versionName);
|
|
4541
|
-
next = name.text;
|
|
4542
|
-
const code = replaceGroup(/("versionCode"\s*:\s*)(\d+)/g, next, 2, String(w.versionCode));
|
|
4543
|
-
next = code.text;
|
|
4544
|
-
if (name.count === 0 && code.count === 0) throw new Error(`${file} 未找到 versionName/versionCode`);
|
|
4545
|
-
return writeIfChanged(file, next);
|
|
4546
|
-
}
|
|
4547
|
-
function writePubspec(file, w) {
|
|
4548
|
-
const text = readText(file);
|
|
4549
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4550
|
-
if (!/^version:\s*\S+/m.test(text)) throw new Error(`${file} 未找到 version: 行`);
|
|
4551
|
-
return writeIfChanged(file, text.replace(/^version:[^\n#]*?(\s*)(#.*)?$/m, (_m, gap, comment) => `version: ${w.versionName}+${w.versionCode}${comment ? gap ?? " " : ""}${comment ?? ""}`));
|
|
4552
|
-
}
|
|
4553
|
-
/** JSON 顶层 version 键改写(两遍:先定位最小缩进行,再只改该行——单遍回调在嵌套键
|
|
4554
|
-
* 先出现时会把它一并改写,篡改依赖声明)。单行 JSON 回退首个匹配。 */
|
|
4555
|
-
function writeJsonVersionKey(file, versionName) {
|
|
4556
|
-
const text = readText(file);
|
|
4557
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4558
|
-
const lines = text.split("\n");
|
|
4559
|
-
const re = /^(\s*)"version"\s*:\s*"([^"]+)"(\s*,?\s*)$/;
|
|
4560
|
-
let target = -1;
|
|
4561
|
-
let bestIndent = Number.POSITIVE_INFINITY;
|
|
4562
|
-
for (let i = 0; i < lines.length; i++) {
|
|
4563
|
-
const m = re.exec(lines[i]);
|
|
4564
|
-
if (!m) continue;
|
|
4565
|
-
const ind = m[1].length;
|
|
4566
|
-
if (ind < bestIndent) {
|
|
4567
|
-
bestIndent = ind;
|
|
4568
|
-
target = i;
|
|
4569
|
-
}
|
|
4570
|
-
}
|
|
4571
|
-
let next;
|
|
4572
|
-
if (target >= 0) {
|
|
4573
|
-
const m = re.exec(lines[target]);
|
|
4574
|
-
lines[target] = `${m[1]}"version": "${versionName}"${m[3]}`;
|
|
4575
|
-
next = lines.join("\n");
|
|
4576
|
-
} else {
|
|
4577
|
-
let topVersion;
|
|
4578
|
-
try {
|
|
4579
|
-
const obj = JSON.parse(text);
|
|
4580
|
-
if (typeof obj.version === "string") topVersion = obj.version;
|
|
4581
|
-
} catch {}
|
|
4582
|
-
const esc = (topVersion ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4583
|
-
const re1 = topVersion ? new RegExp(`"version"\\s*:\\s*"${esc}"`) : /"version"\s*:\s*"([^"]+)"/;
|
|
4584
|
-
if (!re1.test(text)) throw new Error(`${file} 未找到 "version" 键`);
|
|
4585
|
-
next = text.replace(re1, `"version": "${versionName}"`);
|
|
4586
|
-
}
|
|
4587
|
-
return writeIfChanged(file, next);
|
|
4588
|
-
}
|
|
4589
|
-
function writeExpoAppJson(file, w) {
|
|
4590
|
-
const text = readText(file);
|
|
4591
|
-
if (!text) throw new Error(`无法读取 ${file}`);
|
|
4592
|
-
const v = replaceGroup(/("version"\s*:\s*")([^"]+)(")/g, text, 2, w.versionName);
|
|
4593
|
-
if (v.count === 0) throw new Error(`${file} 未找到 expo "version" 键`);
|
|
4594
|
-
let next = v.text;
|
|
4595
|
-
next = replaceGroup(/("versionCode"\s*:\s*)(\d+)/g, next, 2, String(w.versionCode)).text;
|
|
4596
|
-
next = replaceGroup(/("buildNumber"\s*:\s*")([^"]+)(")/g, next, 2, String(w.versionCode)).text;
|
|
4597
|
-
return writeIfChanged(file, next);
|
|
4598
|
-
}
|
|
4599
|
-
/**
|
|
4600
|
-
* 写回各端版本(多端 versionName 同步、versionCode 各端独立)。
|
|
4601
|
-
* 返回实际改动的文件列表(commit 范围)。problem 端跳过并在返回中列出(不阻断其他端)。
|
|
4602
|
-
*/
|
|
4603
|
-
function writeProjectVersions(cwd, framework, writes) {
|
|
4604
|
-
const loc = locateVersionFiles(cwd, framework);
|
|
4605
|
-
const changed = [];
|
|
4606
|
-
const skipped = [];
|
|
4607
|
-
const plat = new Map(writes.map((w) => [w.platform, w]));
|
|
4608
|
-
const apply = (file, fn, key) => {
|
|
4609
|
-
if (!file) {
|
|
4610
|
-
const w = plat.get(key);
|
|
4611
|
-
if (w) skipped.push({
|
|
4612
|
-
platform: w.platform,
|
|
4613
|
-
reason: `未找到版本文件(${key})`
|
|
4614
|
-
});
|
|
4615
|
-
return;
|
|
4616
|
-
}
|
|
4617
|
-
try {
|
|
4618
|
-
if (fn(file)) changed.push(file);
|
|
4619
|
-
} catch (e) {
|
|
4620
|
-
const code = e.code;
|
|
4621
|
-
if (code === "EACCES" || code === "EPERM") throw e;
|
|
4622
|
-
skipped.push({
|
|
4623
|
-
platform: key,
|
|
4624
|
-
reason: e.message
|
|
4625
|
-
});
|
|
4626
|
-
}
|
|
4627
|
-
};
|
|
4628
|
-
if (plat.has("android")) apply(loc.android, (f) => writeGradle(f, plat.get("android")), "android");
|
|
4629
|
-
if (plat.has("ios")) apply(loc.ios, (f) => writePbxproj(f, plat.get("ios")), "ios");
|
|
4630
|
-
if (plat.has("harmony")) apply(loc.harmony, (f) => writeAppJson5(f, plat.get("harmony")), "harmony");
|
|
4631
|
-
if (plat.has("flutter")) apply(loc.pubspec, (f) => writePubspec(f, plat.get("flutter")), "flutter");
|
|
4632
|
-
if (plat.has("js")) apply(loc.packageJson, (f) => writeJsonVersionKey(f, plat.get("js").versionName), "js");
|
|
4633
|
-
if (plat.has("tauri")) apply(loc.tauri, (f) => writeJsonVersionKey(f, plat.get("tauri").versionName), "tauri");
|
|
4634
|
-
if (plat.has("expo")) apply(loc.expoAppJson, (f) => writeExpoAppJson(f, plat.get("expo")), "expo");
|
|
4635
|
-
return {
|
|
4636
|
-
changed: [...new Set(changed)],
|
|
4637
|
-
skipped
|
|
4638
|
-
};
|
|
4639
|
-
}
|
|
4640
|
-
//#endregion
|
|
4641
|
-
//#region src/version/git.ts
|
|
4642
|
-
/**
|
|
4643
|
-
* git 只读/写小助手(bump 编排与推断共用)。
|
|
4644
|
-
* spawnSync + 超时;失败返回 ok:false 由调用方决定语义,不在此抛。
|
|
4645
|
-
*/
|
|
4646
|
-
function git(cwd, args) {
|
|
4647
|
-
const r = spawnSync("git", args, {
|
|
4648
|
-
cwd,
|
|
4649
|
-
encoding: "utf8",
|
|
4650
|
-
timeout: 6e4
|
|
4651
|
-
});
|
|
4652
|
-
return {
|
|
4653
|
-
ok: !r.error && r.status === 0,
|
|
4654
|
-
stdout: (r.stdout ?? "").trim(),
|
|
4655
|
-
stderr: (r.stderr ?? "").trim()
|
|
4656
|
-
};
|
|
4657
|
-
}
|
|
4658
|
-
function isGitRepo(cwd) {
|
|
4659
|
-
return git(cwd, ["rev-parse", "--is-inside-work-tree"]).stdout === "true";
|
|
4660
|
-
}
|
|
4661
|
-
/** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
|
|
4662
|
-
function looseSemver(tag) {
|
|
4663
|
-
const direct = semver.valid(tag);
|
|
4664
|
-
if (direct) return direct;
|
|
4665
|
-
const m = /(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/.exec(tag);
|
|
4666
|
-
return m ? semver.valid(m[1]) ?? void 0 : void 0;
|
|
4667
|
-
}
|
|
4668
|
-
/**
|
|
4669
|
-
* 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
|
|
4670
|
-
* 无 tag / 非 git 仓库返回 undefined。
|
|
4671
|
-
*/
|
|
4672
|
-
function latestSemverTag(cwd) {
|
|
4673
|
-
const r = git(cwd, ["tag", "-l"]);
|
|
4674
|
-
if (!r.ok) return void 0;
|
|
4675
|
-
let best;
|
|
4676
|
-
for (const raw of r.stdout.split("\n")) {
|
|
4677
|
-
const t = raw.trim();
|
|
4678
|
-
if (!t) continue;
|
|
4679
|
-
const v = looseSemver(t);
|
|
4680
|
-
if (!v) continue;
|
|
4681
|
-
if (!best || semver.gt(v, best.v)) best = {
|
|
4682
|
-
raw: t,
|
|
4683
|
-
v
|
|
4684
|
-
};
|
|
4685
|
-
}
|
|
4686
|
-
return best?.raw;
|
|
4687
|
-
}
|
|
4688
|
-
/** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
|
|
4689
|
-
function commitsSince(cwd, ref) {
|
|
4690
|
-
const r = git(cwd, [
|
|
4691
|
-
"log",
|
|
4692
|
-
ref ? `${ref}..HEAD` : "HEAD",
|
|
4693
|
-
"--format=%B%x00",
|
|
4694
|
-
"--no-merges"
|
|
4695
|
-
]);
|
|
4696
|
-
if (!r.ok) return [];
|
|
4697
|
-
return r.stdout.split("\0").map((m) => m.trim()).filter(Boolean);
|
|
4698
|
-
}
|
|
4699
|
-
/** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
|
|
4700
|
-
* 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
|
|
4701
|
-
function statusShort(cwd) {
|
|
4702
|
-
const r = spawnSync("git", ["status", "--porcelain"], {
|
|
4703
|
-
cwd,
|
|
4704
|
-
encoding: "utf8",
|
|
4705
|
-
timeout: 3e4
|
|
4706
|
-
});
|
|
4707
|
-
if (r.error || r.status !== 0) return [];
|
|
4708
|
-
return (r.stdout ?? "").split("\n").map((l) => l.trimEnd()).filter(Boolean);
|
|
4709
|
-
}
|
|
4710
|
-
//#endregion
|
|
4711
4087
|
//#region src/version/infer.ts
|
|
4712
4088
|
/**
|
|
4713
4089
|
* 版本推断与决定:锚点 tag..HEAD 的 conventional commits → bump level(纯函数),
|
|
@@ -5242,4 +4618,4 @@ function fromAapt(artifactPath) {
|
|
|
5242
4618
|
}
|
|
5243
4619
|
}
|
|
5244
4620
|
//#endregion
|
|
5245
|
-
export { APPLIFT_HOME, AgcHarmonyChannel, AscChannel, BetaConfigSchema, CREDENTIALS_PATH, CredentialsFileSchema, CredentialsProfileSchema, DefaultChannelRegistry, GLOBAL_CONFIG_PATH, GlobalConfigSchema, GooglePlayChannel, HISTORY_PATH, HonorChannel, HuaweiAndroidChannel, OppoChannel, PROJECTS_MAP_PATH, PgyerChannel, PluginHost, ProjectConfigSchema,
|
|
4621
|
+
export { APPLIFT_HOME, AgcHarmonyChannel, AscChannel, BetaConfigSchema, CREDENTIALS_PATH, CredentialsFileSchema, CredentialsProfileSchema, DefaultChannelRegistry, GLOBAL_CONFIG_PATH, GlobalConfigSchema, GooglePlayChannel, HISTORY_PATH, HonorChannel, HuaweiAndroidChannel, OppoChannel, PROJECTS_MAP_PATH, PgyerChannel, PluginHost, ProjectConfigSchema, StoreConfigSchema, StoresSchema, VersionCodeConfigSchema, VersionConfigSchema, VivoChannel, WAIT_BEFORE_SUBMIT_MS, XiaomiChannel, YybChannel, appendEntry, applyBump, assertListingFiles, collectReleaseDiff, commitsSince, decideNextVersion, defineConfig, deps, detectBundleIds, electronAdapter, electronFramework, evalVersionCodeFormula, findArtifacts, findUp, flutterAdapter, flutterFramework, formPost, getAgcToken, getHarmonyAppId, git, hmacSign, humanizeError, inferBumpLevel, isGitRepo, knownFrameworkAdapters, latestSemverTag, loadCredentials, loadCredentialsLayered, loadGlobalConfig, loadListingFile, loadProjectConfig, loadProjectProfileMap, looseSemver, md5File, md5String, nativeAndroidAdapter, nativeAndroidFramework, nativeHarmonyAdapter, nativeHarmonyFramework, nativeIosAdapter, nativeIosFramework, nextVersionCode, planBump, projectCredentialsPath, readArtifactVersion, readEntries, readJson, readProjectVersions, redactText, registry, renderChangelogSection, resolveFramework, resolveListing, resolvePlugins, resolveProfile, resolveProfileFor, resolveProfileSource, rnAdapter, rnFramework, routeDelivery, runStage, runSubmit, saveCredentials, saveProjectProfileMap, setProjectProfile, sha256File, sortedQueryString, statusShort, storeVersionName, subdirs, suggestFramework, tagReleaseVersion, tauriAdapter, tauriFramework, toChangelogEntries, tsMillis, tsSeconds, upsertChangelog, writeProjectVersions };
|