@applift/core 0.5.0 → 0.7.0

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";
@@ -41,15 +42,32 @@ profiles: z.record(z.string(), CredentialsProfileSchema) });
41
42
  /** 单渠道配置(appId、pkgName 等渠道侧键) */
42
43
  const StoreConfigSchema = z.record(z.string(), z.string());
43
44
  /**
44
- * stores 三种等价写法,统一规范化为 record:
45
+ * stores 等价写法,统一规范化为 record:
45
46
  * ① 对象:{ huawei: { appId: 'x' } }(原有写法,原样保留)
46
47
  * ② 数组:['oppo', 'vivo'] → { oppo: {}, vivo: {} }(无配置渠道)
47
48
  * ③ 字符串值:{ huawei: '10xxx' } → { huawei: { appId: '10xxx' } }(appId 简写)
49
+ * ④ 混排数组(README 首例):['huawei', { yyb: 'appId值' }] → 元素对象须单键 { 渠道: 值 },
50
+ * 值走 ③ 同款 appId 简写归一
48
51
  */
49
- const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z.string()])), z.array(z.string())]).transform((stores) => {
50
- if (Array.isArray(stores)) return Object.fromEntries(stores.map((id) => [id, {}]));
52
+ const storesNormalize = (stores) => {
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
+ }));
51
59
  return Object.fromEntries(Object.entries(stores).map(([id, cfg]) => [id, typeof cfg === "string" ? { appId: cfg } : cfg]));
52
- }).default({});
60
+ };
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({});
53
71
  /** 项目级配置(applift.config.ts 的形状);beta 段见上方 BetaConfigSchema */
54
72
  /**
55
73
  * 内测分发配置(applift beta 无平台参数时并行发布的渠道开关):
@@ -58,8 +76,6 @@ const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z
58
76
  * - testflight: App Store Connect 上传(ipa 进 TestFlight)
59
77
  */
60
78
  const BetaConfigSchema = z.object({
61
- /** 自动 version:true(走推断)| major/minor/patch/prerelease | <semver>;CLI --bump/--no-bump 覆盖 */
62
- bump: z.union([z.boolean(), z.string()]).optional(),
63
79
  pgyer: z.object({
64
80
  platforms: z.array(z.enum([
65
81
  "android",
@@ -86,35 +102,29 @@ const VersionCodeConfigSchema = z.union([z.literal("increment"), z.object({
86
102
  platforms: z.array(z.string()).optional()
87
103
  })]);
88
104
  const VersionConfigSchema = z.object({ versionCode: VersionCodeConfigSchema.default("increment") });
89
- /** 正式发版段的自动 version 开关(语义同 beta.bump) */
90
- const ReleaseConfigSchema = z.object({ bump: z.union([z.boolean(), z.string()]).optional() });
91
105
  const ProjectConfigSchema = z.object({
92
106
  /** 项目显示名 */
93
107
  name: z.string().optional(),
94
108
  /** 团队默认凭据 profile(个人用 applift use / --profile 覆盖),缺省 default */
95
109
  profile: z.string().default("default"),
96
110
  android: z.object({
97
- pkgName: z.string(),
111
+ /** 缺省经 detectBundleIds 从工程文件自动推导(README 首例即只有 stores 的最小配置) */
112
+ pkgName: z.string().optional(),
98
113
  /** 渠道 → 渠道侧配置(appId 等);stores 里列出的渠道才发 */
99
114
  stores: StoresSchema
100
115
  }).optional(),
101
116
  harmony: z.object({
102
- bundleName: z.string(),
117
+ /** 缺省自动推导(AppScope/app.json5) */
118
+ bundleName: z.string().optional(),
103
119
  appId: z.string().optional()
104
120
  }).optional(),
105
121
  ios: z.object({
106
- scheme: z.string(),
122
+ /** 缺省自动推导(工程 scheme 探测) */
123
+ scheme: z.string().optional(),
107
124
  workspace: z.string().optional(),
108
125
  /** App Store Connect 按 bundleId 定位 App(asc 渠道 submit 用) */
109
126
  bundleId: z.string().optional()
110
127
  }).optional(),
111
- /** 构建命令(缺省用项目自带 npm scripts,见 pipeline 约定) */
112
- build: z.object({
113
- android: z.string().optional(),
114
- harmony: z.string().optional(),
115
- harmonyApp: z.string().optional(),
116
- ios: z.string().optional()
117
- }).optional(),
118
128
  /** 产物目录覆盖(非标准工程):每平台 glob 数组,优先于框架内置 pattern(桌面平台 + 插件自报平台经 catchall 收) */
119
129
  artifacts: z.object({
120
130
  android: z.array(z.string()).optional(),
@@ -126,8 +136,6 @@ const ProjectConfigSchema = z.object({
126
136
  }).catchall(z.array(z.string()).optional()).optional(),
127
137
  /** 版本管理(versionCode 策略等) */
128
138
  version: VersionConfigSchema.optional(),
129
- /** 正式发版自动 version 开关 */
130
- release: ReleaseConfigSchema.optional(),
131
139
  /** 内测分发(beta 无平台参数时的多渠道并行配置) */
132
140
  beta: BetaConfigSchema.optional()
133
141
  });
@@ -208,14 +216,16 @@ function resolveProfile(creds, profile, knownKeys) {
208
216
  for (const [envName, value] of Object.entries(process.env)) if (envName.startsWith("APPLIFT_") && value) merged[envName.slice(8)] = value;
209
217
  return merged;
210
218
  }
211
- /** 全局配置装载(缺失/损坏降级默认值并警告,不阻断) */
219
+ /** 全局配置装载(缺失/损坏降级默认值;警告经返回值交给调用方,不阻断) */
212
220
  function loadGlobalConfig(filePath = GLOBAL_CONFIG_PATH) {
213
- if (!fs.existsSync(filePath)) return GlobalConfigSchema.parse({});
221
+ if (!fs.existsSync(filePath)) return { config: GlobalConfigSchema.parse({}) };
214
222
  try {
215
- return GlobalConfigSchema.parse(parse(fs.readFileSync(filePath, "utf8")));
223
+ return { config: GlobalConfigSchema.parse(parse(fs.readFileSync(filePath, "utf8"))) };
216
224
  } catch (e) {
217
- process.stderr.write?.(`[applift] 全局配置解析失败,使用默认值: ${e.message}\n`);
218
- return GlobalConfigSchema.parse({});
225
+ return {
226
+ config: GlobalConfigSchema.parse({}),
227
+ warning: `全局配置解析失败,使用默认值: ${e.message}`
228
+ };
219
229
  }
220
230
  }
221
231
  /** 写凭据(目录自动创建,文件强制 0600) */
@@ -240,10 +250,19 @@ function appliftAlias() {
240
250
  const distEntry = fileURLToPath(import.meta.resolve("@applift/core"));
241
251
  const coreEntry = fs.existsSync(distEntry) ? distEntry : path.join(path.dirname(path.dirname(distEntry)), "src", "index.ts");
242
252
  if (!fs.existsSync(coreEntry)) return {};
243
- return {
253
+ const alias = {
244
254
  "@applift/core": coreEntry,
245
255
  "@applift/cli": coreEntry
246
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;
247
266
  } catch {
248
267
  return {};
249
268
  }
@@ -266,8 +285,32 @@ async function loadProjectConfig(cwd) {
266
285
  packageJson: false,
267
286
  defaults: ProjectConfigSchema.parse({})
268
287
  });
288
+ const parsed = ProjectConfigSchema.safeParse(result.config ?? {});
289
+ if (!parsed.success) {
290
+ const details = parsed.error.issues.map((i) => `${i.path.join(".") || "(根)"}: ${i.message}`).join(";");
291
+ throw new Error(`applift.config.ts 字段校验失败 → ${details}`);
292
+ }
293
+ const config = parsed.data;
294
+ const rawRec = result.config ?? {};
295
+ const deprecated = [];
296
+ if ("build" in rawRec) deprecated.push({
297
+ key: "build",
298
+ hint: "构建已回归项目自身(applift 不接管构建),请删除该键"
299
+ });
300
+ if ("bump" in (rawRec.beta ?? {})) deprecated.push({
301
+ key: "beta.bump",
302
+ hint: "改用 applift version bump 显式升版本(applift 不再代构建)"
303
+ });
304
+ if ("release" in rawRec && "bump" in (rawRec.release ?? {})) deprecated.push({
305
+ key: "release.bump",
306
+ hint: "改用 applift version bump 显式升版本(applift 不再代构建)"
307
+ });
308
+ if (deprecated.length > 0) {
309
+ const lines = deprecated.map((d) => ` - ${d.key}: ${d.hint}`).join("\n");
310
+ process.stderr.write(`⚠ applift.config.ts 含已废弃配置键(将被忽略):\n${lines}\n`);
311
+ }
269
312
  return {
270
- config: ProjectConfigSchema.parse(result.config ?? {}),
313
+ config,
271
314
  raw: result.config ?? {},
272
315
  cwd: result.cwd ?? cwd
273
316
  };
@@ -305,13 +348,27 @@ function setProjectProfile(cwd, profile, filePath = PROJECTS_MAP_PATH) {
305
348
  saveProjectProfileMap(map, filePath);
306
349
  }
307
350
  /**
308
- * 解析项目生效 profile:projects.json 个人选择 > 项目配置 profile(团队默认)> 'default' 全局默认。
309
- * CLI 的 --profile 旗标在调用方以 `opts.profile ?? resolveProfileFor(...)` 再盖一层(最高优先级)。
351
+ * 解析项目生效 profile 及其来源:projects.json 个人选择 > 项目配置 profile(团队默认)> 'default'。
352
+ * CLI 的 --profile 旗标在调用方以 `opts.profile ?? ...` 再盖一层(最高优先级,来源即"旗标")。
310
353
  */
311
- function resolveProfileFor(cwd, projectCfgProfile, filePath = PROJECTS_MAP_PATH) {
354
+ function resolveProfileSource(cwd, projectCfgProfile, filePath = PROJECTS_MAP_PATH) {
312
355
  const mapped = loadProjectProfileMap(filePath)[path.resolve(cwd)];
313
- if (mapped) return mapped;
314
- return projectCfgProfile || "default";
356
+ if (mapped) return {
357
+ profile: mapped,
358
+ source: "applift use(个人)"
359
+ };
360
+ if (projectCfgProfile) return {
361
+ profile: projectCfgProfile,
362
+ source: "项目配置 profile(团队默认)"
363
+ };
364
+ return {
365
+ profile: "default",
366
+ source: "默认 default"
367
+ };
368
+ }
369
+ /** 解析项目生效 profile(只取名不问来源;来源用 resolveProfileSource) */
370
+ function resolveProfileFor(cwd, projectCfgProfile, filePath = PROJECTS_MAP_PATH) {
371
+ return resolveProfileSource(cwd, projectCfgProfile, filePath).profile;
315
372
  }
316
373
  //#endregion
317
374
  //#region src/channels/shared/sign.ts
@@ -370,8 +427,24 @@ async function formPost(url, params, { timeout = 6e4 } = {}) {
370
427
  }
371
428
  }
372
429
  //#endregion
430
+ //#region src/channels/shared/file-blob.ts
431
+ /**
432
+ * 惰性文件 Blob:FormData multipart 上传的文件部件。
433
+ * 优先 fs.openAsBlob(Node ≥19.8,按需分片读,零整读内存);
434
+ * 能力检测不存在时回退整读(旧运行时兜底)。
435
+ */
436
+ async function fileBlob(p) {
437
+ if (typeof fs.openAsBlob === "function") try {
438
+ return await fs.openAsBlob(p);
439
+ } catch (e) {
440
+ const cause = e instanceof Error ? e.message : String(e);
441
+ throw new Error(`打开上传文件失败 ${p}: ${cause}`);
442
+ }
443
+ return new Blob([await fs.promises.readFile(p)]);
444
+ }
445
+ //#endregion
373
446
  //#region src/util.ts
374
- /** core 内部共用小工具(不进公共 API) */
447
+ /** core 内部共用小工具(sleep/formatSize 仅内部使用;sha256File 经 index 导出,CLI 历史指纹用) */
375
448
  const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
376
449
  /** 分块计算文件 SHA256(产物可达上百 MB,8MB 分块避免整读内存,同 sign.ts md5File 先例) */
377
450
  function sha256File(filePath) {
@@ -413,7 +486,8 @@ async function pgyerPost(host, apiPath, form) {
413
486
  const res = await fetch(`https://${host}${apiPath}`, {
414
487
  method: "POST",
415
488
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
416
- body
489
+ body,
490
+ signal: AbortSignal.timeout(3e4)
417
491
  });
418
492
  const text = await res.text();
419
493
  try {
@@ -422,6 +496,20 @@ async function pgyerPost(host, apiPath, form) {
422
496
  throw new Error(`蒲公英 ${apiPath} 返回非 JSON(HTTP ${res.status}): ${text.slice(0, 200)}`);
423
497
  }
424
498
  }
499
+ /** 按 HOSTS 序尝试的 POST(任一域名成功即返回;全挂抛最后一个错误)——状态查询等幂等接口用 */
500
+ async function pgyerPostFailover(ctx, apiPath, form) {
501
+ let lastErr;
502
+ for (const host of HOSTS) try {
503
+ return await pgyerPost(host, apiPath, form);
504
+ } catch (e) {
505
+ lastErr = e;
506
+ ctx.emit({
507
+ type: "warn",
508
+ message: `${host} 不可用,尝试备用域名`
509
+ });
510
+ }
511
+ throw lastErr instanceof Error ? lastErr : /* @__PURE__ */ new Error(`蒲公英 ${apiPath} 请求失败`);
512
+ }
425
513
  var PgyerChannel = class {
426
514
  id = "pgyer";
427
515
  label = "蒲公英";
@@ -431,6 +519,8 @@ var PgyerChannel = class {
431
519
  scheduled: false,
432
520
  status: false
433
521
  };
522
+ /** 平台错误码 → 人话(1247:源码注释已证 = buildKey 尚未就绪/COS 仍在入库) */
523
+ errorHints = { "1247": "buildKey 尚未就绪(COS 上传仍在入库),属处理中状态,稍候即可" };
434
524
  async plan(ctx) {
435
525
  return {
436
526
  channel: this.id,
@@ -487,7 +577,7 @@ var PgyerChannel = class {
487
577
  form.append("x-cos-security-token", params["x-cos-security-token"]);
488
578
  form.append("key", params.key);
489
579
  form.append("x-cos-meta-file-name", path.basename(ctx.artifactPath));
490
- form.append("file", new Blob([fs.readFileSync(ctx.artifactPath)]), path.basename(ctx.artifactPath));
580
+ form.append("file", await fileBlob(ctx.artifactPath), path.basename(ctx.artifactPath));
491
581
  const upRes = await fetch(endpoint, {
492
582
  method: "POST",
493
583
  body: form,
@@ -500,12 +590,29 @@ var PgyerChannel = class {
500
590
  message: "等待蒲公英处理"
501
591
  });
502
592
  const deadline = Date.now() + 9e5;
593
+ let consecutiveErrors = 0;
503
594
  while (Date.now() < deadline) {
504
595
  await sleep(3e3);
505
- const info = await pgyerPost(HOSTS[0], "/apiv2/app/buildInfo", {
506
- _api_key: apiKey,
507
- buildKey
508
- });
596
+ let info;
597
+ try {
598
+ info = await pgyerPostFailover(ctx, "/apiv2/app/buildInfo", {
599
+ _api_key: apiKey,
600
+ buildKey
601
+ });
602
+ consecutiveErrors = 0;
603
+ } catch (e) {
604
+ consecutiveErrors++;
605
+ const lastErr = e instanceof Error ? e.message : String(e);
606
+ ctx.emit({
607
+ type: "warn",
608
+ message: `buildInfo 状态查询失败(连续 ${consecutiveErrors}/10): ${lastErr}`
609
+ });
610
+ if (consecutiveErrors >= 10) return {
611
+ ok: false,
612
+ error: `蒲公英状态查询持续失败(包已上传,可到控制台确认): ${lastErr}`
613
+ };
614
+ continue;
615
+ }
509
616
  if (info.code === 0 && info.data?.buildShortcutUrl) {
510
617
  ctx.emit({
511
618
  type: "phase",
@@ -556,9 +663,8 @@ function platformOf(artifactPath) {
556
663
  }
557
664
  //#endregion
558
665
  //#region src/channels/shared/agc-token.ts
559
- /** 华为 AGC token(安卓/鸿蒙同源)——自 v1 lib/agc.mjs 迁移 */
560
666
  async function getAgcToken({ clientId, clientSecret }) {
561
- const json = await (await fetch("https://connect-api.cloud.huawei.com/api/oauth2/v1/token", {
667
+ const res = await fetch("https://connect-api.cloud.huawei.com/api/oauth2/v1/token", {
562
668
  method: "POST",
563
669
  headers: { "Content-Type": "application/json" },
564
670
  body: JSON.stringify({
@@ -567,7 +673,14 @@ async function getAgcToken({ clientId, clientSecret }) {
567
673
  client_secret: clientSecret
568
674
  }),
569
675
  signal: AbortSignal.timeout(3e4)
570
- })).json();
676
+ });
677
+ const text = await res.text();
678
+ let json;
679
+ try {
680
+ json = JSON.parse(text);
681
+ } catch {
682
+ throw new Error(`AGC token 接口返回非 JSON(HTTP ${res.status}): ${text.slice(0, 200)}`);
683
+ }
571
684
  if (!json.access_token) {
572
685
  const desc = json.error_description ?? json.ret?.msg ?? json.error ?? JSON.stringify(json).slice(0, 200);
573
686
  throw new Error(`AGC 获取 token 失败: ${desc}(检查 client_id/client_secret)`);
@@ -752,6 +865,7 @@ var HuaweiAndroidChannel = class {
752
865
  if (remarkLen < 10 || remarkLen > 300) throw new Error(`华为提审备注(remark)长度需在 10-300 字之间,当前 ${remarkLen} 字`);
753
866
  }
754
867
  if (ctx.updateDescription !== void 0 && [...ctx.updateDescription].length > 500) throw new Error(`华为更新说明(newFeatures)不能超过 500 字符,当前 ${[...ctx.updateDescription].length} 字符`);
868
+ 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}`);
755
869
  const { headers } = await this.authHeaders(ctx);
756
870
  if (ctx.updateDescription) try {
757
871
  await hwJson(`${BASE$1}/publish/v2/app-language-info?appId=${encodeURIComponent(appId)}`, {
@@ -970,6 +1084,11 @@ var XiaomiChannel = class {
970
1084
  scheduled: false,
971
1085
  status: true
972
1086
  };
1087
+ /** 平台错误码 → 人话(result 码:依据为本仓测试已固化的真实失败路径文案) */
1088
+ errorHints = {
1089
+ "401": "签名无效或账号无该包名权限(检查 MI_CLIENT_SECRET 与 /dev/query 权限)",
1090
+ "1005": "push SIG 校验失败(检查证书公钥与密码是否匹配)"
1091
+ };
973
1092
  async plan(_ctx) {
974
1093
  return {
975
1094
  channel: this.id,
@@ -1051,8 +1170,8 @@ var XiaomiChannel = class {
1051
1170
  const form = new FormData();
1052
1171
  form.append("RequestData", requestJson);
1053
1172
  form.append("SIG", sig);
1054
- form.append("apk", new Blob([fs.readFileSync(ctx.artifactPath)]), fileName);
1055
- form.append("icon", new Blob([fs.readFileSync(iconPath)]), path.basename(iconPath));
1173
+ form.append("apk", await fileBlob(ctx.artifactPath), fileName);
1174
+ form.append("icon", await fileBlob(iconPath), path.basename(iconPath));
1056
1175
  const res = await fetch(`${MI_API_BASE}/dev/push`, {
1057
1176
  method: "POST",
1058
1177
  body: form,
@@ -1375,7 +1494,7 @@ var OppoChannel = class {
1375
1494
  const form = new FormData();
1376
1495
  form.append("type", "apk");
1377
1496
  form.append("sign", sign);
1378
- form.append("file", new Blob([fs.readFileSync(ctx.artifactPath)]), fileName);
1497
+ form.append("file", await fileBlob(ctx.artifactPath), fileName);
1379
1498
  const upRes = await fetch(uploadUrl, {
1380
1499
  method: "POST",
1381
1500
  body: form,
@@ -1444,7 +1563,7 @@ var OppoChannel = class {
1444
1563
  ctx.emit({
1445
1564
  type: "phase",
1446
1565
  phase: "done",
1447
- message: `已提交(${JSON.stringify(upd.data ?? {}).slice(0, 120)})`
1566
+ message: `已提交(versionCode=${versionCode})`
1448
1567
  });
1449
1568
  return {
1450
1569
  ok: true,
@@ -1670,7 +1789,7 @@ var VivoChannel = class {
1670
1789
  params.sign = hmacSign(accessSecret, params);
1671
1790
  const form = new FormData();
1672
1791
  for (const [k, v] of Object.entries(params)) form.append(k, v);
1673
- form.append("file", new Blob([fs.readFileSync(ctx.artifactPath)]), fileName);
1792
+ form.append("file", await fileBlob(ctx.artifactPath), fileName);
1674
1793
  const upRes = await fetch(API_BASE$2, {
1675
1794
  method: "POST",
1676
1795
  body: form,
@@ -1925,7 +2044,7 @@ var HonorChannel = class {
1925
2044
  message: `上传 ${fileName}(${formatSize(fileSize)})`
1926
2045
  });
1927
2046
  const form = new FormData();
1928
- form.append("file", new Blob([fs.readFileSync(ctx.artifactPath)]), fileName);
2047
+ form.append("file", await fileBlob(ctx.artifactPath), fileName);
1929
2048
  const upRes = await fetch(uploadUrl, {
1930
2049
  method: "POST",
1931
2050
  headers: { Authorization: `Bearer ${token}` },
@@ -2117,6 +2236,8 @@ var YybChannel = class {
2117
2236
  scheduled: true,
2118
2237
  status: true
2119
2238
  };
2239
+ /** 平台错误码 → 人话(ret 码:依据为本仓测试已固化的真实失败路径) */
2240
+ errorHints = { "10010": "应用不存在(检查 YYB_APP_ID 与 user_id 是否匹配该应用)" };
2120
2241
  /** stage 产物暂存(serial_number + md5),submit 按 appId 取回 */
2121
2242
  staged = /* @__PURE__ */ new Map();
2122
2243
  required(ctx, key, where) {
@@ -2205,8 +2326,17 @@ var YybChannel = class {
2205
2326
  }
2206
2327
  async submit(ctx) {
2207
2328
  const { userId, accessSecret, appId, pkgName } = this.creds(ctx);
2208
- const stagedInfo = this.staged.get(appId);
2209
- if (!stagedInfo) throw new Error("应用宝渠道缺少上传流水号(stage 未执行,serial_number/md5 不可用)");
2329
+ let stagedInfo = this.staged.get(appId);
2330
+ if (!stagedInfo) {
2331
+ ctx.emit({
2332
+ type: "warn",
2333
+ message: "无本进程 stage 记录(--skip-stage 跨进程),重新上传当前包后提审"
2334
+ });
2335
+ const r = await this.stage(ctx);
2336
+ if (!r.ok) throw new Error(`应用宝重新上传失败: ${r.error ?? "未知错误"}`);
2337
+ stagedInfo = this.staged.get(appId);
2338
+ if (!stagedInfo) throw new Error("应用宝重新上传后仍缺少流水号(内部状态异常)");
2339
+ }
2210
2340
  ctx.emit({
2211
2341
  type: "phase",
2212
2342
  phase: "submit"
@@ -2232,14 +2362,14 @@ var YybChannel = class {
2232
2362
  businessParams["deploy_type"] = "2";
2233
2363
  businessParams["deploy_time"] = String(deployTime);
2234
2364
  } else businessParams["deploy_type"] = "1";
2235
- const submit = await yybApi("/update_app", {
2365
+ await yybApi("/update_app", {
2236
2366
  userId,
2237
2367
  accessSecret
2238
2368
  }, businessParams);
2239
2369
  ctx.emit({
2240
2370
  type: "phase",
2241
2371
  phase: "done",
2242
- message: `已提交审核(${JSON.stringify(submit).slice(0, 120)})`
2372
+ message: `已提交审核(流水号 ${stagedInfo.serialNumber})`
2243
2373
  });
2244
2374
  return {
2245
2375
  ok: true,
@@ -2319,7 +2449,10 @@ const COMPILE_POLL_MS = 15e3;
2319
2449
  const APP_TEST_BASE = "https://connect-api.cloud.huawei.com/api/app-test/v1";
2320
2450
  /** AGC JSON API 封装:判 ret.code !== 0 抛错 —— v1 agc.mjs agcJson 原样 */
2321
2451
  async function agcJson(url, init, context) {
2322
- const res = await fetch(url, init);
2452
+ const res = await fetch(url, {
2453
+ ...init,
2454
+ signal: init.signal ?? AbortSignal.timeout(6e4)
2455
+ });
2323
2456
  let json;
2324
2457
  try {
2325
2458
  json = await res.json();
@@ -2443,8 +2576,9 @@ var AgcHarmonyChannel = class {
2443
2576
  ref: packageId || objectId
2444
2577
  };
2445
2578
  }
2446
- /** submit = v1 releaseHarmonyApp 完整链路:上传+绑定+文案 → 等编译轮询 → 提审(带重试) */
2447
- async submit(ctx) {
2579
+ /** submit = v1 releaseHarmonyApp 完整链路:上传+绑定+文案 → 等编译轮询 → 提审(带重试)。
2580
+ * stagedRef = stage() 返回的 packageId:包已在草稿区,跳过重传直接等编译+提审(.app 常为数百 MB~GB,双倍上传不可接受) */
2581
+ async submit(ctx, stagedRef) {
2448
2582
  if (ctx.target.releaseType === "6") return {
2449
2583
  ok: false,
2450
2584
  error: "AGC 测试发布(releaseType=6)的自动提审走 beta 配置 test['agc'].submit=true(上传绑定后自动提交测试版本审核)"
@@ -2456,7 +2590,13 @@ var AgcHarmonyChannel = class {
2456
2590
  phase: "auth",
2457
2591
  message: "token 获取成功"
2458
2592
  });
2459
- const { packageId } = await this.uploadFormal(ctx, appId, headers);
2593
+ let packageId = stagedRef;
2594
+ if (packageId) ctx.emit({
2595
+ type: "phase",
2596
+ phase: "upload",
2597
+ message: `复用 stage 已上传的软件包(pkgId: ${packageId},跳过重传)`
2598
+ });
2599
+ else ({packageId} = await this.uploadFormal(ctx, appId, headers));
2460
2600
  if (packageId) await this.awaitCompile(ctx, appId, packageId, headers);
2461
2601
  if (!ctx.updateDescription) ctx.emit({
2462
2602
  type: "warn",
@@ -2567,7 +2707,8 @@ var AgcHarmonyChannel = class {
2567
2707
  const upRes = await fetch(urlInfo.url, {
2568
2708
  method,
2569
2709
  headers: upHeaders,
2570
- body: fs.readFileSync(ctx.artifactPath),
2710
+ body: Readable.toWeb(fs.createReadStream(ctx.artifactPath)),
2711
+ duplex: "half",
2571
2712
  signal: AbortSignal.timeout(18e5)
2572
2713
  });
2573
2714
  if (![
@@ -2779,10 +2920,13 @@ var AgcHarmonyChannel = class {
2779
2920
  current: String(current),
2780
2921
  pageSize: "50"
2781
2922
  });
2782
- const res = await fetch(`${APP_TEST_BASE}/test-group/list?${qs}`, { headers: {
2783
- ...headers,
2784
- appId
2785
- } });
2923
+ const res = await fetch(`${APP_TEST_BASE}/test-group/list?${qs}`, {
2924
+ headers: {
2925
+ ...headers,
2926
+ appId
2927
+ },
2928
+ signal: AbortSignal.timeout(6e4)
2929
+ });
2786
2930
  if (!res.ok) throw new Error(`AGC 查询测试群组失败 HTTP ${res.status}`);
2787
2931
  const json = await res.json();
2788
2932
  if (Number(json.rtnCode) !== 0) throw new Error(`AGC 查询测试群组失败: rtnCode=${json.rtnCode ?? "?"}${json.rtnDesc ? ` ${json.rtnDesc}` : ""}`);
@@ -2870,7 +3014,13 @@ async function ascGet(jwt, apiPath) {
2870
3014
  headers: { Authorization: `Bearer ${jwt}` },
2871
3015
  signal: AbortSignal.timeout(3e4)
2872
3016
  });
2873
- const json = await res.json();
3017
+ const text = await res.text();
3018
+ let json;
3019
+ try {
3020
+ json = JSON.parse(text);
3021
+ } catch {
3022
+ throw new Error(`ASC API ${apiPath} 返回非 JSON(HTTP ${res.status}): ${text.slice(0, 300)}`);
3023
+ }
2874
3024
  if (!res.ok) throw new Error(`ASC API ${apiPath} 失败 HTTP ${res.status}: ${JSON.stringify(json.errors ?? json).slice(0, 300)}`);
2875
3025
  return json;
2876
3026
  }
@@ -2970,9 +3120,13 @@ var AscChannel = class {
2970
3120
  issuerId
2971
3121
  ], {
2972
3122
  cwd,
2973
- encoding: "utf8"
3123
+ encoding: "utf8",
3124
+ timeout: 18e5
2974
3125
  });
2975
- if (r.error) throw new Error(`无法执行 xcrun: ${r.error.message}(altool 需 macOS + Xcode)`);
3126
+ if (r.error) {
3127
+ if ("code" in r.error && r.error.code === "ETIMEDOUT") throw new Error("xcrun altool 超时(30min)——检查网络/钥匙串/CI 无 TTY 弹窗");
3128
+ throw new Error(`无法执行 xcrun: ${r.error.message}(altool 需 macOS + Xcode)`);
3129
+ }
2976
3130
  const output = `${r.stdout ?? ""}${r.stderr ?? ""}`.trim().slice(-2e3);
2977
3131
  if (r.status !== 0) throw new Error(`altool 上传失败(exit ${r.status}): ${output || "无输出"}`);
2978
3132
  if (output) ctx.emit({
@@ -3247,6 +3401,8 @@ var GooglePlayChannel = class {
3247
3401
  scheduled: false,
3248
3402
  status: true
3249
3403
  };
3404
+ /** 平台错误码 → 人话(HTTP 状态:依据为本仓测试已固化的失败路径) */
3405
+ errorHints = { "403": "权限不足(检查 Service Account 是否已关联 Play Console 且有发布权限)" };
3250
3406
  async plan(_ctx) {
3251
3407
  return {
3252
3408
  channel: this.id,
@@ -3263,12 +3419,40 @@ var GooglePlayChannel = class {
3263
3419
  ref: await this.uploadAndAssign(ctx, "internal", void 0)
3264
3420
  };
3265
3421
  }
3266
- async submit(ctx) {
3422
+ async submit(ctx, stagedRef) {
3423
+ const reusedVc = stagedRef?.split(":")[1];
3424
+ if (reusedVc !== void 0 && reusedVc !== "") return {
3425
+ ok: true,
3426
+ ref: await this.assignExisting(ctx, "production", Number(reusedVc))
3427
+ };
3267
3428
  return {
3268
3429
  ok: true,
3269
3430
  ref: await this.uploadAndAssign(ctx, "production", ctx.updateDescription)
3270
3431
  };
3271
3432
  }
3433
+ /** 复用 stage 已上传的 versionCode:开新 edit(仍需 auth)→ 部署 production → commit,跳过上传 */
3434
+ async assignExisting(ctx, track, versionCode) {
3435
+ const pkgName = this.requirePkgName(ctx);
3436
+ const token = await getAccessToken(ctx.credentials);
3437
+ ctx.emit({
3438
+ type: "phase",
3439
+ phase: "auth",
3440
+ message: "OAuth2 token 获取成功"
3441
+ });
3442
+ const edit = await apiFetch(`${API_BASE}/${pkgName}/edits`, token, {
3443
+ method: "POST",
3444
+ body: "{}"
3445
+ }, "创建 edit");
3446
+ if (!edit.id) throw new Error(`Google Play 创建 edit 响应缺少 id: ${JSON.stringify(edit).slice(0, 300)}`);
3447
+ const editId = edit.id;
3448
+ ctx.emit({
3449
+ type: "phase",
3450
+ phase: "upload",
3451
+ message: `复用 stage 上传的 versionCode ${versionCode}(跳过重复上传)`
3452
+ });
3453
+ await this.deployAndCommit(ctx, pkgName, token, editId, track, versionCode, ctx.updateDescription);
3454
+ return `${editId}:${versionCode}`;
3455
+ }
3272
3456
  /** 上传 + 部署到指定轨道 + commit,返回 `editId:versionCode` */
3273
3457
  async uploadAndAssign(ctx, track, releaseNotes) {
3274
3458
  const pkgName = this.requirePkgName(ctx);
@@ -3307,6 +3491,15 @@ var GooglePlayChannel = class {
3307
3491
  type: "progress",
3308
3492
  detail: `上传成功(versionCode: ${versionCode})`
3309
3493
  });
3494
+ return this.deployAndCommit(ctx, pkgName, token, editId, track, versionCode, releaseNotes);
3495
+ }
3496
+ /**
3497
+ * 部署 versionCode 到指定轨道并 commit:
3498
+ * 先 GET 轨道现有 releases 再合并 PUT——目标 release(同 status=completed 配置)追加新 versionCode,
3499
+ * 其余 releases 原样保留(status/userFraction 不变),避免整轨覆盖把旧版本从轨道剔除;
3500
+ * :commit 冒号语法(对拍 apkgo googleplay.go 权威;对 production 含提交审核语义,internal 即生效)
3501
+ */
3502
+ async deployAndCommit(ctx, pkgName, token, editId, track, versionCode, releaseNotes) {
3310
3503
  const releases = [...(await apiFetch(`${API_BASE}/${pkgName}/edits/${editId}/tracks/${track}`, token, { method: "GET" }, `读取 ${track} 轨道现有 releases`)).releases ?? []];
3311
3504
  const release = {
3312
3505
  versionCodes: [versionCode],
@@ -3374,6 +3567,11 @@ var GooglePlayChannel = class {
3374
3567
  };
3375
3568
  //#endregion
3376
3569
  //#region src/pipeline.ts
3570
+ /**
3571
+ * 发布编排引擎:构建计划 → 确认协议 → 并发执行 → 汇总。
3572
+ * headless:确认通过注入的 confirm 回调完成(CLI 用 clack/弹窗),core 不碰终端。
3573
+ * 插件:可选 PluginHost,在 plan/stage/submit 生命周期触发钩子(可拦截可修改)。
3574
+ */
3377
3575
  async function runLimited(items, limit, fn) {
3378
3576
  const queue = [...items];
3379
3577
  const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
@@ -3381,6 +3579,14 @@ async function runLimited(items, limit, fn) {
3381
3579
  });
3382
3580
  await Promise.all(workers);
3383
3581
  }
3582
+ /** 错误消息(带一层 cause 解包:fetch 网络层的真实原因如 NotReadableError 藏在 cause 里) */
3583
+ function errText(e) {
3584
+ if (e instanceof Error) {
3585
+ const cause = e.cause instanceof Error ? `(${e.cause.message})` : "";
3586
+ return `${e.message}${cause}`;
3587
+ }
3588
+ return String(e);
3589
+ }
3384
3590
  /** stage 阶段:并发传包到各渠道草稿(不提审);'stage:before' 返回 false 跳过该渠道 */
3385
3591
  async function runStage(input) {
3386
3592
  const plugins = input.plugins;
@@ -3415,7 +3621,8 @@ async function runStage(input) {
3415
3621
  channel: channel.id,
3416
3622
  ok: r.ok,
3417
3623
  ref: r.ref,
3418
- error: r.error
3624
+ error: r.error,
3625
+ artifactPath: effectiveCtx.artifactPath
3419
3626
  });
3420
3627
  const plan = await safePlan(channel, effectiveCtx);
3421
3628
  summary.push({
@@ -3438,7 +3645,7 @@ async function runStage(input) {
3438
3645
  channel: channel.id,
3439
3646
  label: channel.label,
3440
3647
  ok: false,
3441
- stageError: e.message,
3648
+ stageError: errText(e),
3442
3649
  plan
3443
3650
  });
3444
3651
  input.historySink?.({
@@ -3497,12 +3704,13 @@ async function runSubmit(input, options) {
3497
3704
  label: channel.label,
3498
3705
  ok: false,
3499
3706
  submitError: "被插件跳过",
3500
- plan: planFor(plans, basePlans, channel.id)
3707
+ plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
3501
3708
  });
3502
3709
  return;
3503
3710
  }
3504
3711
  effectiveCtx = r.value;
3505
3712
  }
3713
+ let stagedRef;
3506
3714
  if (!options.skipStage && channel.capabilities.stage) {
3507
3715
  const stageResult = await channel.stage(effectiveCtx);
3508
3716
  if (!stageResult.ok) {
@@ -3511,7 +3719,7 @@ async function runSubmit(input, options) {
3511
3719
  label: channel.label,
3512
3720
  ok: false,
3513
3721
  stageError: stageResult.error,
3514
- plan: planFor(plans, basePlans, channel.id)
3722
+ plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
3515
3723
  });
3516
3724
  input.historySink?.({
3517
3725
  channel: channel.id,
@@ -3521,6 +3729,7 @@ async function runSubmit(input, options) {
3521
3729
  });
3522
3730
  return;
3523
3731
  }
3732
+ stagedRef = stageResult.ref;
3524
3733
  input.historySink?.({
3525
3734
  channel: channel.id,
3526
3735
  action: "stage",
@@ -3528,14 +3737,23 @@ async function runSubmit(input, options) {
3528
3737
  ref: stageResult.ref,
3529
3738
  artifactVersion: plans.find((p) => p.channel === channel.id)?.artifactVersion || void 0
3530
3739
  });
3740
+ if (plugins) await plugins.broadcast("stage:after", {
3741
+ channel: channel.id,
3742
+ cwd: input.cwd
3743
+ }, {
3744
+ channel: channel.id,
3745
+ ok: true,
3746
+ ref: stageResult.ref,
3747
+ artifactPath: effectiveCtx.artifactPath
3748
+ });
3531
3749
  }
3532
- const r = await channel.submit(effectiveCtx);
3750
+ const r = await channel.submit(effectiveCtx, stagedRef);
3533
3751
  summary.push({
3534
3752
  channel: channel.id,
3535
3753
  label: channel.label,
3536
3754
  ok: r.ok,
3537
3755
  submitError: r.error,
3538
- plan: planFor(plans, basePlans, channel.id)
3756
+ plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
3539
3757
  });
3540
3758
  input.historySink?.({
3541
3759
  channel: channel.id,
@@ -3549,8 +3767,8 @@ async function runSubmit(input, options) {
3549
3767
  channel: channel.id,
3550
3768
  label: channel.label,
3551
3769
  ok: false,
3552
- submitError: e.message,
3553
- plan: planFor(plans, basePlans, channel.id)
3770
+ submitError: errText(e),
3771
+ plan: planFor(plans, basePlans, channel.id, ctx.artifactPath)
3554
3772
  });
3555
3773
  input.historySink?.({
3556
3774
  channel: channel.id,
@@ -3573,22 +3791,189 @@ async function safePlan(channel, ctx) {
3573
3791
  try {
3574
3792
  return await channel.plan(ctx);
3575
3793
  } catch {
3576
- return fallbackPlan(channel.id);
3794
+ return fallbackPlan(channel.id, ctx.artifactPath);
3577
3795
  }
3578
3796
  }
3579
- /** plan 失败/被插件删除时的兜底条目(保 summary.plan 非可选契约) */
3580
- function fallbackPlan(channelId) {
3797
+ /** 按产物扩展名推断平台(兜底条目用):.ipa→ios,.hap→harmony,.apk/.aab 及其余→android */
3798
+ function platformOfArtifact(artifactPath) {
3799
+ const ext = path.extname(artifactPath).toLowerCase();
3800
+ if (ext === ".ipa") return "ios";
3801
+ if (ext === ".hap") return "harmony";
3802
+ return "android";
3803
+ }
3804
+ /** plan 失败/被插件删除时的兜底条目(保 summary.plan 非可选契约;平台按产物推断不再硬编码 android) */
3805
+ function fallbackPlan(channelId, artifactPath) {
3581
3806
  return {
3582
3807
  channel: channelId,
3583
- platform: "android",
3808
+ platform: platformOfArtifact(artifactPath),
3584
3809
  artifactVersion: "",
3585
3810
  action: "stage+submit",
3586
3811
  afterApproval: "auto-publish"
3587
3812
  };
3588
3813
  }
3589
3814
  /** 汇总条目取计划:plan:before 改写后的 plans 优先,被插件删除时回退改写前快照,永不返回 undefined */
3590
- function planFor(plans, base, id) {
3591
- return plans.find((p) => p.channel === id) ?? base.get(id) ?? fallbackPlan(id);
3815
+ function planFor(plans, base, id, artifactPath) {
3816
+ return plans.find((p) => p.channel === id) ?? base.get(id) ?? fallbackPlan(id, artifactPath);
3817
+ }
3818
+ //#endregion
3819
+ //#region src/version/git.ts
3820
+ /**
3821
+ * git 只读/写小助手(bump 编排与推断共用)。
3822
+ * spawnSync + 超时;失败返回 ok:false 由调用方决定语义,不在此抛。
3823
+ */
3824
+ function git(cwd, args) {
3825
+ const r = spawnSync("git", args, {
3826
+ cwd,
3827
+ encoding: "utf8",
3828
+ timeout: 6e4
3829
+ });
3830
+ return {
3831
+ ok: !r.error && r.status === 0,
3832
+ stdout: (r.stdout ?? "").trim(),
3833
+ stderr: (r.stderr ?? "").trim()
3834
+ };
3835
+ }
3836
+ function isGitRepo(cwd) {
3837
+ return git(cwd, ["rev-parse", "--is-inside-work-tree"]).stdout === "true";
3838
+ }
3839
+ /** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
3840
+ function looseSemver(tag) {
3841
+ const direct = semver.valid(tag);
3842
+ if (direct) return direct;
3843
+ const m = /(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/.exec(tag);
3844
+ return m ? semver.valid(m[1]) ?? void 0 : void 0;
3845
+ }
3846
+ /**
3847
+ * 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
3848
+ * 无 tag / 非 git 仓库返回 undefined。
3849
+ */
3850
+ function latestSemverTag(cwd) {
3851
+ const r = git(cwd, ["tag", "-l"]);
3852
+ if (!r.ok) return void 0;
3853
+ let best;
3854
+ for (const raw of r.stdout.split("\n")) {
3855
+ const t = raw.trim();
3856
+ if (!t) continue;
3857
+ const v = looseSemver(t);
3858
+ if (!v) continue;
3859
+ if (!best || semver.gt(v, best.v)) best = {
3860
+ raw: t,
3861
+ v
3862
+ };
3863
+ }
3864
+ return best?.raw;
3865
+ }
3866
+ /** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
3867
+ function commitsSince(cwd, ref) {
3868
+ const r = git(cwd, [
3869
+ "log",
3870
+ ref ? `${ref}..HEAD` : "HEAD",
3871
+ "--format=%B%x00",
3872
+ "--no-merges"
3873
+ ]);
3874
+ if (!r.ok) return [];
3875
+ return r.stdout.split("\0").map((m) => m.trim()).filter(Boolean);
3876
+ }
3877
+ /** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
3878
+ * 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
3879
+ function statusShort(cwd) {
3880
+ const r = spawnSync("git", ["status", "--porcelain"], {
3881
+ cwd,
3882
+ encoding: "utf8",
3883
+ timeout: 3e4
3884
+ });
3885
+ if (r.error || r.status !== 0) return [];
3886
+ return (r.stdout ?? "").split("\n").map((l) => l.trimEnd()).filter(Boolean);
3887
+ }
3888
+ /**
3889
+ * release 分发成功后补 tag(tag = 实际已发布的正式版本;beta 不打 tag,追溯走 history)。
3890
+ * - tag 已存在(多端发布,前一端已打)→ 跳过
3891
+ * - dryRun / 无版本 / 非 git 仓库 → 不动、不炸(tag 失败不阻断发布主流程)
3892
+ */
3893
+ function tagReleaseVersion(cwd, version, opts = {}) {
3894
+ if (!version) return {
3895
+ tagged: false,
3896
+ reason: "无版本"
3897
+ };
3898
+ if (opts.dryRun) return {
3899
+ tagged: false,
3900
+ reason: "dry-run"
3901
+ };
3902
+ const tag = `v${version}`;
3903
+ if (spawnSync("git", [
3904
+ "rev-parse",
3905
+ "-q",
3906
+ "--verify",
3907
+ `refs/tags/${tag}`
3908
+ ], {
3909
+ cwd,
3910
+ encoding: "utf8"
3911
+ }).status === 0) return {
3912
+ tagged: false,
3913
+ reason: `tag ${tag} 已存在(前一端已发布)`
3914
+ };
3915
+ const r = spawnSync("git", ["tag", tag], {
3916
+ cwd,
3917
+ encoding: "utf8"
3918
+ });
3919
+ if (r.status !== 0) return {
3920
+ tagged: false,
3921
+ reason: `tag ${tag} 创建失败:${r.stderr.trim()}`
3922
+ };
3923
+ return { tagged: true };
3924
+ }
3925
+ //#endregion
3926
+ //#region src/update.ts
3927
+ /**
3928
+ * 热更能力契约:applift 的第三维扩展(framework × channel 之外的 update 维度)。
3929
+ *
3930
+ * provider 与 framework 绑定(如 react-native-update 只服务 RN 工程),由插件经
3931
+ * 'update:register' 注册;beta/release 管线的投递路由(热更 vs 原生包)消费 judge 结果,
3932
+ * 只做推荐与证据展示,最终投递方式由用户在确认界面拍板(CI 非交互时 uncertain 即中止)。
3933
+ */
3934
+ /**
3935
+ * 投递路由:按当前 framework 匹配 provider 并执行判定。
3936
+ * - 无 provider / 无匹配 → null(走原生包路径,原始行为不受影响)
3937
+ * - 多个 provider 匹配同一 framework → 配置错误(一个框架一个热更实现)
3938
+ * 结论是推荐值:最终投递方式由用户在确认界面拍板,CI 非交互时 uncertain 中止
3939
+ */
3940
+ async function routeDelivery(providers, ctx, diff, platform) {
3941
+ const matched = providers.filter((p) => p.frameworks.includes(ctx.framework.id) && (!platform || !p.platforms || p.platforms.includes(platform)));
3942
+ if (matched.length === 0) return null;
3943
+ if (matched.length > 1) throw new Error(`多个热更 provider 匹配 framework「${ctx.framework.id}」:${matched.map((p) => p.id).join(", ")}——一个框架只应有一个热更实现`);
3944
+ const provider = matched[0];
3945
+ return {
3946
+ provider,
3947
+ judgement: await provider.judge(diff, ctx)
3948
+ };
3949
+ }
3950
+ /**
3951
+ * 收集上个发版锚点(最新 semver tag)到工作区的变更文件列表。
3952
+ * 含未提交改动与未跟踪文件(pushy bundle 打包的是工作区内容,判定口径必须一致)。
3953
+ * 无锚点 / 非 git 仓库 → null(调用方按 uncertain 处理,不猜)。
3954
+ */
3955
+ async function collectReleaseDiff(cwd) {
3956
+ const anchor = latestSemverTag(cwd);
3957
+ if (!anchor) return null;
3958
+ const r = spawnSync("git", [
3959
+ "diff",
3960
+ "--name-only",
3961
+ anchor
3962
+ ], {
3963
+ cwd,
3964
+ encoding: "utf8"
3965
+ });
3966
+ const untracked = spawnSync("git", [
3967
+ "ls-files",
3968
+ "--others",
3969
+ "--exclude-standard"
3970
+ ], {
3971
+ cwd,
3972
+ encoding: "utf8"
3973
+ });
3974
+ if (r.status !== 0 || untracked.status !== 0) return null;
3975
+ const files = [...r.stdout.split("\n"), ...untracked.stdout.split("\n")].map((l) => l.trim());
3976
+ return [...new Set(files)].filter(Boolean);
3592
3977
  }
3593
3978
  //#endregion
3594
3979
  //#region src/plugin.ts
@@ -3599,6 +3984,8 @@ var PluginHost = class {
3599
3984
  }
3600
3985
  /**
3601
3986
  * 顺序执行某钩子;支持「链式替换」(返回对象则替换后继续传给下一插件)与「拦截」(返回 false 立即中止)。
3987
+ * 信任边界:返回值按 T 强转、不做形状校验(vite 同款取舍)——插件作者自负其责,
3988
+ * 坏形状会在后续消费点显式抛错,宿主不代为静默兜底。
3602
3989
  * @returns {[被拦截?, 最终值]} false 表示被某插件拦截
3603
3990
  */
3604
3991
  async apply(hook, value, ctx) {
@@ -3635,403 +4022,6 @@ function resolvePlugins(userConfig) {
3635
4022
  return (userConfig.plugins ?? []).map((p) => typeof p === "function" ? p() : p);
3636
4023
  }
3637
4024
  //#endregion
3638
- //#region src/framework.ts
3639
- /**
3640
- * 框架适配器:让 applift 框架无关(RN / Flutter / 原生安卓 / 原生 iOS / 原生鸿蒙)。
3641
- * 探测规则 + 产物 pattern + 缺省构建命令;插件可用 'channel:register' 旁路注册自定义 FrameworkAdapter。
3642
- */
3643
- /** 一层子目录列表(探测用,跳过隐藏目录与 node_modules/Pods) */
3644
- function subdirs(cwd, depth) {
3645
- if (depth <= 0) return [];
3646
- const out = [];
3647
- let names;
3648
- try {
3649
- names = fs.readdirSync(cwd, { withFileTypes: true });
3650
- } catch {
3651
- return [];
3652
- }
3653
- for (const name of names) {
3654
- if (!name.isDirectory() || name.name.startsWith(".") || name.name === "node_modules" || name.name === "Pods") continue;
3655
- const p = path.join(cwd, name.name);
3656
- out.push(p, ...subdirs(p, depth - 1));
3657
- }
3658
- return out;
3659
- }
3660
- /** package.json 读取(容错) */
3661
- function readJson(filePath) {
3662
- try {
3663
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
3664
- } catch {
3665
- return null;
3666
- }
3667
- }
3668
- function deps(pkg) {
3669
- if (!pkg) return {};
3670
- return {
3671
- ...pkg.dependencies,
3672
- ...pkg.devDependencies
3673
- };
3674
- }
3675
- const rnAdapter = {
3676
- id: "rn",
3677
- label: "React Native",
3678
- detect(cwd) {
3679
- return "react-native" in deps(readJson(path.join(cwd, "package.json")));
3680
- },
3681
- /** Expo 项目(依赖 expo 包):同走 RN 产物路径(prebuild 生成原生工程),label 区分展示 */
3682
- expoAwareLabel(cwd) {
3683
- return "expo" in deps(readJson(path.join(cwd, "package.json"))) ? "Expo (React Native)" : "React Native";
3684
- },
3685
- artifactPatterns(platform) {
3686
- if (platform === "android") return ["android/app/build/outputs/apk/release/*-release.apk"];
3687
- if (platform === "ios") return ["build/ios/ipa/*.ipa"];
3688
- return [
3689
- "harmony/entry/build/default/outputs/**/entry-*-signed.hap",
3690
- "harmony/entry/build/default/outputs/**/entry-*.app",
3691
- "harmony/build/outputs/**/*.app",
3692
- "build/outputs/**/*.app"
3693
- ];
3694
- },
3695
- buildCommand(platform) {
3696
- if (platform === "android") return [
3697
- "npm",
3698
- "run",
3699
- "build:test",
3700
- "--",
3701
- "--platform",
3702
- "android"
3703
- ];
3704
- if (platform === "ios") return null;
3705
- return [
3706
- "npm",
3707
- "run",
3708
- "build:test",
3709
- "--",
3710
- "--platform",
3711
- "harmony"
3712
- ];
3713
- }
3714
- };
3715
- const flutterAdapter = {
3716
- id: "flutter",
3717
- label: "Flutter",
3718
- detect(cwd) {
3719
- if (readJson(path.join(cwd, "pubspec.yaml"))) return true;
3720
- try {
3721
- const text = fs.readFileSync(path.join(cwd, "pubspec.yaml"), "utf8");
3722
- return /^sdk:\s*flutter$/m.test(text) || /^name:/m.test(text);
3723
- } catch {
3724
- return false;
3725
- }
3726
- },
3727
- artifactPatterns(platform) {
3728
- if (platform === "android") return ["build/app/outputs/flutter-apk/app-release.apk"];
3729
- if (platform === "ios") return ["build/ios/ipa/*.ipa"];
3730
- return [
3731
- "build/**/outputs/**/*-signed.hap",
3732
- "harmony/entry/build/**/outputs/**/*-signed.hap",
3733
- "ohos/entry/build/**/outputs/**/*-signed.hap",
3734
- "build/**/outputs/**/*.app"
3735
- ];
3736
- },
3737
- buildCommand(platform) {
3738
- if (platform === "android") return [
3739
- "flutter",
3740
- "build",
3741
- "apk",
3742
- "--release"
3743
- ];
3744
- if (platform === "ios") return [
3745
- "flutter",
3746
- "build",
3747
- "ipa",
3748
- "--release"
3749
- ];
3750
- return [
3751
- "flutter",
3752
- "build",
3753
- "hap",
3754
- "--release"
3755
- ];
3756
- }
3757
- };
3758
- const nativeAndroidAdapter = {
3759
- id: "native-android",
3760
- label: "原生 Android",
3761
- detect(cwd) {
3762
- if (rnAdapter.detect(cwd)) return false;
3763
- return fs.existsSync(path.join(cwd, "settings.gradle")) || fs.existsSync(path.join(cwd, "settings.gradle.kts"));
3764
- },
3765
- artifactPatterns() {
3766
- return ["app/build/outputs/apk/release/*-release.apk"];
3767
- },
3768
- buildCommand() {
3769
- return ["./gradlew", "assembleRelease"];
3770
- }
3771
- };
3772
- const nativeIosAdapter = {
3773
- id: "native-ios",
3774
- label: "原生 iOS",
3775
- detect(cwd) {
3776
- if (rnAdapter.detect(cwd)) return false;
3777
- for (const dir of [cwd, ...subdirs(cwd, 1)]) {
3778
- let entries;
3779
- try {
3780
- entries = fs.readdirSync(dir);
3781
- } catch {
3782
- continue;
3783
- }
3784
- if (entries.some((f) => String(f).endsWith(".xcodeproj") || String(f).endsWith(".xcworkspace"))) return true;
3785
- }
3786
- return false;
3787
- },
3788
- artifactPatterns() {
3789
- return ["build/**/*.ipa"];
3790
- },
3791
- buildCommand() {
3792
- return null;
3793
- }
3794
- };
3795
- const nativeHarmonyAdapter = {
3796
- id: "native-harmony",
3797
- label: "原生鸿蒙 (ArkTS)",
3798
- detect(cwd) {
3799
- return fs.existsSync(path.join(cwd, "oh-package.json5")) || fs.existsSync(path.join(cwd, "harmony/oh-package.json5"));
3800
- },
3801
- artifactPatterns() {
3802
- return [
3803
- "entry/build/default/outputs/**/entry-*-signed.hap",
3804
- "entry/build/default/outputs/**/entry-*.app",
3805
- "harmony/entry/build/default/outputs/**/*-signed.hap",
3806
- "build/outputs/**/*.app",
3807
- "harmony/build/outputs/**/*.app",
3808
- "ohos/build/outputs/**/*.app"
3809
- ];
3810
- },
3811
- buildCommand() {
3812
- return [
3813
- "hvigorw",
3814
- "--mode",
3815
- "module",
3816
- "-p",
3817
- "module=entry@default",
3818
- "-p",
3819
- "product=default",
3820
- "-p",
3821
- "buildMode=release",
3822
- "assembleHap",
3823
- "--no-daemon"
3824
- ];
3825
- }
3826
- };
3827
- const electronAdapter = {
3828
- id: "electron",
3829
- label: "Electron",
3830
- detect(cwd) {
3831
- return "electron" in deps(readJson(path.join(cwd, "package.json")));
3832
- },
3833
- artifactPatterns(platform) {
3834
- if (platform === "windows") return [
3835
- "dist/*.exe",
3836
- "dist/**/*.exe",
3837
- "out/make/*.exe"
3838
- ];
3839
- if (platform === "macos") return [
3840
- "dist/*.dmg",
3841
- "dist/**/*.dmg",
3842
- "out/make/*.dmg"
3843
- ];
3844
- if (platform === "linux") return [
3845
- "dist/*.AppImage",
3846
- "dist/**/*.AppImage",
3847
- "out/make/*.AppImage"
3848
- ];
3849
- return [];
3850
- },
3851
- buildCommand(platform) {
3852
- if (platform === "windows") return [
3853
- "npx",
3854
- "electron-forge",
3855
- "make",
3856
- "--platform",
3857
- "win32"
3858
- ];
3859
- if (platform === "macos") return [
3860
- "npx",
3861
- "electron-forge",
3862
- "make",
3863
- "--platform",
3864
- "darwin"
3865
- ];
3866
- return [
3867
- "npx",
3868
- "electron-forge",
3869
- "make",
3870
- "--platform",
3871
- "linux"
3872
- ];
3873
- }
3874
- };
3875
- const tauriAdapter = {
3876
- id: "tauri",
3877
- label: "Tauri",
3878
- detect(cwd) {
3879
- return "@tauri-apps/cli" in deps(readJson(path.join(cwd, "package.json"))) || fs.existsSync(path.join(cwd, "src-tauri"));
3880
- },
3881
- artifactPatterns(platform) {
3882
- if (platform === "windows") return ["src-tauri/target/release/bundle/nsis/*.exe", "src-tauri/target/release/bundle/msi/*.msi"];
3883
- if (platform === "macos") return ["src-tauri/target/release/bundle/dmg/*.dmg", "src-tauri/target/release/bundle/macos/*.app"];
3884
- if (platform === "linux") return ["src-tauri/target/release/bundle/deb/*.deb", "src-tauri/target/release/bundle/appimage/*.AppImage"];
3885
- return [];
3886
- },
3887
- buildCommand(platform) {
3888
- if (platform === "macos") return [
3889
- "npx",
3890
- "tauri",
3891
- "build"
3892
- ];
3893
- return [
3894
- "npx",
3895
- "tauri",
3896
- "build"
3897
- ];
3898
- }
3899
- };
3900
- const BUILTIN_ADAPTERS = [
3901
- rnAdapter,
3902
- flutterAdapter,
3903
- electronAdapter,
3904
- tauriAdapter,
3905
- nativeAndroidAdapter,
3906
- nativeIosAdapter,
3907
- nativeHarmonyAdapter
3908
- ];
3909
- /** 探测项目框架(可注入额外适配器,插件自定义框架的扩展点) */
3910
- function detectFramework(cwd, extra = []) {
3911
- for (const adapter of [...extra, ...BUILTIN_ADAPTERS]) try {
3912
- if (adapter.detect(cwd)) return adapter;
3913
- } catch {}
3914
- return null;
3915
- }
3916
- /** 按 pattern 扫描产物(支持 ** 与 * 简单通配,返回绝对路径,多产物按 mtime 新者在前) */
3917
- function findArtifacts(cwd, patterns) {
3918
- const byPath = /* @__PURE__ */ new Map();
3919
- for (const pattern of patterns) for (const p of globWalk(path.resolve(cwd), pattern.split("/"))) {
3920
- if (byPath.has(p)) continue;
3921
- try {
3922
- byPath.set(p, fs.statSync(p).mtimeMs);
3923
- } catch {}
3924
- }
3925
- return [...byPath].sort((a, b) => b[1] - a[1]).map(([p]) => p);
3926
- }
3927
- /** 文本读取(容错:不存在/不可读返回 null,绝不抛;detectBundleIds 各源共用) */
3928
- function readText$1(filePath) {
3929
- try {
3930
- return fs.readFileSync(filePath, "utf8");
3931
- } catch {
3932
- return null;
3933
- }
3934
- }
3935
- /** 从 JSON 对象按路径取非空字符串(任一层缺失/非字符串返回 undefined) */
3936
- function pickString(obj, ...keys) {
3937
- let cur = obj;
3938
- for (const k of keys) {
3939
- if (cur === null || typeof cur !== "object") return void 0;
3940
- cur = cur[k];
3941
- }
3942
- return typeof cur === "string" && cur.trim() ? cur.trim() : void 0;
3943
- }
3944
- /** 原生安卓 applicationId(groovy/kts 两种赋值语法;app/ 与 android/app/ 两处布局;不中返回 undefined) */
3945
- function gradleApplicationId(cwd) {
3946
- const candidates = [
3947
- path.join(cwd, "app", "build.gradle"),
3948
- path.join(cwd, "app", "build.gradle.kts"),
3949
- path.join(cwd, "android", "app", "build.gradle"),
3950
- path.join(cwd, "android", "app", "build.gradle.kts")
3951
- ];
3952
- for (const file of candidates) {
3953
- const text = readText$1(file);
3954
- if (!text) continue;
3955
- const m = /(?:^|\s)applicationId\s*[=:]?\s*["']([^"'\s]+)["']/.exec(text);
3956
- if (m?.[1]) return m[1];
3957
- }
3958
- }
3959
- /** 鸿蒙 AppScope/app.json5 的 bundleName(json5 允许注释/尾逗号,正则取值而非 JSON 解析) */
3960
- function appJson5BundleName(file) {
3961
- const text = readText$1(file);
3962
- return (text && /"bundleName"\s*:\s*"([^"]+)"/.exec(text))?.[1];
3963
- }
3964
- /** 两层子目录内找首个 *.xcodeproj:pbxproj 第一个非变量(含 $ 的 $(...) 与 ${...})的 PRODUCT_BUNDLE_IDENTIFIER + 工程名(Pods 已被 subdirs 排除) */
3965
- function findPbxproj(cwd) {
3966
- for (const dir of [cwd, ...subdirs(cwd, 2)]) {
3967
- let entries;
3968
- try {
3969
- entries = fs.readdirSync(dir, { withFileTypes: true });
3970
- } catch {
3971
- continue;
3972
- }
3973
- const projects = entries.filter((e) => e.isDirectory() && e.name.endsWith(".xcodeproj")).map((e) => e.name).sort();
3974
- for (const name of projects) {
3975
- const pbx = readText$1(path.join(dir, name, "project.pbxproj"));
3976
- if (!pbx) continue;
3977
- for (const m of pbx.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;]+);/g)) {
3978
- const value = m[1].trim().replace(/^["']|["']$/g, "");
3979
- if (value && !value.includes("$")) return {
3980
- bundleId: value,
3981
- scheme: name.slice(0, -10)
3982
- };
3983
- }
3984
- }
3985
- }
3986
- return null;
3987
- }
3988
- /**
3989
- * 自动推导三端应用标识(项目配置缺省时的兜底,配置可全空):
3990
- * android:RN app.json android.applicationId > 原生 gradle applicationId;
3991
- * harmony:AppScope/app.json5(根或 harmony/ 下)> RN app.json harmony.bundleName;
3992
- * ios:RN app.json ios.bundleIdentifier > 两层内 xcodeproj 的 PRODUCT_BUNDLE_IDENTIFIER(scheme=工程名)。
3993
- * 容错:文件不存在/正则不中只置 undefined,绝不抛。
3994
- */
3995
- function detectBundleIds(cwd) {
3996
- const appJson = readJson(path.join(cwd, "app.json"));
3997
- const pbx = findPbxproj(cwd);
3998
- const expo = appJson?.expo ?? null;
3999
- const expoAndroid = expo ? pickString(expo, "android", "package") : void 0;
4000
- const expoIos = expo ? pickString(expo, "ios", "bundleIdentifier") : void 0;
4001
- const expoName = expo && typeof expo.name === "string" ? expo.name : void 0;
4002
- return {
4003
- androidPkgName: expoAndroid ?? pickString(appJson, "android", "applicationId") ?? gradleApplicationId(cwd),
4004
- harmonyBundleName: appJson5BundleName(path.join(cwd, "AppScope", "app.json5")) ?? appJson5BundleName(path.join(cwd, "harmony", "AppScope", "app.json5")) ?? pickString(appJson, "harmony", "bundleName"),
4005
- iosBundleId: expoIos ?? pickString(appJson, "ios", "bundleIdentifier") ?? pbx?.bundleId,
4006
- iosScheme: pbx?.scheme ?? expoName
4007
- };
4008
- }
4009
- /** 极简 glob 目录游走(** 跨层,* 单段),避免引入 minimatch 依赖 */
4010
- function globWalk(dir, segments) {
4011
- const [head, ...rest] = segments;
4012
- if (!head) return [dir];
4013
- if (!fs.existsSync(dir)) return [];
4014
- if (head === "**") {
4015
- const out = [];
4016
- const walkAll = (d) => {
4017
- out.push(...globWalk(d, rest));
4018
- for (const name of fs.readdirSync(d, { withFileTypes: true })) {
4019
- if (!name.isDirectory() || name.name.startsWith(".") || name.name === "node_modules") continue;
4020
- walkAll(path.join(d, name.name));
4021
- }
4022
- };
4023
- walkAll(dir);
4024
- return [...new Set(out)];
4025
- }
4026
- if (head.includes("*")) {
4027
- const re = new RegExp(`^${head.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")}$`);
4028
- const out = [];
4029
- for (const name of fs.readdirSync(dir)) if (re.test(name)) out.push(...globWalk(path.join(dir, name), rest));
4030
- return out;
4031
- }
4032
- return globWalk(path.join(dir, head), rest);
4033
- }
4034
- //#endregion
4035
4025
  //#region src/history.ts
4036
4026
  /**
4037
4027
  * 发版历史:~/.applift/history.jsonl,每行一条 JSON(HistoryEntry)。
@@ -4073,438 +4063,26 @@ function readEntries(query = {}, filePath = HISTORY_PATH) {
4073
4063
  //#region src/channels/shared/error-codes.ts
4074
4064
  /**
4075
4065
  * 错误码人话化:消费各渠道自带 errorHints(平台内聚——表与渠道实现同文件)。
4066
+ * 网络类故障(AbortSignal 超时/断连/DNS)渠道层不转译,统一在此兜底提示。
4076
4067
  */
4068
+ /** 网络故障特征 → 人话提示(命中即返回,按序优先:文件可读性 > 超时 > 网络故障) */
4069
+ const NETWORK_HINTS = [
4070
+ [/NotReadableError/i, "产物/图标文件在上传期间被修改或不可读(惰性 Blob 读盘中途失败):确认构建已完成、无并发进程改动产物,重跑"],
4071
+ [/aborted due to timeout|TimeoutError/i, "请求超时(服务端无响应或网络不稳):可重跑;大包上传建议在稳定网络环境执行"],
4072
+ [/\b(?:ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|EPIPE)\b|socket hang up|fetch failed/i, "网络故障(连接被重置/DNS 解析失败等):检查网络或代理后重跑"]
4073
+ ];
4074
+ /** 码匹配:纯数字码加数字边界(防 1003 误中 11003/10031);其余按包含匹配 */
4075
+ function matchesCode(message, code) {
4076
+ if (/^\d+$/.test(code)) return new RegExp(`(?<!\\d)${code}(?!\\d)`).test(message);
4077
+ return message.includes(code);
4078
+ }
4077
4079
  function humanizeError(hints, message) {
4080
+ for (const [re, hint] of NETWORK_HINTS) if (re.test(message)) return `${message}\n 提示: ${hint}`;
4078
4081
  if (!hints) return message;
4079
- for (const [code, hint] of Object.entries(hints)) if (message.includes(code)) return `${message}\n 提示: ${hint}`;
4082
+ for (const [code, hint] of Object.entries(hints)) if (matchesCode(message, code)) return `${message}\n 提示: ${hint}`;
4080
4083
  return message;
4081
4084
  }
4082
4085
  //#endregion
4083
- //#region src/version/project.ts
4084
- /**
4085
- * 工程文件版本读写(可接管模式:版本事实源永远是工程文件,applift 只代读代写)。
4086
- * 只认标准模板形态(AGP/Xcode/DevEco/pubspec/package.json/tauri.conf);行级改写、
4087
- * 幂等(字段定位 + 写死目标值,不依赖旧值,跑 N 次 diff 不变——qy-driver sync-app-identity 同款模式)。
4088
- * version catalog 引用形态(libs.versions.xxx.get())读侧识别并标注 problem,不代写。
4089
- */
4090
- function readText(p) {
4091
- try {
4092
- return fs.readFileSync(p, "utf8");
4093
- } catch {
4094
- return null;
4095
- }
4096
- }
4097
- function firstExisting(...paths) {
4098
- return paths.find((p) => fs.existsSync(p));
4099
- }
4100
- function locateIosProject(dir) {
4101
- const find = (d) => {
4102
- try {
4103
- return fs.readdirSync(d, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.endsWith(".xcodeproj") && !e.name.startsWith(".") && e.name !== "Pods.xcodeproj");
4104
- } catch {
4105
- return;
4106
- }
4107
- };
4108
- let xcodeproj = find(dir);
4109
- let base = dir;
4110
- if (!xcodeproj) try {
4111
- const subs = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "Pods");
4112
- for (const s of subs) {
4113
- xcodeproj = find(path.join(dir, s.name));
4114
- if (xcodeproj) {
4115
- base = path.join(dir, s.name);
4116
- break;
4117
- }
4118
- }
4119
- } catch {}
4120
- const pbxproj = xcodeproj ? path.join(base, xcodeproj.name, "project.pbxproj") : void 0;
4121
- const projDir = xcodeproj?.name.replace(/\.xcodeproj$/, "");
4122
- const plist = projDir && fs.existsSync(path.join(base, projDir, "Info.plist")) ? path.join(base, projDir, "Info.plist") : void 0;
4123
- return {
4124
- pbxproj: pbxproj && fs.existsSync(pbxproj) ? pbxproj : void 0,
4125
- plist
4126
- };
4127
- }
4128
- function locateVersionFiles(cwd, framework) {
4129
- const loc = {};
4130
- const id = framework?.id ?? "";
4131
- const tryAll = framework === null;
4132
- 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"));
4133
- if (tryAll || id === "rn" || id === "native-ios" || id === "flutter") {
4134
- const sub = locateIosProject(path.join(cwd, "ios"));
4135
- const root = sub.pbxproj ? sub : locateIosProject(cwd);
4136
- loc.ios = root.pbxproj;
4137
- loc.plist = root.plist;
4138
- }
4139
- 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"));
4140
- if (tryAll || id === "flutter") loc.pubspec = fs.existsSync(path.join(cwd, "pubspec.yaml")) ? path.join(cwd, "pubspec.yaml") : void 0;
4141
- if (tryAll || id === "rn" || id === "electron") loc.packageJson = fs.existsSync(path.join(cwd, "package.json")) ? path.join(cwd, "package.json") : void 0;
4142
- if (tryAll || id === "tauri") {
4143
- loc.tauri = firstExisting(path.join(cwd, "src-tauri", "tauri.conf.json"), path.join(cwd, "src-tauri", "tauri.conf.json5"));
4144
- loc.packageJson = fs.existsSync(path.join(cwd, "package.json")) ? path.join(cwd, "package.json") : void 0;
4145
- }
4146
- if (id === "rn") {
4147
- const appJson = readText(path.join(cwd, "app.json"));
4148
- if (appJson && /"expo"\s*:/.test(appJson)) loc.expoAppJson = path.join(cwd, "app.json");
4149
- }
4150
- return loc;
4151
- }
4152
- function readGradle(file) {
4153
- const text = readText(file);
4154
- if (!text) return {};
4155
- const out = {};
4156
- const codeM = /(versionCode(?!Suffix)\s*=?\s*)(\d+)/.exec(text);
4157
- const nameM = /(versionName(?!Suffix)\s*=?\s*)("[^"]*"|'[^']*'|[\w.+-]+)/.exec(text);
4158
- if (codeM) out.versionCode = codeM[2];
4159
- if (nameM) {
4160
- const nameVal = nameM[2].replace(/^["']|["']$/g, "");
4161
- if (nameVal.startsWith("libs.versions.")) out.problem = "versionName 走 version catalog 引用(libs.versions.*):非标准形态,请把版本字段搬回 app/build.gradle(.kts) 字面量";
4162
- else out.versionName = nameVal;
4163
- }
4164
- if (/(versionCode|versionName)\s*=?\s*libs\.versions\./.test(text) && !out.problem) out.problem = "版本字段走 version catalog 引用(libs.versions.*):非标准形态,请把版本字段搬回 app/build.gradle(.kts) 字面量";
4165
- return out;
4166
- }
4167
- function readPbxproj(file) {
4168
- const text = readText(file);
4169
- if (!text) return {};
4170
- const names = [...text.matchAll(/MARKETING_VERSION\s*=\s*([^;]+);/g)].map((m) => m[1].trim());
4171
- return {
4172
- versionName: names[0],
4173
- versionCode: /CURRENT_PROJECT_VERSION\s*=\s*([^;]+);/.exec(text)?.[1]?.trim(),
4174
- problem: new Set(names).size > 1 ? `多 target MARKETING_VERSION 不一致(${[...new Set(names)].join(" / ")}):读第一个,bump 时将全部同步` : void 0
4175
- };
4176
- }
4177
- function readPlist(file) {
4178
- const text = readText(file);
4179
- if (!text) return {};
4180
- const name = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(text)?.[1];
4181
- const code = /<key>CFBundleVersion<\/key>\s*<string>([^<]+)<\/string>/.exec(text)?.[1];
4182
- if (name && !name.startsWith("$")) return {
4183
- versionName: name,
4184
- versionCode: code
4185
- };
4186
- return {};
4187
- }
4188
- function readAppJson5(file) {
4189
- const text = readText(file);
4190
- if (!text) return {};
4191
- return {
4192
- versionName: /"versionName"\s*:\s*"([^"]+)"/.exec(text)?.[1],
4193
- versionCode: /"versionCode"\s*:\s*(\d+)/.exec(text)?.[1]
4194
- };
4195
- }
4196
- function readPubspec(file) {
4197
- const text = readText(file);
4198
- if (!text) return {};
4199
- const m = /^version:\s*(\S+)(?:\s*#.*)?$/m.exec(text);
4200
- if (!m) return {};
4201
- const [name, code] = m[1].split("+");
4202
- return {
4203
- versionName: name,
4204
- versionCode: code
4205
- };
4206
- }
4207
- function readJsonVersionKey(file) {
4208
- const text = readText(file);
4209
- if (!text) return {};
4210
- let best;
4211
- for (const m of text.matchAll(/^(\s*)"version"\s*:\s*"([^"]+)"\s*,?\s*$/gm)) {
4212
- const indent = m[1].length;
4213
- if (!best || indent < best.indent) best = {
4214
- indent,
4215
- value: m[2]
4216
- };
4217
- }
4218
- if (best) return { versionName: best.value };
4219
- try {
4220
- const obj = JSON.parse(text);
4221
- if (typeof obj.version === "string") return { versionName: obj.version };
4222
- } catch {}
4223
- const flat = /"version"\s*:\s*"([^"]+)"/.exec(text)?.[1];
4224
- return flat ? { versionName: flat } : {};
4225
- }
4226
- function readExpoAppJson(file) {
4227
- const text = readText(file);
4228
- if (!text) return {};
4229
- return {
4230
- versionName: /"version"\s*:\s*"([^"]+)"/.exec(text)?.[1],
4231
- versionCode: /"versionCode"\s*:\s*(\d+)/.exec(text)?.[1] ?? /"buildNumber"\s*:\s*"([^"]+)"/.exec(text)?.[1]
4232
- };
4233
- }
4234
- /** 各端版本实况(按框架布局;未识别框架时尝试全部标准位置) */
4235
- function readProjectVersions(cwd, framework) {
4236
- const loc = locateVersionFiles(cwd, framework);
4237
- const out = [];
4238
- if (loc.android) out.push({
4239
- platform: "android",
4240
- file: loc.android,
4241
- kind: "gradle",
4242
- ...readGradle(loc.android)
4243
- });
4244
- if (loc.ios) out.push({
4245
- platform: "ios",
4246
- file: loc.ios,
4247
- kind: "pbxproj",
4248
- ...readPbxproj(loc.ios)
4249
- });
4250
- else if (loc.plist) {
4251
- const plist = readPlist(loc.plist);
4252
- if (plist.versionName) out.push({
4253
- platform: "ios",
4254
- file: loc.plist,
4255
- kind: "pbxproj",
4256
- ...plist,
4257
- problem: "未找到 pbxproj,读 Info.plist 字面量"
4258
- });
4259
- }
4260
- if (loc.harmony) out.push({
4261
- platform: "harmony",
4262
- file: loc.harmony,
4263
- kind: "app-json5",
4264
- ...readAppJson5(loc.harmony)
4265
- });
4266
- if (loc.pubspec) out.push({
4267
- platform: "flutter",
4268
- file: loc.pubspec,
4269
- kind: "pubspec",
4270
- ...readPubspec(loc.pubspec)
4271
- });
4272
- if (loc.packageJson) out.push({
4273
- platform: "js",
4274
- file: loc.packageJson,
4275
- kind: "package-json",
4276
- ...readJsonVersionKey(loc.packageJson)
4277
- });
4278
- if (loc.tauri) out.push({
4279
- platform: "tauri",
4280
- file: loc.tauri,
4281
- kind: "tauri",
4282
- ...readJsonVersionKey(loc.tauri)
4283
- });
4284
- if (loc.expoAppJson) out.push({
4285
- platform: "expo",
4286
- file: loc.expoAppJson,
4287
- kind: "expo-app-json",
4288
- ...readExpoAppJson(loc.expoAppJson)
4289
- });
4290
- return out;
4291
- }
4292
- /** 指定捕获组替换为 value(其余组原样保留;约定各正则组外无字面量) */
4293
- function replaceGroup(re, text, group, value) {
4294
- let count = 0;
4295
- return {
4296
- text: text.replace(re, (_m, ...groups) => {
4297
- count++;
4298
- const g = groups.slice(0, -2);
4299
- g[group - 1] = value;
4300
- return g.join("");
4301
- }),
4302
- count
4303
- };
4304
- }
4305
- function writeIfChanged(file, next) {
4306
- if (readText(file) === next) return false;
4307
- fs.writeFileSync(file, next, "utf8");
4308
- return true;
4309
- }
4310
- function writeGradle(file, w) {
4311
- const text = readText(file);
4312
- if (!text) throw new Error(`无法读取 ${file}`);
4313
- if (/version(Code|Name)\s*=?\s*libs\.versions\./.test(text)) throw new Error(`${file} 的版本字段是 version catalog 引用形态:请先改造为字面量(applift 只写标准形态)`);
4314
- let next = text;
4315
- const code = replaceGroup(/(versionCode(?!Suffix)\s*=?\s*)(\d+)/g, next, 2, String(w.versionCode));
4316
- next = code.text;
4317
- const name = replaceGroup(/(versionName(?!Suffix)\s*=?\s*)("[^"]*"|'[^']*'|[\w.+-]+)/g, next, 2, `"${w.versionName}"`);
4318
- next = name.text;
4319
- if (code.count === 0 && name.count === 0) throw new Error(`${file} 未找到 versionCode/versionName 字面量`);
4320
- return writeIfChanged(file, next);
4321
- }
4322
- function writePbxproj(file, w) {
4323
- const text = readText(file);
4324
- if (!text) throw new Error(`无法读取 ${file}`);
4325
- let next = text;
4326
- const name = replaceGroup(/(MARKETING_VERSION\s*=\s*)([^;]+)(;)/g, next, 2, w.versionName);
4327
- next = name.text;
4328
- const code = replaceGroup(/(CURRENT_PROJECT_VERSION\s*=\s*)([^;]+)(;)/g, next, 2, String(w.versionCode));
4329
- next = code.text;
4330
- if (name.count === 0 && code.count === 0) throw new Error(`${file} 未找到 MARKETING_VERSION/CURRENT_PROJECT_VERSION`);
4331
- return writeIfChanged(file, next);
4332
- }
4333
- function writeAppJson5(file, w) {
4334
- const text = readText(file);
4335
- if (!text) throw new Error(`无法读取 ${file}`);
4336
- let next = text;
4337
- const name = replaceGroup(/("versionName"\s*:\s*")([^"]+)(")/g, next, 2, w.versionName);
4338
- next = name.text;
4339
- const code = replaceGroup(/("versionCode"\s*:\s*)(\d+)/g, next, 2, String(w.versionCode));
4340
- next = code.text;
4341
- if (name.count === 0 && code.count === 0) throw new Error(`${file} 未找到 versionName/versionCode`);
4342
- return writeIfChanged(file, next);
4343
- }
4344
- function writePubspec(file, w) {
4345
- const text = readText(file);
4346
- if (!text) throw new Error(`无法读取 ${file}`);
4347
- if (!/^version:\s*\S+/m.test(text)) throw new Error(`${file} 未找到 version: 行`);
4348
- return writeIfChanged(file, text.replace(/^version:[^\n#]*?(\s*)(#.*)?$/m, (_m, gap, comment) => `version: ${w.versionName}+${w.versionCode}${comment ? gap ?? " " : ""}${comment ?? ""}`));
4349
- }
4350
- /** JSON 顶层 version 键改写(两遍:先定位最小缩进行,再只改该行——单遍回调在嵌套键
4351
- * 先出现时会把它一并改写,篡改依赖声明)。单行 JSON 回退首个匹配。 */
4352
- function writeJsonVersionKey(file, versionName) {
4353
- const text = readText(file);
4354
- if (!text) throw new Error(`无法读取 ${file}`);
4355
- const lines = text.split("\n");
4356
- const re = /^(\s*)"version"\s*:\s*"([^"]+)"(\s*,?\s*)$/;
4357
- let target = -1;
4358
- let bestIndent = Number.POSITIVE_INFINITY;
4359
- for (let i = 0; i < lines.length; i++) {
4360
- const m = re.exec(lines[i]);
4361
- if (!m) continue;
4362
- const ind = m[1].length;
4363
- if (ind < bestIndent) {
4364
- bestIndent = ind;
4365
- target = i;
4366
- }
4367
- }
4368
- let next;
4369
- if (target >= 0) {
4370
- const m = re.exec(lines[target]);
4371
- lines[target] = `${m[1]}"version": "${versionName}"${m[3]}`;
4372
- next = lines.join("\n");
4373
- } else {
4374
- let topVersion;
4375
- try {
4376
- const obj = JSON.parse(text);
4377
- if (typeof obj.version === "string") topVersion = obj.version;
4378
- } catch {}
4379
- const esc = (topVersion ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4380
- const re1 = topVersion ? new RegExp(`"version"\\s*:\\s*"${esc}"`) : /"version"\s*:\s*"([^"]+)"/;
4381
- if (!re1.test(text)) throw new Error(`${file} 未找到 "version" 键`);
4382
- next = text.replace(re1, `"version": "${versionName}"`);
4383
- }
4384
- return writeIfChanged(file, next);
4385
- }
4386
- function writeExpoAppJson(file, w) {
4387
- const text = readText(file);
4388
- if (!text) throw new Error(`无法读取 ${file}`);
4389
- const v = replaceGroup(/("version"\s*:\s*")([^"]+)(")/g, text, 2, w.versionName);
4390
- if (v.count === 0) throw new Error(`${file} 未找到 expo "version" 键`);
4391
- let next = v.text;
4392
- next = replaceGroup(/("versionCode"\s*:\s*)(\d+)/g, next, 2, String(w.versionCode)).text;
4393
- next = replaceGroup(/("buildNumber"\s*:\s*")([^"]+)(")/g, next, 2, String(w.versionCode)).text;
4394
- return writeIfChanged(file, next);
4395
- }
4396
- /**
4397
- * 写回各端版本(多端 versionName 同步、versionCode 各端独立)。
4398
- * 返回实际改动的文件列表(commit 范围)。problem 端跳过并在返回中列出(不阻断其他端)。
4399
- */
4400
- function writeProjectVersions(cwd, framework, writes) {
4401
- const loc = locateVersionFiles(cwd, framework);
4402
- const changed = [];
4403
- const skipped = [];
4404
- const plat = new Map(writes.map((w) => [w.platform, w]));
4405
- const apply = (file, fn, key) => {
4406
- if (!file) {
4407
- const w = plat.get(key);
4408
- if (w) skipped.push({
4409
- platform: w.platform,
4410
- reason: `未找到版本文件(${key})`
4411
- });
4412
- return;
4413
- }
4414
- try {
4415
- if (fn(file)) changed.push(file);
4416
- } catch (e) {
4417
- const code = e.code;
4418
- if (code === "EACCES" || code === "EPERM") throw e;
4419
- skipped.push({
4420
- platform: key,
4421
- reason: e.message
4422
- });
4423
- }
4424
- };
4425
- if (plat.has("android")) apply(loc.android, (f) => writeGradle(f, plat.get("android")), "android");
4426
- if (plat.has("ios")) apply(loc.ios, (f) => writePbxproj(f, plat.get("ios")), "ios");
4427
- if (plat.has("harmony")) apply(loc.harmony, (f) => writeAppJson5(f, plat.get("harmony")), "harmony");
4428
- if (plat.has("flutter")) apply(loc.pubspec, (f) => writePubspec(f, plat.get("flutter")), "flutter");
4429
- if (plat.has("js")) apply(loc.packageJson, (f) => writeJsonVersionKey(f, plat.get("js").versionName), "js");
4430
- if (plat.has("tauri")) apply(loc.tauri, (f) => writeJsonVersionKey(f, plat.get("tauri").versionName), "tauri");
4431
- if (plat.has("expo")) apply(loc.expoAppJson, (f) => writeExpoAppJson(f, plat.get("expo")), "expo");
4432
- return {
4433
- changed: [...new Set(changed)],
4434
- skipped
4435
- };
4436
- }
4437
- //#endregion
4438
- //#region src/version/git.ts
4439
- /**
4440
- * git 只读/写小助手(bump 编排与推断共用)。
4441
- * spawnSync + 超时;失败返回 ok:false 由调用方决定语义,不在此抛。
4442
- */
4443
- function git(cwd, args) {
4444
- const r = spawnSync("git", args, {
4445
- cwd,
4446
- encoding: "utf8",
4447
- timeout: 6e4
4448
- });
4449
- return {
4450
- ok: !r.error && r.status === 0,
4451
- stdout: (r.stdout ?? "").trim(),
4452
- stderr: (r.stderr ?? "").trim()
4453
- };
4454
- }
4455
- function isGitRepo(cwd) {
4456
- return git(cwd, ["rev-parse", "--is-inside-work-tree"]).stdout === "true";
4457
- }
4458
- /** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
4459
- function looseSemver(tag) {
4460
- const direct = semver.valid(tag);
4461
- if (direct) return direct;
4462
- const m = /(\d+\.\d+\.\d+(?:[-+][\w.-]+)?)/.exec(tag);
4463
- return m ? semver.valid(m[1]) ?? void 0 : void 0;
4464
- }
4465
- /**
4466
- * 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
4467
- * 无 tag / 非 git 仓库返回 undefined。
4468
- */
4469
- function latestSemverTag(cwd) {
4470
- const r = git(cwd, ["tag", "-l"]);
4471
- if (!r.ok) return void 0;
4472
- let best;
4473
- for (const raw of r.stdout.split("\n")) {
4474
- const t = raw.trim();
4475
- if (!t) continue;
4476
- const v = looseSemver(t);
4477
- if (!v) continue;
4478
- if (!best || semver.gt(v, best.v)) best = {
4479
- raw: t,
4480
- v
4481
- };
4482
- }
4483
- return best?.raw;
4484
- }
4485
- /** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
4486
- function commitsSince(cwd, ref) {
4487
- const r = git(cwd, [
4488
- "log",
4489
- ref ? `${ref}..HEAD` : "HEAD",
4490
- "--format=%B%x00",
4491
- "--no-merges"
4492
- ]);
4493
- if (!r.ok) return [];
4494
- return r.stdout.split("\0").map((m) => m.trim()).filter(Boolean);
4495
- }
4496
- /** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
4497
- * 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
4498
- function statusShort(cwd) {
4499
- const r = spawnSync("git", ["status", "--porcelain"], {
4500
- cwd,
4501
- encoding: "utf8",
4502
- timeout: 3e4
4503
- });
4504
- if (r.error || r.status !== 0) return [];
4505
- return (r.stdout ?? "").split("\n").map((l) => l.trimEnd()).filter(Boolean);
4506
- }
4507
- //#endregion
4508
4086
  //#region src/version/infer.ts
4509
4087
  /**
4510
4088
  * 版本推断与决定:锚点 tag..HEAD 的 conventional commits → bump level(纯函数),
@@ -5039,4 +4617,4 @@ function fromAapt(artifactPath) {
5039
4617
  }
5040
4618
  }
5041
4619
  //#endregion
5042
- 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, registry, renderChangelogSection, resolveListing, resolvePlugins, resolveProfile, resolveProfileFor, rnAdapter, runStage, runSubmit, saveCredentials, saveProjectProfileMap, setProjectProfile, sha256File, sortedQueryString, statusShort, storeVersionName, tauriAdapter, toChangelogEntries, tsMillis, tsSeconds, upsertChangelog, writeProjectVersions };
4620
+ 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 };