@huoye/app-ship 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/utils/upload-manager.ts
10
- import path10 from "path";
10
+ import path12 from "path";
11
11
 
12
12
  // node_modules/chalk/source/vendor/ansi-styles/index.js
13
13
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -965,6 +965,280 @@ async function parseHap(filePath) {
965
965
  return info;
966
966
  }
967
967
 
968
+ // src/utils/release-receipt.ts
969
+ import { createHash, randomUUID } from "crypto";
970
+ import { createReadStream } from "fs";
971
+ import { chmod, mkdir, readFile, realpath, rename, stat, writeFile } from "fs/promises";
972
+ import { homedir } from "os";
973
+ import path2 from "path";
974
+ var RELEASE_RECEIPT_SCHEMA_VERSION = 1;
975
+ var DEFAULT_RELEASE_RECEIPT_LIMIT = 100;
976
+ var REMOTE_IDENTIFIER_KEYS = [
977
+ "id",
978
+ "appId",
979
+ "buildId",
980
+ "versionId",
981
+ "releaseId",
982
+ "editId",
983
+ "versionCode",
984
+ "track",
985
+ "apiTrack",
986
+ "status",
987
+ "committed"
988
+ ];
989
+ function resolveReleaseProjectRoot(projectRoot, configPath) {
990
+ return path2.resolve(projectRoot || path2.dirname(path2.resolve(configPath)));
991
+ }
992
+ function sanitizeReleaseMessage(value, maxLength = 500) {
993
+ let message = typeof value === "string" ? value : String(value ?? "");
994
+ message = message.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, "[\u5DF2\u8131\u654F]").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [\u5DF2\u8131\u654F]").replace(
995
+ /(["']?(?:access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key|api[_-]?key|app[_-]?password|authorization|credential|service[_-]?account(?:[_-]?json)?[_-]?content)["']?\s*[:=]\s*)["']?[^\s,"'}]+["']?/gi,
996
+ "$1[\u5DF2\u8131\u654F]"
997
+ ).replace(/[\r\n\t]+/g, " ").replace(/\s{2,}/g, " ").trim();
998
+ return message.length > maxLength ? `${message.slice(0, maxLength - 1)}\u2026` : message;
999
+ }
1000
+ function createUploadTargets(results) {
1001
+ return results.map((result) => {
1002
+ const platform = sanitizeReleaseMessage(result.platform, 80) || "unknown";
1003
+ const data = isRecord(result.data) ? result.data : void 0;
1004
+ const isDraft = result.success && platform === "google_play" && String(data?.status || "").toLowerCase() === "draft";
1005
+ const remote = createRemoteReceipt(result, data);
1006
+ return {
1007
+ platform,
1008
+ state: result.success ? isDraft ? "draft_created" : "uploaded" : "failed",
1009
+ outcome: result.success ? "success" : "failed",
1010
+ message: sanitizeReleaseMessage(result.message) || (result.success ? "\u4E0A\u4F20\u6210\u529F" : "\u4E0A\u4F20\u5931\u8D25"),
1011
+ ...remote ? { remote } : {}
1012
+ };
1013
+ });
1014
+ }
1015
+ async function createReleaseReceipt(input) {
1016
+ const projectRoot = await canonicalizeProjectRoot(input.projectRoot);
1017
+ const startedAt = toDate(input.startedAt ?? Date.now());
1018
+ const finishedAt = toDate(input.finishedAt ?? Date.now());
1019
+ const targets = input.targets.map(sanitizeTarget);
1020
+ return {
1021
+ schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
1022
+ id: randomUUID(),
1023
+ projectKey: createProjectKey(projectRoot),
1024
+ projectRoot,
1025
+ kind: input.kind,
1026
+ ...cleanOptionalText(input.appName, 160) ? { appName: cleanOptionalText(input.appName, 160) } : {},
1027
+ ...cleanOptionalText(input.packageName, 240) ? { packageName: cleanOptionalText(input.packageName, 240) } : {},
1028
+ ...cleanOptionalText(input.versionName, 80) ? { versionName: cleanOptionalText(input.versionName, 80) } : {},
1029
+ ...input.buildNumber !== void 0 ? { buildNumber: cleanOptionalText(String(input.buildNumber), 80) } : {},
1030
+ ...cleanOptionalText(input.changelog, 4e3) ? { changelog: cleanOptionalText(input.changelog, 4e3) } : {},
1031
+ artifacts: await inspectArtifacts(input.artifactPaths),
1032
+ targets,
1033
+ outcome: input.outcome ?? inferOutcome(targets),
1034
+ startedAt: startedAt.toISOString(),
1035
+ finishedAt: finishedAt.toISOString(),
1036
+ durationMs: Math.max(0, finishedAt.getTime() - startedAt.getTime())
1037
+ };
1038
+ }
1039
+ function formatReleaseReceipt(receipt) {
1040
+ const lines = [
1041
+ `${receipt.appName || receipt.packageName || "\u5E94\u7528"} \xB7 ${receipt.kind === "build" ? "\u6784\u5EFA\u56DE\u6267" : "\u4E0A\u4F20\u56DE\u6267"}`,
1042
+ `\u7ED3\u679C\uFF1A${receipt.outcome}`,
1043
+ `\u7248\u672C\uFF1A${receipt.versionName || "-"} (${receipt.buildNumber || "-"})`,
1044
+ `\u5B8C\u6210\u65F6\u95F4\uFF1A${receipt.finishedAt}`
1045
+ ];
1046
+ if (receipt.packageName) lines.push(`\u5305\u540D\uFF1A${receipt.packageName}`);
1047
+ if (receipt.changelog) lines.push(`\u66F4\u65B0\u65E5\u5FD7\uFF1A${receipt.changelog}`);
1048
+ for (const artifact of receipt.artifacts) {
1049
+ lines.push(
1050
+ `\u4EA7\u7269\uFF1A${artifact.path}${artifact.sizeBytes !== void 0 ? ` \xB7 ${artifact.sizeBytes} bytes` : ""}${artifact.sha256 ? ` \xB7 SHA-256 ${artifact.sha256}` : ""}`
1051
+ );
1052
+ }
1053
+ for (const target of receipt.targets) {
1054
+ lines.push(`${target.platform}\uFF1A${target.state} \xB7 ${target.message}`);
1055
+ if (target.remote?.consoleUrl) lines.push(`\u540E\u53F0\uFF1A${target.remote.consoleUrl}`);
1056
+ }
1057
+ return lines.join("\n");
1058
+ }
1059
+ var ReleaseReceiptStore = class {
1060
+ storageRoot;
1061
+ maxEntries;
1062
+ constructor(options = {}) {
1063
+ this.storageRoot = path2.resolve(options.storageRoot || path2.join(homedir(), ".flu-cli", "release-receipts"));
1064
+ this.maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_RELEASE_RECEIPT_LIMIT);
1065
+ }
1066
+ async append(receipt) {
1067
+ await mkdir(this.storageRoot, { recursive: true, mode: 448 });
1068
+ await chmod(this.storageRoot, 448);
1069
+ const filePath = this.getFilePath(receipt.projectKey);
1070
+ const current = await this.readFile(filePath, receipt.projectKey);
1071
+ const receipts = [receipt, ...current.receipts.filter((item) => item.id !== receipt.id)].slice(
1072
+ 0,
1073
+ this.maxEntries
1074
+ );
1075
+ const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
1076
+ await writeFile(
1077
+ tempPath,
1078
+ `${JSON.stringify({
1079
+ schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
1080
+ projectKey: receipt.projectKey,
1081
+ receipts
1082
+ }, null, 2)}
1083
+ `,
1084
+ { encoding: "utf8", mode: 384 }
1085
+ );
1086
+ await rename(tempPath, filePath);
1087
+ await chmod(filePath, 384);
1088
+ }
1089
+ async list(projectRoot, limit = 20) {
1090
+ const canonicalRoot = await canonicalizeProjectRoot(projectRoot);
1091
+ const projectKey = createProjectKey(canonicalRoot);
1092
+ const data = await this.readFile(this.getFilePath(projectKey), projectKey);
1093
+ return data.receipts.slice(0, Math.max(0, limit));
1094
+ }
1095
+ async save(input) {
1096
+ const receipt = await createReleaseReceipt(input);
1097
+ await this.append(receipt);
1098
+ return receipt;
1099
+ }
1100
+ getFilePath(projectKey) {
1101
+ return path2.join(this.storageRoot, `${projectKey}.json`);
1102
+ }
1103
+ async readFile(filePath, projectKey) {
1104
+ try {
1105
+ const raw = JSON.parse(await readFile(filePath, "utf8"));
1106
+ return {
1107
+ schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION,
1108
+ projectKey,
1109
+ receipts: Array.isArray(raw.receipts) ? raw.receipts : []
1110
+ };
1111
+ } catch (error) {
1112
+ if (error?.code === "ENOENT") {
1113
+ return { schemaVersion: RELEASE_RECEIPT_SCHEMA_VERSION, projectKey, receipts: [] };
1114
+ }
1115
+ throw new Error(`\u8BFB\u53D6\u53D1\u5E03\u56DE\u6267\u5931\u8D25: ${sanitizeReleaseMessage(error?.message || error)}`);
1116
+ }
1117
+ }
1118
+ };
1119
+ function createProjectKey(projectRoot) {
1120
+ return createHash("sha256").update(path2.resolve(projectRoot)).digest("hex").slice(0, 24);
1121
+ }
1122
+ function createRemoteReceipt(result, data) {
1123
+ const identifiers = {};
1124
+ for (const key of REMOTE_IDENTIFIER_KEYS) {
1125
+ const value = data?.[key];
1126
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1127
+ identifiers[key] = typeof value === "string" ? sanitizeReleaseMessage(value, 240) : value;
1128
+ }
1129
+ }
1130
+ const consoleUrl = cleanHttpUrl(result.consoleUrl);
1131
+ const downloadUrl = cleanHttpUrl(result.url || result.link);
1132
+ if (!consoleUrl && !downloadUrl && Object.keys(identifiers).length === 0) return void 0;
1133
+ return {
1134
+ ...consoleUrl ? { consoleUrl } : {},
1135
+ ...downloadUrl ? { downloadUrl } : {},
1136
+ ...Object.keys(identifiers).length > 0 ? { identifiers } : {}
1137
+ };
1138
+ }
1139
+ function sanitizeTarget(target) {
1140
+ return {
1141
+ platform: sanitizeReleaseMessage(target.platform, 80) || "unknown",
1142
+ state: target.state,
1143
+ outcome: target.outcome,
1144
+ message: sanitizeReleaseMessage(target.message),
1145
+ ...target.remote ? { remote: sanitizeRemote(target.remote) } : {},
1146
+ ...cleanOptionalText(target.lastKnownRemoteStatus, 120) ? { lastKnownRemoteStatus: cleanOptionalText(target.lastKnownRemoteStatus, 120) } : {},
1147
+ ...cleanOptionalText(target.checkedAt, 80) ? { checkedAt: cleanOptionalText(target.checkedAt, 80) } : {}
1148
+ };
1149
+ }
1150
+ function sanitizeRemote(remote) {
1151
+ const identifiers = {};
1152
+ for (const [key, value] of Object.entries(remote.identifiers || {})) {
1153
+ if (!REMOTE_IDENTIFIER_KEYS.includes(key)) continue;
1154
+ if (typeof value === "string") identifiers[key] = sanitizeReleaseMessage(value, 240);
1155
+ if (typeof value === "number" || typeof value === "boolean") identifiers[key] = value;
1156
+ }
1157
+ const consoleUrl = cleanHttpUrl(remote.consoleUrl);
1158
+ const downloadUrl = cleanHttpUrl(remote.downloadUrl);
1159
+ return {
1160
+ ...consoleUrl ? { consoleUrl } : {},
1161
+ ...downloadUrl ? { downloadUrl } : {},
1162
+ ...Object.keys(identifiers).length > 0 ? { identifiers } : {}
1163
+ };
1164
+ }
1165
+ async function inspectArtifacts(value) {
1166
+ const paths = Array.from(new Set(flattenArtifactPaths(value).map((item) => path2.resolve(item))));
1167
+ return Promise.all(paths.map(inspectArtifact));
1168
+ }
1169
+ async function inspectArtifact(filePath) {
1170
+ const base = { path: filePath, name: path2.basename(filePath) };
1171
+ try {
1172
+ const info = await stat(filePath);
1173
+ if (!info.isFile()) return { ...base, available: false };
1174
+ return {
1175
+ ...base,
1176
+ sizeBytes: info.size,
1177
+ sha256: await hashFile(filePath),
1178
+ available: true
1179
+ };
1180
+ } catch {
1181
+ return { ...base, available: false };
1182
+ }
1183
+ }
1184
+ function hashFile(filePath) {
1185
+ return new Promise((resolve, reject) => {
1186
+ const hash = createHash("sha256");
1187
+ const stream = createReadStream(filePath);
1188
+ stream.on("error", reject);
1189
+ stream.on("data", (chunk) => hash.update(chunk));
1190
+ stream.on("end", () => resolve(hash.digest("hex")));
1191
+ });
1192
+ }
1193
+ function flattenArtifactPaths(value) {
1194
+ if (typeof value === "string" && value.trim()) return [value];
1195
+ if (Array.isArray(value)) return value.flatMap(flattenArtifactPaths);
1196
+ if (isRecord(value)) return Object.values(value).flatMap(flattenArtifactPaths);
1197
+ return [];
1198
+ }
1199
+ function inferOutcome(targets) {
1200
+ if (targets.length === 0) return "failed";
1201
+ if (targets.every((target) => target.outcome === "cancelled")) return "cancelled";
1202
+ const successCount = targets.filter((target) => target.outcome === "success").length;
1203
+ if (successCount === targets.length) return "success";
1204
+ if (successCount > 0) return "partial";
1205
+ return "failed";
1206
+ }
1207
+ async function canonicalizeProjectRoot(projectRoot) {
1208
+ const resolved = path2.resolve(projectRoot);
1209
+ try {
1210
+ return await realpath(resolved);
1211
+ } catch {
1212
+ return resolved;
1213
+ }
1214
+ }
1215
+ function toDate(value) {
1216
+ const date = value instanceof Date ? value : new Date(value);
1217
+ return Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
1218
+ }
1219
+ function cleanOptionalText(value, maxLength) {
1220
+ const cleaned = sanitizeReleaseMessage(value, maxLength);
1221
+ return cleaned || void 0;
1222
+ }
1223
+ function cleanHttpUrl(value) {
1224
+ if (typeof value !== "string") return void 0;
1225
+ try {
1226
+ const url = new URL(value);
1227
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
1228
+ for (const key of Array.from(url.searchParams.keys())) {
1229
+ if (/token|secret|password|signature|credential|key/i.test(key)) {
1230
+ url.searchParams.set(key, "[\u5DF2\u8131\u654F]");
1231
+ }
1232
+ }
1233
+ return url.toString();
1234
+ } catch {
1235
+ return void 0;
1236
+ }
1237
+ }
1238
+ function isRecord(value) {
1239
+ return !!value && typeof value === "object" && !Array.isArray(value);
1240
+ }
1241
+
968
1242
  // src/adapters/pgyer_uploader.ts
969
1243
  var PgyerUploader = class _PgyerUploader {
970
1244
  name = "Pgyer";
@@ -1083,7 +1357,7 @@ var PgyerUploader = class _PgyerUploader {
1083
1357
 
1084
1358
  // src/adapters/huawei_uploader.ts
1085
1359
  import fs4 from "fs-extra";
1086
- import path2 from "path";
1360
+ import path3 from "path";
1087
1361
  import { promisify } from "util";
1088
1362
  import axios2 from "axios";
1089
1363
  import FormData2 from "form-data";
@@ -1162,7 +1436,7 @@ var HuaweiUploader = class _HuaweiUploader {
1162
1436
  const clientSecret = hwConfig.auth.clientSecret;
1163
1437
  const appId = hwConfig.appId;
1164
1438
  const releaseType = hwConfig.releaseType || "draft";
1165
- const suffix = path2.extname(filePath).substring(1);
1439
+ const suffix = path3.extname(filePath).substring(1);
1166
1440
  const retryConfig = config.retry;
1167
1441
  const tokenUrl = this.getTokenUrl(config);
1168
1442
  const publishApiRoot = this.getApiRoot(config);
@@ -1188,8 +1462,8 @@ var HuaweiUploader = class _HuaweiUploader {
1188
1462
  const uploadResults = [];
1189
1463
  const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
1190
1464
  if (!p || !fs4.existsSync(p)) return;
1191
- const currentSuffix = path2.extname(p).substring(1);
1192
- logger.info(`[Huawei] \u6B63\u5728\u4E0A\u4F20 ${abi} \u5305: ${path2.basename(p)}`);
1465
+ const currentSuffix = path3.extname(p).substring(1);
1466
+ logger.info(`[Huawei] \u6B63\u5728\u4E0A\u4F20 ${abi} \u5305: ${path3.basename(p)}`);
1193
1467
  const uploadInfo = await retryStrategy.execute(
1194
1468
  () => this.getUploadUrl(ctx, currentSuffix),
1195
1469
  `Huawei get upload URL for ${abi}`
@@ -1205,7 +1479,7 @@ var HuaweiUploader = class _HuaweiUploader {
1205
1479
  throw uploadFileRes.error || new Error(`File upload failed for ${abi}`);
1206
1480
  }
1207
1481
  uploadResults.push({
1208
- fileName: path2.basename(p),
1482
+ fileName: path3.basename(p),
1209
1483
  fileDestUrl: uploadFileRes.data[0].fileDestUrl
1210
1484
  });
1211
1485
  });
@@ -1377,7 +1651,7 @@ var HuaweiUploader = class _HuaweiUploader {
1377
1651
  }
1378
1652
  /** 通用文件上传(FormData 方式,无需 API 认证头) */
1379
1653
  async executeUploadRequest(uploadUrl, authCode, filePath) {
1380
- const fileName = path2.basename(filePath);
1654
+ const fileName = path3.basename(filePath);
1381
1655
  const fileSize = fs4.statSync(filePath).size;
1382
1656
  const form = new FormData2();
1383
1657
  form.append("file", fs4.createReadStream(filePath), {
@@ -1544,7 +1818,7 @@ var HuaweiUploader = class _HuaweiUploader {
1544
1818
  logger.warn(`Asset file not found, skipping: ${filePath}`);
1545
1819
  continue;
1546
1820
  }
1547
- const suffix = path2.extname(filePath).substring(1).toLowerCase();
1821
+ const suffix = path3.extname(filePath).substring(1).toLowerCase();
1548
1822
  if (!HUAWEI_SUPPORTED_SUFFIXES.image.includes(suffix)) {
1549
1823
  logger.warn(`Unsupported file type ${suffix}, skipping: ${filePath}`);
1550
1824
  continue;
@@ -1599,7 +1873,7 @@ var HuaweiUploader = class _HuaweiUploader {
1599
1873
 
1600
1874
  // src/adapters/xiaomi_uploader.ts
1601
1875
  import fs5 from "fs-extra";
1602
- import path3 from "path";
1876
+ import path4 from "path";
1603
1877
  import axios3 from "axios";
1604
1878
  import FormData3 from "form-data";
1605
1879
  import crypto3 from "crypto";
@@ -1927,14 +2201,14 @@ ${lines.join("\n")}
1927
2201
  if (arm64 && arm32) {
1928
2202
  mainPath = arm64[1];
1929
2203
  secondPath = arm32[1];
1930
- logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u67B6\u6784\u5206\u5305: \u4E3B\u5305(64\u4F4D)=${path3.basename(mainPath)}, \u6B21\u5305(32\u4F4D)=${path3.basename(secondPath)}`);
2204
+ logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u67B6\u6784\u5206\u5305: \u4E3B\u5305(64\u4F4D)=${path4.basename(mainPath)}, \u6B21\u5305(32\u4F4D)=${path4.basename(secondPath)}`);
1931
2205
  } else {
1932
2206
  mainPath = artifactEntries[0][1];
1933
2207
  if (artifactEntries.length > 1) {
1934
2208
  secondPath = artifactEntries[1][1];
1935
- logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u4E2A\u5305\uFF08\u975E\u6807\u51C6\u540D\uFF09\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u987A\u5E8F: \u4E3B\u5305=${path3.basename(mainPath)}, \u6B21\u5305=${path3.basename(secondPath)}`);
2209
+ logger.info(`[Xiaomi] \u8BC6\u522B\u5230\u591A\u4E2A\u5305\uFF08\u975E\u6807\u51C6\u540D\uFF09\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u987A\u5E8F: \u4E3B\u5305=${path4.basename(mainPath)}, \u6B21\u5305=${path4.basename(secondPath)}`);
1936
2210
  } else {
1937
- logger.info(`[Xiaomi] \u4F7F\u7528\u5355\u5305\u6A21\u5F0F: ${path3.basename(mainPath)}`);
2211
+ logger.info(`[Xiaomi] \u4F7F\u7528\u5355\u5305\u6A21\u5F0F: ${path4.basename(mainPath)}`);
1938
2212
  }
1939
2213
  }
1940
2214
  const mainMD5 = await this.calculateFileMD5(mainPath);
@@ -2008,7 +2282,7 @@ ${lines.join("\n")}
2008
2282
 
2009
2283
  // src/adapters/apple_store_uploader.ts
2010
2284
  import fs6 from "fs-extra";
2011
- import path4 from "path";
2285
+ import path5 from "path";
2012
2286
  import { exec as exec2 } from "child_process";
2013
2287
  import { promisify as promisify2 } from "util";
2014
2288
  import axios4 from "axios";
@@ -2174,7 +2448,7 @@ var AppleStoreUploader = class {
2174
2448
  appPassword,
2175
2449
  "-showTransferRate"
2176
2450
  ].join(" ");
2177
- logger.info(`[Transporter] Uploading: ${path4.basename(ipaPath)}`);
2451
+ logger.info(`[Transporter] Uploading: ${path5.basename(ipaPath)}`);
2178
2452
  try {
2179
2453
  const { stdout, stderr } = await execAsync(command, {
2180
2454
  timeout: 30 * 60 * 1e3,
@@ -2225,7 +2499,7 @@ Stderr: ${error.stderr || ""}`);
2225
2499
  });
2226
2500
  command = innerArgs.join(" ");
2227
2501
  }
2228
- logger.info(`[altool] Uploading: ${path4.basename(ipaPath)}`);
2502
+ logger.info(`[altool] Uploading: ${path5.basename(ipaPath)}`);
2229
2503
  logger.info(`[altool] Command prefix: ${tool.command}`);
2230
2504
  try {
2231
2505
  const { stdout, stderr } = await execAsync(command, {
@@ -2809,7 +3083,7 @@ var TestFlightUploader = class _TestFlightUploader {
2809
3083
 
2810
3084
  // src/adapters/adhoc_uploader.ts
2811
3085
  import fs8 from "fs-extra";
2812
- import path5 from "path";
3086
+ import path6 from "path";
2813
3087
  import { execSync } from "child_process";
2814
3088
  var AdHocUploader = class {
2815
3089
  name = "AdHoc";
@@ -2942,8 +3216,8 @@ var AdHocUploader = class {
2942
3216
  * @param changelog 更新日志
2943
3217
  */
2944
3218
  async createManifest(ipaPath, version, bundleId, changelog) {
2945
- const ipaFileName = path5.basename(ipaPath);
2946
- const appName = bundleId || path5.parse(ipaFileName).name;
3219
+ const ipaFileName = path6.basename(ipaPath);
3220
+ const appName = bundleId || path6.parse(ipaFileName).name;
2947
3221
  const ipaUrl = `itms-services://?action=download-manifest&url=https://your-distribution-server.com/manifests/${ipaFileName}.plist`;
2948
3222
  const manifestContent = `<?xml version="1.0" encoding="UTF-8"?>
2949
3223
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -3002,14 +3276,14 @@ var AdHocUploader = class {
3002
3276
  */
3003
3277
  async setupDistributionDirectory(ipaPath, manifestPath) {
3004
3278
  const projectRoot = process.cwd();
3005
- const distributionDir = path5.join(projectRoot, "build", "adhoc-distribution");
3279
+ const distributionDir = path6.join(projectRoot, "build", "adhoc-distribution");
3006
3280
  await fs8.ensureDir(distributionDir);
3007
- await fs8.ensureDir(path5.join(distributionDir, "apps"));
3008
- await fs8.ensureDir(path5.join(distributionDir, "manifests"));
3009
- await fs8.ensureDir(path5.join(distributionDir, "icons"));
3010
- const ipaFileName = path5.basename(ipaPath);
3011
- await fs8.copy(ipaPath, path5.join(distributionDir, "apps", ipaFileName));
3012
- await fs8.copy(manifestPath, path5.join(distributionDir, "manifests", `${ipaFileName}.plist`));
3281
+ await fs8.ensureDir(path6.join(distributionDir, "apps"));
3282
+ await fs8.ensureDir(path6.join(distributionDir, "manifests"));
3283
+ await fs8.ensureDir(path6.join(distributionDir, "icons"));
3284
+ const ipaFileName = path6.basename(ipaPath);
3285
+ await fs8.copy(ipaPath, path6.join(distributionDir, "apps", ipaFileName));
3286
+ await fs8.copy(manifestPath, path6.join(distributionDir, "manifests", `${ipaFileName}.plist`));
3013
3287
  logger.info(`Distribution directory: ${distributionDir}`);
3014
3288
  return distributionDir;
3015
3289
  }
@@ -3019,7 +3293,7 @@ var AdHocUploader = class {
3019
3293
  * @param bundleId Bundle ID
3020
3294
  */
3021
3295
  generateInstallUrl(distributionPath, bundleId) {
3022
- const manifestPath = path5.join(distributionPath, "manifests", `*.plist`);
3296
+ const manifestPath = path6.join(distributionPath, "manifests", `*.plist`);
3023
3297
  return `itms-services://?action=download-manifest&url=file://${manifestPath}`;
3024
3298
  }
3025
3299
  /**
@@ -3028,7 +3302,7 @@ var AdHocUploader = class {
3028
3302
  * @param distributionPath 分发目录
3029
3303
  */
3030
3304
  async generateQRCode(url, distributionPath) {
3031
- const qrCodePath = path5.join(distributionPath, "install-qrcode.png");
3305
+ const qrCodePath = path6.join(distributionPath, "install-qrcode.png");
3032
3306
  try {
3033
3307
  logger.info(`QR code would be saved to: ${qrCodePath}`);
3034
3308
  logger.info(`Install URL: ${url}`);
@@ -3055,8 +3329,8 @@ var AdHocUploader = class {
3055
3329
  logger.newLine();
3056
3330
  logger.info("\u{1F4E6} Distribution Files:");
3057
3331
  logger.info(` Directory: ${distributionPath}`);
3058
- logger.info(` IPA: ${path5.join(distributionPath, "apps", "*.ipa")}`);
3059
- logger.info(` Manifest: ${path5.join(distributionPath, "manifests", "*.plist")}`);
3332
+ logger.info(` IPA: ${path6.join(distributionPath, "apps", "*.ipa")}`);
3333
+ logger.info(` Manifest: ${path6.join(distributionPath, "manifests", "*.plist")}`);
3060
3334
  logger.info(` QR Code: ${qrCodePath}`);
3061
3335
  logger.newLine();
3062
3336
  logger.info("\u{1F517} Installation Link:");
@@ -3115,7 +3389,7 @@ var AdHocUploader = class {
3115
3389
 
3116
3390
  // src/adapters/harmony_uploader.ts
3117
3391
  import fs9 from "fs-extra";
3118
- import path6 from "path";
3392
+ import path7 from "path";
3119
3393
  import axios6 from "axios";
3120
3394
  import FormData5 from "form-data";
3121
3395
  var HarmonyUploader = class _HarmonyUploader {
@@ -3145,7 +3419,7 @@ var HarmonyUploader = class _HarmonyUploader {
3145
3419
  const clientSecret = harmonyConfig.auth.clientSecret;
3146
3420
  const appId = harmonyConfig.appId;
3147
3421
  const releaseType = harmonyConfig.releaseType || "draft";
3148
- const suffix = path6.extname(filePath).substring(1);
3422
+ const suffix = path7.extname(filePath).substring(1);
3149
3423
  const retryConfig = config.retry;
3150
3424
  logger.info("Starting HarmonyOS AppGallery upload...");
3151
3425
  try {
@@ -3262,7 +3536,7 @@ var HarmonyUploader = class _HarmonyUploader {
3262
3536
  * @param filePath 文件路径
3263
3537
  */
3264
3538
  async updateAppFileInfo(token, appId, authCode, filePath) {
3265
- const fileName = path6.basename(filePath);
3539
+ const fileName = path7.basename(filePath);
3266
3540
  const body = {
3267
3541
  fileType: 7,
3268
3542
  // 鸿蒙包的 fileType(需要根据华为最新文档确认)
@@ -3285,7 +3559,7 @@ var HarmonyUploader = class _HarmonyUploader {
3285
3559
 
3286
3560
  // src/adapters/oppo_uploader.ts
3287
3561
  import fs10 from "fs-extra";
3288
- import path7 from "path";
3562
+ import path8 from "path";
3289
3563
  import axios7 from "axios";
3290
3564
  import FormData6 from "form-data";
3291
3565
  import crypto4 from "crypto";
@@ -3326,7 +3600,7 @@ var OppoUploader = class _OppoUploader {
3326
3600
  * 尝试从文件名推断包名
3327
3601
  */
3328
3602
  inferPackageName(filePath) {
3329
- const fileName = path7.basename(filePath);
3603
+ const fileName = path8.basename(filePath);
3330
3604
  const match = fileName.match(/^([\w.]+?)(?:-(?:release|debug|unsigned))?\.apk$/i);
3331
3605
  if (match && match[1].includes(".")) {
3332
3606
  return match[1];
@@ -3607,7 +3881,7 @@ var OppoUploader = class _OppoUploader {
3607
3881
 
3608
3882
  // src/adapters/vivo_uploader.ts
3609
3883
  import fs11 from "fs-extra";
3610
- import path8 from "path";
3884
+ import path9 from "path";
3611
3885
  import axios8 from "axios";
3612
3886
  import FormData7 from "form-data";
3613
3887
  import crypto5 from "crypto";
@@ -3639,7 +3913,7 @@ var VivoUploader = class _VivoUploader {
3639
3913
  const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
3640
3914
  if (!p || !fs11.existsSync(p)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
3641
3915
  const md5 = await this.calculateFileMD5(p);
3642
- logger.info(`[vivo] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${path8.basename(p)}`);
3916
+ logger.info(`[vivo] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${path9.basename(p)}`);
3643
3917
  const serialnumber = await this.uploadApk(accessKey, accessSecret, p, vivoConfig.packageName || "", md5);
3644
3918
  const pkgInfo = await parsePackageInfo(p, { androidSdkPath: config.globalSettings?.androidSdkPath });
3645
3919
  const nativeCodes = pkgInfo?.nativeCode || [];
@@ -3703,7 +3977,7 @@ var VivoUploader = class _VivoUploader {
3703
3977
  const form = new FormData7();
3704
3978
  Object.entries(params).forEach(([k, v]) => form.append(k, v));
3705
3979
  form.append("sign", sign);
3706
- form.append("file", fs11.createReadStream(filePath), { filename: path8.basename(filePath) });
3980
+ form.append("file", fs11.createReadStream(filePath), { filename: path9.basename(filePath) });
3707
3981
  const res = await axios8.post(_VivoUploader.API_BASE, form, { headers: form.getHeaders(), timeout: 3e5 });
3708
3982
  if (res.data.code !== 0) throw new Error(`\u4E0A\u4F20\u5305\u5931\u8D25: ${res.data.msg}`);
3709
3983
  return res.data.data?.serialnumber || res.data.data;
@@ -3743,7 +4017,7 @@ var VivoUploader = class _VivoUploader {
3743
4017
 
3744
4018
  // src/adapters/tencent_uploader.ts
3745
4019
  import fs12 from "fs-extra";
3746
- import path9 from "path";
4020
+ import path10 from "path";
3747
4021
  import axios9 from "axios";
3748
4022
  import crypto6 from "crypto";
3749
4023
  var TencentUploader = class _TencentUploader {
@@ -3783,7 +4057,7 @@ var TencentUploader = class _TencentUploader {
3783
4057
  const uploadPromises = Object.entries(artifacts).map(async ([abi, p]) => {
3784
4058
  if (!p || !fs12.existsSync(p)) throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${p}`);
3785
4059
  const stats = fs12.statSync(p);
3786
- const fileName = path9.basename(p);
4060
+ const fileName = path10.basename(p);
3787
4061
  logger.info(`[Tencent] \u6B63\u5728\u5904\u7406 ${abi} \u67B6\u6784\u5305: ${fileName}`);
3788
4062
  const uploadInfo = await this.getFileUploadInfo(auth, stats.size, fileName, pkgName);
3789
4063
  await this.uploadToCos(uploadInfo.pre_sign_url, p, stats.size);
@@ -3987,6 +4261,225 @@ var TencentUploader = class _TencentUploader {
3987
4261
  }
3988
4262
  };
3989
4263
 
4264
+ // src/adapters/google_play_uploader.ts
4265
+ import fs13 from "fs-extra";
4266
+ import path11 from "path";
4267
+ import axios10 from "axios";
4268
+ import { GoogleAuth } from "google-auth-library";
4269
+ var ANDROID_PUBLISHER_SCOPE = "https://www.googleapis.com/auth/androidpublisher";
4270
+ var PUBLISH_API_ROOT = "https://androidpublisher.googleapis.com/androidpublisher/v3";
4271
+ var UPLOAD_API_ROOT = "https://androidpublisher.googleapis.com/upload/androidpublisher/v3";
4272
+ var GOOGLE_PLAY_CONSOLE_URL = "https://play.google.com/console/developers";
4273
+ var GOOGLE_PLAY_INTERNAL_API_TRACK = "internal";
4274
+ var UPLOAD_TIMEOUT_MS = 5 * 60 * 1e3;
4275
+ var GooglePlayUploader = class {
4276
+ name = "Google Play";
4277
+ httpClient;
4278
+ getAccessToken;
4279
+ constructor(dependencies = {}) {
4280
+ this.httpClient = dependencies.httpClient || axios10;
4281
+ this.getAccessToken = dependencies.getAccessToken || this.createAccessToken.bind(this);
4282
+ }
4283
+ validateConfig(config) {
4284
+ const googleConfig = config?.publish?.google_play;
4285
+ if (!googleConfig?.enable) return false;
4286
+ if (!googleConfig.packageName || typeof googleConfig.packageName !== "string") {
4287
+ logger.error("Google Play \u914D\u7F6E\u7F3A\u5C11 packageName");
4288
+ return false;
4289
+ }
4290
+ const track = googleConfig.track || "internal";
4291
+ if (track !== "internal") {
4292
+ logger.error("Google Play \u9996\u7248\u4EC5\u652F\u6301 internal \u8F68\u9053");
4293
+ return false;
4294
+ }
4295
+ const auth = googleConfig.auth;
4296
+ const hasCredentialContent = typeof auth?.serviceAccountJsonContent === "string" && auth.serviceAccountJsonContent.trim().length > 0;
4297
+ const credentialPath = auth?.serviceAccountJsonPath;
4298
+ const hasCredentialPath = typeof credentialPath === "string" && credentialPath.trim().length > 0 && fs13.existsSync(path11.resolve(credentialPath));
4299
+ if (!hasCredentialContent && !hasCredentialPath) {
4300
+ logger.error("Google Play \u914D\u7F6E\u7F3A\u5C11\u6709\u6548\u7684\u670D\u52A1\u8D26\u53F7 JSON \u6587\u4EF6\u6216\u5185\u5BB9");
4301
+ return false;
4302
+ }
4303
+ return true;
4304
+ }
4305
+ canHandle(filePath) {
4306
+ return path11.extname(filePath).toLowerCase() === ".aab";
4307
+ }
4308
+ async upload(filePath, config, _metadata) {
4309
+ if (!this.validateConfig(config)) {
4310
+ return this.failure("Google Play \u914D\u7F6E\u65E0\u6548\uFF0C\u8BF7\u68C0\u67E5\u5305\u540D\u3001internal \u8F68\u9053\u548C\u670D\u52A1\u8D26\u53F7\u6587\u4EF6");
4311
+ }
4312
+ if (!this.canHandle(filePath)) {
4313
+ return this.failure("Google Play \u4EC5\u63A5\u53D7 Android App Bundle\uFF08.aab\uFF09");
4314
+ }
4315
+ if (!fs13.existsSync(filePath)) {
4316
+ return this.failure(`\u672A\u627E\u5230\u5F85\u4E0A\u4F20\u7684 AAB\uFF1A${filePath}`);
4317
+ }
4318
+ const googleConfig = config.publish.google_play;
4319
+ const packageName = googleConfig.packageName.trim();
4320
+ const encodedPackageName = encodeURIComponent(packageName);
4321
+ let editId;
4322
+ let accessToken;
4323
+ let committed = false;
4324
+ let stage = "create_edit";
4325
+ logger.info(`\u51C6\u5907\u5C06 ${path11.basename(filePath)} \u4E0A\u4F20\u5230 Google Play internal \u8349\u7A3F`);
4326
+ try {
4327
+ accessToken = await this.getAccessToken(googleConfig.auth);
4328
+ const requestConfig = {
4329
+ headers: { Authorization: `Bearer ${accessToken}` }
4330
+ };
4331
+ const editResponse = await this.httpClient.post(
4332
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits`,
4333
+ {},
4334
+ requestConfig
4335
+ );
4336
+ editId = editResponse.data.id;
4337
+ if (!editId) {
4338
+ throw new Error("Google Play \u672A\u8FD4\u56DE editId");
4339
+ }
4340
+ stage = "upload_bundle";
4341
+ const encodedEditId = encodeURIComponent(editId);
4342
+ const bundleResponse = await this.httpClient.post(
4343
+ `${UPLOAD_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}/bundles?uploadType=media`,
4344
+ fs13.createReadStream(filePath),
4345
+ {
4346
+ timeout: UPLOAD_TIMEOUT_MS,
4347
+ maxBodyLength: Infinity,
4348
+ maxContentLength: Infinity,
4349
+ headers: {
4350
+ Authorization: `Bearer ${accessToken}`,
4351
+ "Content-Type": "application/octet-stream",
4352
+ "Content-Length": fs13.statSync(filePath).size
4353
+ }
4354
+ }
4355
+ );
4356
+ const versionCode = bundleResponse.data.versionCode;
4357
+ if (!Number.isInteger(versionCode)) {
4358
+ throw new Error("Google Play \u4E0A\u4F20\u6210\u529F\u4F46\u672A\u8FD4\u56DE\u6709\u6548\u7684 versionCode");
4359
+ }
4360
+ stage = "update_internal_track";
4361
+ await this.httpClient.put(
4362
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}/tracks/${GOOGLE_PLAY_INTERNAL_API_TRACK}`,
4363
+ {
4364
+ track: GOOGLE_PLAY_INTERNAL_API_TRACK,
4365
+ releases: [
4366
+ {
4367
+ versionCodes: [String(versionCode)],
4368
+ status: "draft"
4369
+ }
4370
+ ]
4371
+ },
4372
+ requestConfig
4373
+ );
4374
+ stage = "validate_edit";
4375
+ await this.httpClient.post(
4376
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}:validate`,
4377
+ {},
4378
+ requestConfig
4379
+ );
4380
+ stage = "commit_edit";
4381
+ await this.httpClient.post(
4382
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodedEditId}:commit`,
4383
+ {},
4384
+ requestConfig
4385
+ );
4386
+ committed = true;
4387
+ logger.success(`Google Play internal \u8349\u7A3F\u5DF2\u521B\u5EFA\uFF08versionCode ${versionCode}\uFF09`);
4388
+ return {
4389
+ platform: "google_play",
4390
+ success: true,
4391
+ message: `AAB \u5DF2\u4E0A\u4F20\u5230 Google Play internal \u8349\u7A3F\uFF08versionCode ${versionCode}\uFF09`,
4392
+ consoleUrl: GOOGLE_PLAY_CONSOLE_URL,
4393
+ data: {
4394
+ editId,
4395
+ versionCode,
4396
+ track: "internal",
4397
+ apiTrack: GOOGLE_PLAY_INTERNAL_API_TRACK,
4398
+ status: "draft",
4399
+ committed: true
4400
+ }
4401
+ };
4402
+ } catch (error) {
4403
+ if (editId && accessToken && !committed) {
4404
+ await this.deleteEditBestEffort(encodedPackageName, editId, accessToken);
4405
+ }
4406
+ const message = this.describeError(error, stage);
4407
+ logger.error(`Google Play \u4E0A\u4F20\u5931\u8D25\uFF1A${message}`);
4408
+ return this.failure(message, error);
4409
+ }
4410
+ }
4411
+ async createAccessToken(authConfig) {
4412
+ let credentials;
4413
+ if (authConfig.serviceAccountJsonContent?.trim()) {
4414
+ try {
4415
+ credentials = JSON.parse(authConfig.serviceAccountJsonContent);
4416
+ } catch {
4417
+ throw new Error("\u670D\u52A1\u8D26\u53F7 JSON \u5185\u5BB9\u683C\u5F0F\u65E0\u6548");
4418
+ }
4419
+ }
4420
+ const keyFile = !credentials && authConfig.serviceAccountJsonPath ? path11.resolve(authConfig.serviceAccountJsonPath) : void 0;
4421
+ const auth = new GoogleAuth({
4422
+ scopes: [ANDROID_PUBLISHER_SCOPE],
4423
+ keyFile,
4424
+ credentials
4425
+ });
4426
+ const token = await auth.getAccessToken();
4427
+ if (!token) {
4428
+ throw new Error("Google Play \u8BA4\u8BC1\u672A\u8FD4\u56DE\u8BBF\u95EE\u4EE4\u724C");
4429
+ }
4430
+ return token;
4431
+ }
4432
+ async deleteEditBestEffort(encodedPackageName, editId, accessToken) {
4433
+ try {
4434
+ await this.httpClient.delete(
4435
+ `${PUBLISH_API_ROOT}/applications/${encodedPackageName}/edits/${encodeURIComponent(editId)}`,
4436
+ { headers: { Authorization: `Bearer ${accessToken}` } }
4437
+ );
4438
+ } catch {
4439
+ logger.warn("Google Play \u672A\u63D0\u4EA4 edit \u7684\u81EA\u52A8\u6E05\u7406\u5931\u8D25\uFF0C\u8BF7\u5728 Play Console \u4E2D\u68C0\u67E5\u8349\u7A3F\u72B6\u6001");
4440
+ }
4441
+ }
4442
+ describeError(error, stage) {
4443
+ const status = error?.response?.status;
4444
+ const rawMessage = error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || "\u672A\u77E5\u9519\u8BEF";
4445
+ const safeMessage = this.redactAndTruncate(String(rawMessage));
4446
+ if (/version\s*code|versionCode/i.test(safeMessage)) {
4447
+ return `Google Play \u62D2\u7EDD\u8BE5 versionCode\uFF0C\u8BF7\u63D0\u9AD8 Flutter \u6784\u5EFA\u53F7\u540E\u91CD\u65B0\u751F\u6210 AAB\uFF1A${safeMessage}`;
4448
+ }
4449
+ if (status === 401) {
4450
+ return "Google Play \u8BA4\u8BC1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u8D26\u53F7 JSON \u662F\u5426\u6709\u6548";
4451
+ }
4452
+ if (status === 403) {
4453
+ return "Google Play \u62D2\u7EDD\u8BBF\u95EE\uFF0C\u8BF7\u786E\u8BA4\u670D\u52A1\u8D26\u53F7\u5DF2\u52A0\u5165 Play Console \u5E76\u5177\u6709\u5185\u90E8\u6D4B\u8BD5\u7248\u672C\u4E0A\u4F20\u6743\u9650";
4454
+ }
4455
+ if (status === 404) {
4456
+ if (stage === "update_internal_track") {
4457
+ return "Google Play \u4E2D\u672A\u627E\u5230 internal \u5185\u90E8\u6D4B\u8BD5\u8F68\u9053\uFF0C\u8BF7\u786E\u8BA4\u5E94\u7528\u5DF2\u5728 Play Console \u521B\u5EFA\u5185\u90E8\u6D4B\u8BD5\u8F68\u9053";
4458
+ }
4459
+ if (stage !== "create_edit") {
4460
+ return `Google Play API \u5728 ${stage} \u9636\u6BB5\u672A\u627E\u5230\u76EE\u6807\u8D44\u6E90\uFF1A${safeMessage}`;
4461
+ }
4462
+ return "Google Play \u4E2D\u672A\u627E\u5230\u53EF\u4F9B API \u7F16\u8F91\u7684\u5E94\u7528\u3002\u8BF7\u786E\u8BA4 packageName \u5B8C\u5168\u4E00\u81F4\uFF1B\u82E5\u8FD9\u662F\u9996\u6B21\u4E0A\u4F20\uFF0C\u8BF7\u5148\u5728 Play Console \u624B\u5DE5\u4E0A\u4F20\u5E76\u4FDD\u5B58\u4E00\u4E2A APK/AAB\uFF0C\u518D\u91CD\u8BD5 API \u4E0A\u4F20";
4463
+ }
4464
+ if (error?.code === "ECONNABORTED" || /timeout/i.test(safeMessage)) {
4465
+ return "Google Play \u4E0A\u4F20\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\uFF1B\u672C\u5730 AAB \u672A\u88AB\u5220\u9664";
4466
+ }
4467
+ return `Google Play API \u8BF7\u6C42\u5931\u8D25${status ? `\uFF08HTTP ${status}\uFF09` : ""}\uFF1A${safeMessage}`;
4468
+ }
4469
+ redactAndTruncate(message) {
4470
+ return message.replace(/-----BEGIN PRIVATE KEY-----[\s\S]*?-----END PRIVATE KEY-----/gi, "[\u5DF2\u8131\u654F\u79C1\u94A5]").replace(/Bearer\s+[^\s"']+/gi, "Bearer [\u5DF2\u8131\u654F]").replace(/"private_key"\s*:\s*"[^"]*"/gi, '"private_key":"[\u5DF2\u8131\u654F]"').replace(/"access_token"\s*:\s*"[^"]*"/gi, '"access_token":"[\u5DF2\u8131\u654F]"').slice(0, 500);
4471
+ }
4472
+ failure(message, error) {
4473
+ return {
4474
+ platform: "google_play",
4475
+ success: false,
4476
+ message,
4477
+ // 原始 Axios 错误可能包含 Authorization 请求头,只返回脱敏后的 Error。
4478
+ error: error ? new Error(message) : void 0
4479
+ };
4480
+ }
4481
+ };
4482
+
3990
4483
  // src/utils/upload-manager.ts
3991
4484
  var GROUPED_ARTIFACT_PLATFORMS = /* @__PURE__ */ new Set(["huawei", "xiaomi", "oppo", "vivo", "tencent"]);
3992
4485
  var UploadManager = class {
@@ -4010,6 +4503,7 @@ var UploadManager = class {
4010
4503
  this.registerUploader("oppo", new OppoUploader());
4011
4504
  this.registerUploader("vivo", new VivoUploader());
4012
4505
  this.registerUploader("tencent", new TencentUploader());
4506
+ this.registerUploader("google_play", new GooglePlayUploader());
4013
4507
  }
4014
4508
  /**
4015
4509
  * 注册上传器
@@ -4028,6 +4522,8 @@ var UploadManager = class {
4028
4522
  if (config?.publish?.oppo?.enable) enabled.push("oppo");
4029
4523
  if (config?.publish?.vivo?.enable) enabled.push("vivo");
4030
4524
  if (config?.publish?.tencent?.enable) enabled.push("tencent");
4525
+ if (config?.publish?.harmony?.enable) enabled.push("harmony");
4526
+ if (config?.publish?.google_play?.enable) enabled.push("google_play");
4031
4527
  if (config?.publish?.app_store?.enable) {
4032
4528
  const mode = config.publish.app_store.mode || "app_store";
4033
4529
  enabled.push(mode);
@@ -4128,7 +4624,7 @@ var UploadManager = class {
4128
4624
  }
4129
4625
  logger.info(`Scheduled ${tasks.length} upload task(s) for ${validPlatforms.length} platform(s)`);
4130
4626
  const uploadPromises = tasks.map((task) => {
4131
- logger.info(`[${task.platform}] Matched file: ${path10.basename(task.file)}`);
4627
+ logger.info(`[${task.platform}] Matched file: ${path12.basename(task.file)}`);
4132
4628
  return this.uploadToPlatform(
4133
4629
  task.platform,
4134
4630
  task.file,
@@ -4171,7 +4667,7 @@ var UploadManager = class {
4171
4667
  artifactPath: artifactSelection
4172
4668
  };
4173
4669
  await this.validateArtifactSelection(platform, artifactSelection, effectiveMetadata);
4174
- const pkgInfo = await parsePackageInfo(filePath, {
4670
+ const pkgInfo = path12.extname(filePath).toLowerCase() === ".aab" ? null : await parsePackageInfo(filePath, {
4175
4671
  androidSdkPath: effectiveMetadata.globalSettings?.androidSdkPath
4176
4672
  });
4177
4673
  if (pkgInfo) {
@@ -4233,7 +4729,7 @@ var UploadManager = class {
4233
4729
  );
4234
4730
  }
4235
4731
  inferArtifactKey(filePath, index) {
4236
- const fileName = path10.basename(filePath).toLowerCase();
4732
+ const fileName = path12.basename(filePath).toLowerCase();
4237
4733
  if (fileName.includes("arm64-v8a")) return "arm64-v8a";
4238
4734
  if (fileName.includes("armeabi-v7a")) return "armeabi-v7a";
4239
4735
  if (fileName.includes("x86_64")) return "x86_64";
@@ -4390,7 +4886,7 @@ var UploadManager = class {
4390
4886
  };
4391
4887
 
4392
4888
  // src/validators/config_validator.ts
4393
- import fs13 from "fs-extra";
4889
+ import fs14 from "fs-extra";
4394
4890
  import yaml from "js-yaml";
4395
4891
 
4396
4892
  // src/validators/env_validator.ts
@@ -4416,20 +4912,22 @@ var ConfigValidator = class {
4416
4912
  * @param filePath 配置文件路径
4417
4913
  */
4418
4914
  static loadAndValidate(filePath) {
4419
- if (!fs13.existsSync(filePath)) {
4915
+ if (!fs14.existsSync(filePath)) {
4420
4916
  logger.error(`Config file not found: ${filePath}`);
4421
4917
  return null;
4422
4918
  }
4423
- let content = fs13.readFileSync(filePath, "utf8");
4424
- const envRegex = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
4425
- const matches = [...content.matchAll(envRegex)];
4426
- const requiredEnvs = Array.from(new Set(matches.map((m) => m[1])));
4427
- if (!EnvValidator.validate(requiredEnvs)) {
4428
- return null;
4429
- }
4430
- content = content.replace(envRegex, (_, varName) => process.env[varName] || "");
4431
4919
  try {
4432
- const config = yaml.load(content);
4920
+ const content = fs14.readFileSync(filePath, "utf8");
4921
+ const rawConfig = yaml.load(content);
4922
+ if (!rawConfig || typeof rawConfig !== "object") {
4923
+ logger.error("Config file must contain a YAML object");
4924
+ return null;
4925
+ }
4926
+ const requiredEnvs = this.collectRequiredEnvVars(rawConfig);
4927
+ if (!EnvValidator.validate(requiredEnvs)) {
4928
+ return null;
4929
+ }
4930
+ const config = this.resolveEnvVars(rawConfig);
4433
4931
  if (!this.validateSchema(config)) {
4434
4932
  return null;
4435
4933
  }
@@ -4439,15 +4937,71 @@ var ConfigValidator = class {
4439
4937
  return null;
4440
4938
  }
4441
4939
  }
4940
+ /**
4941
+ * 只收集本次实际启用配置中的环境变量,避免禁用平台阻塞其它上传任务。
4942
+ */
4943
+ static collectRequiredEnvVars(config) {
4944
+ const activeConfig = {
4945
+ ...config,
4946
+ publish: Object.fromEntries(
4947
+ Object.entries(config.publish || {}).filter(([, value]) => value?.enable === true)
4948
+ )
4949
+ };
4950
+ const requiredEnvs = /* @__PURE__ */ new Set();
4951
+ const envRegex = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
4952
+ const visit = (value) => {
4953
+ if (typeof value === "string") {
4954
+ for (const match of value.matchAll(envRegex)) {
4955
+ requiredEnvs.add(match[1]);
4956
+ }
4957
+ return;
4958
+ }
4959
+ if (Array.isArray(value)) {
4960
+ value.forEach(visit);
4961
+ return;
4962
+ }
4963
+ if (value && typeof value === "object") {
4964
+ Object.values(value).forEach(visit);
4965
+ }
4966
+ };
4967
+ visit(activeConfig);
4968
+ return Array.from(requiredEnvs);
4969
+ }
4970
+ /** 在内存中递归解析环境变量,不重新拼接 YAML,避免密钥字符破坏 YAML 结构。 */
4971
+ static resolveEnvVars(value) {
4972
+ if (typeof value === "string") {
4973
+ return value.replace(
4974
+ /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g,
4975
+ (_, varName) => process.env[varName] || ""
4976
+ );
4977
+ }
4978
+ if (Array.isArray(value)) {
4979
+ return value.map((item) => this.resolveEnvVars(item));
4980
+ }
4981
+ if (value && typeof value === "object") {
4982
+ return Object.fromEntries(
4983
+ Object.entries(value).map(([key, item]) => [key, this.resolveEnvVars(item)])
4984
+ );
4985
+ }
4986
+ return value;
4987
+ }
4442
4988
  /**
4443
4989
  * 验证配置 Schema
4444
4990
  * @param config 配置对象
4445
4991
  */
4446
4992
  static validateSchema(config) {
4447
4993
  if (config.publish?.app_store?.enable) {
4994
+ const mode = config.publish.app_store.mode || "app_store";
4448
4995
  const auth = config.publish.app_store.auth;
4449
- if (!auth?.apiKeyId || !auth?.apiIssuerId) {
4450
- logger.error("App Store enabled but missing auth info (apiKeyId, apiIssuerId)");
4996
+ const hasCliAuth = Boolean(auth?.appleId && auth?.appPassword);
4997
+ const hasApiAuth = Boolean(
4998
+ auth?.apiKeyId && auth?.apiIssuerId && (auth?.privateKeyPath || auth?.privateKeyContent)
4999
+ );
5000
+ const hasRequiredAuth = mode === "adhoc" || (mode === "testflight" ? hasApiAuth : hasCliAuth || hasApiAuth);
5001
+ if (!hasRequiredAuth) {
5002
+ logger.error(
5003
+ mode === "testflight" ? "TestFlight enabled but missing complete API key credentials" : "App Store enabled but missing Apple ID credentials or complete API key credentials"
5004
+ );
4451
5005
  return false;
4452
5006
  }
4453
5007
  }
@@ -4493,23 +5047,38 @@ var ConfigValidator = class {
4493
5047
  return false;
4494
5048
  }
4495
5049
  }
5050
+ if (config.publish?.google_play?.enable) {
5051
+ const googlePlay = config.publish.google_play;
5052
+ const auth = googlePlay.auth;
5053
+ if (!googlePlay.packageName || !auth?.serviceAccountJsonPath && !auth?.serviceAccountJsonContent) {
5054
+ logger.error(
5055
+ "Google Play enabled but missing packageName or service account JSON credentials"
5056
+ );
5057
+ return false;
5058
+ }
5059
+ if (googlePlay.track && googlePlay.track !== "internal") {
5060
+ logger.error("Google Play first release only supports the internal track");
5061
+ return false;
5062
+ }
5063
+ }
4496
5064
  return true;
4497
5065
  }
4498
5066
  };
4499
5067
 
4500
5068
  // src/cli.ts
4501
- import path11 from "path";
4502
- import fs14 from "fs-extra";
5069
+ import path13 from "path";
5070
+ import fs15 from "fs-extra";
4503
5071
  import yaml2 from "js-yaml";
4504
- var VERSION = "0.1.0";
5072
+ var VERSION = "0.2.1";
4505
5073
  function printHelp() {
4506
5074
  console.log(`
4507
5075
  app-ship v${VERSION} \u2014 Multi-platform app store uploader
4508
5076
 
4509
5077
  Usage:
4510
- app-ship upload -c <config> -f <file> [--platform pgyer,huawei]
5078
+ app-ship upload -c <config> -f <file> [--platform pgyer,huawei] [--project <dir>]
4511
5079
  app-ship platforms List supported platforms
4512
5080
  app-ship init Generate sample config file
5081
+ app-ship history [--project <dir>] [--json]
4513
5082
  app-ship --help Show this help
4514
5083
  app-ship --version Show version
4515
5084
 
@@ -4520,7 +5089,13 @@ Upload Options:
4520
5089
  -v, --version <ver> Version string (default: from config or "1.0.0")
4521
5090
  -b, --build-number <n> Build number (default: "1")
4522
5091
  --changelog <text> Changelog / release notes
5092
+ --project <dir> Project directory recorded in the release receipt
5093
+ --confirm-google-play Confirm upload to Google Play internal draft
4523
5094
  --verbose Show detailed output
5095
+
5096
+ History Options:
5097
+ --project <dir> Project directory (default: current directory)
5098
+ --json Print structured receipts as JSON
4524
5099
  `);
4525
5100
  }
4526
5101
  function parseArgs(argv) {
@@ -4534,6 +5109,12 @@ function parseArgs(argv) {
4534
5109
  args.showVersion = true;
4535
5110
  } else if (arg === "--verbose") {
4536
5111
  args.verbose = true;
5112
+ } else if (arg === "--confirm-google-play") {
5113
+ args.confirmGooglePlay = true;
5114
+ } else if (arg === "--json") {
5115
+ args.json = true;
5116
+ } else if (arg === "--project" && argv[i + 1]) {
5117
+ args.project = argv[++i];
4537
5118
  } else if ((arg === "-c" || arg === "--config") && argv[i + 1]) {
4538
5119
  args.config = argv[++i];
4539
5120
  } else if ((arg === "-f" || arg === "--file") && argv[i + 1]) {
@@ -4564,9 +5145,9 @@ async function cmdUpload(args) {
4564
5145
  logger.error("Missing file path. Use: app-ship upload -c <config.yaml> -f <file>");
4565
5146
  process.exit(1);
4566
5147
  }
4567
- const absConfig = path11.resolve(configPath);
4568
- const absFile = path11.resolve(filePath);
4569
- if (!fs14.existsSync(absFile)) {
5148
+ const absConfig = path13.resolve(configPath);
5149
+ const absFile = path13.resolve(filePath);
5150
+ if (!fs15.existsSync(absFile)) {
4570
5151
  logger.error(`File not found: ${absFile}`);
4571
5152
  process.exit(1);
4572
5153
  }
@@ -4575,27 +5156,92 @@ async function cmdUpload(args) {
4575
5156
  logger.error("Config validation failed");
4576
5157
  process.exit(1);
4577
5158
  }
4578
- const platforms = args.platform ? args.platform.split(",").map((s) => s.trim()) : void 0;
5159
+ const platforms = args.platform ? Array.from(
5160
+ new Set(
5161
+ args.platform.split(",").map((platform) => platform.trim()).filter(Boolean)
5162
+ )
5163
+ ) : void 0;
5164
+ const manager = new UploadManager({
5165
+ maxConcurrent: 3,
5166
+ continueOnError: true,
5167
+ verbose: !!args.verbose
5168
+ });
5169
+ const effectivePlatforms = platforms || manager.getEnabledPlatforms(config);
5170
+ if (effectivePlatforms.length === 0) {
5171
+ logger.error("No upload platform is enabled. Enable one in config or pass --platform.");
5172
+ process.exit(1);
5173
+ }
4579
5174
  const metadata = {
4580
5175
  version: args.ver || config.version || "1.0.0",
4581
5176
  buildNumber: args.buildNumber || "1",
4582
- changelog: args.changelog || ""
5177
+ changelog: args.changelog || "",
5178
+ platform: effectivePlatforms
4583
5179
  };
4584
5180
  logger.title("App Ship \u2014 Starting Upload");
4585
5181
  logger.info(`File: ${absFile}`);
4586
5182
  logger.info(`Config: ${absConfig}`);
4587
- if (platforms) {
4588
- logger.info(`Platforms: ${platforms.join(", ")}`);
5183
+ logger.info(`Platforms: ${effectivePlatforms.join(", ") || "none"}`);
5184
+ if (effectivePlatforms.includes("google_play")) {
5185
+ if (path13.extname(absFile).toLowerCase() !== ".aab") {
5186
+ logger.error("Google Play only accepts Android App Bundle (.aab) files");
5187
+ process.exit(1);
5188
+ }
5189
+ logger.info(`Google Play package: ${config.publish?.google_play?.packageName || "unknown"}`);
5190
+ logger.info("Google Play target: internal draft");
5191
+ if (!args.confirmGooglePlay) {
5192
+ logger.error(
5193
+ "Google Play upload requires explicit confirmation. Review the preview and rerun with --confirm-google-play"
5194
+ );
5195
+ process.exit(1);
5196
+ }
4589
5197
  }
4590
5198
  logger.newLine();
4591
- const manager = new UploadManager({
4592
- maxConcurrent: 3,
4593
- continueOnError: true,
4594
- verbose: !!args.verbose
4595
- });
4596
- const report = await manager.uploadToMultiplePlatforms(absFile, config, metadata, platforms);
5199
+ const startedAt = Date.now();
5200
+ const report = await manager.uploadToMultiplePlatforms(
5201
+ absFile,
5202
+ config,
5203
+ metadata,
5204
+ effectivePlatforms
5205
+ );
5206
+ try {
5207
+ const receipt = await new ReleaseReceiptStore().save({
5208
+ projectRoot: resolveReleaseProjectRoot(args.project, absConfig),
5209
+ kind: "upload",
5210
+ appName: metadata.appName,
5211
+ packageName: resolvePackageName(config),
5212
+ versionName: metadata.version,
5213
+ buildNumber: metadata.buildNumber,
5214
+ changelog: metadata.changelog,
5215
+ artifactPaths: absFile,
5216
+ targets: createUploadTargets(report.results),
5217
+ startedAt
5218
+ });
5219
+ logger.info(`Receipt: ${receipt.id}`);
5220
+ } catch (error) {
5221
+ logger.warn(`Failed to save release receipt: ${error.message}`);
5222
+ }
4597
5223
  process.exit(report.failureCount > 0 ? 1 : 0);
4598
5224
  }
5225
+ async function cmdHistory(args) {
5226
+ const projectRoot = path13.resolve(args.project || process.cwd());
5227
+ const receipts = await new ReleaseReceiptStore().list(projectRoot);
5228
+ if (args.json) {
5229
+ console.log(JSON.stringify(receipts, null, 2));
5230
+ return;
5231
+ }
5232
+ if (receipts.length === 0) {
5233
+ logger.info(`No release receipts found for ${projectRoot}`);
5234
+ return;
5235
+ }
5236
+ logger.title(`Release Receipts \u2014 ${projectRoot}`);
5237
+ receipts.forEach((receipt, index) => {
5238
+ if (index > 0) console.log("");
5239
+ console.log(formatReleaseReceipt(receipt));
5240
+ });
5241
+ }
5242
+ function resolvePackageName(config) {
5243
+ return config.appId || config.publish?.google_play?.packageName || Object.values(config.publish || {}).find((value) => value?.packageName)?.packageName;
5244
+ }
4599
5245
  function cmdPlatforms() {
4600
5246
  const manager = new UploadManager();
4601
5247
  const platforms = manager.getAvailablePlatforms();
@@ -4608,7 +5254,7 @@ Total: ${platforms.length} platforms`);
4608
5254
  }
4609
5255
  function cmdInit() {
4610
5256
  const filename = "app-ship.yaml";
4611
- if (fs14.existsSync(filename)) {
5257
+ if (fs15.existsSync(filename)) {
4612
5258
  logger.warn(`${filename} already exists. Skipping.`);
4613
5259
  return;
4614
5260
  }
@@ -4616,7 +5262,7 @@ function cmdInit() {
4616
5262
  version: "1.0.0",
4617
5263
  publish: {
4618
5264
  pgyer: {
4619
- enable: true,
5265
+ enable: false,
4620
5266
  apiKey: "YOUR_PGYER_API_KEY"
4621
5267
  },
4622
5268
  huawei: {
@@ -4652,6 +5298,7 @@ function cmdInit() {
4652
5298
  tencent: {
4653
5299
  enable: false,
4654
5300
  auth: {
5301
+ userId: "",
4655
5302
  appId: "",
4656
5303
  accessSecret: ""
4657
5304
  }
@@ -4670,10 +5317,18 @@ function cmdInit() {
4670
5317
  appleId: "",
4671
5318
  appPassword: ""
4672
5319
  }
5320
+ },
5321
+ google_play: {
5322
+ enable: false,
5323
+ auth: {
5324
+ serviceAccountJsonPath: "./service-account.json"
5325
+ },
5326
+ packageName: "com.example.app",
5327
+ track: "internal"
4673
5328
  }
4674
5329
  }
4675
5330
  };
4676
- fs14.writeFileSync(filename, yaml2.dump(sample, { indent: 2 }), "utf8");
5331
+ fs15.writeFileSync(filename, yaml2.dump(sample, { indent: 2 }), "utf8");
4677
5332
  logger.success(`Created ${filename}`);
4678
5333
  logger.info("Edit the file to configure your platform credentials.");
4679
5334
  }
@@ -4698,6 +5353,9 @@ async function main() {
4698
5353
  case "init":
4699
5354
  cmdInit();
4700
5355
  break;
5356
+ case "history":
5357
+ await cmdHistory(args);
5358
+ break;
4701
5359
  default:
4702
5360
  printHelp();
4703
5361
  break;