@gpc-cli/core 0.9.57 → 0.9.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  GpcError,
5
5
  NetworkError,
6
6
  applyReleaseNotes,
7
+ commitWithRescue,
7
8
  createTrack,
8
9
  diffReleases,
9
10
  fetchReleaseNotes,
@@ -14,9 +15,10 @@ import {
14
15
  updateTrackConfig,
15
16
  uploadExternallyHosted,
16
17
  uploadRelease,
18
+ validateAndCommit,
17
19
  validateUploadFile,
18
20
  waitForBundleProcessing
19
- } from "./chunk-QEM7QCBD.js";
21
+ } from "./chunk-IZKB6GBS.js";
20
22
 
21
23
  // src/output.ts
22
24
  import process2 from "process";
@@ -47,6 +49,10 @@ function formatOutput(data, format, redact = true) {
47
49
  return formatYaml(safe);
48
50
  case "markdown":
49
51
  return formatMarkdown(safe);
52
+ case "csv":
53
+ return formatCsv(safe);
54
+ case "tsv":
55
+ return formatTsv(safe);
50
56
  case "table":
51
57
  return formatTable(safe);
52
58
  case "junit":
@@ -221,6 +227,37 @@ function formatMarkdown(data) {
221
227
  ${separator}
222
228
  ${body}`;
223
229
  }
230
+ function escapeCsvField(val) {
231
+ if (val.includes(",") || val.includes('"') || val.includes("\n") || val.includes("\r")) {
232
+ return `"${val.replace(/"/g, '""')}"`;
233
+ }
234
+ return val;
235
+ }
236
+ function formatCsv(data) {
237
+ const rows = toRows(data);
238
+ if (rows.length === 0) return "";
239
+ const firstRow = rows[0];
240
+ if (!firstRow) return "";
241
+ const keys = Object.keys(firstRow);
242
+ if (keys.length === 0) return "";
243
+ const header = keys.map(escapeCsvField).join(",");
244
+ const body = rows.map((row) => keys.map((key) => escapeCsvField(cellValue(row[key]))).join(",")).join("\n");
245
+ return `${header}
246
+ ${body}`;
247
+ }
248
+ function formatTsv(data) {
249
+ const rows = toRows(data);
250
+ if (rows.length === 0) return "";
251
+ const firstRow = rows[0];
252
+ if (!firstRow) return "";
253
+ const keys = Object.keys(firstRow);
254
+ if (keys.length === 0) return "";
255
+ const escape = (val) => val.replace(/\t/g, "\\t").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
256
+ const header = keys.join(" ");
257
+ const body = rows.map((row) => keys.map((key) => escape(cellValue(row[key]))).join(" ")).join("\n");
258
+ return `${header}
259
+ ${body}`;
260
+ }
224
261
  function toRows(data) {
225
262
  if (Array.isArray(data)) {
226
263
  return data.filter(
@@ -880,10 +917,7 @@ async function updateListing(client, packageName, language, data, commitOptions)
880
917
  const edit = await client.edits.insert(packageName);
881
918
  try {
882
919
  const listing = await client.listings.patch(packageName, edit.id, language, data);
883
- if (!commitOptions?.changesNotSentForReview) {
884
- await client.edits.validate(packageName, edit.id);
885
- }
886
- await client.edits.commit(packageName, edit.id, commitOptions);
920
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
887
921
  return listing;
888
922
  } catch (error) {
889
923
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -896,10 +930,7 @@ async function deleteListing(client, packageName, language, commitOptions) {
896
930
  const edit = await client.edits.insert(packageName);
897
931
  try {
898
932
  await client.listings.delete(packageName, edit.id, language);
899
- if (!commitOptions?.changesNotSentForReview) {
900
- await client.edits.validate(packageName, edit.id);
901
- }
902
- await client.edits.commit(packageName, edit.id, commitOptions);
933
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
903
934
  } catch (error) {
904
935
  await client.edits.delete(packageName, edit.id).catch(() => {
905
936
  });
@@ -1022,10 +1053,7 @@ ${details}`,
1022
1053
  const { language, ...data } = listing;
1023
1054
  await client.listings.update(packageName, edit.id, language, data);
1024
1055
  }
1025
- if (!options?.commitOptions?.changesNotSentForReview) {
1026
- await client.edits.validate(packageName, edit.id);
1027
- }
1028
- await client.edits.commit(packageName, edit.id, options?.commitOptions);
1056
+ await validateAndCommit(client, packageName, edit.id, options?.commitOptions);
1029
1057
  return {
1030
1058
  updated: localListings.length,
1031
1059
  languages: localListings.map((l) => l.language)
@@ -1066,10 +1094,7 @@ async function uploadImage(client, packageName, language, imageType, filePath, c
1066
1094
  const edit = await client.edits.insert(packageName);
1067
1095
  try {
1068
1096
  const image = await client.images.upload(packageName, edit.id, language, imageType, filePath);
1069
- if (!commitOptions?.changesNotSentForReview) {
1070
- await client.edits.validate(packageName, edit.id);
1071
- }
1072
- await client.edits.commit(packageName, edit.id, commitOptions);
1097
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
1073
1098
  return image;
1074
1099
  } catch (error) {
1075
1100
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -1082,10 +1107,7 @@ async function deleteImage(client, packageName, language, imageType, imageId, co
1082
1107
  const edit = await client.edits.insert(packageName);
1083
1108
  try {
1084
1109
  await client.images.delete(packageName, edit.id, language, imageType, imageId);
1085
- if (!commitOptions?.changesNotSentForReview) {
1086
- await client.edits.validate(packageName, edit.id);
1087
- }
1088
- await client.edits.commit(packageName, edit.id, commitOptions);
1110
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
1089
1111
  } catch (error) {
1090
1112
  await client.edits.delete(packageName, edit.id).catch(() => {
1091
1113
  });
@@ -1129,7 +1151,7 @@ var ALL_IMAGE_TYPES = [
1129
1151
  ];
1130
1152
  async function exportImages(client, packageName, dir, options) {
1131
1153
  const { mkdir: mkdir8, writeFile: writeFile9 } = await import("fs/promises");
1132
- const { join: join12 } = await import("path");
1154
+ const { join: join13 } = await import("path");
1133
1155
  const edit = await client.edits.insert(packageName);
1134
1156
  try {
1135
1157
  let languages;
@@ -1160,7 +1182,7 @@ async function exportImages(client, packageName, dir, options) {
1160
1182
  const batch = tasks.slice(i, i + concurrency);
1161
1183
  const results = await Promise.all(
1162
1184
  batch.map(async (task) => {
1163
- const dirPath = join12(dir, task.language, task.imageType);
1185
+ const dirPath = join13(dir, task.language, task.imageType);
1164
1186
  await mkdir8(dirPath, { recursive: true });
1165
1187
  const response = await fetch(task.url);
1166
1188
  if (!response.ok) {
@@ -1172,7 +1194,7 @@ async function exportImages(client, packageName, dir, options) {
1172
1194
  );
1173
1195
  }
1174
1196
  const buffer = Buffer.from(await response.arrayBuffer());
1175
- const filePath = join12(dirPath, `${task.index}.png`);
1197
+ const filePath = join13(dirPath, `${task.index}.png`);
1176
1198
  await writeFile9(filePath, buffer);
1177
1199
  return buffer.length;
1178
1200
  })
@@ -1198,10 +1220,7 @@ async function updateAppDetails(client, packageName, details, commitOptions) {
1198
1220
  const edit = await client.edits.insert(packageName);
1199
1221
  try {
1200
1222
  const result = await client.details.patch(packageName, edit.id, details);
1201
- if (!commitOptions?.changesNotSentForReview) {
1202
- await client.edits.validate(packageName, edit.id);
1203
- }
1204
- await client.edits.commit(packageName, edit.id, commitOptions);
1223
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
1205
1224
  return result;
1206
1225
  } catch (error) {
1207
1226
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -1679,6 +1698,7 @@ async function publish(client, packageName, filePath, options) {
1679
1698
  mappingFile: options.mappingFile,
1680
1699
  mappingFileType: options.mappingFileType,
1681
1700
  deviceTierConfigId: options.deviceTierConfigId,
1701
+ validateOnly: options.validateOnly,
1682
1702
  commitOptions: options.commitOptions
1683
1703
  });
1684
1704
  return { validation, upload };
@@ -3202,7 +3222,7 @@ async function handleBreach(event, config, client) {
3202
3222
  }
3203
3223
  case "halt": {
3204
3224
  try {
3205
- const { updateRollout: updateRollout2 } = await import("./releases-LHYBPIUI.js");
3225
+ const { updateRollout: updateRollout2 } = await import("./releases-VFDJ6IX2.js");
3206
3226
  await updateRollout2(client, config.packageName, config.track, "halt");
3207
3227
  halted = true;
3208
3228
  } catch {
@@ -3786,10 +3806,7 @@ async function addTesters(client, packageName, track, groupEmails, commitOptions
3786
3806
  const updated = await client.testers.update(packageName, edit.id, track, {
3787
3807
  googleGroups: [...existing]
3788
3808
  });
3789
- if (!commitOptions?.changesNotSentForReview) {
3790
- await client.edits.validate(packageName, edit.id);
3791
- }
3792
- await client.edits.commit(packageName, edit.id, commitOptions);
3809
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
3793
3810
  return updated;
3794
3811
  } catch (error) {
3795
3812
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -3806,10 +3823,7 @@ async function removeTesters(client, packageName, track, groupEmails, commitOpti
3806
3823
  const updated = await client.testers.update(packageName, edit.id, track, {
3807
3824
  googleGroups: filtered
3808
3825
  });
3809
- if (!commitOptions?.changesNotSentForReview) {
3810
- await client.edits.validate(packageName, edit.id);
3811
- }
3812
- await client.edits.commit(packageName, edit.id, commitOptions);
3826
+ await validateAndCommit(client, packageName, edit.id, commitOptions);
3813
3827
  return updated;
3814
3828
  } catch (error) {
3815
3829
  await client.edits.delete(packageName, edit.id).catch(() => {
@@ -8335,6 +8349,202 @@ function compareFingerprints(a, b) {
8335
8349
  return normalizeFingerprint(a) === normalizeFingerprint(b);
8336
8350
  }
8337
8351
 
8352
+ // src/utils/hash.ts
8353
+ import { createHash } from "crypto";
8354
+ import { stat as stat9 } from "fs/promises";
8355
+ async function sha256File(filePath) {
8356
+ const hash = createHash("sha256");
8357
+ const { size } = await stat9(filePath);
8358
+ if (size === 0) return hash.digest("hex");
8359
+ const { createReadStream } = await import("fs");
8360
+ const stream = createReadStream(filePath);
8361
+ await new Promise((resolve2, reject) => {
8362
+ stream.on("data", (chunk) => hash.update(chunk));
8363
+ stream.on("end", resolve2);
8364
+ stream.on("error", reject);
8365
+ });
8366
+ return hash.digest("hex");
8367
+ }
8368
+
8369
+ // src/commands/image-sync.ts
8370
+ import { readdir as readdir7 } from "fs/promises";
8371
+ import { join as join12, extname as extname5 } from "path";
8372
+ import { PlayApiError } from "@gpc-cli/api";
8373
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
8374
+ var ALL_IMAGE_TYPES2 = [
8375
+ "icon",
8376
+ "featureGraphic",
8377
+ "tvBanner",
8378
+ "phoneScreenshots",
8379
+ "sevenInchScreenshots",
8380
+ "tenInchScreenshots",
8381
+ "tvScreenshots",
8382
+ "wearScreenshots"
8383
+ ];
8384
+ async function scanLocalImages(dir) {
8385
+ try {
8386
+ const entries = await readdir7(dir, { withFileTypes: true });
8387
+ return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(extname5(e.name).toLowerCase())).map((e) => e.name).sort();
8388
+ } catch {
8389
+ return [];
8390
+ }
8391
+ }
8392
+ async function scanLanguages(dir) {
8393
+ try {
8394
+ const entries = await readdir7(dir, { withFileTypes: true });
8395
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
8396
+ } catch {
8397
+ return [];
8398
+ }
8399
+ }
8400
+ async function syncImages(client, packageName, dir, options) {
8401
+ const details = [];
8402
+ let uploaded = 0;
8403
+ let skipped = 0;
8404
+ let deleted = 0;
8405
+ const languages = options?.lang ? [options.lang] : await scanLanguages(dir);
8406
+ if (languages.length === 0) {
8407
+ throw new GpcError(
8408
+ `No language directories found in "${dir}"`,
8409
+ "IMAGE_SYNC_EMPTY",
8410
+ 1,
8411
+ "The directory should contain subdirectories named by language code (e.g., en-US/) with image type subdirectories inside."
8412
+ );
8413
+ }
8414
+ const imageTypes = options?.type ? [options.type] : ALL_IMAGE_TYPES2;
8415
+ const edit = await client.edits.insert(packageName);
8416
+ try {
8417
+ for (const language of languages) {
8418
+ for (const imageType of imageTypes) {
8419
+ const localDir = join12(dir, language, imageType);
8420
+ const localFiles = await scanLocalImages(localDir);
8421
+ let remoteImages;
8422
+ try {
8423
+ remoteImages = await client.images.list(packageName, edit.id, language, imageType);
8424
+ } catch (error) {
8425
+ if (error instanceof PlayApiError && error.statusCode === 404) {
8426
+ remoteImages = [];
8427
+ } else {
8428
+ throw error;
8429
+ }
8430
+ }
8431
+ const remoteSha256Set = new Set(remoteImages.map((img) => img.sha256.toLowerCase()));
8432
+ const localHashes = /* @__PURE__ */ new Map();
8433
+ for (const file of localFiles) {
8434
+ const hash = await sha256File(join12(localDir, file));
8435
+ localHashes.set(file, hash);
8436
+ }
8437
+ const localSha256Set = new Set(localHashes.values());
8438
+ if (options?.delete) {
8439
+ for (const img of remoteImages) {
8440
+ if (!localSha256Set.has(img.sha256.toLowerCase())) {
8441
+ if (!options?.dryRun) {
8442
+ await client.images.delete(packageName, edit.id, language, imageType, img.id);
8443
+ }
8444
+ deleted++;
8445
+ details.push({
8446
+ language,
8447
+ imageType,
8448
+ file: img.id,
8449
+ action: "delete",
8450
+ reason: "not in local"
8451
+ });
8452
+ }
8453
+ }
8454
+ }
8455
+ for (const file of localFiles) {
8456
+ const hash = localHashes.get(file);
8457
+ if (remoteSha256Set.has(hash)) {
8458
+ skipped++;
8459
+ details.push({ language, imageType, file, action: "skip", reason: "sha256 match" });
8460
+ } else {
8461
+ if (!options?.dryRun) {
8462
+ const filePath = join12(localDir, file);
8463
+ const check = await validateImage(filePath, imageType);
8464
+ if (!check.valid) {
8465
+ throw new GpcError(
8466
+ `Image validation failed for ${language}/${imageType}/${file}: ${check.errors.join("; ")}`,
8467
+ "IMAGE_INVALID",
8468
+ 2,
8469
+ "Check image dimensions, file size, and format."
8470
+ );
8471
+ }
8472
+ await client.images.upload(packageName, edit.id, language, imageType, filePath);
8473
+ }
8474
+ uploaded++;
8475
+ details.push({ language, imageType, file, action: "upload", reason: "new or changed" });
8476
+ }
8477
+ }
8478
+ }
8479
+ }
8480
+ if (options?.dryRun || uploaded === 0 && deleted === 0) {
8481
+ await client.edits.delete(packageName, edit.id).catch(() => {
8482
+ });
8483
+ } else {
8484
+ await validateAndCommit(client, packageName, edit.id, options?.commitOptions);
8485
+ }
8486
+ return {
8487
+ uploaded,
8488
+ skipped,
8489
+ deleted,
8490
+ total: uploaded + skipped + deleted,
8491
+ details
8492
+ };
8493
+ } catch (error) {
8494
+ await client.edits.delete(packageName, edit.id).catch(() => {
8495
+ });
8496
+ throw error;
8497
+ }
8498
+ }
8499
+
8500
+ // src/commands/bundles.ts
8501
+ import { PlayApiError as PlayApiError2 } from "@gpc-cli/api";
8502
+ async function listBundles(client, packageName) {
8503
+ const edit = await client.edits.insert(packageName);
8504
+ try {
8505
+ const bundles = await client.bundles.list(packageName, edit.id);
8506
+ await client.edits.delete(packageName, edit.id).catch(() => {
8507
+ });
8508
+ return bundles;
8509
+ } catch (error) {
8510
+ await client.edits.delete(packageName, edit.id).catch(() => {
8511
+ });
8512
+ throw error;
8513
+ }
8514
+ }
8515
+ async function findBundle(client, packageName, versionCode) {
8516
+ const bundles = await listBundles(client, packageName);
8517
+ return bundles.find((b) => b.versionCode === versionCode) ?? null;
8518
+ }
8519
+ function isRetryableError(error) {
8520
+ if (!(error instanceof PlayApiError2)) return false;
8521
+ const status = error.statusCode;
8522
+ return status === 429 || status === 500 || status === 503 || status === 409;
8523
+ }
8524
+ async function waitForBundle(client, packageName, versionCode, options) {
8525
+ const timeout = options?.timeout ?? 6e5;
8526
+ const interval = options?.interval ?? 15e3;
8527
+ const deadline = Date.now() + timeout;
8528
+ while (Date.now() < deadline) {
8529
+ try {
8530
+ const bundles = await listBundles(client, packageName);
8531
+ const match = bundles.find((b) => b.versionCode === versionCode);
8532
+ if (match) return match;
8533
+ } catch (error) {
8534
+ if (!isRetryableError(error)) throw error;
8535
+ }
8536
+ const remaining = deadline - Date.now();
8537
+ if (remaining <= 0) break;
8538
+ await new Promise((r) => setTimeout(r, Math.min(interval, remaining)));
8539
+ }
8540
+ throw new GpcError(
8541
+ `Bundle version code ${versionCode} not found after ${Math.round(timeout / 1e3)}s`,
8542
+ "BUNDLE_WAIT_TIMEOUT",
8543
+ 4,
8544
+ "The bundle may still be processing. Try again with a longer --timeout, or check the Play Console."
8545
+ );
8546
+ }
8547
+
8338
8548
  // src/signing-consistency.ts
8339
8549
  async function checkSigningConsistency(accessToken, packageName, apiHost = "androidpublisher.googleapis.com") {
8340
8550
  const baseUrl = `https://${apiHost}/androidpublisher/v3/applications/${encodeURIComponent(packageName)}`;
@@ -8577,6 +8787,7 @@ export {
8577
8787
  checkThreshold,
8578
8788
  classifyError,
8579
8789
  clearAuditLog,
8790
+ commitWithRescue,
8580
8791
  compareBundles,
8581
8792
  compareFingerprints,
8582
8793
  compareVersionVitals,
@@ -8631,6 +8842,7 @@ export {
8631
8842
  fetchAggregateCost,
8632
8843
  fetchChangelog,
8633
8844
  fetchReleaseNotes,
8845
+ findBundle,
8634
8846
  formatChangelogEntry,
8635
8847
  formatCustomPayload,
8636
8848
  formatDiscordPayload,
@@ -8697,6 +8909,7 @@ export {
8697
8909
  lintLocalListings,
8698
8910
  listAchievements,
8699
8911
  listAuditEvents,
8912
+ listBundles,
8700
8913
  listDeviceTiers,
8701
8914
  listEvents,
8702
8915
  listGeneratedApks,
@@ -8765,9 +8978,11 @@ export {
8765
8978
  searchVitalsErrors,
8766
8979
  sendNotification,
8767
8980
  sendWebhook,
8981
+ sha256File,
8768
8982
  sortResults,
8769
8983
  startTrain,
8770
8984
  statusHasBreach,
8985
+ syncImages,
8771
8986
  syncInAppProducts,
8772
8987
  topFiles,
8773
8988
  trackBreachState,
@@ -8788,6 +9003,7 @@ export {
8788
9003
  uploadImage,
8789
9004
  uploadInternalSharing,
8790
9005
  uploadRelease,
9006
+ validateAndCommit,
8791
9007
  validateBundleForApply,
8792
9008
  validateImage,
8793
9009
  validateLanguageCode,
@@ -8798,6 +9014,7 @@ export {
8798
9014
  validateTrackName,
8799
9015
  validateUploadFile,
8800
9016
  validateVersionCode,
9017
+ waitForBundle,
8801
9018
  waitForBundleProcessing,
8802
9019
  watchVitalsWithAutoHalt,
8803
9020
  wordDiff,