@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/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.map((el) => typeof el === "string" ? [el, {}] : (() => {
53
- const entries = Object.entries(el);
54
- if (entries.length !== 1) throw new Error(`stores 数组内的对象元素必须形如 { 渠道id: appId简写 }(单键),收到: ${JSON.stringify(el)}`);
55
- const [id, cfg] = entries[0];
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]))]).transform(storesNormalize).default({});
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
- return {
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 parsed = ProjectConfigSchema.safeParse(result.config ?? {});
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: parsed.data,
289
- raw: result.config ?? {},
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:projects.json 个人选择 > 项目配置 profile(团队默认)> 'default' 全局默认。
327
- * CLI 的 --profile 旗标在调用方以 `opts.profile ?? resolveProfileFor(...)` 再盖一层(最高优先级)。
352
+ * 解析项目生效 profile 及其来源:projects.json 个人选择 > 项目配置 profile(团队默认)> 'default'。
353
+ * CLI 的 --profile 旗标在调用方以 `opts.profile ?? ...` 再盖一层(最高优先级,来源即"旗标")。
328
354
  */
329
- function resolveProfileFor(cwd, projectCfgProfile, filePath = PROJECTS_MAP_PATH) {
355
+ function resolveProfileSource(cwd, projectCfgProfile, filePath = PROJECTS_MAP_PATH) {
330
356
  const mapped = loadProjectProfileMap(filePath)[path.resolve(cwd)];
331
- if (mapped) return mapped;
332
- return projectCfgProfile || "default";
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") return fs.openAsBlob(p);
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
- const info = await pgyerPost(HOSTS[0], "/apiv2/app/buildInfo", {
538
- _api_key: apiKey,
539
- buildKey
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 json = await (await fetch("https://connect-api.cloud.huawei.com/api/oauth2/v1/token", {
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
- })).json();
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: `已提交(${JSON.stringify(upd.data ?? {}).slice(0, 120)})`
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
- const stagedInfo = this.staged.get(appId);
2248
- if (!stagedInfo) throw new Error("应用宝渠道缺少上传流水号(stage 未执行,serial_number/md5 不可用)");
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
- const submit = await yybApi("/update_app", {
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: `已提交审核(${JSON.stringify(submit).slice(0, 120)})`
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
- async submit(ctx) {
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
- const { packageId } = await this.uploadFormal(ctx, appId, headers);
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.readFileSync(ctx.artifactPath),
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 json = await res.json();
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) throw new Error(`无法执行 xcrun: ${r.error.message}(altool 需 macOS + Xcode)`);
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.message,
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.message,
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 = [[/aborted due to timeout|TimeoutError/i, "请求超时(服务端无响应或网络不稳):可重跑;大包上传建议在稳定网络环境执行"], [/\b(?:ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|EPIPE)\b|socket hang up|fetch failed/i, "网络故障(连接被重置/DNS 解析失败等):检查网络或代理后重跑"]];
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, ReleaseConfigSchema, StoreConfigSchema, StoresSchema, VersionCodeConfigSchema, VersionConfigSchema, VivoChannel, WAIT_BEFORE_SUBMIT_MS, XiaomiChannel, YybChannel, appendEntry, applyBump, assertListingFiles, commitsSince, decideNextVersion, defineConfig, detectBundleIds, detectFramework, electronAdapter, evalVersionCodeFormula, findArtifacts, findUp, flutterAdapter, formPost, getAgcToken, getHarmonyAppId, git, hmacSign, humanizeError, inferBumpLevel, isGitRepo, latestSemverTag, loadCredentials, loadCredentialsLayered, loadGlobalConfig, loadListingFile, loadProjectConfig, loadProjectProfileMap, looseSemver, md5File, md5String, nativeAndroidAdapter, nativeHarmonyAdapter, nativeIosAdapter, nextVersionCode, planBump, projectCredentialsPath, readArtifactVersion, readEntries, readProjectVersions, redactText, registry, renderChangelogSection, resolveListing, resolvePlugins, resolveProfile, resolveProfileFor, rnAdapter, runStage, runSubmit, saveCredentials, saveProjectProfileMap, setProjectProfile, sha256File, sortedQueryString, statusShort, storeVersionName, tauriAdapter, toChangelogEntries, tsMillis, tsSeconds, upsertChangelog, writeProjectVersions };
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 };