@applift/core 0.10.0 → 0.12.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.
@@ -50,11 +50,93 @@ function latestSemverTag(cwd) {
50
50
  }
51
51
  return best?.raw;
52
52
  }
53
+ /**
54
+ * 最新 `chore(release):` 声明提交(applyBump 的产物;squash-merge 会丢标记,见 AGENTS.md 已知边界)。
55
+ * 只认 **subject**:git --grep 的 `^` 是行锚定、作用于整条 message 含 body——release-note 风格的
56
+ * body 行("chore(release): 4.1.0 included below")会劫持锚点,把更早的原生改动挤出判定窗口
57
+ * (对抗审查 P1 实证)。故取最近若干条提交的 subject 在本侧匹配。
58
+ */
59
+ function latestBumpCommitCandidate(cwd) {
60
+ const r = git(cwd, [
61
+ "log",
62
+ "-n",
63
+ "200",
64
+ "--format=%H%x00%ct%x00%s"
65
+ ]);
66
+ if (!r.ok || !r.stdout) return void 0;
67
+ for (const line of r.stdout.split("\n")) {
68
+ const [sha, time, subject] = line.split("\0");
69
+ if (!sha || !time || !subject || !/^\d+$/.test(time)) continue;
70
+ if (!subject.startsWith("chore(release): ")) continue;
71
+ return {
72
+ ref: sha,
73
+ kind: "bump",
74
+ version: looseSemver(subject),
75
+ sha,
76
+ time: Number(time)
77
+ };
78
+ }
79
+ }
80
+ /**
81
+ * 版本声明锚点:最新 semver tag 的提交 vs 最新 `chore(release):` 提交,按 committer time 取新者。
82
+ * - tag 新(已交付)→ kind=tag(判定 diff 从交付点起算)
83
+ * - bump commit 新(声明后未交付)→ kind=bump(bump 写入的版本文件不计入判定 diff,
84
+ * 否则去 tag 化后每次 beta 都会把自己写的版本文件误判为原生改动、连环烧版本号)
85
+ * - 同一提交(旧形态 bump+tag 一步完成)→ 优先 tag(交付态是更强事实)
86
+ * - committer time 同秒平局 → 拓扑序决胜(bump 是 tag 的后代则声明在交付后 → bump 胜)——
87
+ * 秒级精度不足时不依赖时序运气
88
+ * 无 tag 且无 bump commit / 非 git 仓库 → null。
89
+ */
90
+ function latestVersionAnchor(cwd) {
91
+ if (!isGitRepo(cwd)) return null;
92
+ let tagC;
93
+ const tag = latestSemverTag(cwd);
94
+ if (tag) {
95
+ const r = git(cwd, [
96
+ "log",
97
+ "-1",
98
+ "--format=%H%x00%ct",
99
+ tag
100
+ ]);
101
+ const [sha, time] = r.ok ? r.stdout.split("\0") : [];
102
+ if (sha && time && /^\d+$/.test(time)) tagC = {
103
+ ref: tag,
104
+ kind: "tag",
105
+ version: looseSemver(tag),
106
+ sha,
107
+ time: Number(time)
108
+ };
109
+ }
110
+ const bumpC = latestBumpCommitCandidate(cwd);
111
+ let winner;
112
+ if (!tagC) winner = bumpC;
113
+ else if (!bumpC) winner = tagC;
114
+ else if (bumpC.sha === tagC.sha) winner = tagC;
115
+ else if (bumpC.time !== tagC.time) winner = bumpC.time > tagC.time ? bumpC : tagC;
116
+ else winner = git(cwd, [
117
+ "merge-base",
118
+ "--is-ancestor",
119
+ tagC.sha,
120
+ bumpC.sha
121
+ ]).ok ? bumpC : tagC;
122
+ if (!winner) return null;
123
+ return winner.kind === "tag" ? {
124
+ ref: winner.ref,
125
+ kind: "tag",
126
+ version: winner.version,
127
+ label: `v${winner.version}(已发布)`
128
+ } : {
129
+ ref: winner.ref,
130
+ kind: "bump",
131
+ version: winner.version,
132
+ label: winner.version ? `${winner.version}(已声明未发布)` : `bump ${winner.sha.slice(0, 8)}(已声明未发布)`
133
+ };
134
+ }
53
135
  /** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
54
136
  function commitsSince(cwd, ref) {
55
137
  const r = git(cwd, [
56
138
  "log",
57
- ref ? `${ref}..HEAD` : "HEAD",
139
+ `${ref}..HEAD`,
58
140
  "--format=%B%x00",
59
141
  "--no-merges"
60
142
  ]);
@@ -73,8 +155,9 @@ function statusShort(cwd) {
73
155
  return (r.stdout ?? "").split("\n").map((l) => l.trimEnd()).filter(Boolean);
74
156
  }
75
157
  /**
76
- * release 分发成功后补 tag(tag = 实际已发布的正式版本;beta 不打 tag,追溯走 history)。
77
- * - tag 已存在(多端发布,前一端已打)→ 跳过
158
+ * release 分发成功后补 tag(tag = 交付封板标记;bump 只声明不打 tag,见 AGENTS.md 版本语义)。
159
+ * - tag 已存在且指向 HEAD(多端发布,前一端已打)→ 跳过
160
+ * - tag 已存在但指向不同 commit → conflict 告警(历史提前 tag / 驳回重提未移动 / 多端错峰发布)
78
161
  * - dryRun / 无版本 / 非 git 仓库 → 不动、不炸(tag 失败不阻断发布主流程)
79
162
  */
80
163
  function tagReleaseVersion(cwd, version, opts = {}) {
@@ -95,10 +178,19 @@ function tagReleaseVersion(cwd, version, opts = {}) {
95
178
  ], {
96
179
  cwd,
97
180
  encoding: "utf8"
98
- }).status === 0) return {
99
- tagged: false,
100
- reason: `tag ${tag} 已存在(前一端已发布)`
101
- };
181
+ }).status === 0) {
182
+ const tagCommit = git(cwd, ["rev-parse", `refs/tags/${tag}^{commit}`]).stdout;
183
+ const head = git(cwd, ["rev-parse", "HEAD"]).stdout;
184
+ if (tagCommit && tagCommit === head) return {
185
+ tagged: false,
186
+ reason: `tag ${tag} 已存在(前一端已发布)`
187
+ };
188
+ return {
189
+ tagged: false,
190
+ conflict: true,
191
+ reason: `tag ${tag} 已存在但指向 ${tagCommit.slice(0, 8) || "?"} ≠ 当前 HEAD ${head.slice(0, 8) || "?"}——可能原因:①历史提前 tag ②驳回重提后未移动(这两种且 tag 未 push 时可 git tag -d 后重打)③多端错峰发布(前一端已在较早提交交付,HEAD 已前进,属正常形态,勿动 tag)。请人工确认`
192
+ };
193
+ }
102
194
  const r = spawnSync("git", ["tag", tag], {
103
195
  cwd,
104
196
  encoding: "utf8"
@@ -135,17 +227,19 @@ async function routeDelivery(providers, ctx, diff, platform) {
135
227
  };
136
228
  }
137
229
  /**
138
- * 收集上个发版锚点(最新 semver tag)到工作区的变更文件列表。
230
+ * 收集上个发版锚点到工作区的变更文件列表。锚点 = 最新 semver tag 或最新 `chore(release):`
231
+ * bump commit 取新者(见 git.ts latestVersionAnchor)——bump 声明后未 release 的窗口内,
232
+ * 判定不会把 bump 自己写入的版本文件误判为原生改动。
139
233
  * 含未提交改动与未跟踪文件(pushy bundle 打包的是工作区内容,判定口径必须一致)。
140
234
  * 无锚点 / 非 git 仓库 → null(调用方按 uncertain 处理,不猜)。
141
235
  */
142
236
  async function collectReleaseDiff(cwd) {
143
- const anchor = latestSemverTag(cwd);
237
+ const anchor = latestVersionAnchor(cwd);
144
238
  if (!anchor) return null;
145
239
  const r = spawnSync("git", [
146
240
  "diff",
147
241
  "--name-only",
148
- anchor
242
+ anchor.ref
149
243
  ], {
150
244
  cwd,
151
245
  encoding: "utf8"
@@ -1137,4 +1231,4 @@ function fromAapt(artifactPath) {
1137
1231
  }
1138
1232
  }
1139
1233
  //#endregion
1140
- export { resolveFramework as A, tagReleaseVersion as B, suggestFramework as C, detectBundleIds as D, deps as E, git as F, isGitRepo as I, latestSemverTag as L, collectReleaseDiff as M, routeDelivery as N, findArtifacts as O, commitsSince as P, looseSemver as R, rnFramework as S, tauriFramework as T, nativeHarmonyAdapter as _, betaVersionName as a, nativeIosFramework as b, writeProjectVersions as c, electronFramework as d, flutterAdapter as f, nativeAndroidFramework as g, nativeAndroidAdapter as h, assertBetaVersionSupported as i, subdirs as j, readJson as k, redactText as l, knownFrameworkAdapters as m, DEFAULT_BETA_VERSION_CONFIG as n, checkVersionNameShape as o, flutterFramework as p, PLATFORM_VERSION_CAPS as r, readProjectVersions as s, readArtifactVersion as t, electronAdapter as u, nativeHarmonyFramework as v, tauriAdapter as w, rnAdapter as x, nativeIosAdapter as y, statusShort as z };
1234
+ export { resolveFramework as A, statusShort as B, suggestFramework as C, detectBundleIds as D, deps as E, git as F, isGitRepo as I, latestSemverTag as L, collectReleaseDiff as M, routeDelivery as N, findArtifacts as O, commitsSince as P, latestVersionAnchor as R, rnFramework as S, tauriFramework as T, tagReleaseVersion as V, nativeHarmonyAdapter as _, betaVersionName as a, nativeIosFramework as b, writeProjectVersions as c, electronFramework as d, flutterAdapter as f, nativeAndroidFramework as g, nativeAndroidAdapter as h, assertBetaVersionSupported as i, subdirs as j, readJson as k, redactText as l, knownFrameworkAdapters as m, DEFAULT_BETA_VERSION_CONFIG as n, checkVersionNameShape as o, flutterFramework as p, PLATFORM_VERSION_CAPS as r, readProjectVersions as s, readArtifactVersion as t, electronAdapter as u, nativeHarmonyFramework as v, tauriAdapter as w, rnAdapter as x, nativeIosAdapter as y, looseSemver as z };
@@ -184,10 +184,14 @@ declare const BetaConfigSchema: z.ZodObject<{
184
184
  submit: z.ZodDefault<z.ZodBoolean>;
185
185
  group: z.ZodOptional<z.ZodString>;
186
186
  testType: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<3>, z.ZodLiteral<4>]>>;
187
+ privacyPolicy: z.ZodOptional<z.ZodString>;
188
+ appDesc: z.ZodOptional<z.ZodString>;
189
+ cleanup: z.ZodDefault<z.ZodBoolean>;
187
190
  }, z.core.$strip>>;
188
191
  testflight: z.ZodOptional<z.ZodObject<{
189
192
  note: z.ZodOptional<z.ZodString>;
190
193
  }, z.core.$strip>>;
194
+ autoBump: z.ZodDefault<z.ZodBoolean>;
191
195
  }, z.core.$strip>;
192
196
  /** versionCode 策略:"increment"(默认 +1) | 公式(可限定平台) */
193
197
  declare const VersionCodeConfigSchema: z.ZodUnion<readonly [z.ZodLiteral<"increment">, z.ZodObject<{
@@ -295,10 +299,14 @@ declare const ProjectConfigSchema: z.ZodObject<{
295
299
  submit: z.ZodDefault<z.ZodBoolean>;
296
300
  group: z.ZodOptional<z.ZodString>;
297
301
  testType: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<3>, z.ZodLiteral<4>]>>;
302
+ privacyPolicy: z.ZodOptional<z.ZodString>;
303
+ appDesc: z.ZodOptional<z.ZodString>;
304
+ cleanup: z.ZodDefault<z.ZodBoolean>;
298
305
  }, z.core.$strip>>;
299
306
  testflight: z.ZodOptional<z.ZodObject<{
300
307
  note: z.ZodOptional<z.ZodString>;
301
308
  }, z.core.$strip>>;
309
+ autoBump: z.ZodDefault<z.ZodBoolean>;
302
310
  }, z.core.$strip>>;
303
311
  }, z.core.$strip>;
304
312
  type BetaConfig = z.infer<typeof BetaConfigSchema>;
@@ -445,7 +453,9 @@ interface PublishContext extends FrameworkContext {
445
453
  */
446
454
  declare function routeDelivery(providers: UpdateProvider[], ctx: FrameworkContext, diff: string[], platform?: string): Promise<DeliveryRoute | null>;
447
455
  /**
448
- * 收集上个发版锚点(最新 semver tag)到工作区的变更文件列表。
456
+ * 收集上个发版锚点到工作区的变更文件列表。锚点 = 最新 semver tag 或最新 `chore(release):`
457
+ * bump commit 取新者(见 git.ts latestVersionAnchor)——bump 声明后未 release 的窗口内,
458
+ * 判定不会把 bump 自己写入的版本文件误判为原生改动。
449
459
  * 含未提交改动与未跟踪文件(pushy bundle 打包的是工作区内容,判定口径必须一致)。
450
460
  * 无锚点 / 非 git 仓库 → null(调用方按 uncertain 处理,不猜)。
451
461
  */
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as ProjectConfigSchema, A as routeDelivery, B as appendEntry, C as resolvePlugins, Ct as readJson, D as PublishContext, E as FrameworkContext, F as runSubmit, G as BetaVersionConfigSchema, H as BetaConfig, I as HISTORY_PATH, J as CredentialsProfile, K as CredentialsFile, L as HistoryEntry, M as PipelineSummary, N as PipelineSummaryEntry, O as UpdateProvider, P as runStage, Q as ProjectConfig, R as HistoryEvent, S as defineConfig, St as findArtifacts, T as DeliveryRoute, Tt as subdirs, U as BetaConfigSchema, V as readEntries, W as BetaVersionConfig, X as GlobalConfig, Y as CredentialsProfileSchema, Z as GlobalConfigSchema, _ as AppliftPlugin, _t as BundleIds, a as knownFrameworkAdapters, at as VersionCodeConfigSchema, b as PluginHookName, bt as deps, c as nativeHarmonyAdapter, ct as Channel, d as nativeIosFramework, dt as ChannelEvent, et as StoreConfig, f as rnAdapter, ft as ChannelRegistry, g as tauriFramework, gt as SubmitResult, h as tauriAdapter, ht as StageResult, i as flutterFramework, it as VersionCodeConfig, j as PipelineInput, k as collectReleaseDiff, l as nativeHarmonyFramework, lt as ChannelCapabilities, m as suggestFramework, mt as ReviewStatus, n as electronFramework, nt as Stores, o as nativeAndroidAdapter, ot as VersionConfig, p as rnFramework, pt as ReleasePlanEntry, q as CredentialsFileSchema, r as flutterAdapter, rt as StoresSchema, s as nativeAndroidFramework, st as VersionConfigSchema, t as electronAdapter, tt as StoreConfigSchema, u as nativeIosAdapter, ut as ChannelContext, v as AppliftUserConfig, vt as FrameworkAdapter, w as DeliveryJudgement, wt as resolveFramework, x as PluginHost, xt as detectBundleIds, y as HookContext, yt as Platform, z as HistoryQuery } from "./frameworks-B_GxUuWQ.mjs";
1
+ import { $ as ProjectConfigSchema, A as routeDelivery, B as appendEntry, C as resolvePlugins, Ct as readJson, D as PublishContext, E as FrameworkContext, F as runSubmit, G as BetaVersionConfigSchema, H as BetaConfig, I as HISTORY_PATH, J as CredentialsProfile, K as CredentialsFile, L as HistoryEntry, M as PipelineSummary, N as PipelineSummaryEntry, O as UpdateProvider, P as runStage, Q as ProjectConfig, R as HistoryEvent, S as defineConfig, St as findArtifacts, T as DeliveryRoute, Tt as subdirs, U as BetaConfigSchema, V as readEntries, W as BetaVersionConfig, X as GlobalConfig, Y as CredentialsProfileSchema, Z as GlobalConfigSchema, _ as AppliftPlugin, _t as BundleIds, a as knownFrameworkAdapters, at as VersionCodeConfigSchema, b as PluginHookName, bt as deps, c as nativeHarmonyAdapter, ct as Channel, d as nativeIosFramework, dt as ChannelEvent, et as StoreConfig, f as rnAdapter, ft as ChannelRegistry, g as tauriFramework, gt as SubmitResult, h as tauriAdapter, ht as StageResult, i as flutterFramework, it as VersionCodeConfig, j as PipelineInput, k as collectReleaseDiff, l as nativeHarmonyFramework, lt as ChannelCapabilities, m as suggestFramework, mt as ReviewStatus, n as electronFramework, nt as Stores, o as nativeAndroidAdapter, ot as VersionConfig, p as rnFramework, pt as ReleasePlanEntry, q as CredentialsFileSchema, r as flutterAdapter, rt as StoresSchema, s as nativeAndroidFramework, st as VersionConfigSchema, t as electronAdapter, tt as StoreConfigSchema, u as nativeIosAdapter, ut as ChannelContext, v as AppliftUserConfig, vt as FrameworkAdapter, w as DeliveryJudgement, wt as resolveFramework, x as PluginHost, xt as detectBundleIds, y as HookContext, yt as Platform, z as HistoryQuery } from "./frameworks-B-t5Ywma.mjs";
2
2
  //#region src/registry.d.ts
3
3
  export declare class DefaultChannelRegistry implements ChannelRegistry {
4
4
  private readonly channels;
@@ -372,10 +372,23 @@ export declare class AgcHarmonyChannel implements Channel {
372
372
  /**
373
373
  * 测试发布(releaseType=6,2026-10 起走 Testing API——旧 app-package-info 直绑链路已被华为下线,
374
374
  * 其 releaseType 枚举仅剩 1=全网 / 7=企业非公开):
375
- * 上传 .app → 新建测试版本(testType 3=邀请测试 4=公开测试)→ 添加测试软件包 → 等云端编译 → 更新测试版本绑定;
375
+ * 上传 .app → 清理旧测试版本(cleanupOldTestVersions,默认开)→ 新建测试版本(testType 3=邀请测试
376
+ * 4=公开测试)→ 添加测试软件包 → 等云端编译 → 更新测试版本绑定;
376
377
  * target.submitTest='true' 时:(可选按 testGroup 绑测试群组)自动提交测试版本审核;否则由控制台人工完成。
377
378
  */
378
379
  private uploadBeta;
380
+ /**
381
+ * 发布前清理旧测试版本(target.cleanup === 'false' 关闭,默认开;config 键 beta.agc.cleanup):
382
+ * AGC 测试版本数量约束——在审上限(提交报 204144692 "count of harmony test in audit")、
383
+ * 在架上限(邀请测试 3 / 公开测试 1 / 总数 100)——旧版本不清理会阻塞新版本提审(AGC 不自动下架)。
384
+ * 发现:brief-info/list 反向过滤 testType 3/4(与 status() 的商用过滤互补)。故意不解读 state:
385
+ * 测试版本 state 枚举与商用 releaseState 表不一致且未经实机验证,清理全部旧版本无需依赖 state。
386
+ * 清理:stop(完全下架)→ delete(官方指南顺序);best-effort——单个失败仅告警不阻断,
387
+ * 名额仍被占时提审步骤的 204144692 errorHint 兜底提示人工处理。
388
+ * 影响面:已装设备 App 本体与 pushy 热更均不受影响(只移除 AGC 在架记录);
389
+ * 受影响的仅审核空窗期内想经 AGC 邀请链接新装的人(蒲公英通道不受影响)。
390
+ */
391
+ private cleanupOldTestVersions;
379
392
  /**
380
393
  * 按群组名查测试群组 groupId(AppTest API GET /test-group/list,appId 走 header,rtnCode 外壳)。
381
394
  * 分页遍历;匹配不到报错提示到控制台建组。
@@ -519,6 +532,64 @@ export declare function writeProjectVersions(cwd: string, framework: FrameworkAd
519
532
  }>;
520
533
  };
521
534
  //#endregion
535
+ //#region src/version/git.d.ts
536
+ export interface GitResult {
537
+ ok: boolean;
538
+ stdout: string;
539
+ stderr: string;
540
+ }
541
+ export declare function git(cwd: string, args: string[]): GitResult;
542
+ export declare function isGitRepo(cwd: string): boolean;
543
+ /** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
544
+ export declare function looseSemver(tag: string): string | undefined;
545
+ /**
546
+ * 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
547
+ * 无 tag / 非 git 仓库返回 undefined。
548
+ */
549
+ export declare function latestSemverTag(cwd: string): string | undefined;
550
+ /** 版本声明锚点:tag(已交付)或 bump commit(已声明未交付),判定与推断共用 */
551
+ export interface VersionAnchor {
552
+ /** 可直接传给 git diff / git log 的 ref(tag 名或 commit sha) */
553
+ ref: string;
554
+ /** tag = 已交付;bump = 已声明未交付(chore(release): commit) */
555
+ kind: "tag" | "bump";
556
+ /** 从 tag 名 / commit message 解析的 semver(解析失败缺省) */
557
+ version?: string;
558
+ /** 展示用:"v4.0.0(已发布)" / "4.1.0(已声明未发布)" */
559
+ label: string;
560
+ }
561
+ /**
562
+ * 版本声明锚点:最新 semver tag 的提交 vs 最新 `chore(release):` 提交,按 committer time 取新者。
563
+ * - tag 新(已交付)→ kind=tag(判定 diff 从交付点起算)
564
+ * - bump commit 新(声明后未交付)→ kind=bump(bump 写入的版本文件不计入判定 diff,
565
+ * 否则去 tag 化后每次 beta 都会把自己写的版本文件误判为原生改动、连环烧版本号)
566
+ * - 同一提交(旧形态 bump+tag 一步完成)→ 优先 tag(交付态是更强事实)
567
+ * - committer time 同秒平局 → 拓扑序决胜(bump 是 tag 的后代则声明在交付后 → bump 胜)——
568
+ * 秒级精度不足时不依赖时序运气
569
+ * 无 tag 且无 bump commit / 非 git 仓库 → null。
570
+ */
571
+ export declare function latestVersionAnchor(cwd: string): VersionAnchor | null;
572
+ /** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
573
+ export declare function commitsSince(cwd: string, ref: string): string[];
574
+ /** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
575
+ * 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
576
+ export declare function statusShort(cwd: string): string[];
577
+ export interface TagResult {
578
+ tagged: boolean;
579
+ reason?: string;
580
+ /** tag 已存在但指向不同 commit(疑似历史提前 tag / 驳回重提后未移动)——调用方应告警而非静默跳过 */
581
+ conflict?: boolean;
582
+ }
583
+ /**
584
+ * release 分发成功后补 tag(tag = 交付封板标记;bump 只声明不打 tag,见 AGENTS.md 版本语义)。
585
+ * - tag 已存在且指向 HEAD(多端发布,前一端已打)→ 跳过
586
+ * - tag 已存在但指向不同 commit → conflict 告警(历史提前 tag / 驳回重提未移动 / 多端错峰发布)
587
+ * - dryRun / 无版本 / 非 git 仓库 → 不动、不炸(tag 失败不阻断发布主流程)
588
+ */
589
+ export declare function tagReleaseVersion(cwd: string, version: string | undefined, opts?: {
590
+ dryRun?: boolean;
591
+ }): TagResult;
592
+ //#endregion
522
593
  //#region src/version/infer.d.ts
523
594
  export type BumpLevel = "major" | "minor" | "patch";
524
595
  export interface InferResult {
@@ -555,6 +626,16 @@ export interface DecideResult {
555
626
  export declare function decideNextVersion(current: string, opts: DecideOptions, infer?: InferResult): DecideResult;
556
627
  /** 工程文件写入用的三段数字 versionName(prerelease 只留在 tag,不进商店) */
557
628
  export declare function storeVersionName(full: string): string;
629
+ /** compareDeclared 结果:ahead=已声明未发布(只需重建);not-ahead=未声明/持平(需 bump);unknown=无法比较 */
630
+ export type DeclaredComparison = "ahead" | "not-ahead" | "unknown";
631
+ /**
632
+ * 三态判定核心比较(beta 原生路径用):工程 versionName 是否领先于最近发布 tag。
633
+ * - ahead:版本已声明(bump 过未 release)→ 引导重建 + --delivery native,不再 bump
634
+ * - not-ahead:未声明或持平 → 引导/自动 bump
635
+ * - unknown:工程版本非 semver(历史非规范值)→ 调用方按需 bump 分支并告警,不猜
636
+ * releasedTag 缺省(从未发布过)→ 任何合法工程版本都算已声明。
637
+ */
638
+ export declare function compareDeclared(project: string | undefined, releasedTag: string | undefined): DeclaredComparison;
558
639
  //#endregion
559
640
  //#region src/version/bump.d.ts
560
641
  export interface BumpOptions {
@@ -569,7 +650,8 @@ export interface BumpOptions {
569
650
  platforms?: string[];
570
651
  }
571
652
  export interface BumpPlan {
572
- anchorTag?: string;
653
+ /** 版本声明锚点(tag=已交付 / bump commit=已声明未交付,取新者);首次无锚点 */
654
+ anchor?: VersionAnchor;
573
655
  /** 疑似失败重跑(HEAD 是 release commit 且锚点后无新业务提交)——CLI 在 --yes 且无显式 level 时拒绝 */
574
656
  suspectRerun?: boolean;
575
657
  /** 基线版本(多端中最大 semver) */
@@ -587,9 +669,9 @@ export interface BumpPlan {
587
669
  }
588
670
  export declare function planBump(opts: BumpOptions): BumpPlan;
589
671
  export interface ApplyOptions {
590
- /** 代 commit(默认 true;--no-commit 跳过) */
672
+ /** 代 commit(CLI 默认开;--no-commit 跳过) */
591
673
  commit: boolean;
592
- /** 代打 tag(默认 true;--no-tag 跳过) */
674
+ /** 代打 tag(CLI 默认不打——tag 是交付标记,归 release 提审成功后补;--tag 显式开) */
593
675
  tag: boolean;
594
676
  }
595
677
  export interface ApplyResult {
@@ -676,39 +758,6 @@ export declare function renderChangelogSection(version: string, date: string, en
676
758
  */
677
759
  export declare function upsertChangelog(cwd: string, version: string, section: string): string;
678
760
  //#endregion
679
- //#region src/version/git.d.ts
680
- export interface GitResult {
681
- ok: boolean;
682
- stdout: string;
683
- stderr: string;
684
- }
685
- export declare function git(cwd: string, args: string[]): GitResult;
686
- export declare function isGitRepo(cwd: string): boolean;
687
- /** 宽松 semver 解析(可选 v/前缀,如 v4.1.0 / 4.1.0-beta.3 / release-4.1.0 里取 semver 部分) */
688
- export declare function looseSemver(tag: string): string | undefined;
689
- /**
690
- * 最新 semver tag(按版本序取最大,非创建时间——修复补打旧版本 tag 的场景更稳)。
691
- * 无 tag / 非 git 仓库返回 undefined。
692
- */
693
- export declare function latestSemverTag(cwd: string): string | undefined;
694
- /** 锚点 tag..HEAD 的完整 commit messages(%B 含 footer,NUL 分隔——body 内建换行安全) */
695
- export declare function commitsSince(cwd: string, ref?: string): string[];
696
- /** 工作区状态(--porcelain)。注意:不走通用 git()——其 stdout.trim() 会剥掉首行前导空格,
697
- * 破坏 porcelain 的两列状态位(" M file" → "M file"),此处保留原始行仅去行尾。 */
698
- export declare function statusShort(cwd: string): string[];
699
- export interface TagResult {
700
- tagged: boolean;
701
- reason?: string;
702
- }
703
- /**
704
- * release 分发成功后补 tag(tag = 实际已发布的正式版本;beta 不打 tag,追溯走 history)。
705
- * - tag 已存在(多端发布,前一端已打)→ 跳过
706
- * - dryRun / 无版本 / 非 git 仓库 → 不动、不炸(tag 失败不阻断发布主流程)
707
- */
708
- export declare function tagReleaseVersion(cwd: string, version: string | undefined, opts?: {
709
- dryRun?: boolean;
710
- }): TagResult;
711
- //#endregion
712
761
  //#region src/version/artifact.d.ts
713
762
  export interface ArtifactVersion {
714
763
  versionName?: string;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as resolveFramework, B as tagReleaseVersion, C as suggestFramework, D as detectBundleIds, E as deps, F as git, I as isGitRepo, L as latestSemverTag, M as collectReleaseDiff, N as routeDelivery, O as findArtifacts, P as commitsSince, R as looseSemver, S as rnFramework, T as tauriFramework, _ as nativeHarmonyAdapter, a as betaVersionName, b as nativeIosFramework, c as writeProjectVersions, d as electronFramework, f as flutterAdapter, g as nativeAndroidFramework, h as nativeAndroidAdapter, i as assertBetaVersionSupported, j as subdirs, k as readJson, l as redactText, m as knownFrameworkAdapters, n as DEFAULT_BETA_VERSION_CONFIG, o as checkVersionNameShape, p as flutterFramework, r as PLATFORM_VERSION_CAPS, s as readProjectVersions, t as readArtifactVersion, u as electronAdapter, v as nativeHarmonyFramework, w as tauriAdapter, x as rnAdapter, y as nativeIosAdapter, z as statusShort } from "./artifact-DFD8C7bE.mjs";
1
+ import { A as resolveFramework, B as statusShort, C as suggestFramework, D as detectBundleIds, E as deps, F as git, I as isGitRepo, L as latestSemverTag, M as collectReleaseDiff, N as routeDelivery, O as findArtifacts, P as commitsSince, R as latestVersionAnchor, S as rnFramework, T as tauriFramework, V as tagReleaseVersion, _ as nativeHarmonyAdapter, a as betaVersionName, b as nativeIosFramework, c as writeProjectVersions, d as electronFramework, f as flutterAdapter, g as nativeAndroidFramework, h as nativeAndroidAdapter, i as assertBetaVersionSupported, j as subdirs, k as readJson, l as redactText, m as knownFrameworkAdapters, n as DEFAULT_BETA_VERSION_CONFIG, o as checkVersionNameShape, p as flutterFramework, r as PLATFORM_VERSION_CAPS, s as readProjectVersions, t as readArtifactVersion, u as electronAdapter, v as nativeHarmonyFramework, w as tauriAdapter, x as rnAdapter, y as nativeIosAdapter, z as looseSemver } from "./artifact-DbTqPUbk.mjs";
2
2
  import { z } from "zod";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
@@ -90,9 +90,31 @@ const BetaConfigSchema = z.object({
90
90
  /** 测试群组名(submit 时按名查 groupId 绑定;匹配不到报错提示到控制台建组) */
91
91
  group: z.string().optional(),
92
92
  /** 3=邀请测试(默认)4=公开测试 */
93
- testType: z.union([z.literal(3), z.literal(4)]).default(3)
93
+ testType: z.union([z.literal(3), z.literal(4)]).default(3),
94
+ /**
95
+ * 隐私政策 URL(提审必填:AGC Testing API 校验 extInfo.privacyPolicy,
96
+ * 控制台对应版本创建时的「自定义协议」步骤;缺失提审报 204144692
97
+ * privacyPolicy is necessary——2026-10-25 qy-driver 实测)。透传 target.privacyPolicy。
98
+ */
99
+ privacyPolicy: z.string().optional(),
100
+ /**
101
+ * 测试版本描述(languages[].appDesc):AGC 校验 appDesc/newFeatures/线上 briefInfo
102
+ * 三者不得相同(2026-10-25 实测),appDesc 与 newFeatures(--note)来源必须互异。
103
+ * 未配置时用通用基线文案;透传 target.appDesc。
104
+ */
105
+ appDesc: z.string().optional(),
106
+ /**
107
+ * 发布前自动清理旧测试版本(stop+delete,默认 true):
108
+ * AGC 测试版本在审/在架数量有上限(在审超限提交报 204144692、在架邀请测试 3 个),
109
+ * 旧版本不清理会阻塞新版本提审。清理只移除 AGC 在架记录——已装设备与热更均不受影响,
110
+ * 受影响的仅审核空窗期内想新装的人(期间 AGC 邀请链接装不了)。
111
+ */
112
+ cleanup: z.boolean().default(true)
94
113
  }).optional(),
95
- testflight: z.object({ note: z.string().optional() }).optional()
114
+ testflight: z.object({ note: z.string().optional() }).optional(),
115
+ /** 判定为「原生·需 bump」时自动升版(等价于永远带 --bump;非交互环境须配合 --yes)。
116
+ * 热更(ota)与「已声明」态不受影响——热更不动版本号,已声明只引导重建 */
117
+ autoBump: z.boolean().default(false)
96
118
  });
97
119
  /** versionCode 策略:"increment"(默认 +1) | 公式(可限定平台) */
98
120
  const VersionCodeConfigSchema = z.union([z.literal("increment"), z.object({
@@ -2464,6 +2486,8 @@ var YybChannel = class {
2464
2486
  *
2465
2487
  * 测试发布(ctx.target.releaseType === '6',2026-10 起走 Testing API;旧 releaseType=6 直绑 app-package-info 链路已下线,枚举仅剩 1/7):
2466
2488
  * 0. GET /api/publish/v2/upload-url/for-obs?releaseType=6 .hap 上传地址 → PUT OBS
2489
+ * 0.5 (cleanup !== 'false',默认开)清理旧测试版本:brief-info/list 发现 testType 3/4 →
2490
+ * POST /api/publish/v2/test/version/stop(完全下架)→ DELETE /api/publish/v2/test/app/version
2467
2491
  * 1. POST /api/publish/v2/test/app/version 新建测试版本(testType 3=邀请测试)→ versionId
2468
2492
  * 2. POST /api/publish/v2/test/version/pkg 添加测试软件包(distributeMode=1)→ pkgId
2469
2493
  * 3. GET /api/publish/v3/package/compile/status 轮询云端编译
@@ -2538,7 +2562,7 @@ var AgcHarmonyChannel = class {
2538
2562
  "1003": "client_id/client_secret 无效(AGC → 项目设置 → 常规 → API Client 重新获取)",
2539
2563
  "204144702": "签名/证书不匹配:检查签名证书指纹(SHA256)与 AGC 后台登记是否一致",
2540
2564
  "204144677": "bundleName 与应用不匹配:确认 appId 对应的应用包名",
2541
- "204144692": "提审前置校验失败(按 msg 判断):privacyPolicy → 控制台「更多服务→协议服务」登记隐私政策;count of harmony test in audit → 在审测试版本超限,先停止/删除在审的测试版本再提"
2565
+ "204144692": "提审前置校验失败(按 msg 对症,2026-10-25 实测):privacyPolicy → 配置 beta.agc.privacyPolicy(Testing API extInfo.privacyPolicy,控制台对应版本创建的「自定义协议」);displayArea → 邀请测试必填测试专区(CLI 已默认携带,仍报则到控制台核对版本);count of harmony test in audit → 在审测试版本超限(beta.agc.cleanup 默认已自动清理,仍出现请到控制台核对是否有删不掉的旧测试版本)"
2542
2566
  };
2543
2567
  async authHeaders(ctx) {
2544
2568
  const clientId = required$1(ctx, "AGC_CLIENT_ID");
@@ -2837,7 +2861,8 @@ var AgcHarmonyChannel = class {
2837
2861
  /**
2838
2862
  * 测试发布(releaseType=6,2026-10 起走 Testing API——旧 app-package-info 直绑链路已被华为下线,
2839
2863
  * 其 releaseType 枚举仅剩 1=全网 / 7=企业非公开):
2840
- * 上传 .app → 新建测试版本(testType 3=邀请测试 4=公开测试)→ 添加测试软件包 → 等云端编译 → 更新测试版本绑定;
2864
+ * 上传 .app → 清理旧测试版本(cleanupOldTestVersions,默认开)→ 新建测试版本(testType 3=邀请测试
2865
+ * 4=公开测试)→ 添加测试软件包 → 等云端编译 → 更新测试版本绑定;
2841
2866
  * target.submitTest='true' 时:(可选按 testGroup 绑测试群组)自动提交测试版本审核;否则由控制台人工完成。
2842
2867
  */
2843
2868
  async uploadBeta(ctx, appId, headers) {
@@ -2885,6 +2910,7 @@ var AgcHarmonyChannel = class {
2885
2910
  type: "progress",
2886
2911
  detail: "文件上传完成"
2887
2912
  });
2913
+ await this.cleanupOldTestVersions(ctx, appId, headers);
2888
2914
  const testType = ctx.target.testType === "4" ? 4 : 3;
2889
2915
  const versionId = (await agcJson(`${BASE}/publish/v2/test/app/version?appId=${encodeURIComponent(appId)}`, {
2890
2916
  method: "POST",
@@ -2920,6 +2946,10 @@ var AgcHarmonyChannel = class {
2920
2946
  }, "添加测试软件包")).pkgVersion?.[0] ?? "";
2921
2947
  if (!pkgId) throw new Error("AGC 添加测试软件包未返回 pkgVersion");
2922
2948
  await this.awaitCompile(ctx, appId, pkgId, headers);
2949
+ const privacyPolicy = ctx.target.privacyPolicy?.trim();
2950
+ const note = ctx.updateDescription ?? "applift 测试发布";
2951
+ const appDesc = (ctx.target.appDesc?.trim() || "内测验证版本(internal test build),更新内容以新特性说明为准。").slice(0, 100);
2952
+ const newFeatures = note.slice(0, 100);
2923
2953
  await agcJson(`${BASE}/publish/v2/test/app/version?appId=${encodeURIComponent(appId)}`, {
2924
2954
  method: "PUT",
2925
2955
  headers: {
@@ -2928,7 +2958,13 @@ var AgcHarmonyChannel = class {
2928
2958
  },
2929
2959
  body: JSON.stringify({
2930
2960
  versionId,
2931
- pkgId
2961
+ pkgId,
2962
+ ...privacyPolicy ? { extInfo: { privacyPolicy } } : {},
2963
+ languages: [{
2964
+ language: "zh-CN",
2965
+ appDesc,
2966
+ newFeatures
2967
+ }]
2932
2968
  })
2933
2969
  }, "绑定测试软件包");
2934
2970
  if (ctx.target.submitTest === "true") {
@@ -2948,10 +2984,13 @@ var AgcHarmonyChannel = class {
2948
2984
  openTestInfo: {
2949
2985
  startTime: now,
2950
2986
  endTime: now + 2592e6,
2951
- testTaskInfo: { groupInfos: [{
2952
- groupId,
2953
- groupName
2954
- }] }
2987
+ testTaskInfo: {
2988
+ groupInfos: [{
2989
+ groupId,
2990
+ groupName
2991
+ }],
2992
+ displayArea: "1"
2993
+ }
2955
2994
  }
2956
2995
  })
2957
2996
  }, "绑定测试群组");
@@ -2994,6 +3033,92 @@ var AgcHarmonyChannel = class {
2994
3033
  };
2995
3034
  }
2996
3035
  /**
3036
+ * 发布前清理旧测试版本(target.cleanup === 'false' 关闭,默认开;config 键 beta.agc.cleanup):
3037
+ * AGC 测试版本数量约束——在审上限(提交报 204144692 "count of harmony test in audit")、
3038
+ * 在架上限(邀请测试 3 / 公开测试 1 / 总数 100)——旧版本不清理会阻塞新版本提审(AGC 不自动下架)。
3039
+ * 发现:brief-info/list 反向过滤 testType 3/4(与 status() 的商用过滤互补)。故意不解读 state:
3040
+ * 测试版本 state 枚举与商用 releaseState 表不一致且未经实机验证,清理全部旧版本无需依赖 state。
3041
+ * 清理:stop(完全下架)→ delete(官方指南顺序);best-effort——单个失败仅告警不阻断,
3042
+ * 名额仍被占时提审步骤的 204144692 errorHint 兜底提示人工处理。
3043
+ * 影响面:已装设备 App 本体与 pushy 热更均不受影响(只移除 AGC 在架记录);
3044
+ * 受影响的仅审核空窗期内想经 AGC 邀请链接新装的人(蒲公英通道不受影响)。
3045
+ */
3046
+ async cleanupOldTestVersions(ctx, appId, headers) {
3047
+ if (ctx.target.cleanup === "false") return;
3048
+ const packageName = ctx.target.bundleName;
3049
+ let json;
3050
+ try {
3051
+ json = await agcJson(`${BASE}/publish/v3/version/brief-info/list`, {
3052
+ method: "POST",
3053
+ headers: {
3054
+ ...headers,
3055
+ appId,
3056
+ "Content-Type": "application/json"
3057
+ },
3058
+ body: JSON.stringify(packageName ? { packageName } : {})
3059
+ }, "查询旧测试版本");
3060
+ } catch (e) {
3061
+ ctx.emit({
3062
+ type: "warn",
3063
+ message: `旧测试版本查询失败(跳过清理,不阻断): ${e.message}`
3064
+ });
3065
+ return;
3066
+ }
3067
+ const seen = /* @__PURE__ */ new Set();
3068
+ const stale = (json.versionList ?? []).filter((v) => {
3069
+ if (v.testType !== 3 && v.testType !== 4 || !v.versionId || seen.has(v.versionId)) return false;
3070
+ seen.add(v.versionId);
3071
+ return true;
3072
+ });
3073
+ if (stale.length === 0) {
3074
+ ctx.emit({
3075
+ type: "progress",
3076
+ detail: "旧测试版本清理:无残留"
3077
+ });
3078
+ return;
3079
+ }
3080
+ ctx.emit({
3081
+ type: "progress",
3082
+ detail: `发现 ${stale.length} 个旧测试版本,自动停止并删除(beta.agc.cleanup 默认开;已装设备与热更不受影响)`
3083
+ });
3084
+ for (const v of stale) {
3085
+ const vid = v.versionId;
3086
+ const label = `${v.packageList?.[0]?.versionName ?? "?"}(versionId ${vid})`;
3087
+ try {
3088
+ await agcJson(`${BASE}/publish/v2/test/version/stop?appId=${encodeURIComponent(appId)}`, {
3089
+ method: "POST",
3090
+ headers: {
3091
+ ...headers,
3092
+ "Content-Type": "application/json"
3093
+ },
3094
+ body: JSON.stringify({ versionId: vid })
3095
+ }, "停止测试版本");
3096
+ } catch (e) {
3097
+ const msg = e.message;
3098
+ const hint = msg.includes("state is 9998") ? "——该版本在审中,AGC 不允许停/删:到控制台撤回该审核或等审毕后下次发版自动清理" : "";
3099
+ ctx.emit({
3100
+ type: "warn",
3101
+ message: `停止测试版本 ${label}失败(继续尝试删除): ${msg}${hint}`
3102
+ });
3103
+ }
3104
+ try {
3105
+ await agcJson(`${BASE}/publish/v2/test/app/version?versionId=${encodeURIComponent(vid)}&appId=${encodeURIComponent(appId)}`, {
3106
+ method: "DELETE",
3107
+ headers
3108
+ }, "删除测试版本");
3109
+ ctx.emit({
3110
+ type: "progress",
3111
+ detail: `旧测试版本已清理: ${label}`
3112
+ });
3113
+ } catch (e) {
3114
+ ctx.emit({
3115
+ type: "warn",
3116
+ message: `删除测试版本 ${label}失败: ${e.message}`
3117
+ });
3118
+ }
3119
+ }
3120
+ }
3121
+ /**
2997
3122
  * 按群组名查测试群组 groupId(AppTest API GET /test-group/list,appId 走 header,rtnCode 外壳)。
2998
3123
  * 分页遍历;匹配不到报错提示到控制台建组。
2999
3124
  * 文档: agc-help-test-api-get-test-grouplist-0000002271000717
@@ -4291,6 +4416,20 @@ function storeVersionName(full) {
4291
4416
  const v = new semver.SemVer(full);
4292
4417
  return `${v.major}.${v.minor}.${v.patch}`;
4293
4418
  }
4419
+ /**
4420
+ * 三态判定核心比较(beta 原生路径用):工程 versionName 是否领先于最近发布 tag。
4421
+ * - ahead:版本已声明(bump 过未 release)→ 引导重建 + --delivery native,不再 bump
4422
+ * - not-ahead:未声明或持平 → 引导/自动 bump
4423
+ * - unknown:工程版本非 semver(历史非规范值)→ 调用方按需 bump 分支并告警,不猜
4424
+ * releasedTag 缺省(从未发布过)→ 任何合法工程版本都算已声明。
4425
+ */
4426
+ function compareDeclared(project, releasedTag) {
4427
+ const p = project ? semver.valid(project) ?? void 0 : void 0;
4428
+ if (!p) return "unknown";
4429
+ const r = releasedTag ? looseSemver(releasedTag) ?? void 0 : void 0;
4430
+ if (!r) return "ahead";
4431
+ return semver.gt(p, r) ? "ahead" : "not-ahead";
4432
+ }
4294
4433
  //#endregion
4295
4434
  //#region src/version/changelog.ts
4296
4435
  /**
@@ -4500,18 +4639,18 @@ function lastCommitIsRelease(cwd) {
4500
4639
  ]).stdout.startsWith("chore(release): ");
4501
4640
  }
4502
4641
  function planBump(opts) {
4503
- let anchorTag;
4642
+ let anchor;
4504
4643
  let commits = [];
4505
4644
  let infer;
4506
4645
  if (isGitRepo(opts.cwd)) {
4507
- anchorTag = latestSemverTag(opts.cwd);
4508
- if (anchorTag) {
4509
- commits = commitsSince(opts.cwd, anchorTag);
4646
+ anchor = latestVersionAnchor(opts.cwd) ?? void 0;
4647
+ if (anchor) {
4648
+ commits = commitsSince(opts.cwd, anchor.ref);
4510
4649
  if (commits.length > 0) infer = inferBumpLevel(commits);
4511
4650
  }
4512
4651
  }
4513
4652
  const currents = readProjectVersions(opts.cwd, opts.framework).filter((c) => (c.versionName || c.versionCode) && !opts.platforms || opts.platforms?.includes(c.platform));
4514
- const anchorVersion = anchorTag ? looseSemver(anchorTag) ?? void 0 : void 0;
4653
+ const anchorVersion = anchor?.version;
4515
4654
  const base = (anchorVersion ? semver.prerelease(anchorVersion) !== null : false) ? anchorVersion : baselineVersion([...currents, ...anchorVersion ? [{
4516
4655
  platform: "__tag__",
4517
4656
  versionName: anchorVersion
@@ -4523,9 +4662,9 @@ function planBump(opts) {
4523
4662
  level: opts.level,
4524
4663
  pre: opts.pre
4525
4664
  }, infer);
4526
- if (!anchorTag && !opts.level) decided.reason = "无发版锚点 tag(首次 bump)→ patch 兜底;打第一个 tag 后激活 git 推断";
4527
- const suspectRerun = !!anchorTag && lastCommitIsRelease(opts.cwd) && commits.every((c) => c.startsWith("chore(release): "));
4528
- if (suspectRerun) decided.reason += ";⚠ 锚点后无新业务提交且 HEAD 已是 release commit——疑似失败重跑,建议 --no-bump 复用已 bump 版本";
4665
+ if (!anchor && !opts.level) decided.reason = "无发版锚点(首次 bump)→ patch 兜底;首个 bump commit 或 tag 后激活 git 推断";
4666
+ const suspectRerun = !!anchor && lastCommitIsRelease(opts.cwd) && commits.every((c) => c.startsWith("chore(release): "));
4667
+ if (suspectRerun) decided.reason += ";⚠ 锚点后无新业务提交且 HEAD 已是 release commit——疑似失败重跑,建议不带 --bump 重跑复用已 bump 版本";
4529
4668
  const formulaCfg = opts.versionCode && opts.versionCode !== "increment" ? opts.versionCode : void 0;
4530
4669
  if (opts.platforms && opts.platforms.length > 0) {
4531
4670
  const known = new Set(currents.map((c) => c.platform));
@@ -4563,7 +4702,7 @@ function planBump(opts) {
4563
4702
  if (formulaCfg && !formulaApplied && writes.length > 0) formulaNote += `;⚠ 公式 ${formulaCfg.formula} 未命中任何端${formulaCfg.platforms ? `(platforms: ${formulaCfg.platforms.join("/")},可用端: ${writes.map((w) => w.platform).join("/")})` : ""}——全部端走 +1 兜底`;
4564
4703
  if (resetCodes.length > 0) formulaNote += `;⚠ 非数字 versionCode 已从 1 重新起号(${resetCodes.join(", ")})——商店单调性可能拒绝,建议配置 version.versionCode 公式或手动规范化`;
4565
4704
  return {
4566
- anchorTag,
4705
+ anchor,
4567
4706
  currentVersions: currents,
4568
4707
  writes,
4569
4708
  base: base ?? decided.next,
@@ -4638,4 +4777,4 @@ function applyBump(cwd, plan, framework, opts) {
4638
4777
  }
4639
4778
  }
4640
4779
  //#endregion
4641
- export { APPLIFT_HOME, AgcHarmonyChannel, AscChannel, BetaConfigSchema, BetaVersionConfigSchema, CREDENTIALS_PATH, CredentialsFileSchema, CredentialsProfileSchema, DEFAULT_BETA_VERSION_CONFIG, DefaultChannelRegistry, GLOBAL_CONFIG_PATH, GlobalConfigSchema, GooglePlayChannel, HISTORY_PATH, HonorChannel, HuaweiAndroidChannel, OppoChannel, PLATFORM_VERSION_CAPS, PROJECTS_MAP_PATH, PgyerChannel, PluginHost, ProjectConfigSchema, StoreConfigSchema, StoresSchema, VersionCodeConfigSchema, VersionConfigSchema, VivoChannel, WAIT_BEFORE_SUBMIT_MS, XiaomiChannel, YybChannel, appendEntry, applyBump, assertBetaVersionSupported, assertListingFiles, betaVersionName, checkVersionNameShape, 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 };
4780
+ export { APPLIFT_HOME, AgcHarmonyChannel, AscChannel, BetaConfigSchema, BetaVersionConfigSchema, CREDENTIALS_PATH, CredentialsFileSchema, CredentialsProfileSchema, DEFAULT_BETA_VERSION_CONFIG, DefaultChannelRegistry, GLOBAL_CONFIG_PATH, GlobalConfigSchema, GooglePlayChannel, HISTORY_PATH, HonorChannel, HuaweiAndroidChannel, OppoChannel, PLATFORM_VERSION_CAPS, PROJECTS_MAP_PATH, PgyerChannel, PluginHost, ProjectConfigSchema, StoreConfigSchema, StoresSchema, VersionCodeConfigSchema, VersionConfigSchema, VivoChannel, WAIT_BEFORE_SUBMIT_MS, XiaomiChannel, YybChannel, appendEntry, applyBump, assertBetaVersionSupported, assertListingFiles, betaVersionName, checkVersionNameShape, collectReleaseDiff, commitsSince, compareDeclared, decideNextVersion, defineConfig, deps, detectBundleIds, electronAdapter, electronFramework, evalVersionCodeFormula, findArtifacts, findUp, flutterAdapter, flutterFramework, formPost, getAgcToken, getHarmonyAppId, git, hmacSign, humanizeError, inferBumpLevel, isGitRepo, knownFrameworkAdapters, latestSemverTag, latestVersionAnchor, 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 };
@@ -1,4 +1,4 @@
1
- import { E as FrameworkContext, O as UpdateProvider, W as BetaVersionConfig, _ as AppliftPlugin, a as knownFrameworkAdapters, c as nativeHarmonyAdapter, d as nativeIosFramework, f as rnAdapter, g as tauriFramework, h as tauriAdapter, i as flutterFramework, l as nativeHarmonyFramework, m as suggestFramework, n as electronFramework, o as nativeAndroidAdapter, p as rnFramework, r as flutterAdapter, s as nativeAndroidFramework, t as electronAdapter, u as nativeIosAdapter, w as DeliveryJudgement } from "../frameworks-B_GxUuWQ.mjs";
1
+ import { E as FrameworkContext, O as UpdateProvider, W as BetaVersionConfig, _ as AppliftPlugin, a as knownFrameworkAdapters, c as nativeHarmonyAdapter, d as nativeIosFramework, f as rnAdapter, g as tauriFramework, h as tauriAdapter, i as flutterFramework, l as nativeHarmonyFramework, m as suggestFramework, n as electronFramework, o as nativeAndroidAdapter, p as rnFramework, r as flutterAdapter, s as nativeAndroidFramework, t as electronAdapter, u as nativeIosAdapter, w as DeliveryJudgement } from "../frameworks-B-t5Ywma.mjs";
2
2
  //#region src/plugins/dingtalk-notify.d.ts
3
3
  interface DingtalkNotifyOptions {
4
4
  webhook: string;
@@ -44,12 +44,24 @@ interface PushyCli {
44
44
  packageVersion: string;
45
45
  rollout?: number;
46
46
  }): Promise<void>;
47
- /** 版本列表(取号与状态展示用) */
47
+ /** 版本列表(取号与状态展示用)。packageVersion:该热更绑定的原生基线版本名
48
+ * (基线自动替换的安全判据;服务端未回显时字段缺省,调用方按无法确认处理) */
48
49
  versions(appId: number): Promise<Array<{
49
50
  id: number;
50
51
  hash: string;
51
52
  name: string;
53
+ packageVersion?: string;
52
54
  }>>;
55
+ /** 基线包列表(GET /app/:id/package/list;409 自动替换的发现手段) */
56
+ packages(appId: number): Promise<Array<{
57
+ id: number;
58
+ hash: string;
59
+ name: string;
60
+ status: string;
61
+ }>>;
62
+ /** 删除基线包(DELETE /app/:id/package/:packageId,2026-10-25 实测端点存在)。
63
+ * 调用方必须先确认该基线无热更版本绑定(删除被绑定的基线会孤儿化热更)。 */
64
+ deletePackage(appId: number, packageId: number): Promise<void>;
53
65
  }
54
66
  /** credentials.json 中的 pushy token 键名 */
55
67
  export declare const PUSHY_TOKEN_KEY = "PUSHY_API_TOKEN";
@@ -1,4 +1,4 @@
1
- import { C as suggestFramework, M as collectReleaseDiff, O as findArtifacts, S as rnFramework, T as tauriFramework, _ as nativeHarmonyAdapter, a as betaVersionName, b as nativeIosFramework, d as electronFramework, f as flutterAdapter, g as nativeAndroidFramework, h as nativeAndroidAdapter, i as assertBetaVersionSupported, l as redactText, m as knownFrameworkAdapters, n as DEFAULT_BETA_VERSION_CONFIG, p as flutterFramework, s as readProjectVersions, t as readArtifactVersion, u as electronAdapter, v as nativeHarmonyFramework, w as tauriAdapter, x as rnAdapter, y as nativeIosAdapter } from "../artifact-DFD8C7bE.mjs";
1
+ import { C as suggestFramework, M as collectReleaseDiff, O as findArtifacts, S as rnFramework, T as tauriFramework, _ as nativeHarmonyAdapter, a as betaVersionName, b as nativeIosFramework, d as electronFramework, f as flutterAdapter, g as nativeAndroidFramework, h as nativeAndroidAdapter, i as assertBetaVersionSupported, l as redactText, m as knownFrameworkAdapters, n as DEFAULT_BETA_VERSION_CONFIG, p as flutterFramework, s as readProjectVersions, t as readArtifactVersion, u as electronAdapter, v as nativeHarmonyFramework, w as tauriAdapter, x as rnAdapter, y as nativeIosAdapter } from "../artifact-DbTqPUbk.mjs";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { createHmac } from "node:crypto";
@@ -94,7 +94,9 @@ async function query(f, baseUrl, token, api, init) {
94
94
  }
95
95
  if (!res.ok) {
96
96
  const msg = json?.message || res.statusText || `HTTP ${res.status}`;
97
- throw new Error(res.status === 401 ? `pushy API 鉴权失败(token 无效或过期):请更新 credentials.json 的 ${PUSHY_TOKEN_KEY}` : `pushy API ${api} 失败(HTTP ${res.status}):${msg}`);
97
+ const err = /* @__PURE__ */ new Error(res.status === 401 ? `pushy API 鉴权失败(token 无效或过期):请更新 credentials.json 的 ${PUSHY_TOKEN_KEY}` : `pushy API ${api} 失败(HTTP ${res.status}):${msg}`);
98
+ err.status = res.status;
99
+ throw err;
98
100
  }
99
101
  return json ?? {};
100
102
  }
@@ -271,11 +273,31 @@ function createPushyApi(opts) {
271
273
  for (const v of data) out.push({
272
274
  id: v.id,
273
275
  hash: v.hash ?? "",
274
- name: v.name
276
+ name: v.name,
277
+ ...v.packageVersion !== void 0 ? { packageVersion: v.packageVersion } : {}
275
278
  });
276
279
  if (data.length < limit) break;
277
280
  }
278
281
  return out;
282
+ },
283
+ async packages(appId) {
284
+ const out = [];
285
+ const limit = 100;
286
+ for (let offset = 0; offset < 1e4; offset += limit) {
287
+ const { data } = await q(`/app/${appId}/package/list?offset=${offset}&limit=${limit}`);
288
+ if (!data?.length) break;
289
+ for (const p of data) out.push({
290
+ id: p.id,
291
+ hash: p.hash ?? "",
292
+ name: p.name,
293
+ status: p.status ?? ""
294
+ });
295
+ if (data.length < limit) break;
296
+ }
297
+ return out;
298
+ },
299
+ async deletePackage(appId, packageId) {
300
+ await q(`/app/${appId}/package/${packageId}`, { method: "DELETE" });
279
301
  }
280
302
  };
281
303
  }
@@ -725,7 +747,21 @@ function rnUpdate(opts = {}, cli) {
725
747
  const key = `${platform}:${versionName}`;
726
748
  if (uploadedBaselines.has(key)) return;
727
749
  uploadedBaselines.add(key);
728
- await resolvePushyClient(opts, cli, hookCtx.credentials).uploadApp(appId, result.artifactPath, versionName, readDepVersions(hookCtx.cwd));
750
+ const pushy = resolvePushyClient(opts, cli, hookCtx.credentials);
751
+ try {
752
+ await pushy.uploadApp(appId, result.artifactPath, versionName, readDepVersions(hookCtx.cwd));
753
+ } catch (e) {
754
+ if (e.status !== 409) throw e;
755
+ const sameName = (await pushy.packages(appId)).filter((p) => p.name === versionName);
756
+ if (sameName.length === 0) throw new Error(`基线 409 但未发现同名基线(hash 重复?):${versionName}——请人工核对 pushy 后台`);
757
+ const vers = await pushy.versions(appId);
758
+ const bound = vers.filter((v) => v.packageVersion === versionName || v.packageVersion === void 0 && sameName.some((p) => v.hash === p.hash && v.name === versionName));
759
+ if (vers.length > 0 && bound.length > 0) throw new Error(`基线 ${versionName} 已绑定 ${bound.length} 个热更版本,自动替换会使热更孤儿化——请人工处理(pushy 后台迁移热更或删除后重发)`);
760
+ if (vers.length > 0 && vers.every((v) => v.packageVersion === void 0)) throw new Error(`服务端未回显热更绑定关系,无法确认 ${versionName} 基线可安全替换——请人工核对后删除重传`);
761
+ for (const p of sameName) await pushy.deletePackage(appId, p.id);
762
+ process.stderr.write(`⚠ pushy 基线 ${versionName} 内容已变(同版本名重建),自动删除旧基线 ${sameName.map((p) => `#${p.id}(${p.hash})`).join(",")} 并重传\n`);
763
+ await pushy.uploadApp(appId, result.artifactPath, versionName, readDepVersions(hookCtx.cwd));
764
+ }
729
765
  } catch (e) {
730
766
  uploadedBaselines.delete(`${platform}:${readArtifactVersion(result.artifactPath)?.versionName ?? ""}`);
731
767
  process.stderr.write(`⚠ pushy 基线上传失败(请人工补传):${e.message}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applift/core",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/hcg1023/applift.git",