@applift/core 0.3.0 → 0.5.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
@@ -3,11 +3,14 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
5
  import { parse } from "smol-toml";
6
+ import { fileURLToPath } from "node:url";
6
7
  import { loadConfig } from "c12";
7
8
  import { createJiti } from "jiti";
8
9
  import crypto, { X509Certificate, constants, createHash, createHmac, createSign, publicEncrypt } from "node:crypto";
9
10
  import { Readable } from "node:stream";
10
11
  import { spawnSync } from "node:child_process";
12
+ import semver from "semver";
13
+ import { CommitParser } from "conventional-commits-parser";
11
14
  //#region src/registry.ts
12
15
  var DefaultChannelRegistry = class {
13
16
  channels = /* @__PURE__ */ new Map();
@@ -55,6 +58,8 @@ const StoresSchema = z.union([z.record(z.string(), z.union([StoreConfigSchema, z
55
58
  * - testflight: App Store Connect 上传(ipa 进 TestFlight)
56
59
  */
57
60
  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(),
58
63
  pgyer: z.object({
59
64
  platforms: z.array(z.enum([
60
65
  "android",
@@ -73,6 +78,16 @@ const BetaConfigSchema = z.object({
73
78
  }).optional(),
74
79
  testflight: z.object({ note: z.string().optional() }).optional()
75
80
  });
81
+ /** versionCode 策略:"increment"(默认 +1) | 公式(可限定平台) */
82
+ const VersionCodeConfigSchema = z.union([z.literal("increment"), z.object({
83
+ /** 白名单表达式:major/minor/patch 与 + - * ( ),如 "major*1000000+minor*1000+patch" */
84
+ formula: z.string(),
85
+ /** 仅限平台(缺省全部);实际写入 = max(公式值, 现值+1) */
86
+ platforms: z.array(z.string()).optional()
87
+ })]);
88
+ 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() });
76
91
  const ProjectConfigSchema = z.object({
77
92
  /** 项目显示名 */
78
93
  name: z.string().optional(),
@@ -109,6 +124,10 @@ const ProjectConfigSchema = z.object({
109
124
  macos: z.array(z.string()).optional(),
110
125
  linux: z.array(z.string()).optional()
111
126
  }).catchall(z.array(z.string()).optional()).optional(),
127
+ /** 版本管理(versionCode 策略等) */
128
+ version: VersionConfigSchema.optional(),
129
+ /** 正式发版自动 version 开关 */
130
+ release: ReleaseConfigSchema.optional(),
112
131
  /** 内测分发(beta 无平台参数时的多渠道并行配置) */
113
132
  beta: BetaConfigSchema.optional()
114
133
  });
@@ -211,11 +230,33 @@ function saveCredentials(data, filePath = CREDENTIALS_PATH) {
211
230
  * 项目配置装载:c12(社区主流)加载 applift.config.ts。
212
231
  * c12 负责:TS 配置文件执行、缺省值、多源合并 —— 非自研。
213
232
  */
233
+ /**
234
+ * 裸工程兼容:applift.config.ts 里的 `@applift/core` / `@applift/cli` 导入按目标工程解析会
235
+ * 找不到包(工程未安装依赖)——jiti 别名统一指到 CLI 进程自带的那份 core 入口,
236
+ * 使 `import { defineConfig } from "@applift/cli"`(或 @applift/core)在任何工程可用。
237
+ */
238
+ function appliftAlias() {
239
+ try {
240
+ const distEntry = fileURLToPath(import.meta.resolve("@applift/core"));
241
+ const coreEntry = fs.existsSync(distEntry) ? distEntry : path.join(path.dirname(path.dirname(distEntry)), "src", "index.ts");
242
+ if (!fs.existsSync(coreEntry)) return {};
243
+ return {
244
+ "@applift/core": coreEntry,
245
+ "@applift/cli": coreEntry
246
+ };
247
+ } catch {
248
+ return {};
249
+ }
250
+ }
214
251
  async function loadProjectConfig(cwd) {
215
252
  const cfgPath = findUp(cwd, "applift.config.ts");
216
253
  const cfgDir = cfgPath ? path.dirname(cfgPath) : cwd;
217
254
  const bunRuntime = globalThis.Bun;
218
- const jitiInstance = bunRuntime ? createJiti(import.meta.url, { transform: (opts) => ({ code: new bunRuntime.Transpiler({ loader: "ts" }).transformSync(opts.source) }) }) : void 0;
255
+ const alias = appliftAlias();
256
+ const jitiInstance = bunRuntime ? createJiti(import.meta.url, {
257
+ alias,
258
+ transform: (opts) => ({ code: new bunRuntime.Transpiler({ loader: "ts" }).transformSync(opts.source) })
259
+ }) : Object.keys(alias).length ? createJiti(import.meta.url, { alias }) : void 0;
219
260
  const result = await loadConfig({
220
261
  name: "applift",
221
262
  cwd: cfgDir,
@@ -2335,7 +2376,8 @@ var AgcHarmonyChannel = class {
2335
2376
  "204144660": "提交失败,包可能仍在解析(稍后重试或到控制台查看)",
2336
2377
  "1003": "client_id/client_secret 无效(AGC → 项目设置 → 常规 → API Client 重新获取)",
2337
2378
  "204144702": "签名/证书不匹配:检查签名证书指纹(SHA256)与 AGC 后台登记是否一致",
2338
- "204144677": "bundleName 与应用不匹配:确认 appId 对应的应用包名"
2379
+ "204144677": "bundleName 与应用不匹配:确认 appId 对应的应用包名",
2380
+ "204144692": "提审前置校验失败(按 msg 判断):privacyPolicy → 控制台「更多服务→协议服务」登记隐私政策;count of harmony test in audit → 在审测试版本超限,先停止/删除在审的测试版本再提"
2339
2381
  };
2340
2382
  async authHeaders(ctx) {
2341
2383
  const clientId = required$1(ctx, "AGC_CLIENT_ID");
@@ -2664,6 +2706,7 @@ var AgcHarmonyChannel = class {
2664
2706
  const groupName = ctx.target.testGroup?.trim();
2665
2707
  if (groupName) {
2666
2708
  const groupId = await this.resolveTestGroupId(ctx, appId, groupName, headers);
2709
+ const now = Date.now();
2667
2710
  await agcJson(`${BASE}/publish/v2/test/app/version?appId=${encodeURIComponent(appId)}`, {
2668
2711
  method: "PUT",
2669
2712
  headers: {
@@ -2673,10 +2716,14 @@ var AgcHarmonyChannel = class {
2673
2716
  body: JSON.stringify({
2674
2717
  versionId,
2675
2718
  pkgId,
2676
- openTestInfo: { testTaskInfo: { groupInfos: [{
2677
- groupId,
2678
- groupName
2679
- }] } }
2719
+ openTestInfo: {
2720
+ startTime: now,
2721
+ endTime: now + 2592e6,
2722
+ testTaskInfo: { groupInfos: [{
2723
+ groupId,
2724
+ groupName
2725
+ }] }
2726
+ }
2680
2727
  })
2681
2728
  }, "绑定测试群组");
2682
2729
  ctx.emit({
@@ -3638,7 +3685,12 @@ const rnAdapter = {
3638
3685
  artifactPatterns(platform) {
3639
3686
  if (platform === "android") return ["android/app/build/outputs/apk/release/*-release.apk"];
3640
3687
  if (platform === "ios") return ["build/ios/ipa/*.ipa"];
3641
- return ["harmony/entry/build/default/outputs/**/entry-*-signed.hap", "harmony/entry/build/default/outputs/**/entry-*.app"];
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
+ ];
3642
3694
  },
3643
3695
  buildCommand(platform) {
3644
3696
  if (platform === "android") return [
@@ -3750,7 +3802,10 @@ const nativeHarmonyAdapter = {
3750
3802
  return [
3751
3803
  "entry/build/default/outputs/**/entry-*-signed.hap",
3752
3804
  "entry/build/default/outputs/**/entry-*.app",
3753
- "harmony/entry/build/default/outputs/**/*-signed.hap"
3805
+ "harmony/entry/build/default/outputs/**/*-signed.hap",
3806
+ "build/outputs/**/*.app",
3807
+ "harmony/build/outputs/**/*.app",
3808
+ "ohos/build/outputs/**/*.app"
3754
3809
  ];
3755
3810
  },
3756
3811
  buildCommand() {
@@ -3870,7 +3925,7 @@ function findArtifacts(cwd, patterns) {
3870
3925
  return [...byPath].sort((a, b) => b[1] - a[1]).map(([p]) => p);
3871
3926
  }
3872
3927
  /** 文本读取(容错:不存在/不可读返回 null,绝不抛;detectBundleIds 各源共用) */
3873
- function readText(filePath) {
3928
+ function readText$1(filePath) {
3874
3929
  try {
3875
3930
  return fs.readFileSync(filePath, "utf8");
3876
3931
  } catch {
@@ -3895,7 +3950,7 @@ function gradleApplicationId(cwd) {
3895
3950
  path.join(cwd, "android", "app", "build.gradle.kts")
3896
3951
  ];
3897
3952
  for (const file of candidates) {
3898
- const text = readText(file);
3953
+ const text = readText$1(file);
3899
3954
  if (!text) continue;
3900
3955
  const m = /(?:^|\s)applicationId\s*[=:]?\s*["']([^"'\s]+)["']/.exec(text);
3901
3956
  if (m?.[1]) return m[1];
@@ -3903,7 +3958,7 @@ function gradleApplicationId(cwd) {
3903
3958
  }
3904
3959
  /** 鸿蒙 AppScope/app.json5 的 bundleName(json5 允许注释/尾逗号,正则取值而非 JSON 解析) */
3905
3960
  function appJson5BundleName(file) {
3906
- const text = readText(file);
3961
+ const text = readText$1(file);
3907
3962
  return (text && /"bundleName"\s*:\s*"([^"]+)"/.exec(text))?.[1];
3908
3963
  }
3909
3964
  /** 两层子目录内找首个 *.xcodeproj:pbxproj 第一个非变量(含 $ 的 $(...) 与 ${...})的 PRODUCT_BUNDLE_IDENTIFIER + 工程名(Pods 已被 subdirs 排除) */
@@ -3917,7 +3972,7 @@ function findPbxproj(cwd) {
3917
3972
  }
3918
3973
  const projects = entries.filter((e) => e.isDirectory() && e.name.endsWith(".xcodeproj")).map((e) => e.name).sort();
3919
3974
  for (const name of projects) {
3920
- const pbx = readText(path.join(dir, name, "project.pbxproj"));
3975
+ const pbx = readText$1(path.join(dir, name, "project.pbxproj"));
3921
3976
  if (!pbx) continue;
3922
3977
  for (const m of pbx.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;]+);/g)) {
3923
3978
  const value = m[1].trim().replace(/^["']|["']$/g, "");
@@ -4025,7 +4080,890 @@ function humanizeError(hints, message) {
4025
4080
  return message;
4026
4081
  }
4027
4082
  //#endregion
4028
- //#region src/version.ts
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
+ //#region src/version/infer.ts
4509
+ /**
4510
+ * 版本推断与决定:锚点 tag..HEAD 的 conventional commits → bump level(纯函数),
4511
+ * 版本演进全用 semver(inc / prerelease 剥离),不自写版本数学。
4512
+ * 纯函数输入 git log 文本,便于契约测试;git 交互在 git.ts。
4513
+ */
4514
+ /** conventional commits → bump level:BREAKING > feat(minor) > fix(patch);无可识别提交 → patch */
4515
+ function inferBumpLevel(messages) {
4516
+ let breaking = 0;
4517
+ let feat = 0;
4518
+ let fix = 0;
4519
+ const parser = new CommitParser();
4520
+ for (const raw of messages) {
4521
+ let c = null;
4522
+ try {
4523
+ c = parser.parse(raw);
4524
+ } catch {
4525
+ continue;
4526
+ }
4527
+ if (!c) continue;
4528
+ const type = (c.type ?? "").toLowerCase();
4529
+ if ((c.notes ?? []).some((n) => /breaking/i.test(n.title ?? ""))) breaking++;
4530
+ else if (type === "feat") feat++;
4531
+ else if (type === "fix") fix++;
4532
+ }
4533
+ const total = messages.length;
4534
+ let level;
4535
+ let reason;
4536
+ if (breaking > 0) {
4537
+ level = "major";
4538
+ reason = `git 推断:${breaking} 个 BREAKING CHANGE → major`;
4539
+ } else if (feat > 0) {
4540
+ level = "minor";
4541
+ reason = `git 推断:feat ${feat} 个(fix ${fix} 个)→ minor`;
4542
+ } else if (fix > 0) {
4543
+ level = "patch";
4544
+ reason = `git 推断:fix ${fix} 个,无 feat/BREAKING → patch`;
4545
+ } else {
4546
+ level = "patch";
4547
+ reason = total === 0 ? "锚点 tag..HEAD 无提交 → patch 兜底" : `git 推断:${total} 个提交无 feat/fix/BREAKING(chore/docs 等)→ patch 兜底`;
4548
+ }
4549
+ return {
4550
+ level,
4551
+ reason,
4552
+ total,
4553
+ breaking,
4554
+ feat,
4555
+ fix
4556
+ };
4557
+ }
4558
+ /**
4559
+ * 从当前版本决定下一版本(纯函数)。
4560
+ * - 显式 level:semver.inc;当前是 prerelease 且不带 --pre → 剥离转正式(4.0.0-beta.3 + patch → 4.0.0)
4561
+ * - 无 level:用 infer 结果
4562
+ * - prerelease level:当前有 pre → 同基线递进(beta.3 → beta.4);无 pre → 报错(应使用 --pre 进入)
4563
+ * - --pre:从 level 出发挂 prerelease(4.0.0 + minor + beta → 4.1.0-beta.1)
4564
+ * current 非 semver 时抛错(调用方要求显式指定或先手动规范化)。
4565
+ */
4566
+ function decideNextVersion(current, opts, infer) {
4567
+ const base = semver.valid(current);
4568
+ if (!base) throw new Error(`当前版本 "${current}" 不是合法 semver(如前导零 3.0.08):显式指定目标版本(bump 4.0.0)或先手动规范化`);
4569
+ const level = opts.level;
4570
+ if (level && ![
4571
+ "major",
4572
+ "minor",
4573
+ "patch",
4574
+ "prerelease"
4575
+ ].includes(level)) {
4576
+ const explicit = semver.valid(level);
4577
+ if (!explicit) throw new Error(`无法识别的版本/级别 "${level}"`);
4578
+ if (!semver.gt(explicit, base)) throw new Error(`目标版本 ${explicit} 必须大于当前 ${base}`);
4579
+ return {
4580
+ next: explicit,
4581
+ reason: "显式指定"
4582
+ };
4583
+ }
4584
+ const hasPre = semver.prerelease(base) !== null;
4585
+ const preId = opts.pre ?? semver.prerelease(base)?.[0];
4586
+ if (level === "prerelease") {
4587
+ if (hasPre) {
4588
+ const next = preId ? semver.inc(base, "prerelease", preId) : semver.inc(base, "prerelease");
4589
+ if (!next) throw new Error(`prerelease 递进失败:${base}`);
4590
+ return {
4591
+ next,
4592
+ reason: `prerelease 递进(${base} → ${next})`
4593
+ };
4594
+ }
4595
+ throw new Error(`当前 ${base} 不是 prerelease:用 --pre beta(进入 beta.1)或显式版本(bump 4.1.0-beta.1)`);
4596
+ }
4597
+ const effectiveLevel = level ?? infer?.level ?? "patch";
4598
+ const reasonPrefix = level ? `显式 ${level}` : infer ? infer.reason : "无依据 → patch 兜底";
4599
+ if (opts.pre) {
4600
+ const next = semver.inc(base, `pre${effectiveLevel}`, opts.pre);
4601
+ if (!next) throw new Error(`pre${effectiveLevel} 计算失败:${base}`);
4602
+ return {
4603
+ next,
4604
+ reason: `${reasonPrefix} + prerelease(${opts.pre})`
4605
+ };
4606
+ }
4607
+ const next = semver.inc(base, effectiveLevel);
4608
+ if (!next) throw new Error(`${effectiveLevel} 递进失败:${base}`);
4609
+ return {
4610
+ next,
4611
+ reason: reasonPrefix
4612
+ };
4613
+ }
4614
+ /** 工程文件写入用的三段数字 versionName(prerelease 只留在 tag,不进商店) */
4615
+ function storeVersionName(full) {
4616
+ const v = new semver.SemVer(full);
4617
+ return `${v.major}.${v.minor}.${v.patch}`;
4618
+ }
4619
+ //#endregion
4620
+ //#region src/version/changelog.ts
4621
+ /**
4622
+ * CHANGELOG 生成:conventional commits 归类渲染(conventional-changelog 风格),
4623
+ * 倒序插入项目根 CHANGELOG.md(同版本 section 幂等替换)。
4624
+ * 渲染自研(conventional-changelog-core 依赖链对 CLI 太重),解析复用 parser。
4625
+ */
4626
+ /** raw commit messages → 结构化条目(feat/fix/BREAKING 之外的 type 也保留,渲染时归类) */
4627
+ function toChangelogEntries(messages) {
4628
+ const parser = new CommitParser();
4629
+ const out = [];
4630
+ for (const raw of messages) {
4631
+ let c = null;
4632
+ try {
4633
+ c = parser.parse(raw);
4634
+ } catch {
4635
+ continue;
4636
+ }
4637
+ if (!c || !c.header) continue;
4638
+ const headerType = /^(\w+)(\([^)]*\))?!?:\s/.exec(c.header)?.[1]?.toLowerCase();
4639
+ const type = (c.type ?? headerType)?.toLowerCase();
4640
+ if (!type) continue;
4641
+ const breaking = (c.notes ?? []).some((n) => /breaking/i.test(n.title ?? ""));
4642
+ out.push({
4643
+ type,
4644
+ scope: c.scope || void 0,
4645
+ subject: (c.subject || c.header || "").trim(),
4646
+ breaking,
4647
+ hash: c.hash || void 0
4648
+ });
4649
+ }
4650
+ return out;
4651
+ }
4652
+ function line(e) {
4653
+ const scope = e.scope ? `(${e.scope})` : "";
4654
+ const hash = e.hash ? ` (${e.hash.slice(0, 7)})` : "";
4655
+ return `- ${e.type}${scope}: ${e.subject}${hash}`;
4656
+ }
4657
+ /** 渲染单个版本段落(## [4.1.0] - 2026-10-17) */
4658
+ function renderChangelogSection(version, date, entries) {
4659
+ const body = [
4660
+ ["⚠ BREAKING CHANGES", entries.filter((e) => e.breaking)],
4661
+ ["Features", entries.filter((e) => !e.breaking && e.type === "feat")],
4662
+ ["Bug Fixes", entries.filter((e) => !e.breaking && e.type === "fix")],
4663
+ ["Others", entries.filter((e) => !e.breaking && e.type !== "feat" && e.type !== "fix")]
4664
+ ].filter(([, list]) => list.length > 0).map(([title, list]) => `### ${title}\n\n${list.map(line).join("\n")}`).join("\n\n");
4665
+ const heading = `## [${version}] - ${date}`;
4666
+ return body ? `${heading}\n\n${body}` : heading;
4667
+ }
4668
+ const HEADER_RE = /^#\s*changelog\b/i;
4669
+ /**
4670
+ * 插入/替换 CHANGELOG.md 的版本段落(幂等):
4671
+ * - 已有 `## [version]` → 整段替换为最新渲染
4672
+ * - 否则插到首个 # Changelog 标题之后;无标题/无文件 → 新建带标题
4673
+ * 返回写入路径;entries 为空也写入 heading(bump 闭环保持 tag ↔ changelog 对齐)。
4674
+ */
4675
+ function upsertChangelog(cwd, version, section) {
4676
+ const filePath = `${cwd}/CHANGELOG.md`;
4677
+ let content = "";
4678
+ if (fs.existsSync(filePath)) content = fs.readFileSync(filePath, "utf8");
4679
+ const lines = content.split("\n");
4680
+ const start = lines.findIndex((l) => l.startsWith(`## [${version}]`));
4681
+ if (start >= 0) {
4682
+ let end = lines.length;
4683
+ for (let i = start + 1; i < lines.length; i++) if (lines[i].startsWith("## ")) {
4684
+ end = i;
4685
+ break;
4686
+ }
4687
+ content = [
4688
+ ...lines.slice(0, start),
4689
+ ...section.split("\n"),
4690
+ "",
4691
+ ...lines.slice(end)
4692
+ ].join("\n");
4693
+ } else if (HEADER_RE.test(content)) content = content.replace(/(^#\s*changelog\b[^\n]*\n+)/im, `$1\n${section}\n`);
4694
+ else content = `# Changelog\n\n${section}\n${content ? `\n${content}` : ""}`;
4695
+ fs.writeFileSync(filePath, content.endsWith("\n") ? content : `${content}\n`, "utf8");
4696
+ return filePath;
4697
+ }
4698
+ //#endregion
4699
+ //#region src/version/version-code.ts
4700
+ /**
4701
+ * versionCode 计算:默认递增 +1;可配公式(如 major*1000000+minor*1000+patch)。
4702
+ * 实际写入 = max(公式值, 现值+1)——公式在同 versionName 重发时算出同值,
4703
+ * +1 兜底保证单调(AGC 同版本不能重复提交 / Google Play 单调 / App Store 单调)。
4704
+ */
4705
+ /** 公式白名单求值(仅支持 + - * ( ) 数字 与 major/minor/patch 变量;不 eval) */
4706
+ function evalVersionCodeFormula(formula, v) {
4707
+ const tokens = [];
4708
+ let i = 0;
4709
+ const src = formula.replace(/\s+/g, "");
4710
+ while (i < src.length) {
4711
+ const c = src.charAt(i);
4712
+ if (c === "+" || c === "-" || c === "*" || c === "(" || c === ")") {
4713
+ tokens.push({ op: c });
4714
+ i++;
4715
+ } else if (/\d/.test(c)) {
4716
+ let j = i;
4717
+ while (j < src.length && /\d/.test(src.charAt(j))) j++;
4718
+ tokens.push({ n: Number(src.slice(i, j)) });
4719
+ i = j;
4720
+ } else {
4721
+ const word = /^(major|minor|patch)/.exec(src.slice(i));
4722
+ if (!word) return void 0;
4723
+ const name = word[1];
4724
+ tokens.push({ n: v[name] });
4725
+ i += name.length;
4726
+ }
4727
+ }
4728
+ if (tokens.length === 0) return void 0;
4729
+ let pos = 0;
4730
+ const peek = () => tokens[pos];
4731
+ function factor() {
4732
+ const t = peek();
4733
+ if (!t) return void 0;
4734
+ if ("n" in t) {
4735
+ pos++;
4736
+ return t.n;
4737
+ }
4738
+ if (t.op === "(") {
4739
+ pos++;
4740
+ const inner = expr();
4741
+ const close = peek();
4742
+ if (inner === void 0 || !close || !("op" in close) || close.op !== ")") return void 0;
4743
+ pos++;
4744
+ return inner;
4745
+ }
4746
+ if (t.op === "-") {
4747
+ pos++;
4748
+ const f = factor();
4749
+ return f === void 0 ? void 0 : -f;
4750
+ }
4751
+ }
4752
+ function term() {
4753
+ let left = factor();
4754
+ for (;;) {
4755
+ const t = peek();
4756
+ if (left === void 0 || !t || !("op" in t) || t.op !== "*") return left;
4757
+ pos++;
4758
+ const right = factor();
4759
+ if (right === void 0) return void 0;
4760
+ left = left * right;
4761
+ }
4762
+ }
4763
+ function expr() {
4764
+ let left = term();
4765
+ for (;;) {
4766
+ const t = peek();
4767
+ if (left === void 0 || !t || !("op" in t) || t.op !== "+" && t.op !== "-") return left;
4768
+ pos++;
4769
+ const right = term();
4770
+ if (right === void 0) return void 0;
4771
+ left = t.op === "+" ? left + right : left - right;
4772
+ }
4773
+ }
4774
+ const result = expr();
4775
+ if (result === void 0 || pos !== tokens.length) return void 0;
4776
+ return Number.isFinite(result) && Number.isInteger(result) && result > 0 ? result : void 0;
4777
+ }
4778
+ /**
4779
+ * 下一 versionCode:max(公式值, 现值+1)。
4780
+ * - 无现值(首接工程无该字段):公式值,否则 1
4781
+ * - 有现值:至少现值+1(吞 3052/4000000 等任意历史值,首次 bump 即归一)
4782
+ */
4783
+ function nextVersionCode(current, formulaValue) {
4784
+ if (current === void 0) return formulaValue ?? 1;
4785
+ return Math.max(formulaValue ?? Number.NEGATIVE_INFINITY, current + 1);
4786
+ }
4787
+ //#endregion
4788
+ //#region src/version/bump.ts
4789
+ /** 多端 versionName 的基线:取 semver 合法值中最大;全部非法 → null(需显式指定) */
4790
+ function baselineVersion(currents) {
4791
+ let best = null;
4792
+ for (const c of currents) {
4793
+ const v = semver.valid(c.versionName ?? "");
4794
+ if (v && (!best || semver.gt(v, best))) best = v;
4795
+ }
4796
+ return best;
4797
+ }
4798
+ /** 并发 bump 互斥锁(O_EXCL 原子创建;>10 分钟视为陈旧自动接管) */
4799
+ function acquireBumpLock(cwd) {
4800
+ const lockPath = path.join(cwd, ".git", "applift-bump.lock");
4801
+ try {
4802
+ const fd = fs.openSync(lockPath, "wx");
4803
+ fs.closeSync(fd);
4804
+ } catch (e) {
4805
+ if (e.code !== "EEXIST") throw e;
4806
+ try {
4807
+ if (Date.now() - fs.statSync(lockPath).mtimeMs > 6e5) fs.writeFileSync(lockPath, String(process.pid));
4808
+ else return /* @__PURE__ */ new Error(`另一 bump 进程正在运行(${lockPath})——并发 bump 会产出内容错位的提交,稍后重试或确认无并发后删除锁文件`);
4809
+ } catch {
4810
+ return /* @__PURE__ */ new Error(`无法获取 bump 锁(${lockPath})`);
4811
+ }
4812
+ }
4813
+ return () => {
4814
+ try {
4815
+ fs.unlinkSync(lockPath);
4816
+ } catch {}
4817
+ };
4818
+ }
4819
+ /** HEAD 提交是否是我们打的 release commit(失败重跑识别用) */
4820
+ function lastCommitIsRelease(cwd) {
4821
+ return git(cwd, [
4822
+ "log",
4823
+ "-1",
4824
+ "--format=%s"
4825
+ ]).stdout.startsWith("chore(release): ");
4826
+ }
4827
+ function planBump(opts) {
4828
+ let anchorTag;
4829
+ let commits = [];
4830
+ let infer;
4831
+ if (isGitRepo(opts.cwd)) {
4832
+ anchorTag = latestSemverTag(opts.cwd);
4833
+ if (anchorTag) {
4834
+ commits = commitsSince(opts.cwd, anchorTag);
4835
+ if (commits.length > 0) infer = inferBumpLevel(commits);
4836
+ }
4837
+ }
4838
+ const currents = readProjectVersions(opts.cwd, opts.framework).filter((c) => (c.versionName || c.versionCode) && !opts.platforms || opts.platforms?.includes(c.platform));
4839
+ const anchorVersion = anchorTag ? looseSemver(anchorTag) ?? void 0 : void 0;
4840
+ const base = (anchorVersion ? semver.prerelease(anchorVersion) !== null : false) ? anchorVersion : baselineVersion([...currents, ...anchorVersion ? [{
4841
+ platform: "__tag__",
4842
+ versionName: anchorVersion
4843
+ }] : []]);
4844
+ if (!base) {
4845
+ if (!opts.level || !semver.valid(opts.level)) throw new Error(`各端均无可解析的 semver versionName(${currents.map((c) => `${c.platform}=${c.versionName ?? "?"}`).join(" ")}):显式指定目标版本,如 applift version bump 4.0.0`);
4846
+ }
4847
+ const decided = decideNextVersion(base ?? "0.0.0", {
4848
+ level: opts.level,
4849
+ pre: opts.pre
4850
+ }, infer);
4851
+ if (!anchorTag && !opts.level) decided.reason = "无发版锚点 tag(首次 bump)→ patch 兜底;打第一个 tag 后激活 git 推断";
4852
+ const suspectRerun = !!anchorTag && lastCommitIsRelease(opts.cwd) && commits.every((c) => c.startsWith("chore(release): "));
4853
+ if (suspectRerun) decided.reason += ";⚠ 锚点后无新业务提交且 HEAD 已是 release commit——疑似失败重跑,建议 --no-bump 复用已 bump 版本";
4854
+ const formulaCfg = opts.versionCode && opts.versionCode !== "increment" ? opts.versionCode : void 0;
4855
+ if (opts.platforms && opts.platforms.length > 0) {
4856
+ const known = new Set(currents.map((c) => c.platform));
4857
+ const unknown = opts.platforms.filter((p) => !known.has(p));
4858
+ if (unknown.length > 0) throw new Error(`未知平台: ${unknown.join(", ")}(可用: ${[...known].join("/") || "无"})`);
4859
+ }
4860
+ const writes = [];
4861
+ let formulaNote = "递增 +1";
4862
+ let formulaApplied = false;
4863
+ const resetCodes = [];
4864
+ for (const c of currents) {
4865
+ if (opts.platforms && !opts.platforms.includes(c.platform)) continue;
4866
+ const cur = c.versionCode !== void 0 && /^\d+$/.test(c.versionCode) ? Number(c.versionCode) : void 0;
4867
+ if (c.versionCode !== void 0 && cur === void 0) resetCodes.push(`${c.platform}=${c.versionCode}`);
4868
+ let formulaValue;
4869
+ if (formulaCfg && (!formulaCfg.platforms || formulaCfg.platforms.includes(c.platform))) {
4870
+ const v = new semver.SemVer(decided.next);
4871
+ formulaValue = evalVersionCodeFormula(formulaCfg.formula, {
4872
+ major: v.major,
4873
+ minor: v.minor,
4874
+ patch: v.patch
4875
+ });
4876
+ if (formulaValue === void 0) throw new Error(`versionCode 公式无法求值:${formulaCfg.formula}(仅支持 + - * ( ) 与 major/minor/patch)`);
4877
+ formulaNote = `max(公式 ${formulaCfg.formula}, 现值+1)`;
4878
+ formulaApplied = true;
4879
+ }
4880
+ const code = nextVersionCode(cur, formulaValue);
4881
+ if (code > 21e8) throw new Error(`versionCode ${code} 超过商店上限 2100000000(${c.platform}):检查公式或手动规范化`);
4882
+ writes.push({
4883
+ platform: c.platform,
4884
+ versionName: storeVersionName(decided.next),
4885
+ versionCode: code
4886
+ });
4887
+ }
4888
+ if (formulaCfg && !formulaApplied && writes.length > 0) formulaNote += `;⚠ 公式 ${formulaCfg.formula} 未命中任何端${formulaCfg.platforms ? `(platforms: ${formulaCfg.platforms.join("/")},可用端: ${writes.map((w) => w.platform).join("/")})` : ""}——全部端走 +1 兜底`;
4889
+ if (resetCodes.length > 0) formulaNote += `;⚠ 非数字 versionCode 已从 1 重新起号(${resetCodes.join(", ")})——商店单调性可能拒绝,建议配置 version.versionCode 公式或手动规范化`;
4890
+ return {
4891
+ anchorTag,
4892
+ currentVersions: currents,
4893
+ writes,
4894
+ base: base ?? decided.next,
4895
+ nextVersion: decided.next,
4896
+ storeName: storeVersionName(decided.next),
4897
+ reason: decided.reason,
4898
+ infer,
4899
+ commits,
4900
+ suspectRerun,
4901
+ versionCodeNote: formulaNote
4902
+ };
4903
+ }
4904
+ function applyBump(cwd, plan, framework, opts) {
4905
+ const lock = isGitRepo(cwd) ? acquireBumpLock(cwd) : void 0;
4906
+ if (lock instanceof Error) throw lock;
4907
+ try {
4908
+ const { changed, skipped } = writeProjectVersions(cwd, framework, plan.writes);
4909
+ if (plan.writes.length === 0) throw new Error(`没有可写入的端(读取期被丢弃或 --platform 过滤为空)——不生成 release commit/tag;catalog 引用形态需先改造为字面量`);
4910
+ if (plan.writes.length > 0 && changed.length === 0 && skipped.length > 0) throw new Error(`所有端的版本写入均被跳过:${skipped.map((s) => `${s.platform}(${s.reason})`).join(";")}——不生成 release commit/tag`);
4911
+ if (plan.writes.length > 0 && changed.length === 0 && skipped.length === 0) {}
4912
+ const warnings = [];
4913
+ if (opts.commit && isGitRepo(cwd)) {
4914
+ const staging = new Set([...changed, `${cwd}/CHANGELOG.md`].map((f) => path.relative(cwd, f) || f));
4915
+ const dirty = statusShort(cwd).filter((line) => {
4916
+ const f = line.slice(3).split(" -> ").pop().trim().replace(/\/$/, "");
4917
+ return ![...staging].some((s) => s === f || s.startsWith(`${f}/`));
4918
+ });
4919
+ if (dirty.length > 0) throw new Error(`工作区有计划外未提交变更,拒绝代 commit(防混入 release 提交):\n${dirty.join("\n")}\n先提交它们或移出工作区(.gitignore),或用 --no-commit`);
4920
+ }
4921
+ const entries = toChangelogEntries(plan.commits);
4922
+ const changelogFile = upsertChangelog(cwd, plan.nextVersion, renderChangelogSection(plan.nextVersion, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries));
4923
+ const changedFiles = [.../* @__PURE__ */ new Set([...changed, changelogFile])];
4924
+ let committed = false;
4925
+ let tagged = false;
4926
+ if (opts.commit || opts.tag) {
4927
+ if (!isGitRepo(cwd)) warnings.push("非 git 仓库:跳过 commit/tag");
4928
+ else {
4929
+ const files = changedFiles.map((f) => path.relative(cwd, f) || f);
4930
+ if (opts.commit) {
4931
+ const add = git(cwd, [
4932
+ "add",
4933
+ "--",
4934
+ ...files
4935
+ ]);
4936
+ const commit = add.ok ? git(cwd, [
4937
+ "commit",
4938
+ "-m",
4939
+ `chore(release): ${plan.nextVersion}`,
4940
+ "--",
4941
+ ...files
4942
+ ]) : add;
4943
+ if (commit.ok) committed = true;
4944
+ else warnings.push(`git commit 失败:${commit.stderr || commit.stdout || "未知错误"}`);
4945
+ }
4946
+ if (opts.tag && (!opts.commit || committed)) {
4947
+ const tag = git(cwd, ["tag", `v${plan.nextVersion}`]);
4948
+ if (tag.ok) tagged = true;
4949
+ else warnings.push(`git tag 失败:${tag.stderr || tag.stdout || "未知错误"}`);
4950
+ }
4951
+ }
4952
+ }
4953
+ return {
4954
+ changedFiles,
4955
+ skipped,
4956
+ changelogFile,
4957
+ committed,
4958
+ tagged,
4959
+ warnings
4960
+ };
4961
+ } finally {
4962
+ lock?.();
4963
+ }
4964
+ }
4965
+ //#endregion
4966
+ //#region src/version/artifact.ts
4029
4967
  /**
4030
4968
  * 产物版本读取(只读不写——版本号变更属开发提交,发版工具只负责读版本防回退)。
4031
4969
  * .hap/.app:pack.info 是纯 JSON,零依赖直接读;
@@ -4057,12 +4995,17 @@ function fromPackInfo(artifactPath) {
4057
4995
  if (v) return v;
4058
4996
  } catch {}
4059
4997
  }
4998
+ /** 真实鸿蒙 pack.info schema(qy-driver 实测样本):summary.app.version.{name,code,build};
4999
+ * 顶层 versionName/versionCode 为历史兼容形态。 */
4060
5000
  function parsePackInfo(text, source) {
4061
5001
  try {
4062
5002
  const info = JSON.parse(text);
4063
- if (info.versionName || info.versionCode !== void 0) return {
4064
- versionName: info.versionName,
4065
- versionCode: String(info.versionCode ?? ""),
5003
+ const nested = info.summary?.app?.version;
5004
+ const versionName = nested?.name ?? info.versionName;
5005
+ const versionCode = nested?.code ?? info.versionCode;
5006
+ if (versionName || versionCode !== void 0) return {
5007
+ versionName,
5008
+ versionCode: String(versionCode ?? ""),
4066
5009
  source
4067
5010
  };
4068
5011
  } catch {}
@@ -4096,4 +5039,4 @@ function fromAapt(artifactPath) {
4096
5039
  }
4097
5040
  }
4098
5041
  //#endregion
4099
- 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, VivoChannel, WAIT_BEFORE_SUBMIT_MS, XiaomiChannel, YybChannel, appendEntry, assertListingFiles, defineConfig, detectBundleIds, detectFramework, electronAdapter, findArtifacts, findUp, flutterAdapter, formPost, getAgcToken, getHarmonyAppId, hmacSign, humanizeError, loadCredentials, loadCredentialsLayered, loadGlobalConfig, loadListingFile, loadProjectConfig, loadProjectProfileMap, md5File, md5String, nativeAndroidAdapter, nativeHarmonyAdapter, nativeIosAdapter, projectCredentialsPath, readArtifactVersion, readEntries, registry, resolveListing, resolvePlugins, resolveProfile, resolveProfileFor, rnAdapter, runStage, runSubmit, saveCredentials, saveProjectProfileMap, setProjectProfile, sha256File, sortedQueryString, tauriAdapter, tsMillis, tsSeconds };
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 };