@gpc-cli/core 0.9.5 → 0.9.7

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
@@ -459,9 +459,46 @@ function formatSize(bytes) {
459
459
  // src/commands/releases.ts
460
460
  async function uploadRelease(client, packageName, filePath, options) {
461
461
  const validation = await validateUploadFile(filePath);
462
+ if (options.dryRun) {
463
+ const plannedStatus = options.status || (options.userFraction ? "inProgress" : "completed");
464
+ let currentReleases = [];
465
+ const edit2 = await client.edits.insert(packageName);
466
+ try {
467
+ const trackData = await client.tracks.get(packageName, edit2.id, options.track);
468
+ currentReleases = (trackData.releases || []).map((r) => ({
469
+ versionCodes: r.versionCodes || [],
470
+ status: r.status,
471
+ ...r.userFraction !== void 0 && { userFraction: r.userFraction }
472
+ }));
473
+ } catch {
474
+ } finally {
475
+ await client.edits.delete(packageName, edit2.id).catch(() => {
476
+ });
477
+ }
478
+ return {
479
+ dryRun: true,
480
+ file: {
481
+ path: filePath,
482
+ valid: validation.valid,
483
+ errors: validation.errors,
484
+ warnings: validation.warnings
485
+ },
486
+ track: options.track,
487
+ currentReleases,
488
+ plannedRelease: {
489
+ status: plannedStatus,
490
+ ...options.userFraction !== void 0 && { userFraction: options.userFraction }
491
+ }
492
+ };
493
+ }
462
494
  if (!validation.valid) {
463
- throw new Error(`File validation failed:
464
- ${validation.errors.join("\n")}`);
495
+ throw new GpcError(
496
+ `File validation failed:
497
+ ${validation.errors.join("\n")}`,
498
+ "RELEASE_INVALID_FILE",
499
+ 2,
500
+ "Check that the file is a valid AAB or APK and is not corrupted."
501
+ );
465
502
  }
466
503
  const edit = await client.edits.insert(packageName);
467
504
  try {
@@ -527,10 +564,20 @@ async function promoteRelease(client, packageName, fromTrack, toTrack, options)
527
564
  (r) => r.status === "completed" || r.status === "inProgress"
528
565
  );
529
566
  if (!currentRelease) {
530
- throw new Error(`No active release found on track "${fromTrack}"`);
567
+ throw new GpcError(
568
+ `No active release found on track "${fromTrack}"`,
569
+ "RELEASE_NOT_FOUND",
570
+ 1,
571
+ `Ensure there is a completed or in-progress release on the "${fromTrack}" track before promoting.`
572
+ );
531
573
  }
532
574
  if (options?.userFraction && (options.userFraction <= 0 || options.userFraction > 1)) {
533
- throw new Error("Rollout percentage must be between 0 and 1 (e.g., 0.1 for 10%)");
575
+ throw new GpcError(
576
+ "Rollout percentage must be between 0 and 1 (e.g., 0.1 for 10%)",
577
+ "RELEASE_INVALID_FRACTION",
578
+ 2,
579
+ "Use a decimal value like 0.1 for 10%, 0.5 for 50%, or 1.0 for 100%."
580
+ );
534
581
  }
535
582
  const release = {
536
583
  versionCodes: currentRelease.versionCodes,
@@ -561,15 +608,30 @@ async function updateRollout(client, packageName, track, action, userFraction) {
561
608
  (r) => r.status === "inProgress" || r.status === "halted"
562
609
  );
563
610
  if (!currentRelease) {
564
- throw new Error(`No active rollout found on track "${track}"`);
611
+ throw new GpcError(
612
+ `No active rollout found on track "${track}"`,
613
+ "ROLLOUT_NOT_FOUND",
614
+ 1,
615
+ `There is no in-progress or halted rollout on the "${track}" track. Start a staged rollout first with: gpc releases upload --track ${track} --status inProgress --fraction 0.1`
616
+ );
565
617
  }
566
618
  let newStatus;
567
619
  let newFraction;
568
620
  switch (action) {
569
621
  case "increase":
570
- if (!userFraction) throw new Error("--to <percentage> is required for rollout increase");
622
+ if (!userFraction) throw new GpcError(
623
+ "--to <percentage> is required for rollout increase",
624
+ "ROLLOUT_MISSING_FRACTION",
625
+ 2,
626
+ "Specify the target rollout percentage with --to, e.g.: gpc rollout increase --to 0.5"
627
+ );
571
628
  if (userFraction <= 0 || userFraction > 1) {
572
- throw new Error("Rollout percentage must be between 0 and 1 (e.g., 0.1 for 10%)");
629
+ throw new GpcError(
630
+ "Rollout percentage must be between 0 and 1 (e.g., 0.1 for 10%)",
631
+ "RELEASE_INVALID_FRACTION",
632
+ 2,
633
+ "Use a decimal value like 0.1 for 10%, 0.5 for 50%, or 1.0 for 100%."
634
+ );
573
635
  }
574
636
  newStatus = "inProgress";
575
637
  newFraction = userFraction;
@@ -866,7 +928,12 @@ function diffListings(local, remote) {
866
928
  // src/commands/listings.ts
867
929
  function validateLanguage(lang) {
868
930
  if (!isValidBcp47(lang)) {
869
- throw new Error(`Invalid language tag "${lang}". Must be a valid Google Play BCP 47 code.`);
931
+ throw new GpcError(
932
+ `Invalid language tag "${lang}". Must be a valid Google Play BCP 47 code.`,
933
+ "LISTING_INVALID_LANGUAGE",
934
+ 2,
935
+ "Use a valid BCP 47 language code such as en-US, de-DE, or ja-JP. See the Google Play Console for supported language codes."
936
+ );
870
937
  }
871
938
  }
872
939
  async function getListings(client, packageName, language) {
@@ -931,7 +998,12 @@ async function pullListings(client, packageName, dir) {
931
998
  async function pushListings(client, packageName, dir, options) {
932
999
  const localListings = await readListingsFromDir(dir);
933
1000
  if (localListings.length === 0) {
934
- throw new Error(`No listings found in directory "${dir}"`);
1001
+ throw new GpcError(
1002
+ `No listings found in directory "${dir}"`,
1003
+ "LISTING_DIR_EMPTY",
1004
+ 1,
1005
+ `The directory must contain subdirectories named by language code (e.g., en-US/) with listing metadata files. Pull existing listings first with: gpc listings pull --dir "${dir}"`
1006
+ );
935
1007
  }
936
1008
  for (const listing of localListings) {
937
1009
  validateLanguage(listing.language);
@@ -977,7 +1049,12 @@ async function uploadImage(client, packageName, language, imageType, filePath) {
977
1049
  validateLanguage(language);
978
1050
  const imageCheck = await validateImage(filePath, imageType);
979
1051
  if (!imageCheck.valid) {
980
- throw new Error(`Image validation failed: ${imageCheck.errors.join("; ")}`);
1052
+ throw new GpcError(
1053
+ `Image validation failed: ${imageCheck.errors.join("; ")}`,
1054
+ "IMAGE_INVALID",
1055
+ 2,
1056
+ "Check image dimensions, file size, and format. Google Play requires PNG or JPEG images within specific size limits per image type."
1057
+ );
981
1058
  }
982
1059
  if (imageCheck.warnings.length > 0) {
983
1060
  for (const w of imageCheck.warnings) {
@@ -1009,6 +1086,19 @@ async function deleteImage(client, packageName, language, imageType, imageId) {
1009
1086
  throw error;
1010
1087
  }
1011
1088
  }
1089
+ async function diffListingsCommand(client, packageName, dir) {
1090
+ const localListings = await readListingsFromDir(dir);
1091
+ const edit = await client.edits.insert(packageName);
1092
+ try {
1093
+ const remoteListings = await client.listings.list(packageName, edit.id);
1094
+ await client.edits.delete(packageName, edit.id);
1095
+ return diffListings(localListings, remoteListings);
1096
+ } catch (error) {
1097
+ await client.edits.delete(packageName, edit.id).catch(() => {
1098
+ });
1099
+ throw error;
1100
+ }
1101
+ }
1012
1102
  async function getCountryAvailability(client, packageName, track) {
1013
1103
  const edit = await client.edits.insert(packageName);
1014
1104
  try {
@@ -1044,7 +1134,12 @@ async function readReleaseNotesFromDir(dir) {
1044
1134
  try {
1045
1135
  entries = await readdir2(dir);
1046
1136
  } catch {
1047
- throw new Error(`Release notes directory not found: ${dir}`);
1137
+ throw new GpcError(
1138
+ `Release notes directory not found: ${dir}`,
1139
+ "RELEASE_NOTES_DIR_NOT_FOUND",
1140
+ 1,
1141
+ `Create the directory and add .txt files named by language code (e.g., en-US.txt). Path: ${dir}`
1142
+ );
1048
1143
  }
1049
1144
  const notes = [];
1050
1145
  for (const entry of entries) {
@@ -1185,6 +1280,14 @@ async function publish(client, packageName, filePath, options) {
1185
1280
  track: options.track || "internal",
1186
1281
  notes: releaseNotes
1187
1282
  });
1283
+ if (options.dryRun) {
1284
+ const upload2 = await uploadRelease(client, packageName, filePath, {
1285
+ track: options.track || "internal",
1286
+ userFraction: options.rolloutPercent ? options.rolloutPercent / 100 : void 0,
1287
+ dryRun: true
1288
+ });
1289
+ return { dryRun: true, validation, upload: upload2 };
1290
+ }
1188
1291
  if (!validation.valid) {
1189
1292
  return { validation };
1190
1293
  }
@@ -1233,12 +1336,20 @@ async function getReview(client, packageName, reviewId, translationLanguage) {
1233
1336
  var MAX_REPLY_LENGTH = 350;
1234
1337
  async function replyToReview(client, packageName, reviewId, replyText) {
1235
1338
  if (replyText.length > MAX_REPLY_LENGTH) {
1236
- throw new Error(
1237
- `Reply text exceeds ${MAX_REPLY_LENGTH} characters (${replyText.length}). Google Play limits replies to ${MAX_REPLY_LENGTH} characters.`
1339
+ throw new GpcError(
1340
+ `Reply text exceeds ${MAX_REPLY_LENGTH} characters (${replyText.length}). Google Play limits replies to ${MAX_REPLY_LENGTH} characters.`,
1341
+ "REVIEW_REPLY_TOO_LONG",
1342
+ 2,
1343
+ `Shorten your reply to ${MAX_REPLY_LENGTH} characters or fewer. Current length: ${replyText.length}.`
1238
1344
  );
1239
1345
  }
1240
1346
  if (replyText.length === 0) {
1241
- throw new Error("Reply text cannot be empty.");
1347
+ throw new GpcError(
1348
+ "Reply text cannot be empty.",
1349
+ "REVIEW_REPLY_EMPTY",
1350
+ 2,
1351
+ "Provide a non-empty reply text with --text or -t."
1352
+ );
1242
1353
  }
1243
1354
  return client.reviews.reply(packageName, reviewId, replyText);
1244
1355
  }
@@ -1571,7 +1682,12 @@ async function syncInAppProducts(client, packageName, dir, options) {
1571
1682
  try {
1572
1683
  localProducts.push(JSON.parse(content));
1573
1684
  } catch {
1574
- throw new Error(`Failed to parse ${file}: invalid JSON`);
1685
+ throw new GpcError(
1686
+ `Failed to parse ${file}: invalid JSON`,
1687
+ "IAP_INVALID_JSON",
1688
+ 1,
1689
+ `Check that "${file}" contains valid JSON. You can validate it with: cat "${file}" | jq .`
1690
+ );
1575
1691
  }
1576
1692
  }
1577
1693
  const response = await client.inappproducts.list(packageName);
@@ -1705,12 +1821,22 @@ function isValidStatsDimension(dim) {
1705
1821
  function parseMonth(monthStr) {
1706
1822
  const match = /^(\d{4})-(\d{2})$/.exec(monthStr);
1707
1823
  if (!match) {
1708
- throw new Error(`Invalid month format "${monthStr}". Expected YYYY-MM (e.g., 2026-03).`);
1824
+ throw new GpcError(
1825
+ `Invalid month format "${monthStr}". Expected YYYY-MM (e.g., 2026-03).`,
1826
+ "REPORT_INVALID_MONTH",
1827
+ 2,
1828
+ "Use the format YYYY-MM, for example: --month 2026-03"
1829
+ );
1709
1830
  }
1710
1831
  const year = Number(match[1]);
1711
1832
  const month = Number(match[2]);
1712
1833
  if (month < 1 || month > 12) {
1713
- throw new Error(`Invalid month "${month}". Must be between 01 and 12.`);
1834
+ throw new GpcError(
1835
+ `Invalid month "${month}". Must be between 01 and 12.`,
1836
+ "REPORT_INVALID_MONTH",
1837
+ 2,
1838
+ "The month value must be between 01 and 12."
1839
+ );
1714
1840
  }
1715
1841
  return { year, month };
1716
1842
  }
@@ -1720,21 +1846,31 @@ async function listReports(client, packageName, reportType, year, month) {
1720
1846
  }
1721
1847
  async function downloadReport(client, packageName, reportType, year, month) {
1722
1848
  const reports = await listReports(client, packageName, reportType, year, month);
1849
+ const monthPadded = String(month).padStart(2, "0");
1723
1850
  if (reports.length === 0) {
1724
- throw new Error(
1725
- `No ${reportType} reports found for ${year}-${String(month).padStart(2, "0")}.`
1851
+ throw new GpcError(
1852
+ `No ${reportType} reports found for ${year}-${monthPadded}.`,
1853
+ "REPORT_NOT_FOUND",
1854
+ 1,
1855
+ `Reports may not be available yet for this period. Financial reports are typically available a few days after the month ends. Try a different month or report type.`
1726
1856
  );
1727
1857
  }
1728
1858
  const bucket = reports[0];
1729
1859
  if (!bucket) {
1730
- throw new Error(
1731
- `No ${reportType} reports found for ${year}-${String(month).padStart(2, "0")}.`
1860
+ throw new GpcError(
1861
+ `No ${reportType} reports found for ${year}-${monthPadded}.`,
1862
+ "REPORT_NOT_FOUND",
1863
+ 1,
1864
+ `Reports may not be available yet for this period. Try a different month or report type.`
1732
1865
  );
1733
1866
  }
1734
1867
  const uri = bucket.uri;
1735
1868
  const response = await fetch(uri);
1736
1869
  if (!response.ok) {
1737
- throw new Error(`Failed to download report from signed URI: HTTP ${response.status}`);
1870
+ throw new NetworkError(
1871
+ `Failed to download report from signed URI: HTTP ${response.status}`,
1872
+ "The signed download URL may have expired. Retry the command to generate a fresh URL."
1873
+ );
1738
1874
  }
1739
1875
  return response.text();
1740
1876
  }
@@ -1785,8 +1921,11 @@ async function removeUser(client, developerId, userId) {
1785
1921
  function parseGrantArg(grantStr) {
1786
1922
  const colonIdx = grantStr.indexOf(":");
1787
1923
  if (colonIdx === -1) {
1788
- throw new Error(
1789
- `Invalid grant format "${grantStr}". Expected <packageName>:<PERMISSION>[,<PERMISSION>...]`
1924
+ throw new GpcError(
1925
+ `Invalid grant format "${grantStr}". Expected <packageName>:<PERMISSION>[,<PERMISSION>...]`,
1926
+ "USER_INVALID_GRANT",
1927
+ 2,
1928
+ "Use the format: com.example.app:VIEW_APP_INFORMATION,MANAGE_STORE_LISTING"
1790
1929
  );
1791
1930
  }
1792
1931
  const packageName = grantStr.slice(0, colonIdx);
@@ -1847,12 +1986,189 @@ async function importTestersFromCsv(client, packageName, track, csvPath) {
1847
1986
  const content = await readFile5(csvPath, "utf-8");
1848
1987
  const emails = content.split(/[,\n\r]+/).map((e) => e.trim()).filter((e) => e.length > 0 && e.includes("@"));
1849
1988
  if (emails.length === 0) {
1850
- throw new Error(`No valid email addresses found in ${csvPath}.`);
1989
+ throw new GpcError(
1990
+ `No valid email addresses found in ${csvPath}.`,
1991
+ "TESTER_NO_EMAILS",
1992
+ 1,
1993
+ "The CSV file must contain email addresses separated by commas or newlines. Each email must contain an @ symbol."
1994
+ );
1851
1995
  }
1852
1996
  const testers = await addTesters(client, packageName, track, emails);
1853
1997
  return { added: emails.length, testers };
1854
1998
  }
1855
1999
 
2000
+ // src/utils/git-notes.ts
2001
+ import { execFile as execFileCb } from "child_process";
2002
+ import { promisify } from "util";
2003
+ var execFile = promisify(execFileCb);
2004
+ var COMMIT_TYPE_HEADERS = {
2005
+ feat: "New",
2006
+ fix: "Fixed",
2007
+ perf: "Improved"
2008
+ };
2009
+ var DEFAULT_MAX_LENGTH = 500;
2010
+ function parseConventionalCommit(subject) {
2011
+ const match = subject.match(/^(\w+)(?:\([^)]*\))?:\s*(.+)$/);
2012
+ if (match) {
2013
+ return { type: match[1], message: match[2].trim() };
2014
+ }
2015
+ return { type: "other", message: subject.trim() };
2016
+ }
2017
+ function formatNotes(commits, maxLength) {
2018
+ const groups = /* @__PURE__ */ new Map();
2019
+ for (const commit of commits) {
2020
+ const header = COMMIT_TYPE_HEADERS[commit.type] || "Changes";
2021
+ if (!groups.has(header)) {
2022
+ groups.set(header, []);
2023
+ }
2024
+ groups.get(header).push(commit.message);
2025
+ }
2026
+ const order = ["New", "Fixed", "Improved", "Changes"];
2027
+ const sections = [];
2028
+ for (const header of order) {
2029
+ const items = groups.get(header);
2030
+ if (!items || items.length === 0) continue;
2031
+ const bullets = items.map((m) => `\u2022 ${m}`).join("\n");
2032
+ sections.push(`${header}:
2033
+ ${bullets}`);
2034
+ }
2035
+ let text = sections.join("\n\n");
2036
+ if (text.length > maxLength) {
2037
+ text = text.slice(0, maxLength - 3) + "...";
2038
+ }
2039
+ return text;
2040
+ }
2041
+ async function gitExec(args) {
2042
+ try {
2043
+ const { stdout } = await execFile("git", args);
2044
+ return stdout.trim();
2045
+ } catch (error) {
2046
+ const err = error;
2047
+ if (err.code === "ENOENT") {
2048
+ throw new GpcError(
2049
+ "git is not available on this system",
2050
+ "GIT_NOT_FOUND",
2051
+ 1,
2052
+ "Install git and ensure it is in your PATH."
2053
+ );
2054
+ }
2055
+ throw error;
2056
+ }
2057
+ }
2058
+ async function generateNotesFromGit(options) {
2059
+ const language = options?.language || "en-US";
2060
+ const maxLength = options?.maxLength ?? DEFAULT_MAX_LENGTH;
2061
+ let since = options?.since;
2062
+ if (!since) {
2063
+ try {
2064
+ since = await gitExec(["describe", "--tags", "--abbrev=0"]);
2065
+ } catch (e) {
2066
+ if (e instanceof GpcError && e.code === "GIT_NOT_FOUND") throw e;
2067
+ throw new GpcError(
2068
+ "No git tags found. Cannot determine commit range for release notes.",
2069
+ "GIT_NO_TAGS",
2070
+ 1,
2071
+ "Create a tag first (e.g., git tag v1.0.0) or use --since <ref> to specify a starting point."
2072
+ );
2073
+ }
2074
+ }
2075
+ let logOutput;
2076
+ try {
2077
+ logOutput = await gitExec(["log", `${since}..HEAD`, "--format=%s"]);
2078
+ } catch (error) {
2079
+ const err = error;
2080
+ const msg = err.stderr || err.message || String(error);
2081
+ throw new GpcError(
2082
+ `Failed to read git log from "${since}": ${msg}`,
2083
+ "GIT_LOG_FAILED",
2084
+ 1,
2085
+ `Verify that "${since}" is a valid git ref (tag, branch, or SHA).`
2086
+ );
2087
+ }
2088
+ if (!logOutput) {
2089
+ return {
2090
+ language,
2091
+ text: "No changes since last release.",
2092
+ commitCount: 0,
2093
+ since
2094
+ };
2095
+ }
2096
+ const subjects = logOutput.split("\n").filter((line) => line.length > 0);
2097
+ const commits = subjects.map(parseConventionalCommit);
2098
+ const text = formatNotes(commits, maxLength);
2099
+ return {
2100
+ language,
2101
+ text,
2102
+ commitCount: subjects.length,
2103
+ since
2104
+ };
2105
+ }
2106
+
2107
+ // src/commands/app-recovery.ts
2108
+ async function listRecoveryActions(client, packageName) {
2109
+ return client.appRecovery.list(packageName);
2110
+ }
2111
+ async function cancelRecoveryAction(client, packageName, recoveryId) {
2112
+ return client.appRecovery.cancel(packageName, recoveryId);
2113
+ }
2114
+ async function deployRecoveryAction(client, packageName, recoveryId) {
2115
+ return client.appRecovery.deploy(packageName, recoveryId);
2116
+ }
2117
+
2118
+ // src/commands/data-safety.ts
2119
+ import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2120
+ async function getDataSafety(client, packageName) {
2121
+ const edit = await client.edits.insert(packageName);
2122
+ try {
2123
+ const dataSafety = await client.dataSafety.get(packageName, edit.id);
2124
+ await client.edits.delete(packageName, edit.id);
2125
+ return dataSafety;
2126
+ } catch (error) {
2127
+ await client.edits.delete(packageName, edit.id).catch(() => {
2128
+ });
2129
+ throw error;
2130
+ }
2131
+ }
2132
+ async function updateDataSafety(client, packageName, data) {
2133
+ const edit = await client.edits.insert(packageName);
2134
+ try {
2135
+ const result = await client.dataSafety.update(packageName, edit.id, data);
2136
+ await client.edits.validate(packageName, edit.id);
2137
+ await client.edits.commit(packageName, edit.id);
2138
+ return result;
2139
+ } catch (error) {
2140
+ await client.edits.delete(packageName, edit.id).catch(() => {
2141
+ });
2142
+ throw error;
2143
+ }
2144
+ }
2145
+ async function exportDataSafety(client, packageName, outputPath) {
2146
+ const dataSafety = await getDataSafety(client, packageName);
2147
+ await writeFile2(outputPath, JSON.stringify(dataSafety, null, 2) + "\n", "utf-8");
2148
+ return dataSafety;
2149
+ }
2150
+ async function importDataSafety(client, packageName, filePath) {
2151
+ const content = await readFile6(filePath, "utf-8");
2152
+ let data;
2153
+ try {
2154
+ data = JSON.parse(content);
2155
+ } catch {
2156
+ throw new Error(`Failed to parse data safety JSON from "${filePath}"`);
2157
+ }
2158
+ return updateDataSafety(client, packageName, data);
2159
+ }
2160
+
2161
+ // src/commands/external-transactions.ts
2162
+ async function createExternalTransaction(client, packageName, data) {
2163
+ return client.externalTransactions.create(packageName, data);
2164
+ }
2165
+ async function getExternalTransaction(client, packageName, transactionId) {
2166
+ return client.externalTransactions.get(packageName, transactionId);
2167
+ }
2168
+ async function refundExternalTransaction(client, packageName, transactionId, refundData) {
2169
+ return client.externalTransactions.refund(packageName, transactionId, refundData);
2170
+ }
2171
+
1856
2172
  // src/utils/safe-path.ts
1857
2173
  import { resolve, normalize } from "path";
1858
2174
  function safePath(userPath) {
@@ -1862,13 +2178,58 @@ function safePathWithin(userPath, baseDir) {
1862
2178
  const resolved = safePath(userPath);
1863
2179
  const base = safePath(baseDir);
1864
2180
  if (!resolved.startsWith(base + "/") && resolved !== base) {
1865
- throw new Error(`Path "${userPath}" resolves outside the expected directory "${baseDir}"`);
2181
+ throw new GpcError(
2182
+ `Path "${userPath}" resolves outside the expected directory "${baseDir}"`,
2183
+ "PATH_TRAVERSAL",
2184
+ 2,
2185
+ "The path must stay within the target directory. Remove any ../ segments or use an absolute path within the expected directory."
2186
+ );
1866
2187
  }
1867
2188
  return resolved;
1868
2189
  }
1869
2190
 
2191
+ // src/utils/sort.ts
2192
+ function getNestedValue(obj, path) {
2193
+ const parts = path.split(".");
2194
+ let current = obj;
2195
+ for (const part of parts) {
2196
+ if (current === null || current === void 0 || typeof current !== "object") {
2197
+ return void 0;
2198
+ }
2199
+ current = current[part];
2200
+ }
2201
+ return current;
2202
+ }
2203
+ function compare(a, b) {
2204
+ if (a === void 0 && b === void 0) return 0;
2205
+ if (a === void 0) return 1;
2206
+ if (b === void 0) return -1;
2207
+ if (typeof a === "number" && typeof b === "number") {
2208
+ return a - b;
2209
+ }
2210
+ return String(a).localeCompare(String(b));
2211
+ }
2212
+ function sortResults(items, sortSpec) {
2213
+ if (!sortSpec || items.length === 0) {
2214
+ return items;
2215
+ }
2216
+ const descending = sortSpec.startsWith("-");
2217
+ const field = descending ? sortSpec.slice(1) : sortSpec;
2218
+ const hasField = items.some((item) => getNestedValue(item, field) !== void 0);
2219
+ if (!hasField) {
2220
+ return items;
2221
+ }
2222
+ const sorted = [...items].sort((a, b) => {
2223
+ const aVal = getNestedValue(a, field);
2224
+ const bVal = getNestedValue(b, field);
2225
+ const result = compare(aVal, bVal);
2226
+ return descending ? -result : result;
2227
+ });
2228
+ return sorted;
2229
+ }
2230
+
1870
2231
  // src/commands/plugin-scaffold.ts
1871
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
2232
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
1872
2233
  import { join as join4 } from "path";
1873
2234
  async function scaffoldPlugin(options) {
1874
2235
  const { name, dir, description = `GPC plugin: ${name}` } = options;
@@ -1911,7 +2272,7 @@ async function scaffoldPlugin(options) {
1911
2272
  vitest: "^3.0.0"
1912
2273
  }
1913
2274
  };
1914
- await writeFile2(join4(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
2275
+ await writeFile3(join4(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
1915
2276
  files.push("package.json");
1916
2277
  const tsconfig = {
1917
2278
  compilerOptions: {
@@ -1927,7 +2288,7 @@ async function scaffoldPlugin(options) {
1927
2288
  },
1928
2289
  include: ["src"]
1929
2290
  };
1930
- await writeFile2(join4(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
2291
+ await writeFile3(join4(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
1931
2292
  files.push("tsconfig.json");
1932
2293
  const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
1933
2294
  import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
@@ -1960,7 +2321,7 @@ export const plugin = definePlugin({
1960
2321
  },
1961
2322
  });
1962
2323
  `;
1963
- await writeFile2(join4(srcDir, "index.ts"), srcContent);
2324
+ await writeFile3(join4(srcDir, "index.ts"), srcContent);
1964
2325
  files.push("src/index.ts");
1965
2326
  const testContent = `import { describe, it, expect, vi } from "vitest";
1966
2327
  import { plugin } from "../src/index";
@@ -1985,7 +2346,7 @@ describe("${pluginName}", () => {
1985
2346
  });
1986
2347
  });
1987
2348
  `;
1988
- await writeFile2(join4(testDir, "plugin.test.ts"), testContent);
2349
+ await writeFile3(join4(testDir, "plugin.test.ts"), testContent);
1989
2350
  files.push("tests/plugin.test.ts");
1990
2351
  return { dir, files };
1991
2352
  }
@@ -2038,6 +2399,111 @@ function createAuditEntry(command, args, app) {
2038
2399
  args
2039
2400
  };
2040
2401
  }
2402
+
2403
+ // src/utils/webhooks.ts
2404
+ function formatSlackPayload(payload) {
2405
+ const status = payload.success ? "\u2713" : "\u2717";
2406
+ const color = payload.success ? "#2eb886" : "#e01e5a";
2407
+ const durationSec = (payload.duration / 1e3).toFixed(1);
2408
+ const fields = [
2409
+ { title: "Command", value: payload.command, short: true },
2410
+ { title: "Duration", value: `${durationSec}s`, short: true }
2411
+ ];
2412
+ if (payload.app) {
2413
+ fields.push({ title: "App", value: payload.app, short: true });
2414
+ }
2415
+ if (payload.error) {
2416
+ fields.push({ title: "Error", value: payload.error, short: false });
2417
+ }
2418
+ if (payload.details) {
2419
+ for (const [key, value] of Object.entries(payload.details)) {
2420
+ fields.push({ title: key, value: String(value), short: true });
2421
+ }
2422
+ }
2423
+ return {
2424
+ attachments: [
2425
+ {
2426
+ color,
2427
+ fallback: `GPC: ${payload.command} ${status}`,
2428
+ title: `GPC: ${payload.command} ${status}`,
2429
+ fields,
2430
+ footer: "GPC CLI",
2431
+ ts: Math.floor(Date.now() / 1e3)
2432
+ }
2433
+ ]
2434
+ };
2435
+ }
2436
+ function formatDiscordPayload(payload) {
2437
+ const status = payload.success ? "\u2713" : "\u2717";
2438
+ const color = payload.success ? 3061894 : 14687834;
2439
+ const durationSec = (payload.duration / 1e3).toFixed(1);
2440
+ const fields = [
2441
+ { name: "Command", value: payload.command, inline: true },
2442
+ { name: "Duration", value: `${durationSec}s`, inline: true }
2443
+ ];
2444
+ if (payload.app) {
2445
+ fields.push({ name: "App", value: payload.app, inline: true });
2446
+ }
2447
+ if (payload.error) {
2448
+ fields.push({ name: "Error", value: payload.error, inline: false });
2449
+ }
2450
+ if (payload.details) {
2451
+ for (const [key, value] of Object.entries(payload.details)) {
2452
+ fields.push({ name: key, value: String(value), inline: true });
2453
+ }
2454
+ }
2455
+ return {
2456
+ embeds: [
2457
+ {
2458
+ title: `GPC: ${payload.command} ${status}`,
2459
+ color,
2460
+ fields,
2461
+ footer: { text: "GPC CLI" },
2462
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2463
+ }
2464
+ ]
2465
+ };
2466
+ }
2467
+ function formatCustomPayload(payload) {
2468
+ return { ...payload };
2469
+ }
2470
+ var FORMATTERS = {
2471
+ slack: formatSlackPayload,
2472
+ discord: formatDiscordPayload,
2473
+ custom: formatCustomPayload
2474
+ };
2475
+ var WEBHOOK_TIMEOUT_MS = 5e3;
2476
+ async function sendSingle(url, body) {
2477
+ const controller = new AbortController();
2478
+ const timer = setTimeout(() => controller.abort(), WEBHOOK_TIMEOUT_MS);
2479
+ try {
2480
+ await fetch(url, {
2481
+ method: "POST",
2482
+ headers: { "Content-Type": "application/json" },
2483
+ body: JSON.stringify(body),
2484
+ signal: controller.signal
2485
+ });
2486
+ } finally {
2487
+ clearTimeout(timer);
2488
+ }
2489
+ }
2490
+ async function sendWebhook(config, payload, target) {
2491
+ try {
2492
+ const targets = target ? [target] : Object.keys(FORMATTERS);
2493
+ const promises = [];
2494
+ for (const t of targets) {
2495
+ const url = config[t];
2496
+ if (!url) continue;
2497
+ const formatter = FORMATTERS[t];
2498
+ if (!formatter) continue;
2499
+ const body = formatter(payload);
2500
+ promises.push(sendSingle(url, body).catch(() => {
2501
+ }));
2502
+ }
2503
+ await Promise.all(promises);
2504
+ } catch {
2505
+ }
2506
+ }
2041
2507
  export {
2042
2508
  ApiError,
2043
2509
  ConfigError,
@@ -2050,12 +2516,14 @@ export {
2050
2516
  activateBasePlan,
2051
2517
  activateOffer,
2052
2518
  addTesters,
2519
+ cancelRecoveryAction,
2053
2520
  cancelSubscriptionPurchase,
2054
2521
  checkThreshold,
2055
2522
  compareVitalsTrend,
2056
2523
  consumeProductPurchase,
2057
2524
  convertRegionPrices,
2058
2525
  createAuditEntry,
2526
+ createExternalTransaction,
2059
2527
  createInAppProduct,
2060
2528
  createOffer,
2061
2529
  createSubscription,
@@ -2068,14 +2536,23 @@ export {
2068
2536
  deleteListing,
2069
2537
  deleteOffer,
2070
2538
  deleteSubscription,
2539
+ deployRecoveryAction,
2071
2540
  detectOutputFormat,
2072
2541
  diffListings,
2542
+ diffListingsCommand,
2073
2543
  discoverPlugins,
2074
2544
  downloadReport,
2545
+ exportDataSafety,
2075
2546
  exportReviews,
2547
+ formatCustomPayload,
2548
+ formatDiscordPayload,
2076
2549
  formatOutput,
2550
+ formatSlackPayload,
2551
+ generateNotesFromGit,
2077
2552
  getAppInfo,
2078
2553
  getCountryAvailability,
2554
+ getDataSafety,
2555
+ getExternalTransaction,
2079
2556
  getInAppProduct,
2080
2557
  getListings,
2081
2558
  getOffer,
@@ -2093,6 +2570,7 @@ export {
2093
2570
  getVitalsOverview,
2094
2571
  getVitalsRendering,
2095
2572
  getVitalsStartup,
2573
+ importDataSafety,
2096
2574
  importTestersFromCsv,
2097
2575
  initAudit,
2098
2576
  inviteUser,
@@ -2104,6 +2582,7 @@ export {
2104
2582
  listImages,
2105
2583
  listInAppProducts,
2106
2584
  listOffers,
2585
+ listRecoveryActions,
2107
2586
  listReports,
2108
2587
  listReviews,
2109
2588
  listSubscriptions,
@@ -2121,6 +2600,7 @@ export {
2121
2600
  readListingsFromDir,
2122
2601
  readReleaseNotesFromDir,
2123
2602
  redactSensitive,
2603
+ refundExternalTransaction,
2124
2604
  refundOrder,
2125
2605
  removeTesters,
2126
2606
  removeUser,
@@ -2130,8 +2610,11 @@ export {
2130
2610
  safePathWithin,
2131
2611
  scaffoldPlugin,
2132
2612
  searchVitalsErrors,
2613
+ sendWebhook,
2614
+ sortResults,
2133
2615
  syncInAppProducts,
2134
2616
  updateAppDetails,
2617
+ updateDataSafety,
2135
2618
  updateInAppProduct,
2136
2619
  updateListing,
2137
2620
  updateOffer,